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

View File

@@ -18,6 +18,7 @@ from src.claude_session import (
set_session_model,
VALID_MODELS,
)
from src.jsonlock import read_locked, write_locked
from src.planning_orchestrator import PlanningOrchestrator
from src.planning_session import (
clear_planning_state,
@@ -210,15 +211,20 @@ def _model_command(channel_id: str, text: str) -> str:
def _load_approved_tasks() -> dict:
"""Load approved-tasks.json, return empty structure if missing."""
if APPROVED_TASKS_FILE.exists():
return json.loads(APPROVED_TASKS_FILE.read_text())
return {"projects": [], "last_updated": None}
"""Load approved-tasks.json under a shared flock; empty structure if missing."""
try:
data = read_locked(str(APPROVED_TASKS_FILE))
except FileNotFoundError:
return {"projects": [], "last_updated": None}
if not data:
return {"projects": [], "last_updated": None}
return data
def _save_approved_tasks(data: dict) -> None:
"""Persist approved-tasks.json under an exclusive flock + atomic replace."""
data["last_updated"] = datetime.now(timezone.utc).isoformat()
APPROVED_TASKS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
write_locked(str(APPROVED_TASKS_FILE), lambda _existing: data)
RALPH_CMDS = {