feat: full channels.json UI + http_allowlist settings

Notifications page:
- NC Talk section expanded: url, bot_secret, notification_room,
  nc_username, nc_app_password — all fields from channels.json now editable
- Per-channel sections use <details>/<summary> collapsibles; auto-open
  when values are present
- Secrets use type=password with "leave blank to keep" semantics
- Google Chat outbound webhook in its own collapsible section

Account settings:
- HTTP POST Allowlist section added (same textarea pattern as email allowlist)
- POST /settings/http-allowlist route saves home/{user}/http_allowlist.json
- Example placeholder shows ha.dgrzone.com and n8n patterns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-09 13:57:18 -04:00
parent 7b443b40a4
commit 348ca120c1
3 changed files with 229 additions and 33 deletions

View File

@@ -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 ''})."))