+ {s.cmd.text} +
+ )} + {s.lastError && ( ++ {s.lastError} +
+ )} + + {s.section === "runs" ? ( +diff --git a/README.md b/README.md index a013270..6b313fd 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,29 @@ covered. The seams — `control.tmux`, `control.forge`, `control.gitops`, `hooks and `control.spawn.resume` — are the mock points that stand in for live `claude`/`tmux`/`mise`/`forge`/`git`, and the drop-in points for wiring them up for real. +### Frontend + +The dashboard (`frontend/`) is a **Next.js** app (React + TypeScript) that builds to a +**static export** — the `Claude Activity` Control Center: a left-nav hub over Runs, +Repositories, Agents, Approvals, Git Servers, Activity, and Shared. It is a pure client of +the API (same contract as `curl`): the browser prompts for the token once, stores it in +`localStorage`, and attaches it to every call. All API values render as React text +(never `dangerouslySetInnerHTML`) so agent-authored strings can't inject markup. + +The build output is committed to `src/handler/api/static/` so the Python wheel ships it and +FastAPI serves it same-origin — there is no separate frontend server and no node step in the +Docker image. Rebuild after changing the UI: + +```bash +cd frontend +npm install +npm run build # static export → frontend/out/ +npm run export # build, then sync frontend/out/ → src/handler/api/static/ +``` + +`npm run dev` runs the UI against a live API on another origin — set +`NEXT_PUBLIC_API_BASE=http://127.0.0.1:8000` and enable `CORS_ORIGINS` on the API. + ## Project layout ``` @@ -348,6 +371,8 @@ src/handler/ # forge/gitops seams, credentials, skills_gen, CI poller hooks/ # Stop/SessionEnd, PreToolUse gate (push + approval), Notification migrations/ # Alembic env + versions + api/static/ # built Next.js export (generated — see frontend/) +frontend/ # Next.js dashboard source (builds to api/static/) tests/ # DB, API, hook, and control tests (SQLite) docs/PLAN.md # full design + phased roadmap (the original plan of action) ``` diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..dbc14e7 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,8 @@ +# Frontend build artifacts. The *built* static export is committed under +# src/handler/api/static/ (that is what the Python package ships and FastAPI serves); +# everything below is regenerated by `npm install` / `npm run build`. +/node_modules +/.next +/out +/next-env.d.ts +*.tsbuildinfo diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..7e25881 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,911 @@ +/* ============================================================ + * Handler dashboard — visual language ported from the Leeworks + * design system (flat, dark, border-led "developer-native"). + * Token hex values are copied verbatim from the design system. + * ============================================================ */ + +:root { + /* ---- surfaces ---- */ + --lw-bg: #0f1117; + --lw-surface: #1a1d27; + --lw-surface-2: #232733; + --lw-surface-3: #2b3040; + /* ---- borders ---- */ + --lw-border: #2d3748; + --lw-border-strong: #3a4861; + /* ---- text ---- */ + --lw-white: #ffffff; + --lw-text: #e2e8f0; + --lw-text-muted: #a0aec0; + --lw-text-faint: #718096; + /* ---- accents ---- */ + --lw-blue-200: #bee3f8; + --lw-blue-300: #90cdf4; + --lw-blue-400: #63b3ed; + --lw-blue-500: #4299e1; + --lw-indigo-400: #7f9cf5; + --lw-indigo-500: #667eea; + --lw-indigo-600: #5a67d8; + --lw-indigo-700: #4c51bf; + /* ---- status chip pairs ---- */ + --lw-success-bg: #1a4731; + --lw-success-fg: #9ae6b4; + --lw-warning-bg: #744210; + --lw-warning-fg: #fbd38d; + --lw-danger-bg: #63171b; + --lw-danger-fg: #feb2b2; + --lw-info-bg: #1a365d; + --lw-info-fg: #90cdf4; + --lw-neutral-bg: #232733; + --lw-neutral-fg: #a0aec0; + + /* ---- semantic aliases ---- */ + --surface-page: var(--lw-bg); + --surface-card: var(--lw-surface); + --surface-raised: var(--lw-surface-2); + --surface-inset: var(--lw-surface-3); + --border-default: var(--lw-border); + --border-strong: var(--lw-border-strong); + --text-heading: var(--lw-white); + --text-body: var(--lw-text); + --text-muted: var(--lw-text-muted); + --text-faint: var(--lw-text-faint); + --link: var(--lw-blue-300); + --link-hover: var(--lw-white); + --accent: var(--lw-blue-300); + --accent-hover: var(--lw-blue-400); + --action-bg: var(--lw-indigo-500); + --action-bg-hover: var(--lw-indigo-600); + --action-bg-active: var(--lw-indigo-700); + --action-fg: var(--lw-white); + --focus-ring: var(--lw-blue-400); + + /* ---- type ---- */ + --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace; + --fw-regular: 400; + --fw-medium: 500; + --fw-semibold: 600; + --fw-bold: 700; + --fw-black: 800; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-md: 0.95rem; + --text-base: 1rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --text-2xl: 2rem; + --lh-tight: 1.15; + --lh-snug: 1.3; + --lh-normal: 1.6; + --ls-tight: -0.02em; + --ls-wide: 0.04em; + + /* ---- radii / motion ---- */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-full: 9999px; + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45); + --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55); + --shadow-focus: 0 0 0 3px rgba(99, 179, 237, 0.45); + --ease-standard: cubic-bezier(0.4, 0, 0.2, 1); + --dur-fast: 120ms; + --dur-normal: 180ms; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-sans); + font-size: var(--text-base); + line-height: var(--lh-normal); + color: var(--text-body); + background: var(--surface-page); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +a { + color: var(--link); + text-decoration: none; + transition: color var(--dur-fast) var(--ease-standard); +} +a:hover { + color: var(--link-hover); +} + +::selection { + background: rgba(102, 126, 234, 0.4); + color: #fff; +} +:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 2px; +} +::-webkit-scrollbar { + width: 9px; + height: 9px; +} +::-webkit-scrollbar-thumb { + background: var(--border-strong); + border-radius: var(--radius-full); +} +::-webkit-scrollbar-track { + background: transparent; +} + +.mono { + font-family: var(--font-mono); +} +.muted { + color: var(--text-muted); +} +.faint { + color: var(--text-faint); +} +.nowrap { + white-space: nowrap; +} +.truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ---------------- Badge ---------------- */ +.badge { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: var(--text-xs); + font-weight: var(--fw-semibold); + line-height: 1; + padding: 4px 9px; + border-radius: var(--radius-sm); + white-space: nowrap; +} +.badge.pill { + border-radius: var(--radius-full); +} +.badge-success { + background: var(--lw-success-bg); + color: var(--lw-success-fg); +} +.badge-warning { + background: var(--lw-warning-bg); + color: var(--lw-warning-fg); +} +.badge-danger { + background: var(--lw-danger-bg); + color: var(--lw-danger-fg); +} +.badge-info { + background: var(--lw-info-bg); + color: var(--lw-info-fg); +} +.badge-neutral { + background: var(--lw-neutral-bg); + color: var(--lw-neutral-fg); +} +.badge .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} + +/* ---------------- Card ---------------- */ +.card { + background: var(--surface-card); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + padding: 20px 22px; + transition: border-color var(--dur-normal) var(--ease-standard); +} +.card.interactive { + cursor: pointer; +} +.card.interactive:hover { + border-color: var(--border-strong); +} + +/* ---------------- Button ---------------- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + font-family: inherit; + font-size: var(--text-sm); + font-weight: var(--fw-semibold); + line-height: 1; + padding: 0 16px; + height: 40px; + border-radius: var(--radius-md); + border: 1px solid transparent; + cursor: pointer; + white-space: nowrap; + transition: background var(--dur-normal) var(--ease-standard), + border-color var(--dur-normal) var(--ease-standard), color var(--dur-normal) var(--ease-standard); +} +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.btn-sm { + height: 32px; + padding: 0 12px; + font-size: var(--text-xs); +} +.btn-primary { + background: var(--action-bg); + color: var(--action-fg); +} +.btn-primary:not(:disabled):hover { + background: var(--action-bg-hover); +} +.btn-primary:not(:disabled):active { + background: var(--action-bg-active); +} +.btn-secondary { + background: transparent; + border-color: var(--border-strong); + color: var(--text-body); +} +.btn-secondary:not(:disabled):hover { + border-color: var(--accent); + color: var(--text-heading); +} +.btn-ghost { + background: transparent; + color: var(--accent); +} +.btn-ghost:not(:disabled):hover { + color: var(--link-hover); + background: rgba(255, 255, 255, 0.04); +} +.btn-danger { + background: transparent; + border-color: var(--border-strong); + color: var(--lw-danger-fg); +} +.btn-danger:not(:disabled):hover { + border-color: var(--lw-danger-fg); + background: rgba(99, 23, 27, 0.35); +} + +/* ---------------- Field (Input / Select / Textarea) ---------------- */ +.field { + display: flex; + flex-direction: column; + gap: 6px; +} +.field-label { + font-size: var(--text-xs); + font-weight: var(--fw-semibold); + color: var(--text-faint); + text-transform: uppercase; + letter-spacing: var(--ls-wide); +} +.input, +.select, +.textarea { + width: 100%; + font-family: inherit; + font-size: var(--text-sm); + color: var(--text-body); + background: var(--surface-page); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + padding: 0 12px; + height: 40px; + transition: border-color var(--dur-normal) var(--ease-standard), + box-shadow var(--dur-normal) var(--ease-standard); +} +.textarea { + height: auto; + padding: 10px 12px; + line-height: var(--lh-normal); + resize: vertical; + min-height: 68px; +} +.input::placeholder, +.textarea::placeholder { + color: var(--text-faint); +} +.input:focus, +.select:focus, +.textarea:focus { + outline: none; + border-color: var(--accent); + box-shadow: var(--shadow-focus); +} +.select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23718096' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 32px; +} +.select option { + background: var(--surface-card); + color: var(--text-body); +} + +/* ---------------- Tabs ---------------- */ +.tabs { + display: inline-flex; + gap: 4px; + background: var(--surface-page); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + padding: 3px; +} +.tab { + font-family: inherit; + font-size: var(--text-sm); + font-weight: var(--fw-semibold); + color: var(--text-muted); + background: transparent; + border: 0; + border-radius: 6px; + padding: 6px 12px; + cursor: pointer; + transition: background var(--dur-fast), color var(--dur-fast); +} +.tab:hover { + color: var(--text-body); +} +.tab.active { + background: var(--surface-inset); + color: var(--text-heading); +} + +/* ---------------- Stat ---------------- */ +.stat-value { + font-size: var(--text-2xl); + font-weight: var(--fw-bold); + line-height: 1.1; + color: var(--text-heading); + letter-spacing: var(--ls-tight); +} +.stat-value.accent { + color: var(--accent); +} +.stat-label { + font-size: var(--text-sm); + color: var(--text-muted); + margin-top: 4px; +} +.stat-sub { + font-size: var(--text-xs); + color: var(--text-faint); + font-family: var(--font-mono); +} + +/* ---------------- Callout ---------------- */ +.callout { + border-radius: var(--radius-md); + padding: 12px 14px; + font-size: var(--text-sm); + border: 1px solid transparent; +} +.callout-info { + background: rgba(26, 54, 93, 0.35); + border-color: rgba(144, 205, 244, 0.25); + color: var(--lw-blue-200); +} +.callout-danger { + background: rgba(99, 23, 27, 0.35); + border-color: rgba(254, 178, 178, 0.25); + color: var(--lw-danger-fg); +} +.callout-success { + background: rgba(26, 71, 49, 0.35); + border-color: rgba(154, 230, 180, 0.25); + color: var(--lw-success-fg); +} + +/* ---------------- CodeBlock ---------------- */ +.codeblock { + background: var(--surface-page); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + overflow: hidden; +} +.codeblock-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 7px 12px; + border-bottom: 1px solid var(--border-default); + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--text-faint); + background: var(--surface-card); +} +.codeblock pre { + margin: 0; + padding: 14px 16px; + overflow-x: auto; + font-family: var(--font-mono); + font-size: var(--text-xs); + line-height: 1.55; + color: var(--text-body); +} +.codeblock .diff-add { + color: var(--lw-success-fg); +} +.codeblock .diff-del { + color: var(--lw-danger-fg); +} + +/* ---------------- Toggle ---------------- */ +.toggle { + width: 38px; + height: 22px; + border-radius: var(--radius-full); + background: var(--surface-inset); + border: 0; + position: relative; + cursor: pointer; + flex-shrink: 0; + transition: background var(--dur-normal); + padding: 0; +} +.toggle.on { + background: var(--action-bg); +} +.toggle .knob { + position: absolute; + top: 2px; + left: 2px; + width: 18px; + height: 18px; + border-radius: 50%; + background: #fff; + transition: left var(--dur-normal); +} +.toggle.on .knob { + left: 18px; +} + +/* ---------------- App shell ---------------- */ +.app { + display: flex; + min-height: 100vh; +} +.sidebar { + width: 216px; + flex-shrink: 0; + border-right: 1px solid var(--border-default); + padding: 20px 12px; + display: flex; + flex-direction: column; + gap: 3px; + position: sticky; + top: 0; + height: 100vh; + overflow-y: auto; +} +.brand { + display: flex; + align-items: center; + gap: 9px; + font-size: var(--text-md); + font-weight: var(--fw-bold); + color: var(--text-heading); + padding: 2px 10px 16px; +} +.brand .logo { + width: 22px; + height: 22px; + border-radius: 6px; + background: linear-gradient(135deg, #90cdf4, #667eea); + flex-shrink: 0; +} +.nav-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 9px 10px; + border-radius: var(--radius-md); + color: var(--text-muted); + font-size: var(--text-sm); + font-weight: var(--fw-semibold); + background: transparent; + border: 0; + width: 100%; + cursor: pointer; + text-align: left; + transition: background var(--dur-fast), color var(--dur-fast); +} +.nav-item:hover { + background: var(--surface-raised); + color: var(--text-body); +} +.nav-item.active { + background: var(--surface-inset); + color: var(--text-heading); +} +.nav-item .count { + font-size: var(--text-xs); + font-family: var(--font-mono); + color: var(--text-faint); +} +.nav-item.active .count { + color: var(--accent); +} +.sidebar-spacer { + flex: 1; +} +.sidebar-foot { + border-top: 1px solid var(--border-default); + padding-top: 12px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} +.main-scroll { + flex: 1; + overflow-y: auto; +} + +/* section header */ +.section-head { + padding: 26px 32px 18px; +} +.section-title { + font-size: var(--text-xl); + font-weight: var(--fw-bold); + color: var(--text-heading); + letter-spacing: var(--ls-tight); +} +.section-desc { + font-size: var(--text-sm); + color: var(--text-muted); + margin-top: 4px; +} +.section-body { + padding: 0 32px 40px; + display: flex; + flex-direction: column; + gap: 18px; +} + +/* stat row */ +.stat-row { + display: flex; + background: var(--surface-card); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + overflow: hidden; + flex-wrap: wrap; +} +.stat-cell { + flex: 1; + min-width: 150px; + padding: 18px 22px; + border-left: 1px solid var(--border-default); +} +.stat-cell:first-child { + border-left: 0; +} + +/* toolbar / form grid */ +.row { + display: flex; + gap: 12px; + align-items: flex-end; + flex-wrap: wrap; +} +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + gap: 12px; +} +.grid-2 { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px; +} +.grow { + flex: 1; + min-width: 180px; +} +.eyebrow { + font-size: var(--text-xs); + font-weight: var(--fw-semibold); + color: var(--text-faint); + text-transform: uppercase; + letter-spacing: var(--ls-wide); +} + +/* inbox split (Runs) */ +.runs { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} +.runs-stats { + padding: 16px 20px; + border-bottom: 1px solid var(--border-default); + flex-shrink: 0; +} +.split { + display: flex; + flex: 1; + min-height: 0; +} +.split-list { + width: 340px; + flex-shrink: 0; + border-right: 1px solid var(--border-default); + display: flex; + flex-direction: column; + height: 100%; +} +.split-list-head { + padding: 22px 20px 12px; + display: flex; + flex-direction: column; + gap: 12px; +} +.split-list-scroll { + flex: 1; + overflow-y: auto; + padding: 10px; + display: flex; + flex-direction: column; + gap: 4px; +} +.split-detail { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.run-row { + cursor: pointer; + padding: 10px 12px; + border-radius: var(--radius-md); + border-left: 3px solid transparent; + display: flex; + flex-direction: column; + gap: 5px; + background: transparent; + border-top: 0; + border-right: 0; + border-bottom: 0; + width: 100%; + text-align: left; + font: inherit; + color: inherit; + transition: background var(--dur-fast); +} +.run-row:hover { + background: var(--surface-raised); +} +.run-row.selected { + background: var(--surface-inset); + border-left-color: var(--accent); +} +.run-row-top { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 8px; +} +.run-project { + color: var(--accent); + font-size: var(--text-sm); + font-weight: var(--fw-semibold); +} + +/* key/value */ +.kv { + display: grid; + grid-template-columns: 150px 1fr; + gap: 8px 16px; + margin: 0; +} +.kv dt { + color: var(--text-faint); + font-size: var(--text-sm); +} +.kv dd { + margin: 0; + color: var(--text-body); + font-size: var(--text-sm); +} +.kv dd ul { + margin: 0; + padding-left: 18px; +} + +/* table */ +.table-wrap { + overflow-x: auto; + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); +} +table.tbl { + width: 100%; + border-collapse: collapse; + font-size: var(--text-sm); +} +table.tbl th { + text-align: left; + font-size: var(--text-xs); + font-weight: var(--fw-semibold); + color: var(--text-faint); + text-transform: uppercase; + letter-spacing: var(--ls-wide); + padding: 11px 14px; + border-bottom: 1px solid var(--border-default); + background: var(--surface-card); + white-space: nowrap; +} +table.tbl td { + padding: 11px 14px; + border-bottom: 1px solid var(--border-default); + color: var(--text-body); + vertical-align: top; +} +table.tbl tr:last-child td { + border-bottom: 0; +} +table.tbl tbody tr:hover { + background: rgba(255, 255, 255, 0.02); +} + +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + border-radius: var(--radius-sm); + background: var(--surface-card); + border: 1px solid var(--border-default); + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--text-body); + cursor: pointer; + transition: border-color var(--dur-fast); +} +.chip:hover { + border-color: var(--border-strong); +} +.chip.selected { + border-color: var(--accent); +} + +.card-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.card-title { + color: var(--accent); + font-weight: var(--fw-bold); + font-size: var(--text-lg); +} +.stack { + display: flex; + flex-direction: column; + gap: 10px; +} +.divider { + height: 1px; + background: var(--border-default); + border: 0; + margin: 0; +} +.empty { + color: var(--text-muted); + font-size: var(--text-sm); + padding: 4px 0; +} +.pager { + display: flex; + align-items: center; + gap: 12px; +} + +/* banner */ +.banner { + margin: 0 32px; + padding: 10px 14px; + border-radius: var(--radius-md); + font-size: var(--text-sm); +} +.banner.ok { + background: rgba(26, 71, 49, 0.35); + color: var(--lw-success-fg); + border: 1px solid rgba(154, 230, 180, 0.25); +} +.banner.err { + background: rgba(99, 23, 27, 0.35); + color: var(--lw-danger-fg); + border: 1px solid rgba(254, 178, 178, 0.25); +} + +/* ---------------- Token gate ---------------- */ +.gate { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} +.gate-card { + width: 100%; + max-width: 420px; + background: var(--surface-card); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + padding: 32px; + display: flex; + flex-direction: column; + gap: 14px; + box-shadow: var(--shadow-lg); +} +.gate-brand { + display: flex; + align-items: center; + gap: 10px; + font-size: var(--text-xl); + font-weight: var(--fw-bold); + color: var(--text-heading); +} + +/* small helpers */ +.hstack { + display: flex; + align-items: center; + gap: 10px; +} +.hstack.wrap { + flex-wrap: wrap; +} +.spacer { + flex: 1; +} +.mt8 { + margin-top: 8px; +} +.mt14 { + margin-top: 14px; +} + +@media (max-width: 860px) { + .split-list { + width: 280px; + } + .grid-2 { + grid-template-columns: 1fr; + } +} diff --git a/frontend/app/icon.svg b/frontend/app/icon.svg new file mode 100644 index 0000000..d300948 --- /dev/null +++ b/frontend/app/icon.svg @@ -0,0 +1,10 @@ + diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..8fc1d73 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Handler · Claude Activity", + description: "Monitor and manage Claude Code agents across projects.", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + +
{children} + + ); +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..5aaf390 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,49 @@ +/* Root page: token gate → dashboard. Client-only; the exported HTML is a shell and every + * byte of data is fetched by the browser from the authed API after the token is supplied. */ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { DashboardProvider } from "@/components/store"; +import { Dashboard } from "@/components/Dashboard"; +import { TokenGate } from "@/components/TokenGate"; + +const TOKEN_KEY = "handler_token"; + +export default function Home() { + const [token, setToken] = useState+ {s.cmd.text} +
+ )} + {s.lastError && ( ++ {s.lastError} +
+ )} + + {s.section === "runs" ? ( +| When | +Type | +Repository | +Agent | +Status | +Result / Error | +
|---|---|---|---|---|---|
| {fmtFull(c.created_at)} | +{c.type} | +{c.project_id || "—"} | +{c.agent_name || "—"} | +
+ |
+ + {c.error || (c.result ? JSON.stringify(c.result) : "—")} + | +
| Name | +Role | +Status | +Working dir | +Created | ++ |
|---|---|---|---|---|---|
| {a.name} | +{a.role ? |
+
+ |
+ {a.working_dir} | +{fmtFull(a.created_at)} | +
+
+
+
+
+
+ |
+
| When | +Branch | +Verdict | +By | +SHA | +Note | +
|---|---|---|---|---|---|
| {fmtFull(ap.created_at)} | +{ap.branch} | +
+ |
+ {ap.approved_by_agent_id ? `agent ${ap.approved_by_agent_id}` : ap.actor || "—"} | +{shortSha(ap.approved_sha)} | +{ap.note || "—"} | +
+ {CRED_HELP} +
+| When | +Status | +Summary | +Q / A | +Push | +CI | +
|---|---|---|---|---|---|
| {fmtFull(e.created_at)} | +
+ |
+ {e.summary || "—"} | +
+ {e.question && (
+
+ Q: {e.question}
+
+ )}
+ {e.answer && (
+
+ A: {e.answer}
+
+ )}
+ {!e.question && !e.answer && "—"}
+ |
+ {shortSha(e.push_sha)} | +
+ |
+
| When | +Agent | +Status | +Summary | +CI | +
|---|---|---|---|---|
| {fmtFull(e.created_at)} | +{e.agent_id} | +
+ |
+ {e.summary || "—"} | +
+ |
+
+ Requires the shared-context write token (or admin/global if unset). +
+| Key | +Value | +Updated | +
|---|---|---|
| {c.key} | +{c.value} | +{fmtFull(c.updated_at)} | +
+ {lines.map((line, i) => {
+ const cls = line.startsWith("+")
+ ? "diff-add"
+ : line.startsWith("-")
+ ? "diff-del"
+ : undefined;
+ return (
+
+ {line}
+ {i < lines.length - 1 ? "\n" : ""}
+
+ );
+ })}
+
+ e||125 Select a project. No agents in this project. No checkpoint recorded yet. No log entries. Select a project. No approvals recorded. credential_ref is a pointer, never the token. Maps a git host to the token env var to inject at spawn (and the credential-helper scope).
- Holds no secrets — only the env-var name. No hosts registered (built-in host map still applies). No commands yet. No global log entries. Requires the shared-context write token (or admin/global if unset). No shared context keys.Agents
- Spawn an agent
-
-
-
-
-
-
-
-
-
-
-
-
- Checkmark
-
-
- Answer this question
-
-
- Log (newest first)
-
-
-
-
-
-
-
- When Status Summary Q / A Vis Push CI
-
-
-
-
-
-
-
-
-
-
-
- Approvals
- Record a verdict
-
-
-
-
-
- When Branch Verdict By SHA Note
-
-
-
-
-
-
-
-
-
- Projects
- cmd: is CLI-only.
-
-
-
-
- ID root_dir git_remote credential_ref
-
-
-
-
-
-
-
-
-
-
-
- Forge hosts
-
-
-
-
-
- Hostname Type Token env var Base URL
-
-
-
-
-
-
-
-
-
-
-
- Activity · control commands
-
-
-
-
-
-
-
-
- When Type Project Agent Status Result / Error
-
-
-
-
-
-
-
-
-
- Global feed
-
-
-
-
-
- When Agent Status Summary CI
-
-
-
-
-
-
-
-
- Shared context
- Set a key
-
-
-
-
-
- Key Value Updated
-
-
-
-
-
-
-