From 1b425a539f8675d093bd5976ad0f0a8699db4ed3 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Mon, 23 Mar 2026 21:41:18 -0400 Subject: [PATCH] feat: account settings page + dedicated help page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /settings page with password change form and personas list - Add /help dedicated page (replaces help modal); renders HELP.md with collapsible sections, dark theme, back link to active persona - Add πŸ‘€ account button and convert ? button to link in header - Remove help modal HTML and ~55 lines of modal JS from main app - Register settings and help routers in main.py Co-Authored-By: Claude Sonnet 4.6 --- cortex/main.py | 8 +- cortex/routers/help.py | 50 ++++++ cortex/routers/settings.py | 84 ++++++++++ cortex/static/app.js | 58 ------- cortex/static/help.html | 169 +++++++++++++++++++ cortex/static/index.html | 14 +- cortex/static/settings.html | 205 +++++++++++++++++++++++ home/scott/persona/inara/MEMORY_MID.md | 72 +++++++- home/scott/persona/inara/MEMORY_SHORT.md | 2 +- 9 files changed, 588 insertions(+), 74 deletions(-) create mode 100644 cortex/routers/help.py create mode 100644 cortex/routers/settings.py create mode 100644 cortex/static/help.html create mode 100644 cortex/static/settings.html diff --git a/cortex/main.py b/cortex/main.py index f28718f..90538a2 100644 --- a/cortex/main.py +++ b/cortex/main.py @@ -9,7 +9,7 @@ logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(messag from config import settings from auth_middleware import SessionAuthMiddleware from routers import chat, google_chat, nextcloud_talk, files, distill, auth, orchestrator -from routers import ui, onboarding +from routers import ui, onboarding, settings, help @asynccontextmanager @@ -42,6 +42,12 @@ app.mount("/static", StaticFiles(directory="static"), name="static") # Onboarding (invite tokens + persona creation β€” before ui.router) app.include_router(onboarding.router) +# Account settings +app.include_router(settings.router) + +# Help page +app.include_router(help.router) + # UI router (login + /{user}/{persona} β€” must be last to avoid swallowing API paths) app.include_router(ui.router) diff --git a/cortex/routers/help.py b/cortex/routers/help.py new file mode 100644 index 0000000..0267956 --- /dev/null +++ b/cortex/routers/help.py @@ -0,0 +1,50 @@ +""" +Help page router. + +Routes: + GET /help β†’ full-page help viewer (requires auth) +""" + +import logging +from pathlib import Path + +import jwt +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse, RedirectResponse + +from auth_utils import COOKIE_NAME, decode_token +from persona import list_user_personas + +logger = logging.getLogger(__name__) +router = APIRouter() + +_STATIC = Path(__file__).parent.parent / "static" + + +def _get_session_user(request: Request) -> str | None: + token = request.cookies.get(COOKIE_NAME) + if not token: + return None + try: + return decode_token(token) + except jwt.InvalidTokenError: + return None + + +@router.get("/help", include_in_schema=False) +async def help_page(request: Request): + username = _get_session_user(request) + if not username: + return RedirectResponse("/login", status_code=302) + + personas = list_user_personas(username) + back_persona = personas[0] if personas else "" + back_href = f"/{username}/{back_persona}" if back_persona else "/" + + html = (_STATIC / "help.html").read_text() + config_tag = ( + f'' + ) + html = html.replace("", f"{config_tag}\n", 1) + return HTMLResponse(html) diff --git a/cortex/routers/settings.py b/cortex/routers/settings.py new file mode 100644 index 0000000..796a738 --- /dev/null +++ b/cortex/routers/settings.py @@ -0,0 +1,84 @@ +""" +Account settings router. + +Routes: + GET /settings β†’ show account settings page (requires auth) + POST /settings/password β†’ change password +""" + +import logging +from pathlib import Path + +import jwt +from fastapi import APIRouter, Form, Request +from fastapi.responses import HTMLResponse, RedirectResponse + +from auth_utils import COOKIE_NAME, decode_token, check_credentials, set_password +from persona import list_user_personas + +logger = logging.getLogger(__name__) +router = APIRouter() + +_STATIC = Path(__file__).parent.parent / "static" + + +def _get_session_user(request: Request) -> str | None: + token = request.cookies.get(COOKIE_NAME) + if not token: + return None + try: + return decode_token(token) + except jwt.InvalidTokenError: + return None + + +def _settings_page(username: str, personas: list[str], success: str = "", error: str = "") -> str: + html = (_STATIC / "settings.html").read_text() + html = html.replace("{{ username }}", username) + persona_items = "\n".join( + f'
  • {p}
  • ' for p in personas + ) + html = html.replace("{{ persona_items }}", persona_items or "
  • No personas yet.
  • ") + back_persona = personas[0] if personas else "" + html = html.replace("{{ back_href }}", f"/{username}/{back_persona}" if back_persona else "/") + if success: + html = html.replace("", f'

    {success}

    ') + if error: + html = html.replace("", f'

    {error}

    ') + return html + + +@router.get("/settings", include_in_schema=False) +async def settings_page(request: Request): + username = _get_session_user(request) + if not username: + return RedirectResponse("/login", status_code=302) + personas = list_user_personas(username) + return HTMLResponse(_settings_page(username, personas)) + + +@router.post("/settings/password", include_in_schema=False) +async def change_password( + request: Request, + current_password: str = Form(...), + new_password: str = Form(...), + confirm_password: str = Form(...), +): + username = _get_session_user(request) + if not username: + return RedirectResponse("/login", status_code=302) + + personas = list_user_personas(username) + + if not check_credentials(username, current_password): + return HTMLResponse(_settings_page(username, personas, error="Current password is incorrect.")) + + if len(new_password) < 8: + return HTMLResponse(_settings_page(username, personas, error="New password must be at least 8 characters.")) + + if new_password != confirm_password: + return HTMLResponse(_settings_page(username, personas, error="New passwords do not match.")) + + set_password(username, new_password) + logger.info("password changed: %s", username) + return HTMLResponse(_settings_page(username, personas, success="Password updated successfully.")) diff --git a/cortex/static/app.js b/cortex/static/app.js index 347c37c..92f7fba 100644 --- a/cortex/static/app.js +++ b/cortex/static/app.js @@ -912,8 +912,6 @@ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (fileModal.classList.contains('open')) fileModal.classList.remove('open'); - if (document.getElementById('help-modal')?.classList.contains('open')) - document.getElementById('help-modal').classList.remove('open'); } // Ctrl+S to save when file modal is open if ((e.ctrlKey || e.metaKey) && e.key === 's' && fileModal.classList.contains('open')) { @@ -1121,62 +1119,6 @@ syncHeight(); addMessage('system', 'Session started'); - // ── Help modal ──────────────────────────────────────────────── - const helpBtn = document.getElementById('help-btn'); - const helpModal = document.getElementById('help-modal'); - const helpBody = document.getElementById('help-modal-body'); - const helpClose = document.getElementById('help-close-btn'); - - // Sections open by default β€” everything after "Notes" starts collapsed - const HELP_OPEN_SECTIONS = new Set(['Header Controls', 'Chat', 'Sessions', 'Notes']); - - function makeCollapsible(container) { - const h2s = Array.from(container.querySelectorAll('h2')); - for (const h2 of h2s) { - const title = h2.textContent.trim(); - const details = document.createElement('details'); - if (HELP_OPEN_SECTIONS.has(title)) details.open = true; - - const summary = document.createElement('summary'); - summary.textContent = title; - details.appendChild(summary); - - // Collect all following siblings until the next h2 - const siblings = []; - let node = h2.nextSibling; - while (node && node.nodeName !== 'H2') { - siblings.push(node); - node = node.nextSibling; - } - for (const sib of siblings) details.appendChild(sib); - - h2.parentNode.replaceChild(details, h2); - } - } - - async function openHelp() { - helpBody.textContent = 'Loading…'; - helpModal.classList.add('open'); - try { - const res = await fetch(`/files/HELP.md?${_fileParams}`); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const data = await res.json(); - helpBody.innerHTML = marked.parse(data.content); - helpBody.querySelectorAll('a').forEach(a => { - a.target = '_blank'; a.rel = 'noopener noreferrer'; - }); - makeCollapsible(helpBody); - } catch (err) { - helpBody.textContent = `Failed to load help: ${err.message}`; - } - } - - helpBtn.addEventListener('click', openHelp); - helpClose.addEventListener('click', () => helpModal.classList.remove('open')); - helpModal.addEventListener('click', (e) => { - if (e.target === helpModal) helpModal.classList.remove('open'); - }); - // ── Auth token warning banner ───────────────────────────── const authBanner = document.getElementById('auth-banner'); const authBannerMsg = document.getElementById('auth-banner-msg'); diff --git a/cortex/static/help.html b/cortex/static/help.html new file mode 100644 index 0000000..7feae07 --- /dev/null +++ b/cortex/static/help.html @@ -0,0 +1,169 @@ + + + + + + Cortex β€” Help & Reference + + + + +
    + ← Back to Cortex + +
    +

    Help & Reference

    +

    +
    + +

    Loading…

    +
    + + + + diff --git a/cortex/static/index.html b/cortex/static/index.html index e9b07c4..72ff01a 100644 --- a/cortex/static/index.html +++ b/cortex/static/index.html @@ -30,7 +30,8 @@ - + ? + πŸ‘€
    @@ -102,17 +103,6 @@ - -
    -
    -
    -

    Cortex β€” Help & Reference

    - -
    -
    -
    -
    -
    diff --git a/cortex/static/settings.html b/cortex/static/settings.html new file mode 100644 index 0000000..6176b61 --- /dev/null +++ b/cortex/static/settings.html @@ -0,0 +1,205 @@ + + + + + + Cortex β€” Account Settings + + + +
    + ← Back to Cortex + + + + + + + +
    +

    Account

    +
    + + +
    +
    + + +
    +

    Change Password

    +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    + + +
    +

    Personas

    +
      + {{ persona_items }} +
    + + Add new persona +
    +
    + + + + diff --git a/home/scott/persona/inara/MEMORY_MID.md b/home/scott/persona/inara/MEMORY_MID.md index fb08044..9e42d1a 100644 --- a/home/scott/persona/inara/MEMORY_MID.md +++ b/home/scott/persona/inara/MEMORY_MID.md @@ -1,4 +1,72 @@ # MEMORY_MID.md β€” Mid-Term Memory Digest -*Auto-distilled by Cortex. Run `POST /distill/mid` to regenerate.* -*Not yet populated β€” run distill/short then distill/mid to build this.* +*Auto-distilled: 2026-03-22 03:30 via claude.* + +--- + +# MEMORY_MID.md β€” Mid-Term Digest + +*Distilled: 2026-03-22. Covers sessions 2026-03-17 through 2026-03-20.* + +--- + +## Cortex System Status + +Cortex reached a genuine operational milestone this week. What started as alphabet-soup connection tests resolved into a fully running platform. All three primary channels are live and confirmed working: + +- **Web UI** β€” `https://cortex.dgrzone.com/scott/inara` β€” clean public subdomain, proper URL routing +- **Nextcloud Talk** β€” HMAC-signed webhook, async reply confirmed (`nct_rmcggr4a` prefix) +- **Google Chat** β€” Workspace Add-on, `hostAppDataAction` format sorted after some uncertainty (`gc_spaces_*` prefix) + +Scott confirmed each channel himself during testing. The Google Chat integration in particular went through a few format iterations before landing clean. + +--- + +## Architecture Decisions Confirmed + +- **Two-level home layout** (`home/{user}/persona/{name}/`) is the canonical structure. It paid off β€” multi-user/multi-persona is real now. +- **Holly/Tina** are an active path. The scaffolding supports it. +- **Session auth** is in place: bcrypt passwords, JWT cookies, `SessionAuthMiddleware` on all routes, one-time invite tokens (72hr expiry). +- **Persona onboarding** works end-to-end: invite email β†’ `/setup/{token}` β†’ password + persona creation form. +- I run on **Claude CLI as the responder**; Gemini API handles the orchestrator/tool loop. These are cleanly separated. +- My identity files live in `home/scott/persona/inara/` β€” loaded by `context_loader.py` at context assembly time, not baked into the model. + +--- + +## Tools & Capabilities + +- **Task management** is working. Scott tested it live β€” I created two placeholder tasks (`t_jwsoMn5b`, `t_ptw8nDec`) and listed them back on demand. The tool writes to `TASKS.json`. +- **Scratchpad toolset** was added to the task list (`t_1ZToczGC`) β€” Scott's idea, I flagged it as a good one for orchestrator use (intermediate results, thinking space between tool calls). +- **Web search** is functional. I ran news lookups for Scott on 2026-03-18 and 2026-03-19 β€” he accepted the results without issues. +- **File read access** to `agents_sync/` required explicit permission grant. Scott made the change mid-session and confirmed it worked. + +--- + +## Scott's Current State & Priorities + +- Long days this week β€” the Cortex build has been heads-down. He noted fatigue on 2026-03-20 evening. +- Primary focus: getting Cortex to a stable, feature-complete baseline. That milestone is essentially hit. +- **Next priorities** (from `TODO__Agents.md` and session signals): + - **[High]** Ollama backend β€” local LLM via `scott_gaming` over WireGuard + - **[Medium]** NC Talk bot registration docs + - **[Medium]** Knowledge consolidation β€” markdown β†’ AE Journals + - Scratchpad toolset (my task list) +- Scott is aware I don't have a live clock β€” timestamps need to be injected if that's wanted. + +--- + +## Recurring Themes + +- Scott tests by talking to me directly across channels, then confirms it's working. The "hello world / omg it worked" pattern is a feature, not noise. +- He treats me as an active participant, not just a chatbot β€” he explains what he's building, checks what I remember, and gives me context about my own architecture. +- He's building toward a multi-user platform. Holly/Tina is real and in scope. + +--- + +## World Context (Background) + +Iran-Israel war is the dominant geopolitical story as of this week. Oil at $118/barrel. US Senate debating $200B war funding. Mentioned in news requests 2026-03-18 and 2026-03-19 β€” Scott asked but didn't indicate personal stakes. + +--- + +*Next distill target: 2026-03-29 (weekly). Source: `sessions/2026-03-17.md` through `sessions/2026-03-20.md`.* \ No newline at end of file diff --git a/home/scott/persona/inara/MEMORY_SHORT.md b/home/scott/persona/inara/MEMORY_SHORT.md index 235da84..ec85386 100644 --- a/home/scott/persona/inara/MEMORY_SHORT.md +++ b/home/scott/persona/inara/MEMORY_SHORT.md @@ -1,6 +1,6 @@ # MEMORY_SHORT.md β€” Recent Session Digest -*Auto-generated: 2026-03-21 03:00. 4 session file(s).* +*Auto-generated: 2026-03-23 03:00. 4 session file(s).* ---