refactor(admin): split the 1280-line dashboard into typed modules - #10
Open
gtko wants to merge 12 commits into
Open
refactor(admin): split the 1280-line dashboard into typed modules#10gtko wants to merge 12 commits into
gtko wants to merge 12 commits into
Conversation
Bootstraps the infrastructure required to refactor app/admin without drifting examples/greenfield away from packages/templates/files. - bin/sync-from-source.mjs: SHA-256 walk of files/, copies divergent entries to examples/greenfield. Supports --check for CI. - workflows/templates-sync.yml: PR guard that fails on any divergence touching packages/templates/files/**, examples/greenfield/** or the sync script itself. - root scripts: pnpm sync:templates[:check]. - packages/templates: vitest 2 + tsconfig (extends base, JSX preserve, @/* alias to files/*), passWithNoTests so pnpm test stays green until the first util test lands. - manifest.json: add the three admin routes that were used by the dashboard but never shipped to consumers (bucket-info, shadow-history, rollback-shadow). Without this fix npx copy . produces a broken UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dashboard-client.tsx had 9 inline types and 9 inline utilities mixed
with the orchestrator logic. Pulled them into per-concern modules so
the orchestrator stops being the only place that defines shared shapes.
types/dashboard.ts - Status, BucketInfo(Map), ShadowHistoryEntry,
Segment, SloCheck, DashboardProps
types/modal.ts - ModalState (kept null arm for now; replaced in
step 9 with a {kind:'closed'} discriminant)
constants/config.ts - REFRESH_INTERVAL_MS, TICK_MS, STEP_*,
SLO_CHECK_PERIOD_MS, SHADOW_HISTORY_CAP,
SLO_CHECKS_CAP (replaces 10_000, 1000, '4',
15 * 60_000, "20", "10" magic literals)
constants/labels.ts - STATUS_LABEL + phase labels (FR strings out of
the JSX)
utils/status.ts - deriveStatus, statusToPhase, new isCanaryLive
utils/format.ts - prettyTimeAgo, formatDuration, shortHost +
new firstLine (fixes split('\n')[0] under
noUncheckedIndexedAccess)
utils/time.ts - nextCronFireMs, parisHour, phaseLabel
utils/step.ts - stepSize (uses STEP_MIN/STEP_MAX)
utils/traffic.ts - computeTrafficShares replaces 5 inline derived
consts
utils/timing.ts - computeTiming replaces the SLO-anchored
expectedNextTs / msToNext / overdue block
Tests cover every util branch (40 cases). Greenfield mirror synced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dashboard-client.tsx had 4 raw GETs + 9 inline POST call sites with
unsafe casts (as BucketInfoMap, as {entries: ...}) and silent error
swallowing (.catch(() => ({}))). Move every endpoint behind a typed
client so the casts disappear and route mistakes surface at compile
time.
- api/admin-client.ts: 13 typed endpoints (fetchState, fetchDeployments,
fetchBucketInfo, fetchShadowHistory + pause, resume, cancel, promote,
stepBack, stepForward, setShadowPercent, rollback, rollbackShadow).
getJson<T>/postJson wrappers default to cache: 'no-store' and parse
the response into the declared shape.
- api/errors.ts: AdminApiError(status) + parseJsonError(res) replace
the `{ error: ... } || 'HTTP <status>'` pattern.
- dashboard-client.tsx:
* refresh() now Promise.allSettled — a single failing endpoint keeps
partial data instead of nuking the whole snapshot.
* run(id, fn) takes a thunk instead of (path, body), so the call
sites just pass `adminApi.pause` / `() => adminApi.stepForward(n)`.
* zero `as` casts, zero swallowed JSON errors.
13 new tests for the client (fetch mocked) + error parser.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulled the orchestrator state out of AdminDashboard into 4 dedicated
hooks. dashboard-client.tsx had 9 useStates and three useEffects mixing
data fetching, wall-clock ticking and per-action progress tracking —
they now live in modules with explicit signatures and pure reducers.
- hooks/use-dashboard-state.ts: useReducer over
{ config, deployments, bucketInfo, shadowHistory, error, refreshing }
with refresh/start + refresh/end actions. refresh() is exposed
alongside state; the reducer is exported separately for unit tests.
- hooks/use-action-runner.ts: useReducer over { pendingAction,
actionError } with action/start | success | error actions. run(id,
fn) bails out if another action is already pending. onSuccess is
captured by ref so a non-memoized caller does not rebuild run.
- hooks/use-wall-clock.ts: SSR-safe wall-clock tick. Returns null
during the first render to preserve the React #418 invariant.
- hooks/use-poll-refresh.ts: setInterval wrapper for the data refresh.
Net effect on the orchestrator: 9 useStates -> 2 (modal, stepInput),
3 useEffects -> 0. Both reducers come with tests covering every action
arm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the small JSX building blocks that AdminDashboard inlined into their own files. No behavior change — every consumer site keeps the same DOM and className soup. - components/state-banners.tsx: error + actionError banners (replaces ~14 lines of inline JSX in the orchestrator) - components/status-line.tsx: status dot + label + pct - components/timing-line.tsx: phase / elapsed / next-check copy - components/traffic-bar.tsx: bar + legend (Segment type now imported from types/dashboard.ts) - components/action-button.tsx: ex-ActionBtn, renamed to make the abbreviation drop. Same className composition, same pending check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both cards were sitting inline at ~130 and ~70 lines respectively in the orchestrator. They each have their own little state (expand set for SLO, draft input for shadow percent) so they read more naturally as standalone components. - components/slo-log-card.tsx (was SloLog): also splits the per-row rendering into a SloRow sub-component to keep the parent under 100 lines. SLO_CHECKS_CAP replaces the hardcoded "/ 10" badge. - components/shadow-percent-card.tsx (was ShadowPercentCard): same behavior — draft mirrors the current prop, validates 0–100, only enables "Enregistrer" when changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ShadowHistoryRow and DeploymentRow were 60-line near-duplicates: same
state pill, same meta line, same rollback button — only the source
object shape differed. Unified them behind a discriminated-union
DeployRow that adapts via variant: 'prod' | 'shadow'.
- components/deploy-row.tsx: viewOfProd / viewOfShadow map each
variant's source object to a common { ref, sha, msg, state, url,
createdAt } view model, then a single JSX block renders it. Shadow
variant inherits the "disable when state === 'ERROR'" rule via
buttonDisabled; prod variant gets the same effect from its parent.
- components/deployments-card.tsx (new): wraps DeployRow with variant
prod, owns the isCurrent check against prodHost.
- components/shadow-history-card.tsx (new): same with variant shadow,
uses the SHADOW_HISTORY_CAP constant instead of the inline "20".
Net effect: ~180 lines of inline JSX in dashboard-client.tsx replaced
with two <Card /> calls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "État du canary" section was the largest inline block of the orchestrator: ~180 lines of header + StatusLine + TimingLine + TrafficBar segment array + Pause/Resume/Cancel + step input + step-back/step-forward + Promote. Split it into a card with two focused action rows. - components/canary-state-card.tsx: orchestrates the card layout and consumes the segment array from utils. - components/canary-actions.tsx: Pause / Resume / Cancel canary (primary action row). Encodes the disable matrix per status. - components/canary-manual-controls.tsx: step input + step-back / step-forward / promote (secondary row). Internalises stepSize + STEP_MIN/MAX/DEFAULT. - utils/segments.ts: buildTrafficSegments() builds the [shadow, previous?, new] array from the shares + bucket info. Pulled the 0.5-pt "effective hint" threshold out as EFFECTIVE_HINT_THRESHOLD so the magic number is named. dashboard-client.tsx now renders <CanaryStateCard … /> with a wide but flat prop bag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dashboard-client.tsx had ~150 lines of repeated <ConfirmModal> blocks
with `modal?.kind === '...'` narrowing duplicated in `open`, `body`,
`confirmPhrase`, `pending`, and `onConfirm`. Replaced the lot with a
single <CanaryModals/> and a tighter modal type.
- types/modal.ts: replace `null` arm with `{ kind: 'closed' }`. The
union is now exhaustive: discriminated narrowing in canary-modals
doesn't need optional chaining on modal, and exported type guards
(isRollback, isRollbackShadow) give callers a way to refine.
Exported CLOSED_MODAL singleton so the orchestrator doesn't
re-allocate the closed state on every render.
- components/canary-modals.tsx: one component that owns the 4 dialogs.
rollback and rollback-shadow are pulled into RollbackModal /
RollbackShadowModal local components so each modal's body sits with
its own narrowed data — no more inline `modal?.kind === 'rollback'`
guards inside JSX.
- dashboard-client.tsx: uses CLOSED_MODAL for initial state +
closeModal. Action callbacks receive the deploy / target object
directly via props (no narrowing leak into the orchestrator).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap up the 10-step refactor of packages/templates/files/app/admin:
- dashboard-client.tsx is down to 167 lines (from 1280, -87%); drop
the now-empty trailing "Sub-components" banner.
- manifest 0.2.1 -> 0.3.0: the admin payload now ships 26 new
modules (types, constants, utils, hooks, api, components) on top
of the original 8.
Verified on the final state:
- pnpm sync:templates:check OK
- pnpm test core 69 + templates 61 = 130
- pnpm --filter=greenfield build OK
- grep "as BucketInfoMap|as { entries|as any|catch(() => ({}))"
in app/admin zero matches
- LOC: no file > 200 lines in app/admin; orchestrator < 100 lines
of actual logic (the rest is the CanaryStateCard prop bag).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tambouil
reviewed
May 22, 2026
Tambouil
reviewed
May 22, 2026
Tambouil
previously approved these changes
May 22, 2026
Co-authored-by: Valentin Berceaux <60550907+Tambouil@users.noreply.github.com>
Co-authored-by: Valentin Berceaux <60550907+Tambouil@users.noreply.github.com>
Contributor
Author
|
@paul.babin tu pourrais me dire si on peut valider ça, c'est pour avoir un code plus propre, j'ai lancer l'IA dessus. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
packages/templates/files/app/admin/dashboard-client.tsxwas a single 1280-line file mixing 9 React components, 9 inline utilities, 4 inline modals, rawfetch()calls with unsafeascasts, and the wholeuseState-soup of the orchestrator. A lead flagged the quality. This PR breaks it apart in 10 reviewable steps — one commit per step — and adds the infra to keepexamples/greenfieldbyte-equivalent to the templates payload.Headline numbers:
dashboard-client.tsxLOCapp/admin/canary-modals.tsx)useStateinAdminDashboardas/ silent.catch(()=>({}))npx copy .How to review
Each commit is a self-contained step that builds + tests green on its own. Read them in order:
154fb92infra —bin/sync-from-source.mjs(SHA-256 walk +--check),templates-sync.ymlCI guard, Vitest 2 +tsconfig.json,pnpm sync:templates[:check]root scripts. Bug fix: 3 admin routes (bucket-info,shadow-history,rollback-shadow) were used by the dashboard but missing frommanifest.json, so users' dashboards were broken afternpx ... copy ..df9b396types/constants/utils — pulls 9 inline types and 9 inline utilities intotypes/,constants/,utils/. Adds 40 unit tests (deriveStatus,prettyTimeAgo,nextCronFireMs,computeTrafficShares,computeTiming, …).a8abdc9typed API client —api/admin-client.tsexposes 13 endpoints with response shapes.api/errors.tsexportsAdminApiError(status)+parseJsonError(res). Zeroascasts, zero swallowed errors. 13 new tests withfetchmocked.7647cfbhooks — 9useState→ 2. NewuseDashboardState(useReducer),useActionRunner(useReducer),useWallClock(SSR-safe),usePollRefresh. Reducers are pure and tested.7f0b618atomic components —StateBanners,StatusLine,TimingLine,TrafficBar,ActionButton(ex-ActionBtn).4b11338isolated cards —SloLogCard,ShadowPercentCard.fc96d45deploy rows merged —DeployRowwithvariant: 'prod' | 'shadow'replaces the two near-duplicate row components. Wrapped inDeploymentsCard/ShadowHistoryCard.cba8f9ccanary state card — the largest inline block (~180 lines) becomesCanaryStateCard+CanaryActions+CanaryManualControls. Segment array building extracted toutils/segments.ts.a5275a9modals —ModalStategains an explicit{ kind: 'closed' }variant (replaces thenullarm). 4 inline<ConfirmModal>blocks →<CanaryModals/>with type-guard refinement.e226326finalize — manifest 0.2.1 → 0.3.0 (ships the 26 new admin modules), drop the empty trailing banner.New layout
examples/greenfield/app/admin/mirrorspackages/templates/files/app/admin/byte-for-byte and is kept in sync bypnpm sync:templates. A new CI workflow (templates-sync.yml) fails the PR if the two diverge.Test plan
pnpm sync:templates:check— passes (greenfield in sync)pnpm test— 130 tests acrosscore(69) andtemplates(61) all greenpnpm --filter=greenfield build— Next.js compiles all 22 routesgrep "as BucketInfoMap\\| as { entries\\| as any\\|catch(() => ({}))" packages/templates/files/app/admin— zero matchesapp/admin/Out of scope (follow-up PR)
The audit also flagged in
packages/core/:vercelFetch()+checkEnv()duplicated indeployments.ts,promote.ts,patch.ts— extract a sharedvercel/client.tscompose.tsis a 283-line function with deep nesting around the bucket resolutiondeployments.ts,promote.ts,patch.ts)as ShadowConfigcast inedge-config/patch.tscould use a runtime guardLeft intentionally untouched to keep this PR scoped to the admin dashboard.
🤖 Generated with Claude Code