Skip to content

fix(dashboard): first paint from local data, never block on GitHub (#65) - #70

Merged
CameronCrow merged 1 commit into
mainfrom
fix/issue-65-dashboard-first-paint-local
Jul 20, 2026
Merged

CameronCrow merged 1 commit into
mainfrom
fix/issue-65-dashboard-first-paint-local

Conversation

@CameronCrow

Copy link
Copy Markdown
Owner

Closes #65

Problem

The team dashboard's first paint blocked on GitHub-backed PR queries. buildRoster awaited readLatestSessionActivity + readLatestSessionStats + fetchGitHubPRStatus together per workspace, so the whole roster (including live status dots) waited on the 10-35s gh/git spawns that dominate on slow-DNS networks. The local half (session activity/stats from JSONL on disk) is fast and shouldn't wait.

Change

Split the roster into a fast local half and a slow GitHub overlay.

server-core (packages/server-core/src/team-dashboard.ts)

  • buildRoster is now local only — activity + stats. pr is always null and status comes from activity alone (deriveRosterStatus(status, null)), so an entry is never "blocked" before the overlay arrives.
  • New buildRosterGitHub(workspaces): RosterPROverlay[] — the slow half, one PR overlay per workspace from fetchGitHubPRStatus. Never throws; a per-workspace failure degrades that entry to pr: null.
  • New pure applyRosterOverlay(roster, overlay) — merges the overlay onto the local roster, reusing deriveRosterStatus so a failing PR surfaces as blocked only once the overlay is present; a missing overlay row leaves the entry exactly as built. This is the canonical, unit-tested precedence seam the client mirrors.
  • deriveRosterStatus precedence unchanged (its first param was widened to the roster-status union so the overlay merge can reuse it — "blocked" still only comes from checksStatus === "failure").
  • Acceptance hook: rosterGitHubDelayMs() reads PAPYRUS_DASHBOARD_GH_DELAY_MS; when set, buildRosterGitHub sleeps that long before its gh calls. This simulates a 30s-slow GitHub without touching the shared github-team.ts module.

routers — mirrored a rosterGitHub procedure in both, matching the existing roster shape:

  • apps/desktop/src/lib/trpc/routers/team-dashboard.ts (publicProcedure)
  • apps/server/src/routers/team-dashboard.ts (authedProcedure)

client (.../team-dashboard/)

  • useTeamDashboard fires roster (5s poll) and rosterGitHub (30s poll) independently. RosterHero renders from the local query immediately; the PR overlay is merged in when it resolves. Merge order: GitHub overlay first (a failing PR → blocked), then the live pane-status overlay — server/GitHub blocked still always wins.
  • AgentCard reserves the PR slot with a placeholder while the overlay is loading (prPending), so hydration causes no layout shift.
  • ActivityRail/WorkBoard already render per-panel placeholders and never gate TeamDashboard mount — verified, no change needed.

Verification

  • bun test in packages/server-core on team-dashboard.test.ts: 10/10 pass. Extended with applyRosterOverlay precedence cases (missing overlay never blocks, failing overlay blocks, null-PR overlay, workspaceId matching) and rosterGitHubDelayMs env parsing.
  • tsc --noEmit in apps/server and apps/desktop: both clean. Confirmed the desktop @papyrus/server-core symlink resolves to this worktree's server-core, so the router typecheck validated against the new buildRosterGitHub export (not a stale copy).
  • Full packages/server-core suite has 11 pre-existing failures in unrelated suites (agent-mail, agent-scaffold, agent-repo, changes, github-team snapshot, terminal-host) — all spawn external binaries (claude/gh/git/node-pty) and time out at ~5s in this sandbox (native node-pty bindings aren't built here). None touch team-dashboard; all were untouched by this PR.

Delayed-GitHub acceptance check

The env-gated delay is verified at the server-core boundary by rosterGitHubDelayMs unit tests. I could not drive the full Electron app in this environment, so the end-to-end visual check is documented here for a manual run:

  1. Launch the desktop app with PAPYRUS_DASHBOARD_GH_DELAY_MS=30000.
  2. Open a project's team dashboard.
  3. Expected: agent cards appear with live status dots in <1s (the local roster query), each showing a reserved/placeholder PR slot; ~30s later the PR badges hydrate in with no layout shift. No agent shows "blocked" until its PR overlay arrives.

🤖 Generated with Claude Code

The team dashboard's roster blocked first paint on GitHub-backed PR
queries whose gh/git spawns cost 10-35s on slow-DNS networks. Split the
roster so the local half (session activity/stats from JSONL on disk)
paints immediately and the PR column hydrates when the slow half arrives.

server-core (team-dashboard.ts):
- buildRoster is now LOCAL ONLY (activity + stats); pr is null and status
  comes from activity alone, so an entry is never "blocked" pre-overlay.
- New buildRosterGitHub returns a per-workspace RosterPROverlay (PR +
  checks), gated by an env delay hook (PAPYRUS_DASHBOARD_GH_DELAY_MS) so
  the acceptance test can simulate a 30s-slow GitHub without touching the
  shared github-team module.
- New pure applyRosterOverlay merges the overlay onto the local roster;
  deriveRosterStatus precedence unchanged (failing PR => blocked only once
  the overlay is present; missing overlay => activity status).

routers: mirror a rosterGitHub procedure in both the desktop
(publicProcedure) and server (authedProcedure) teamDashboard routers.

client: useTeamDashboard fires roster (5s) + rosterGitHub (30s)
independently, renders RosterHero from the local query, then merges the
PR overlay (blocked wins over the live pane overlay). AgentCard reserves
the PR slot while the overlay loads so hydration causes no layout shift.
ActivityRail/WorkBoard already render per-panel placeholders and never
gate mount (verified — no change needed).

Tests: extend team-dashboard.test.ts for applyRosterOverlay precedence
and the delay-hook env parsing (10/10 pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@CameronCrow
CameronCrow merged commit a3a6309 into main Jul 20, 2026
2 checks passed
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.

dashboard: first paint from local data — never block on GitHub

1 participant