From 4e846044425325f2b1afbeea82afc8f429b51f69 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Fri, 19 Jun 2026 20:32:46 -0700 Subject: [PATCH] chore(dashboard): always-on systemd --user service + keep narration fresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So the unified report (videos, graphs, images, narrated) is CONSTANTLY available: - scripts/install-dashboard-service.sh: installs the dashboard as a systemd --user service (Restart=always, enabled on boot, linger) β†’ auto-restarts on crash, comes back on reboot, runs while logged out. Verified: kill -9 the process β†’ systemd brings it back in ~5s with all endpoints 200. - scripts/dashboard-loop.sh: now systemd-aware (starts/restarts via the service when installed, else nohup β€” no double-start), and regenerates the narrated report whenever a run report.json is newer than the current narration (voice permitting), so the 🎧 Narrated tab tracks the latest runs. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 1 + scripts/dashboard-loop.sh | 39 +++++++++++++++--- scripts/install-dashboard-service.sh | 60 ++++++++++++++++++++++++++++ tests/lib/dashboard/README.md | 13 ++++++ 4 files changed, 108 insertions(+), 5 deletions(-) create mode 100755 scripts/install-dashboard-service.sh 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