feat: Phase 3b — proper Web UI for memories, projects, settings

Replaces the debug /me + /connect pages with a real authed app shell.

Pages
- /                       — anonymous landing; redirects to /dashboard once signed in
- /dashboard              — recent memories + top projects, quick "new memory" action
- /memories               — searchable list with hybrid (vector+FTS+tags) scoring;
                            per-result rank breakdown shown inline
- /memories/[id]          — view + inline edit toggle + delete
- /memories/new           — create form with project autocomplete
- /projects               — list with memory counts and last-activity
- /projects/[key]         — that project's memories
- /settings               — read-only Authentik profile + link to tokens
- /settings/tokens        — list / create / revoke CLI tokens

Old URLs preserved as redirects:
- /me      → /dashboard
- /connect → /settings/tokens

Stack additions
- Tailwind v4 with CSS-first @theme tokens (dark only for now)
- App shell in app/(authed)/ — auth guard + top nav with global search box
- Lightweight UI primitives in app/_components/ui/ (Button, Input, Card,
  Badge, EmptyState, Container, PageHeader)
- Search logic extracted from MCP tool into lib/memories.ts so Web UI and
  MCP both call the same RRF code path
- Memory CRUD via Server Actions in lib/memory-actions.ts; audit_log
  rows are tagged actor='web' to distinguish from MCP writes

Per-token revoke
- New cli_tokens table (id, user_id, jti unique, name, created_at,
  last_used_at, expires_at, revoked_at) — migration 0001_cli_tokens.sql
- mintCliToken now records jti + name; verifyCliToken enforces revocation
  for tracked tokens. Legacy tokens minted before this change (no jti)
  are accepted on signature alone until they expire naturally.
- /settings/tokens lists active + revoked tokens with one-click revoke

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 10:57:17 -07:00
co-authored by Claude Opus 4.7
parent 609039f098
commit ff6baab393
33 changed files with 2475 additions and 395 deletions
+73 -48
View File
@@ -1,71 +1,96 @@
:root {
color-scheme: light dark;
--bg: #0b0d10;
--fg: #e7e9ec;
--muted: #8a9099;
--accent: #6ea8fe;
--surface: #14181d;
--border: #232a31;
@import "tailwindcss";
/* --------------------------------------------------------------------------
* Design tokens.
*
* Dark-first palette (the only theme right now). Light mode can come later
* by extending these tokens.
* -------------------------------------------------------------------------- */
@theme {
/* Brand */
--color-accent-300: oklch(0.79 0.13 250);
--color-accent-400: oklch(0.72 0.16 250);
--color-accent-500: oklch(0.65 0.19 250);
--color-accent-600: oklch(0.55 0.18 250);
/* Surface stack */
--color-bg: #0b0d10;
--color-surface-1: #11151b;
--color-surface-2: #161b22;
--color-surface-3: #1c222b;
/* Foreground */
--color-fg: #e7e9ec;
--color-fg-muted: #9aa3ad;
--color-fg-subtle: #6c7480;
/* Borders */
--color-border: #232a32;
--color-border-strong: #353c46;
/* Semantic */
--color-success: #5fd49d;
--color-danger: #ff6b6b;
--color-warning: #f5c071;
/* Radius */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.625rem;
/* Font */
--font-sans:
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
}
* {
box-sizing: border-box;
}
/* --------------------------------------------------------------------------
* Base layer — global styling reset (light layer over Tailwind's preflight)
* -------------------------------------------------------------------------- */
html,
body {
margin: 0;
padding: 0;
min-height: 100%;
background: var(--bg);
color: var(--fg);
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-sans);
font-size: 15px;
line-height: 1.55;
min-height: 100%;
}
::selection {
background: color-mix(in srgb, var(--color-accent-500) 35%, transparent);
}
/* Avoid bright white default focus ring when using accent buttons. */
*:focus-visible {
outline: 2px solid var(--color-accent-400);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
a {
color: var(--accent);
color: var(--color-accent-300);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
font: inherit;
color: var(--fg);
background: var(--surface);
border: 1px solid var(--border);
padding: 0.5rem 0.9rem;
border-radius: 0.375rem;
cursor: pointer;
}
button:hover {
border-color: var(--accent);
code,
pre {
font-family: var(--font-mono);
}
pre {
background: var(--surface);
border: 1px solid var(--border);
background: var(--color-surface-1);
border: 1px solid var(--color-border);
padding: 1rem;
border-radius: 0.5rem;
border-radius: var(--radius-md);
overflow-x: auto;
font-size: 13px;
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.container {
max-width: 880px;
margin: 0 auto;
padding: 2rem 1.25rem;
}
.muted {
color: var(--muted);
}