Skip to content

fix(deploy): serve fresh dashboard on hosted deploys + staleness guardrail - #227

Merged
aterrylu merged 1 commit into
mainfrom
terry/hosted-deploy-fresh-dashboard
Jun 20, 2026
Merged

fix(deploy): serve fresh dashboard on hosted deploys + staleness guardrail#227
aterrylu merged 1 commit into
mainfrom
terry/hosted-deploy-fresh-dashboard

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

Problem

Hosted (make deploy) deployments were silently serving a stale dashboard. The server prefers packages/server/src/_embedded_dashboard/ over packages/dashboard/dist/ (run.ts resolution order), but _embedded_dashboard is a binary-build artifact — and the deploy pipeline mishandled it two ways:

  • make deploy's rsync shipped a stale local _embedded_dashboard to the remote (it lives under src/, not excluded alongside dist/node_modules).
  • make prod rebuilt dashboard/dist but never refreshed the embedded copy.

Net: the freshly-built dist sat unused while the server served a months-old embedded bundle — it caused an actual regression on forge and needed manual patching multiple times.

flowchart LR
    subgraph before["Before"]
      L["local stale<br/>_embedded_dashboard"] -->|rsync ships it| R1["remote _embedded_dashboard (stale)"]
      MP1["make prod → dist (fresh)"] --> D1["dist (fresh, unused)"]
      R1 -->|server prefers #1| S1["serves STALE ❌"]
    end
    subgraph after["After"]
      MP2["make prod: rm -rf _embedded_dashboard<br/>then build dist"] --> D2["dist (fresh)"]
      D2 -->|"no embedded → fallback #2"| S2["serves FRESH ✅"]
    end
    style S1 fill:#fdd
    style S2 fill:#dfd
Loading

Fix — keep embedded a binary-only concern; hosted serves dist

  • make prod: rm -rf packages/server/src/_embedded_dashboard before the vite build → the hosted/tsx server falls through to the fresh dist. (Runs on the remote during make deploy, so it also self-heals any pre-existing stale copy there.)
  • make deploy: --exclude _embedded_dashboard from the rsync → the local binary artifact is never shipped.

Guardrail — make staleness observable, not silent

The recurring pain was diagnosing stale serves. Now:

  • Server (dashboardBuild.ts + run.ts): reads the served bundle id (hashed index-*.js) + index.html mtime; logs them at startup and exposes them on /api/host as dashboard: { build, builtAt }. The best-effort reader now logs on failure (it's only called after existsSync confirmed index.html, so a throw is a real read/permission problem — not a benign null).
  • Dashboard (dashboardFreshness.ts + StatusBar.tsx): reads its own loaded bundle id and console.warns when the server is serving a newer one (deploy-while-open, or a stale serve). Unknown ids (dev server / older server) never warn.

Testing

  • make check green: biome, tsc, server tests (incl. extractDashboardBuildId + the readDashboardBuild failure-logs-and-doesn't-throw case), 246 dashboard tests (incl. isDashboardStale truth table + getLoadedDashboardBuildId DOM reads).
  • /polish (code-reviewer + simplifier + silent-failure-hunter): reviewer ship-ready (verified Makefile flow, non-breaking /api/host, no new leak on the unauthenticated endpoint); simplifier no-change; silent-failure-hunter's one MEDIUM (bare catch swallowing the reason) applied.
  • Will verify on forge: no _embedded_dashboard present, startup log shows today's build mtime, /api/host carries dashboard.

Operational note

forge's duplicate install was cleaned up out-of-band as part of this work: a crash-looping systemd-user autonomos.service (running a stale ~/.local/share/autonomos CLI install) was fighting pm2 for :3100; it's been stopped/disabled/removed so a single mechanism (pm2) owns the port. ~/.autonomos config/data preserved.

ADR

This encodes a deploy-contract decision (the embedded dashboard belongs to the binary distribution; the hosted server serves dist). An ADR draft is being surfaced to TeamLead.

🤖 Generated with Claude Code

@aterrylu
aterrylu enabled auto-merge (squash) June 20, 2026 07:49
…drail

The hosted server prefers packages/server/src/_embedded_dashboard over
packages/dashboard/dist (run.ts), but that embedded copy is a *binary-build*
artifact — and `make deploy` was rsyncing a stale local copy of it to the
remote while `make prod` only rebuilt dist. Result: the hosted (tsx) server
served a stale dashboard that shadowed the freshly-built dist, silently. It
caused an actual regression (forge served a months-old UI) and needed manual
patching multiple times.

Fix (keep embedded a binary-only concern; hosted serves dist):
- make prod: `rm -rf packages/server/src/_embedded_dashboard` before the vite
  build, so the hosted/tsx server falls through to the fresh dist.
- make deploy: exclude `_embedded_dashboard` from the rsync, so the local
  binary artifact is never shipped to the remote.

Guardrail (make staleness observable, not silent):
- Server reads the served dashboard's bundle id (hashed index-*.js filename)
  and index.html mtime, logs them at startup, and exposes them on /api/host
  as `dashboard: { build, builtAt }`.
- Dashboard reads its own loaded bundle id and console.warns when the server
  is serving a newer one (deploy-while-open, or a stale serve).

Tests: extractDashboardBuildId (server), getLoadedDashboardBuildId +
isDashboardStale (dashboard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwFzy8JiM6H2NENce8Hmpx
@aterrylu
aterrylu force-pushed the terry/hosted-deploy-fresh-dashboard branch from 05adf3c to 019d215 Compare June 20, 2026 07:49

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — fix correctly attacks both legs of the stale-serve bug (rsync exclude on the source side, rm -rf on the build side) and the staleness guardrail is well-bounded: isDashboardStale only fires when both ids are known, the /api/host payload addition is non-breaking for existing consumers (CLI status, ConnectionStatusBarItem), and the unauthenticated endpoint only leaks a bundle hash + build mtime. Tests cover the truth table and the failure-logs-not-throws case.

A couple of follow-up observations (non-blocking):

  • The staleness check in StatusBar runs once on mount; the PR description says it catches "deploy-while-open" but in practice it only catches "reload-after-stale-deploy". Worth either qualifying the claim or piggy-backing on ConnectionStatusBarItem's existing /api/host poll if you want true mid-session detection.
  • extractDashboardBuildId's regex relies on the entry script being the first /assets/index-*.js reference in the HTML, which holds for current Vite output but would silently drift if Vite ever reorders modulepreload links before the entry. The DOM-side selector is already more precise (script[type="module"]); tightening the server regex similarly (e.g. requiring <script type="module"[^>]*src="...") would make the two readers agree by construction. Not urgent.

@aterrylu
aterrylu merged commit e4f033f into main Jun 20, 2026
10 checks passed
@aterrylu
aterrylu deleted the terry/hosted-deploy-fresh-dashboard branch June 20, 2026 07:51
aterrylu added a commit that referenced this pull request Jun 20, 2026
Captures the decision behind PR #227's deploy fix: _embedded_dashboard is a
binary-distribution artifact; the hosted server serves dashboard/dist. Enforced
via rm in make prod + rsync --exclude in make deploy, with observability
guardrails (served bundle id + mtime in /api/host, dashboard console.warn on
mismatch). Records the load-bearing role of the rm (self-heals stale remotes),
the long-term direction toward install.sh-built-bundle convergence, and the
two operational issues surfaced during forge deployment.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants