diff --git a/cortex/routers/settings.py b/cortex/routers/settings.py index 9695bca..bb321be 100644 --- a/cortex/routers/settings.py +++ b/cortex/routers/settings.py @@ -56,14 +56,25 @@ def _preferred_persona(request: Request, username: str) -> str: def _notifications_page(username: str, back_persona: str = "", success: str = "", error: str = "") -> str: html = (_STATIC / "notifications.html").read_text() - channels = get_user_channels(username) - notify_ch = _html.escape(channels.get("notification_channel", "") or "") - notify_email = _html.escape(channels.get("notification_email", "") or "") - nc_room = _html.escape((channels.get("nextcloud") or {}).get("notification_room", "") or "") - gc_webhook = _html.escape((channels.get("google_chat") or {}).get("outbound_webhook", "") or "") + channels = get_user_channels(username) + nct = channels.get("nextcloud") or {} + + notify_ch = _html.escape(channels.get("notification_channel", "") or "") + notify_email = _html.escape(channels.get("notification_email", "") or "") + nc_url = _html.escape(nct.get("url", "") or "") + nc_bot_secret = _html.escape(nct.get("bot_secret", "") or "") + nc_room = _html.escape(nct.get("notification_room", "") or "") + nc_username = _html.escape(nct.get("nc_username", "") or "") + nc_app_password = _html.escape(nct.get("nc_app_password", "") or "") + gc_webhook = _html.escape((channels.get("google_chat") or {}).get("outbound_webhook", "") or "") + html = html.replace("{{ notify_channel }}", notify_ch) html = html.replace("{{ notify_email_override }}", notify_email) + html = html.replace("{{ nc_url }}", nc_url) + html = html.replace("{{ nc_bot_secret }}", nc_bot_secret) html = html.replace("{{ nc_notify_room }}", nc_room) + html = html.replace("{{ nc_username }}", nc_username) + html = html.replace("{{ nc_app_password }}", nc_app_password) html = html.replace("{{ gc_webhook }}", gc_webhook) html = html.replace("{{ back_href }}", f"/{username}/{back_persona}" if back_persona else "/") html = html.replace("{{ help_href }}", f"/help?persona={back_persona}" if back_persona else "/help") @@ -94,6 +105,14 @@ def _settings_page(username: str, personas: list[str], back_persona: str = "", s allowlist_text = "" html = html.replace("{{ email_allowlist }}", allowlist_text) + http_al_path = app_settings.home_root() / username / "http_allowlist.json" + try: + http_prefixes = json.loads(http_al_path.read_text()) + http_allowlist_text = _html.escape("\n".join(str(p) for p in http_prefixes if str(p).strip())) + except Exception: + http_allowlist_text = "" + html = html.replace("{{ http_allowlist }}", http_allowlist_text) + # Tool permission policy policy = get_tool_policy(username) tool_allow_text = _html.escape("\n".join(policy.get("allow", []))) @@ -284,7 +303,11 @@ async def save_notifications( request: Request, notification_channel: str = Form(""), notification_email: str = Form(""), + nc_url: str = Form(""), + nc_bot_secret: str = Form(""), nc_notification_room: str = Form(""), + nc_username: str = Form(""), + nc_app_password: str = Form(""), gc_outbound_webhook: str = Form(""), ): username = _get_session_user(request) @@ -313,10 +336,20 @@ async def save_notifications( else: channels.pop("notification_email", None) - # NC Talk notification room — nested under "nextcloud" + # Nextcloud Talk — full config nested under "nextcloud" if "nextcloud" not in channels: channels["nextcloud"] = {} - channels["nextcloud"]["notification_room"] = nc_notification_room.strip() + nct = channels["nextcloud"] + if nc_url.strip(): + nct["url"] = nc_url.strip().rstrip("/") + # Only overwrite secrets if a new value was provided (blank = keep existing) + if nc_bot_secret.strip(): + nct["bot_secret"] = nc_bot_secret.strip() + nct["notification_room"] = nc_notification_room.strip() + if nc_username.strip(): + nct["nc_username"] = nc_username.strip() + if nc_app_password.strip(): + nct["nc_app_password"] = nc_app_password.strip() # Google Chat outbound webhook — nested under "google_chat" if "google_chat" not in channels: @@ -365,3 +398,21 @@ async def save_email_allowlist( path.write_text(json.dumps(lines, indent=2)) logger.info("email allowlist updated for %s (%d patterns)", username, len(lines)) return HTMLResponse(_settings_page(username, personas, back_persona, success=f"Email allowlist saved ({len(lines)} pattern{'s' if len(lines) != 1 else ''}).")) + + +@router.post("/settings/http-allowlist", include_in_schema=False) +async def save_http_allowlist( + request: Request, + prefixes: str = Form(""), +): + username = _get_session_user(request) + if not username: + return RedirectResponse("/login", status_code=302) + + personas = list_user_personas(username) + back_persona = _preferred_persona(request, username) + lines = [ln.strip() for ln in prefixes.splitlines() if ln.strip()] + path = app_settings.home_root() / username / "http_allowlist.json" + path.write_text(json.dumps(lines, indent=2)) + logger.info("http allowlist updated for %s (%d prefixes)", username, len(lines)) + return HTMLResponse(_settings_page(username, personas, back_persona, success=f"HTTP allowlist saved ({len(lines)} prefix{'es' if len(lines) != 1 else ''}).")) diff --git a/cortex/static/notifications.html b/cortex/static/notifications.html index af902fd..17208d6 100644 --- a/cortex/static/notifications.html +++ b/cortex/static/notifications.html @@ -46,7 +46,7 @@ border-radius: 12px; padding: 2.5rem 2rem; width: 100%; - max-width: 480px; + max-width: 520px; } .page-nav { @@ -109,6 +109,7 @@ transition: border-color 0.15s; } input:focus, select:focus { border-color: #7c3aed; } + input[type="password"] { font-family: monospace; letter-spacing: 0.05em; } .field { margin-bottom: 1rem; } @@ -157,7 +158,49 @@ } .test-result.ok { background: rgba(74, 222, 128, 0.1); color: #4ade80; border: 1px solid rgba(74, 222, 128, 0.25); } .test-result.err { background: rgba(248, 113, 113, 0.1); color: #f87171; border: 1px solid rgba(248, 113, 113, 0.25); } + .hint { font-size: 0.78rem; color: var(--pg-dim); margin-top: 0.35rem; line-height: 1.5; } + + /* Channel config blocks */ + details.channel-block { + border: 1px solid var(--pg-border); + border-radius: 8px; + margin-bottom: 0.75rem; + overflow: hidden; + } + details.channel-block summary { + padding: 0.75rem 1rem; + font-size: 0.85rem; + font-weight: 600; + color: var(--pg-muted); + cursor: pointer; + list-style: none; + display: flex; + align-items: center; + gap: 0.5rem; + user-select: none; + background: var(--pg-bg); + } + details.channel-block summary::-webkit-details-marker { display: none; } + details.channel-block summary::before { + content: '▶'; + font-size: 0.65rem; + color: var(--pg-dimmer); + transition: transform 0.15s; + flex-shrink: 0; + } + details.channel-block[open] summary::before { transform: rotate(90deg); } + details.channel-block[open] summary { border-bottom: 1px solid var(--pg-border); } + .channel-block-body { + padding: 1rem 1rem 0.25rem; + } + .channel-hint { + font-size: 0.75rem; + color: var(--pg-dimmer); + margin-top: -0.6rem; + margin-bottom: 1rem; + line-height: 1.5; + }
@@ -179,12 +222,13 @@ - -
+ Configure to send and receive messages via your Nextcloud Talk bot.
+ Sending requires the bot URL, secret, and notification room.
+ Reading history (nc_talk_history tool) additionally
+ requires a Nextcloud username and app password.
+
+ Set these up in your Nextcloud Talk room → Bot settings. + See the setup guide for step-by-step instructions. +
+Generated when you registered the bot in Nextcloud Talk.
+The token at the end of the Talk room URL — e.g. abc123def.
+ Required for the nc_talk_history orchestrator tool.
+ Generate an app password in Nextcloud → Settings → Security → App passwords.
+
+ Outbound webhook for proactive messages to a Google Chat space. + Incoming messages are handled separately via the Google Chat Add-on. +
+ ++ Create a webhook in your Google Chat space → Manage webhooks. + Paste the full URL here. +
+Fire a notification via your configured channel or run the reminder check diff --git a/cortex/static/settings.html b/cortex/static/settings.html index b4a76fb..8285712 100644 --- a/cortex/static/settings.html +++ b/cortex/static/settings.html @@ -345,6 +345,25 @@
+ One URL prefix per line. The http_post
+ tool will only POST to URLs that start with a listed prefix.
+ Leave blank to block all outbound POST requests.
+