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:
@@ -37,7 +37,6 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
@@ -52,6 +51,7 @@ from src.claude_session import (
|
||||
_run_claude,
|
||||
_safe_env,
|
||||
)
|
||||
from src.jsonlock import read_locked, write_locked
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_invoke_log = logging.getLogger("echo-core.invoke")
|
||||
@@ -106,33 +106,17 @@ def _channel_key(adapter: str, channel_id: str) -> str:
|
||||
|
||||
|
||||
def _load_planning_state() -> dict:
|
||||
"""Load planning sessions from disk. Returns {} if missing or empty."""
|
||||
"""Load planning sessions from disk under a shared flock. Returns {} if missing."""
|
||||
try:
|
||||
text = PLANNING_STATE_FILE.read_text(encoding="utf-8")
|
||||
if not text.strip():
|
||||
return {}
|
||||
return json.loads(text)
|
||||
return read_locked(str(PLANNING_STATE_FILE))
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def _save_planning_state(data: dict) -> None:
|
||||
"""Atomically write planning sessions via tempfile + os.replace."""
|
||||
"""Persist planning sessions under an exclusive flock + atomic replace."""
|
||||
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp_path = tempfile.mkstemp(
|
||||
dir=SESSIONS_DIR, prefix=".planning_", suffix=".json"
|
||||
)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
os.replace(tmp_path, PLANNING_STATE_FILE)
|
||||
except BaseException:
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
write_locked(str(PLANNING_STATE_FILE), lambda _existing: data)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user