diff --git a/CLAUDE.md b/CLAUDE.md index 81c877f9..1003bdc0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -184,6 +184,7 @@ open test-artifacts/unified/report.html npm run dashboard # serve at http://localhost:3199 (read-only, localhost) npm run dashboard:open # …and open the browser make dashboard +bash scripts/install-dashboard-service.sh # ALWAYS-on: systemd --user service (auto-restart + boot) ``` Leave it running while you work. It watches every `graphdone.unified-report/1` run in BOTH repos (Core `test-artifacts/unified*` + Cloud `live-full-report/`), diff --git a/scripts/dashboard-loop.sh b/scripts/dashboard-loop.sh index 783966f2..d79093fa 100755 --- a/scripts/dashboard-loop.sh +++ b/scripts/dashboard-loop.sh @@ -1,12 +1,15 @@ #!/usr/bin/env bash # Autonomous continuous-improvement loop for GraphDone, driven by cron so it # survives session limits AND reboots. Each invocation (cron passes "once"): -# 1. ensures the live test dashboard server is up (the guiding light) +# 1. ensures the live test dashboard server is up (prefers the systemd --user +# service graphdone-dashboard if installed, else nohup) + restarts on stale code # 2. self-checks the dashboard (its own unit tests) +# 2.5. regenerates the narrated report when a run is newer than the narration # 3. runs ONE bounded headless `claude -p` improvement iteration — the agent # decides whether to run a real test suite (which is what feeds the # dashboard fresh run data); the driver never fabricates synthetic runs. # Single-instance via flock. Set NO_AGENT=1 to skip step 3 (mechanical only). +# For an ALWAYS-up server prefer the systemd service: bash scripts/install-dashboard-service.sh # # bash scripts/dashboard-loop.sh once set -uo pipefail @@ -34,8 +37,11 @@ log "── iteration start (pid $$) ──" health() { curl -s -o /dev/null -w '%{http_code}' "http://localhost:$PORT/api/state" 2>/dev/null; } +have_service() { systemctl --user cat graphdone-dashboard.service >/dev/null 2>&1; } + start_dashboard() { - nohup npm run dashboard >/tmp/gd-dash.log 2>&1 & disown + if have_service; then systemctl --user start graphdone-dashboard 2>/dev/null + else nohup npm run dashboard >/tmp/gd-dash.log 2>&1 & disown; fi for _ in 1 2 3 4 5 6; do sleep 1; [ "$(health)" = "200" ] && break; done } @@ -54,9 +60,29 @@ restart_if_stale() { [ -z "$newest" ] && return 0 if [ "$newest" -gt "$pstart" ]; then log "dashboard code newer than running server (pid $pid) → restarting" - kill "$pid" 2>/dev/null - sleep 1 - start_dashboard + if have_service; then + systemctl --user restart graphdone-dashboard 2>/dev/null + for _ in 1 2 3 4 5 6; do sleep 1; [ "$(health)" = "200" ] && break; done + else + kill "$pid" 2>/dev/null; sleep 1; start_dashboard + fi + fi +} + +# Keep the narrated report current: regenerate only when a run report.json is +# newer than the existing narration (and a piper voice is present). The server +# serves narration.json fresh per request, so no restart is needed. +refresh_narration_if_stale() { + [ -d "$STATE/voices" ] && ls "$STATE/voices"/*.onnx >/dev/null 2>&1 || { log "narration: no voice model, skipping"; return 0; } + local narr newest_report narr_mtime + narr="$STATE/narration/narration.json" + newest_report=$(find "$CORE/test-artifacts/unified" "$CORE"/test-artifacts/unified-* "$CORE/../GraphDone-Cloud/live-full-report" -name report.json -printf '%T@\n' 2>/dev/null | sort -n | tail -1 | cut -d. -f1) + narr_mtime=$( [ -f "$narr" ] && stat -c %Y "$narr" 2>/dev/null || echo 0 ) + if [ -n "$newest_report" ] && [ "$newest_report" -gt "${narr_mtime:-0}" ]; then + log "narration stale → regenerating" + if node scripts/narrate-report.mjs >>"$LOG" 2>&1; then log "narration regenerated"; else log "narration regen failed (rc=$?)"; fi + else + log "narration up to date" fi } @@ -72,6 +98,9 @@ log "dashboard health=$(health)" # 2) self-check the dashboard tool (cheap; does NOT fabricate runs) if node --test tests/lib/dashboard/ >>"$LOG" 2>&1; then log "dashboard lib tests ok"; else log "dashboard lib tests FAILED"; fi +# 2.5) keep the narrated report current with the latest runs +refresh_narration_if_stale + # 3) one bounded headless agent iteration (skippable) if [ "${NO_AGENT:-0}" = "1" ]; then log "NO_AGENT=1 → skipping agent iteration" diff --git a/scripts/install-dashboard-service.sh b/scripts/install-dashboard-service.sh new file mode 100755 index 00000000..2c6d3a3e --- /dev/null +++ b/scripts/install-dashboard-service.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Install the live test dashboard as a systemd --user service so it is ALWAYS up: +# auto-restarts on crash, starts on boot, and (with linger) runs even when logged +# out. Serves the latest unified report — videos, graphs, images, narration — at +# http://localhost:3199. Idempotent; re-run to update. +# +# bash scripts/install-dashboard-service.sh # install + start + enable +# systemctl --user status graphdone-dashboard # check +# systemctl --user restart graphdone-dashboard # restart +# systemctl --user disable --now graphdone-dashboard # stop + remove from boot +set -euo pipefail + +CORE="/home/scubasonar/Code/graphdone-repos/GraphDone-Core" +PORT="${DASHBOARD_PORT:-3199}" +NODE="$(command -v node || echo /home/scubasonar/.nvm/versions/node/v20.20.2/bin/node)" +UNIT_DIR="$HOME/.config/systemd/user" +UNIT="$UNIT_DIR/graphdone-dashboard.service" + +if ! command -v systemctl >/dev/null 2>&1; then + echo "❌ systemctl not available — fall back to the cron loop (scripts/dashboard-loop.sh) which keeps it alive every ~30 min + @reboot." + exit 1 +fi + +mkdir -p "$UNIT_DIR" + +# free the port from any stray non-systemd instance so ExecStart can bind +pkill -f 'dashboard/server.mjs' 2>/dev/null || true +sleep 1 + +cat > "$UNIT" </dev/null && echo "✅ linger enabled (survives logout)" || echo "ℹ️ linger not enabled (service still runs while logged in; @reboot cron covers boot)" + +sleep 2 +echo +systemctl --user --no-pager status graphdone-dashboard | head -6 || true +echo +code="$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$PORT/api/state" 2>/dev/null || echo 000)" +echo "🌐 dashboard http://localhost:$PORT → HTTP $code" +echo " (Restart=always · enabled on boot · auto-restarts on crash)" diff --git a/tests/lib/dashboard/README.md b/tests/lib/dashboard/README.md index 93718e06..6ca556b5 100644 --- a/tests/lib/dashboard/README.md +++ b/tests/lib/dashboard/README.md @@ -11,6 +11,19 @@ npm run dashboard:open # …and open the browser make dashboard # same, via make ``` +**Always-on (recommended):** install it as a systemd `--user` service so it is +*constantly* available — auto-restarts on crash, starts on boot, runs even when +logged out: + +```bash +bash scripts/install-dashboard-service.sh # install + start + enable + linger +systemctl --user status graphdone-dashboard # check +systemctl --user disable --now graphdone-dashboard # uninstall +``` + +The cron loop (`scripts/dashboard-loop.sh`) also keeps it alive (every ~30 min + +`@reboot`) and prefers the service when installed, so the two don't conflict. + Then, in another terminal, run tests as usual — each completed run appears live: ```bash