feat(dashboard): unified workspace hub — cookie auth, 9-state projects, planning chat

Merges workspace.html + ralph.html into a single unified project hub with:
- Cookie-based auth (DASHBOARD_TOKEN, HttpOnly, SameSite=Strict)
- 9-state project badge system (running-ralph/manual, planning, approved,
  pending, blocked, failed, complete, idle) with BUTTONS_FOR_STATE matrix
- SSE realtime + polling fallback, version-based optimistic concurrency (If-Match)
- Planning chat modal (phase stepper, markdown bubbles, 50s+ wait state, auto-resume)
- Propose modal (Variant B: inline Plan-with-Echo checkbox)
- 5-type toast taxonomy (success/info/warning/busy/error, 3px colored left-bar)
- Inter font self-hosted + shared tokens.css design system + DESIGN.md
- src/jsonlock.py (flock helper, sidecar .lock for stable inode)
- src/approved_tasks_cli.py (shell-safe wrapper for cron/ralph.sh)
- 55 new tests (T#1–T#30) + real jsonlock bug fix caught by T#16/T#28
- No emoji anywhere (enforced by test_dashboard_no_emoji.py)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 07:26:19 +00:00
parent e771479d67
commit 5e930ade02
26 changed files with 5700 additions and 1569 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

159
dashboard/static/tokens.css Normal file
View File

@@ -0,0 +1,159 @@
/*
* Echo Dashboard — Design Tokens
* Single source of truth for all CSS variables, fonts, and shared
* mobile-modal behavior. Loaded via /echo/static/tokens.css on every
* dashboard page (in addition to common.css for now).
*
* Token coverage:
* - Colors (dark default + light theme override)
* - Status palette (running, blocked, failed, complete, idle,
* planning, pending, approved)
* - Typography (Inter sans + JetBrains Mono mono, size scale)
* - Spacing (8px grid)
* - Radius scale
* - Shadows / transitions
*/
/* ==========================================================
@font-face — Inter (self-hosted, woff2 only)
========================================================== */
@font-face {
font-family: 'Inter';
src: url('/echo/static/fonts/inter-400.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/echo/static/fonts/inter-500.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/echo/static/fonts/inter-600.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/echo/static/fonts/inter-700.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* ==========================================================
Tokens — dark theme (default)
========================================================== */
:root {
/* Colors — dark surface */
--bg-base: #13131a;
--bg-surface: rgba(255, 255, 255, 0.12);
--bg-surface-hover: rgba(255, 255, 255, 0.16);
--bg-surface-active: rgba(255, 255, 255, 0.20);
--bg-elevated: rgba(255, 255, 255, 0.14);
--text-primary: #ffffff;
--text-secondary: #f5f5f5;
--text-muted: #e5e5e5;
--accent: #3b82f6;
--accent-hover: #2563eb;
--accent-subtle: rgba(59, 130, 246, 0.2);
--border: rgba(255, 255, 255, 0.3);
--border-focus: rgba(59, 130, 246, 0.7);
--header-bg: rgba(19, 19, 26, 0.95);
--success: #22c55e;
--warning: #eab308;
--error: #ef4444;
/* Status palette — used by .status-pill[data-status] */
--status-running: rgb(34, 197, 94); /* green — running-ralph / running-manual */
--status-blocked: rgb(245, 158, 11); /* amber */
--status-failed: rgb(239, 68, 68); /* red */
--status-complete: rgb(156, 163, 175); /* slate (done = neutral) */
--status-idle: var(--text-muted);
/* Status palette — extended (workflow states) */
--status-planning: rgb(167, 139, 250); /* violet — Echo is planning */
--status-pending: rgb(96, 165, 250); /* sky — awaiting approval */
--status-approved: rgb(234, 179, 8); /* gold — approved tonight */
/* Spacing — 8px grid */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
/* Typography */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
/* Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
/* Motion */
--transition-fast: 0.15s ease;
--transition-base: 0.2s ease;
}
/* ==========================================================
Light theme override
========================================================== */
[data-theme="light"] {
--bg-base: #f8f9fa;
--bg-surface: rgba(0, 0, 0, 0.04);
--bg-surface-hover: rgba(0, 0, 0, 0.08);
--bg-surface-active: rgba(0, 0, 0, 0.12);
--bg-elevated: rgba(0, 0, 0, 0.06);
--text-primary: #1a1a1a;
--text-secondary: #444444;
--text-muted: #666666;
--border: rgba(0, 0, 0, 0.12);
--border-focus: rgba(59, 130, 246, 0.5);
--accent-subtle: rgba(59, 130, 246, 0.12);
--header-bg: rgba(255, 255, 255, 0.95);
}
/* ==========================================================
Mobile modal — shared across all pages with .modal-overlay
========================================================== */
@media (max-width: 640px) {
.modal-overlay { padding: 0; align-items: stretch; }
.modal { max-width: 100vw !important; max-height: 100vh !important; border-radius: 0; height: 100vh; }
.modal-header { position: sticky; top: 0; background: var(--bg-base); }
.modal-footer { position: sticky; bottom: 0; padding-bottom: max(var(--space-4), env(safe-area-inset-bottom)); }
.phase-stepper .phase-step:not(.active) span:not(.step-num) { display: none; }
}