feat: account settings page + dedicated help page
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
50
cortex/routers/help.py
Normal file
50
cortex/routers/help.py
Normal file
@@ -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'<script>window.HELP_CONFIG = '
|
||||
f'{{user: "{username}", persona: "{back_persona}", backHref: "{back_href}"}};</script>'
|
||||
)
|
||||
html = html.replace("</head>", f"{config_tag}\n</head>", 1)
|
||||
return HTMLResponse(html)
|
||||
84
cortex/routers/settings.py
Normal file
84
cortex/routers/settings.py
Normal file
@@ -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'<li><a href="/{username}/{p}">{p}</a></li>' for p in personas
|
||||
)
|
||||
html = html.replace("{{ persona_items }}", persona_items or "<li><em>No personas yet.</em></li>")
|
||||
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("<!-- SUCCESS -->", f'<p class="success">{success}</p>')
|
||||
if error:
|
||||
html = html.replace("<!-- ERROR -->", f'<p class="error">{error}</p>')
|
||||
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."))
|
||||
@@ -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');
|
||||
|
||||
169
cortex/static/help.html
Normal file
169
cortex/static/help.html
Normal file
@@ -0,0 +1,169 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cortex — Help & Reference</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background: #0f1117;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
color: #e2e8f0;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.back-link:hover { color: #a78bfa; }
|
||||
|
||||
header {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #2d3148;
|
||||
}
|
||||
header h1 { font-size: 1.5rem; font-weight: 700; color: #a78bfa; }
|
||||
header p { font-size: 0.85rem; color: #64748b; margin-top: 0.25rem; }
|
||||
|
||||
#help-body { line-height: 1.7; }
|
||||
|
||||
/* Collapsible sections */
|
||||
details {
|
||||
margin-bottom: 0.75rem;
|
||||
background: #1a1d27;
|
||||
border: 1px solid #2d3148;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
summary {
|
||||
padding: 0.85rem 1rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: #cbd5e1;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
summary::before {
|
||||
content: '▶';
|
||||
font-size: 0.65rem;
|
||||
color: #64748b;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
details[open] summary::before { transform: rotate(90deg); }
|
||||
summary::-webkit-details-marker { display: none; }
|
||||
|
||||
details > *:not(summary) {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
#help-body p { margin: 0.5rem 0; font-size: 0.9rem; color: #cbd5e1; }
|
||||
#help-body ul { margin: 0.5rem 0 0.5rem 1.25rem; }
|
||||
#help-body li { font-size: 0.9rem; color: #cbd5e1; margin-bottom: 0.25rem; }
|
||||
#help-body strong { color: #e2e8f0; }
|
||||
#help-body code {
|
||||
background: #0f1117;
|
||||
border: 1px solid #2d3148;
|
||||
border-radius: 4px;
|
||||
padding: 0.1em 0.4em;
|
||||
font-size: 0.85em;
|
||||
color: #a78bfa;
|
||||
}
|
||||
#help-body a { color: #a78bfa; }
|
||||
|
||||
#help-body h3 {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0.75rem 0 0.25rem;
|
||||
}
|
||||
|
||||
#loading { color: #64748b; font-size: 0.9rem; padding: 1rem 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<a id="back-link" href="/" class="back-link">← Back to Cortex</a>
|
||||
|
||||
<header>
|
||||
<h1>Help & Reference</h1>
|
||||
<p id="persona-label"></p>
|
||||
</header>
|
||||
|
||||
<div id="help-body"><p id="loading">Loading…</p></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const cfg = window.HELP_CONFIG || {};
|
||||
const user = cfg.user || 'scott';
|
||||
const persona = cfg.persona || 'inara';
|
||||
const params = `user=${encodeURIComponent(user)}&persona=${encodeURIComponent(persona)}`;
|
||||
|
||||
// Wire up back link and persona label
|
||||
document.getElementById('back-link').href = cfg.backHref || '/';
|
||||
if (persona) {
|
||||
document.getElementById('persona-label').textContent =
|
||||
`${persona.charAt(0).toUpperCase() + persona.slice(1)} · ${user}`;
|
||||
}
|
||||
|
||||
const 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 (OPEN_SECTIONS.has(title)) details.open = true;
|
||||
|
||||
const summary = document.createElement('summary');
|
||||
summary.textContent = title;
|
||||
details.appendChild(summary);
|
||||
|
||||
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 loadHelp() {
|
||||
try {
|
||||
const res = await fetch(`/files/HELP.md?${params}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
const body = document.getElementById('help-body');
|
||||
body.innerHTML = marked.parse(data.content);
|
||||
body.querySelectorAll('a').forEach(a => {
|
||||
a.target = '_blank'; a.rel = 'noopener noreferrer';
|
||||
});
|
||||
makeCollapsible(body);
|
||||
} catch (err) {
|
||||
document.getElementById('help-body').textContent = `Failed to load help: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
loadHelp();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -30,7 +30,8 @@
|
||||
<button id="sessions-btn" class="hdr-btn">Sessions</button>
|
||||
<button id="files-btn" class="hdr-btn">Files</button>
|
||||
<button id="ctx-open-btn" class="hdr-btn" title="Settings">⚙<span class="tier-badge">2</span></button>
|
||||
<button id="help-btn" class="hdr-btn" title="Help & reference">?</button>
|
||||
<a href="/help" class="hdr-btn" title="Help & reference" style="text-decoration:none">?</a>
|
||||
<a id="account-btn" href="/settings" class="hdr-btn" title="Account settings" style="text-decoration:none">👤</a>
|
||||
<form method="POST" action="/logout" style="margin:0">
|
||||
<button type="submit" class="hdr-btn" title="Sign out" id="logout-btn">⏏</button>
|
||||
</form>
|
||||
@@ -102,17 +103,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help modal -->
|
||||
<div id="help-modal">
|
||||
<div id="help-modal-inner">
|
||||
<div id="help-modal-header">
|
||||
<h2>Cortex — Help & Reference</h2>
|
||||
<button class="fm-btn" id="help-close-btn">✕</button>
|
||||
</div>
|
||||
<div id="help-modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth warning banner — shown when Claude CLI token is near expiry -->
|
||||
<div id="auth-banner">
|
||||
<div id="auth-banner-text">
|
||||
|
||||
205
cortex/static/settings.html
Normal file
205
cortex/static/settings.html
Normal file
@@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cortex — Account Settings</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #0f1117;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
color: #e2e8f0;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1a1d27;
|
||||
border: 1px solid #2d3148;
|
||||
border-radius: 12px;
|
||||
padding: 2.5rem 2rem;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.back-link:hover { color: #a78bfa; }
|
||||
|
||||
.logo {
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
.logo h1 { font-size: 1.4rem; font-weight: 700; color: #a78bfa; }
|
||||
.logo p { font-size: 0.8rem; color: #64748b; margin-top: 0.2rem; }
|
||||
|
||||
h2 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 1px solid #2d3148;
|
||||
}
|
||||
|
||||
.section { margin-bottom: 2rem; }
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.65rem 0.85rem;
|
||||
background: #0f1117;
|
||||
border: 1px solid #2d3148;
|
||||
border-radius: 6px;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
input:focus { border-color: #7c3aed; }
|
||||
input[readonly] { color: #64748b; cursor: default; }
|
||||
|
||||
.field { margin-bottom: 1rem; }
|
||||
|
||||
button[type="submit"] {
|
||||
width: 100%;
|
||||
padding: 0.7rem;
|
||||
margin-top: 0.25rem;
|
||||
background: #7c3aed;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
button[type="submit"]:hover { background: #6d28d9; }
|
||||
|
||||
.error {
|
||||
color: #f87171;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #4ade80;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.persona-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.persona-list li a {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.75rem;
|
||||
background: #0f1117;
|
||||
border: 1px solid #2d3148;
|
||||
border-radius: 20px;
|
||||
color: #a78bfa;
|
||||
font-size: 0.85rem;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.persona-list li a:hover { border-color: #7c3aed; }
|
||||
.persona-list li em { color: #475569; font-size: 0.85rem; }
|
||||
|
||||
.add-persona {
|
||||
display: inline-block;
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
}
|
||||
.add-persona:hover { color: #a78bfa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<a href="{{ back_href }}" class="back-link">← Back to Cortex</a>
|
||||
|
||||
<div class="logo">
|
||||
<h1>Account Settings</h1>
|
||||
<p>Manage your account and personas.</p>
|
||||
</div>
|
||||
|
||||
<!-- SUCCESS -->
|
||||
<!-- ERROR -->
|
||||
|
||||
<!-- Account info -->
|
||||
<div class="section">
|
||||
<h2>Account</h2>
|
||||
<div class="field">
|
||||
<label>Username</label>
|
||||
<input type="text" value="{{ username }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change password -->
|
||||
<div class="section">
|
||||
<h2>Change Password</h2>
|
||||
<form method="POST" action="/settings/password">
|
||||
<div class="field">
|
||||
<label for="current_password">Current password</label>
|
||||
<input type="password" id="current_password" name="current_password"
|
||||
autocomplete="current-password" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="new_password">New password</label>
|
||||
<input type="password" id="new_password" name="new_password"
|
||||
autocomplete="new-password" required minlength="8">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="confirm_password">Confirm new password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password"
|
||||
autocomplete="new-password" required>
|
||||
</div>
|
||||
<button type="submit">Update password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Personas -->
|
||||
<div class="section">
|
||||
<h2>Personas</h2>
|
||||
<ul class="persona-list">
|
||||
{{ persona_items }}
|
||||
</ul>
|
||||
<a href="/setup/persona" class="add-persona">+ Add new persona</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector('form').addEventListener('submit', e => {
|
||||
const np = document.getElementById('new_password').value;
|
||||
const cfm = document.getElementById('confirm_password').value;
|
||||
if (np !== cfm) {
|
||||
e.preventDefault();
|
||||
alert('New passwords do not match.');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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`.*
|
||||
@@ -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).*
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user