- budget page: guard spend bar against missing/zero ceiling (no NaN width);
show an explicit "no ceiling set" state. Add pure budgetUsagePct() helper.
- job detail: replace silent live/poll toggle with an explicit stream-mode
badge (Live vs Polling) so operators see when SSE degrades to polling.
- fleet-client: extend patchJob to carry optional checkpoint/blockedReason
matching the server PatchJobSchema; add FleetCheckpoint type.
- tests: unit cover budgetUsagePct + patchJob checkpoint forwarding; e2e
asserts the polling indicator appears when the stream is unavailable.
- ci: add a Gitea Playwright e2e job that runs the fleet control-plane specs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the §14 Phase 3 review gate. requestReview() routes a building
job into the review stage (fencing any worker), carrying a normalized policy
(requiredApprovals + reviewer allowlist) and clearing prior decisions.
submitReview() records one decision per reviewer (last-write-wins, identity-
normalized), advances the job to testing once distinct approvals reach the
quorum, and treats any reject as a veto that returns the job to queued for
rework. Adds POST /fleet/jobs/:id/review/request and POST /fleet/jobs/:id/review,
a typed client, and a review-gate card on the job-detail page.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds coordinator.fleetMetrics() computing queue depth, stage histogram,
oldest-queued age (starvation signal), factory health and seat utilization,
plus derived alerts (no_live_capacity, all_factories_down, queue_starvation,
saturated, stale_factories). Exposed via GET /fleet/metrics and surfaced as a
metrics+alerts panel on the fleet overview. Thresholds injectable for tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New e2e/fleet.spec.ts with a method- and URL-aware /api/fleet/** mock that
holds mutable state so operator actions and budget toggles reflect in
follow-up GETs. Covers: fleet overview (factory cards + recent jobs), jobs
table + stage filter, job detail requeue (stage building->queued) with the
SSE-driven Live badge, and budget pause/resume. All 4 specs green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Backend: GET /fleet/jobs/:id/events/stream emits a snapshot (seq > Last-Event-ID)
then long-polls the append-only event log, closing after a bounded window so
EventSource-style clients reconnect cleanly. Honors Last-Event-ID resume,
keepalive comments, and a terminal error frame.
Frontend: subscribeJobEvents uses fetch streaming (to send auth + product
headers) with parseSseFrames, Last-Event-ID resume, reconnect backoff, and a
fatal-on-error-frame fallback to polling. Job detail page subscribes live
(deduped by seq), falls back to 4s polling on failure, and shows a Live badge;
refresh() now merges events so a slow snapshot can't clobber streamed ones.
Tests: +3 route (snapshot, resume cursor, append-after-connect), +5 client
(parseSseFrames x2, subscribe deliver/error/resume/error-frame). fleet 150, web 222.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- coordinator.costBurndown() aggregates completed run cost (insights.costUsd)
by UTC day over a window, returning a gap-free cumulative series + ceiling
- repository.listRunsByProduct() cross-partition run query
- GET /fleet/budgets/:productId/burndown?days=N route
- fleet-client.getBudgetBurndown() + CostBurndown/BurndownPoint types
- BurndownChart on the budget page: cumulative daily bars with a dashed
ceiling overlay; bars turn red past the ceiling; degrades gracefully
- Tests: +2 coordinator, +1 routes, +2 fleet-client (fleet 147, web 216)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 'why does this job route here?' to the §7 scheduler:
- coordinator.explainJob() re-runs scoreCandidate against every live factory,
returning per-factory weighted breakdown, eligibility + reasons, deps state,
and the best eligible factory (read-only, side-effect free)
- GET /fleet/jobs/:id/explain route (404 when job missing)
- fleet-client.getJobExplain() + JobExplain/ScoreBreakdown types
- ExplainPanel on the job detail page: score table per factory with the six
weighted terms, eligibility, and unmet-deps note; degrades gracefully
- Tests: +2 coordinator, +1 routes, +2 fleet-client (fleet 144 green,
tracker-web 214 green)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fleet overview page with factory cards + recent jobs polling
- Job table with stage filter tabs
- Job detail page with events timeline, runs, artifacts, DAG subtree, SHIP action
- Budget page with usage bar, pause/resume controls
- API proxy route forwarding /api/fleet/* to platform-service
- Typed fleet-client.ts with graceful 404 degradation
- 16 unit tests for fleet-client (198 total tracker-web tests green)
- Added Fleet nav item to dashboard layout
- Full monorepo build + test green
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- packages/llm: use nullish coalescing (??) in GeminiProvider constructor
so explicit empty-string apiKey is not overridden by env var
- dashboards/admin-web,tracker-web: exclude .next/ from vitest test glob
to prevent Next.js internal test files from being picked up
- services/cowork-service: use platform-safe .kill() instead of SIGTERM
which is invalid on Windows
- packages/use-keyboard-shortcuts: add @testing-library/react devDep
- scripts/npmrc.template: use https:// for Gitea registry
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- UX-6 system banners DEFERRED: platform-service (:4003) is unreachable in this
environment, so there is no real broadcasts/maintenance feed to surface.
Per the wave's explicit condition, banners are not added against an empty feed.
Recorded in the waves list + Deferrals table with a follow-up.
- CC.1-CC.6 ticked: suite/build green every wave; dark-mode parity via the bridge;
zero new color literals; a11y labels on all new controls; charts/palette/motion
code-split via next/dynamic (chart chunk ~3.8 KB gzip); size:check has no
bundlesize config in-repo so gzip sizes recorded inline (follow-up logged).
- Add token-bridge guard test (CC.2/CC.3): asserts every --bl-* maps to an admin
var that flips under .dark and that the bridge contains no raw color literals.
Verify: typecheck+lint+build green (123 routes); vitest 22 files / 183 tests;
format:check no new failures (29 pre-existing); e2e 11 passed / 80 failed
(unchanged vs UX-1 baseline — environmental, no backend).
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- @bytelyst/motion added workspace:* (importer-only lockfile change;
--frozen-lockfile clean).
- Dashboard overview only: KPI cards grid wrapped in StaggerList (from up,
50ms stagger); the Model-Usage / Recent-Users table row wrapped in Reveal.
- Primitives honor prefers-reduced-motion and resolve to opacity 1, so no
element is stranded transparent (no contrast/a11y regression); prefersReduced
is SSR-safe. Motion is confined to the auth-gated dashboard, not the public
e2e surfaces, per tracker-web's axe/opacity caution.
- vitest.config: inline @bytelyst/motion + react dedupe for the render test.
Tests: happy-dom asserts Reveal/StaggerList end visible and render all children.
Verify: typecheck+lint+build green (123 routes); vitest 21 files / 170 tests
(+2); format:check no new failures; e2e 11 passed / 80 failed (unchanged vs
UX-1 baseline — environmental).
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- error.tsx -> ErrorPage (keep telemetry on mount; retry wired to Next reset).
- (dashboard)/loading.tsx -> LoadingSpinner inside the existing skeleton.
- not-found.tsx already used NotFoundPage (confirmed, unchanged).
- dashboard overview page.tsx header -> PageHeader (Refresh as actions; the
subtitle/last-updated line preserved directly below).
Rich detail headers (e.g. users/[id] back-button + plan/status badges) left
bespoke on purpose: PageHeader has no subtitle/badge slot, so forcing it would
regress them (additive-only rule). dashboard-components reads --color-* which
admin maps via @theme inline, so it themes in light + dark.
Verify: typecheck+lint+build green (123 routes); vitest 20 files / 168 tests
(+3 happy-dom chrome render tests); format:check no new failures; e2e 11 passed
/ 80 failed (unchanged vs UX-1 baseline — environmental).
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Mount CommandRegistryProvider in (dashboard)/layout.tsx and a CommandMenu
that binds the global Cmd-K / Ctrl-K hotkey (useCommandPalette) and lazy-loads
the dialog via next/dynamic (own chunk; dynamic target is a local re-export
command-palette-dialog.tsx because the package declares only an `import`
export condition).
- src/lib/admin-commands.ts: pure builder for 21 navigate-mode commands across
the major surfaces (Users, Subscriptions, Licenses, Billing, Usage,
Broadcasts, Flags, Experiments, Audit, Ops, …) plus theme-toggle and sign-out
actions wired to the existing auth/theme contexts; onNavigate -> router.push.
- @bytelyst/command-palette added as workspace:* (importer-only lockfile change;
--frozen-lockfile clean).
- vitest.config: inline command-palette + dedupe react for the interaction test.
Tests: pure command-set assertions + a happy-dom Cmd-K/Ctrl-K interaction test
(react-dom/client + act, no new deps).
Verify: typecheck+lint+build green (123 routes); vitest 19 files / 165 tests
(+6); format:check no new failures; e2e 11 passed / 80 failed (unchanged vs
UX-1 baseline — environmental, no backend).
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Replace all 5 direct recharts usages with the shared, token-themed SVG
primitives, lazy-loaded for bundle savings:
- dashboard, usage, users/[id], ops/client-logs, extraction/entity-chart
now render AreaChart/BarChart/Donut from @bytelyst/charts.
- new src/components/charts: next/dynamic wrappers (own chunk, ssr:false)
that dynamic-import a local static re-export (primitives.tsx) — the chart
packages declare only an `import` export condition, so a direct
import('@bytelyst/charts') trips Next's resolver.
- new src/lib/chart-data.ts: pure, finite-safe data mappers (unit-tested).
- recharts removed from package.json + the admin-web lockfile importer entry
(now fully unused). Lockfile delta is importer-only (+charts/+data-viz as
workspace:*, -recharts); no monorepo re-normalization; --frozen-lockfile clean.
- vitest.config: inline @bytelyst/{charts,data-viz} + dedupe react so the
SSR no-NaN render tests use a single React copy.
Fidelity notes (charts are single-series/vertical; StackedBar is charts 0.2.x):
stacked severity chart -> single bars colored by dominant severity; pie charts
-> Donut; horizontal bars -> vertical.
Verify: typecheck+lint+build green (123 routes); vitest 18 files / 159 tests
(+19); format:check no new failures; e2e 11 passed / 80 failed (unchanged vs
UX-1 baseline — failures are environmental, no backend).
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Additive phase-1 foundation for ByteLyst UX integration:
- globals.css: bridge the shared --bl-* contract onto admin's shadcn OKLCH
ramp (surfaces/borders/text/accent/danger/focus) so @bytelyst/* components
theme correctly in light AND dark. Mappings reference admin --* vars that
flip under .dark, so parity is inherited with zero new color literals.
Status hues (success/warning/info) intentionally inherit design-tokens.
- eslint.config.mjs: no-restricted-imports ratchet forbidding direct
@bytelyst/ui imports outside the Primitives.tsx adapter seam.
- primitives-exports.test.ts: export-presence guard for the adapter surface.
- roadmap: author verified baseline audit + green/red gate table + e2e baseline.
Verify: typecheck+lint+build green; vitest 17 files / 140 tests (+29);
format:check no new failures (29 pre-existing, out of scope); e2e baseline
11 passed / 80 failed (80 environmental — no backend).
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Mark UX-12.3 (rich-text) and UX-13.1 (NotificationCenter) as
🔒 blocked-on-backend rather than open — they are excluded from the ✅
count and each now carries a one-paragraph spec of the exact
platform-service change required:
- UX-12.3: server-side HTML sanitization (allowlist tags/attrs; strip
scripts/event-handlers/js: + data: URLs) on items.description +
comments.body write paths, so RichTextEditor/RichTextViewer can be
safely adopted.
- UX-13.1: emit notifications into platform-service's existing
notifications module on tracker events (new comment, status change,
vote milestone) targeted to the item author/subscribers with productId,
exposed via the /api/tracker proxy, so NotificationCenter binds a real
feed.
Add BACKEND_ENABLERS.md tracking both follow-ups (title, blocking item,
target module, acceptance criteria, backward-compat constraint —
platform-service is shared by 9 products). Update the Expand tracker line
and notes to show all client-only waves complete and these two
backend-blocked. Docs only — no source/dep/lockfile changes.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Replace the hand-rolled sticky top nav in the dashboard layout with the
shared AppShell (AppShellSidebar/AppShellNav/AppShellNavItem/AppShellMain/
AppShellSkipLink + mobile toggle + overlay). The sidebar keeps the
ProductSwitcher, user email and Sign out, and adds a ⌘K trigger (replays
the global hotkey) and a theme toggle. Nav items use aria-current for the
active route and client-side navigation; the skip-link targets the
focusable main region. AppShell exports are routed through the Primitives
adapter (CC.6 ratchet) and covered by the export-presence test.
AppShellPageHeader is intentionally not used so the per-page PageHeader
(UX-10) remains the single h1 per route (no duplicate headings).
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add @bytelyst/motion (workspace:* + minimal lockfile importer entry).
Apply Reveal to the dashboard overview cards/charts (staggered) and the
items DataTable, and NumberFlow to the overview KPI totals. All motion
primitives no-op under prefers-reduced-motion.
The public /roadmap is intentionally left without entrance motion: the
offline @axe-core gate scans the page synchronously and the Reveal
transient sub-1 opacity trips color-contrast (a hard CC.4 a11y gate). The
dashboard/items surfaces are auth-gated and not axe-scanned, so they keep
the motion.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Mount the shared ToastProvider in providers.tsx and replace the inline
error/success banners across the overview, items list, board, item detail
and public roadmap with toast() calls — load errors, create, delete, vote,
status/priority/visibility updates, comment add, and idea submission. The
roadmap submit now toasts + closes the dialog instead of an in-modal
banner; the item-detail page keeps a single inline empty-state for the
hard "couldn't load this item" case.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add @bytelyst/command-palette (workspace:* + minimal lockfile importer
entry). Mount CommandRegistryProvider + a lazily-loaded CommandMenu in
providers.tsx, opened with ⌘K / Ctrl-K. Register navigate commands
(Overview/Items/Board/Roadmap), New item (navigates to items with ?new=1
which auto-opens the create modal), Toggle theme, Sign out, and per-product
Switch commands wired to setProductId. Command building lives in the pure
src/lib/command-registry.ts. Add command-menu.test.tsx (jsdom) asserting the
builder set and that the palette opens on ⌘K and lists commands.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add @bytelyst/charts + @bytelyst/data-viz as workspace:* deps (minimal
importer link entries in the lockfile). Replace the badge-pill breakdowns
on /dashboard with KpiCards (total/open/in-progress/done) and a dynamically
imported chart surface: Donut for By Status + By Type (centered total) and
a BarChart for By Priority, coloured from the bridged --bl-chart-* palette.
Pure transforms live in src/lib/overview-charts.ts (finite-coerced, no NaN
reaches the SVG); the heavy chart surface is split into overview-charts.tsx
and loaded via next/dynamic (ssr:false) to keep it out of the route bundle.
Add overview-charts.test.tsx rendering the surface with mocked stats via
react-dom/server (no NaN in paths) + transform unit tests; dedupe react in
vitest so the SSR render uses a single React instance.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>