The palette fixes had no guard: no scanned page renders a colored badge or a
destructive fill, so a shade edit could silently drop a variant under AA and
nothing would fail. Two comments were standing in for that coverage.
Mount one probe per variant from the component's own cva — a class list copied
into the spec would only ever re-test itself — and measure what the browser
actually paints, so oklch tokens and translucent fills are read as rendered.
Every variant is checked at rest and on hover, in both themes.
The measurement is deliberately unrounded: green-700 comes out at 4.4991, which
two decimals report as a passing "4.5". With rounding in place the guard passed
against a deliberately regressed green; without it, it fails and names the
variant, the state and the ratio.
Specs can now import component modules, so the e2e tsconfig gains jsx.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three comments added with the contrast fixes were mostly narration: two restated
the class strings they sat above, and two defended the implementation rather than
naming a contract. The one fact that is not visible from the code — green sits a
step darker than its siblings — belongs in a test that fails when it regresses,
not in prose asking the reader not to touch it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four states were under AA, not one: the dark badge and button at rest (3.71), the
light badge on hover (4.34) and the dark button on hover (4.40), plus the dark
destructive menu item (4.01). Badge and button had also drifted apart — one faded
to 80% on hover, the other to 90% — so each failed in a different theme.
The token itself cannot be retuned: sweeping its lightness finds no value that
serves both roles, because the same red is a fill under white text and the text
colour on the page. Darkening it for the fill would break the twenty-four
text-destructive call sites.
Tune the fill's opacity per theme instead, keeping white text and adding no tokens.
A translucent red composites toward what is behind it, so the same fade darkens it
on the dark ground and lightens it on the light one: full strength on light fading
to 90% on hover, 80% on dark dropping to 70%. Hover now raises contrast in both
themes. All four states land between 5.04 and 6.28, verified on a built bundle
against every dark surface token (worst case 5.13 on secondary).
The dark destructive menu item drops its focus wash from 20% to 10%, matching light
and restoring the label to 4.52 at the cost of a fainter highlight.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every colored Badge variant carried a single text shade for both themes, and no
one shade clears WCAG AA on both grounds. Measured against the current tints,
five of the seven failed on light and four on dark, with red and pink failing on
both — the badge text is 12px semibold, so the bar is 4.5:1.
Use the darker shade on the light tint and the lighter one on dark:
text-{color}-700 with a dark:text-{color}-400 override. Green takes 800 instead
of 700 because a shade number is not a constant lightness across hues — green-700
measures 4.4991, just under the bar.
Every variant now lands between 4.6 and 9.7. The knowledges a11y waiver is gone
and both theme scans pass without it; the knowledges dark visual baseline is
regenerated for the lighter blue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measuring the variants (browser-resolved sRGB, the 10% tint composited over the
real card surface, WCAG formula) showed the defect is not dark-only: five of the
seven colored variants fail AA on light and four on dark, and no single text shade
clears both grounds. Blue happens to pass on light and fail on dark, which is why
only the dark scan flags it here. The waiver itself is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only the Terminal tab had a spec; Tasks, Agents, Searches and Vector Store — all
fed by the FlowDocument query — were uncovered. Add a populated flow cassette (one
entry per tab) and a spec that opens each tab and asserts its seeded content
renders. Files and Screenshots are REST-backed and left for a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The route-manifest visual sweep only captured the empty providers state. Add a
populated cassette (enabled defaults + a user-defined provider row) and a dedicated
visual spec with light/dark baselines, generated in the pinned container. Dates
render deterministically because the visual project inherits the mock clock via the
backend fixture.
Also bind --update-snapshots=all in run-visual.sh so a trailing spec-file filter is
not swallowed as the option's optional [mode] argument.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite tags every spec but the docs only hinted at "@smoke, …". List the real
tags (@smoke/@flows/@crud/@coverage/@cross/@visual/@real/@stand) with their meaning
and how to filter, and spell out the two conventions the mock gate reserves:
@quarantine to isolate a flaky spec (excluded via --grep-invert) and @generated for
recorder-derived cassettes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
axe only ran in the light theme, so dark-only contrast regressions were invisible.
Parametrize every a11y scan over light and dark. The dark pass surfaced a real,
pre-existing issue: colored Badge variants use text-{color}-600 with no dark
override and fail contrast on the dark background. Waive it narrowly on the
knowledges list (the blue badge) as named debt — a palette fix is queued
separately — so the rest of the dark surface is now gated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The period-switch test only proved the usageStatsByPeriod refetch fired; it would
still pass if the UI rendered stale data. Key the cassette's usageStatsByPeriod by
period (week vs month) with distinct date ranges, and assert the Token Usage chart
x-axis shows the month range after the switch and drops the week range.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The comment claimed no non-backslash byte is lost, but GFM legitimately trims
cell-edge and collapsed whitespace, so the test asserts the weaker true invariant:
every byte other than backslashes and that trimmed whitespace survives in order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The base compose file pins fixed network names, so any network left un-overridden
in this e2e override joins the stack to the developer's live observability and
langfuse networks. Give all three networks e2e-scoped names. Add a healthcheck to
the mock-LLM service and make pentagi depend on service_healthy, so a crash-looping
mock can never read as a started dependency and silently mask itself.
Update frontend/docs/e2e.md to match reality: the stand tier runs @stand only
(flow-run drives a real paid agent and stays local), every mock run rebuilds its
own bundle by design, and the trend/affected-routes notes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The co-located components/shared/README.md documented hooks that no longer
exist. `usePagination` was never a hook, and the writer `useTableQueryFilter`
was replaced by the unified `useTableState`, so the copy-paste recipe it shipped
would not compile. Detail pages had likewise moved from spreading a `toolbarProps`
object to passing a `controller` prop.
Rewrite it as frontend/docs/list_detail_pages.md, matching the flat
per-subsystem layout backend/docs already uses: exact signatures for every
hook, component and storage helper, the wiring the five live list pages
actually use, and an explicit table of removed names so they are not
reintroduced. Drop the source-adjacent README — no other directory under
frontend/src or backend/pkg carries one.
Fix the comments that still named the removed hook, plus two claims in them
that contradict the code: the controlled filter is URL-backed only (DataTable
persists sorting, column visibility, page size and search columns, never the
filter), and `busy` on InlineEditInput disables the Save and Cancel buttons
while leaving the input editable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dashboard partial-errors spec matched cards via getByTestId('dashboard-card'),
but that attribute lives only in an uncommitted working-tree change, so on a clean
checkout the locator matched nothing and the spec failed. Match the data-slot="card"
attribute the shadcn Card primitive already emits. Also lift the inline xterm buffer
type in the message-rendering spec into named types so it satisfies prettier and the
union/module sort lint rules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The smoke only exercised a successful login. Adds a cassette whose
/auth/login answers an error envelope and asserts the surfaced error plus
that Sign in stays disabled (react-hook-form holds it until a field
changes) — the negative path a live run confirmed but no spec covered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The message cassettes were all Answer-typed, so flow-message.tsx's
type-specific branches never ran under test. A live QA run against a real
Bedrock flow showed real streams emit thoughts/report/terminal/input.
Adds a cassette with one message per path and asserts each: the
thinking toggle (thinking + message), a report that auto-expands its
details, a Terminal-format report that mounts the xterm renderer with
its output, and an input message's right alignment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Real-tier (Tier 2/3) fixes, the blocking set:
- stand-smoke asserts a per-route anchor (breadcrumb title span) instead
of a shared sidebar link that 0-matches on /settings/* and double-matches
on /templates — the smoke was structurally red on 3 of 5 routes.
- stand-tier trace/video forced off (they embedded the session cookie and
the stand password in a public-repo artifact) and the stand project
selects @stand via config grep (the CI step selected zero tests).
- run-local-tier's sandbox filter is anchored to the seeded 9xxxx range
(`^/?pentagi-terminal-9[0-9]{4,}$`) so it can't force-remove a dev's
flow-9 sandbox; sandbox cleanup moved behind the keep-stack gate; compose
logs are captured before teardown.
- mock-LLM wraps the request handler (a non-JSON body crashed the process);
its transcript now issues a real terminal exec via a delegate chain and
flow-run asserts the terminal output; OpenAI-shaped error envelope.
Mock-gate rigor + coverage:
- order-insensitive deep-subset variable matching; unmatched WS subscribes
are now accounted at teardown; REST/GraphQL entries can pin a body/vars
subset; favorite-star asserts the persisted (not optimistic) value.
- new flows/create smoke (the primary journey had no mock coverage), a
graphql-ws protocol unit test against the real client, and a
route-manifest completeness test.
- affected-routes no longer under-scopes shared component dirs.
Hygiene: .mjs tools now linted/prettier'd; dead gitignore line; JSDoc/
sheet aria-label/schema-compat/trend/serve-dist/report-comment fixes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
new RegExp(route.replace(/\//g,'\\/')) tripped CodeQL
js/incomplete-sanitization (only slashes escaped). The routes are
constants, so it was a false positive — but the predicate form is
cleaner and asserts the exact pathname.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
schema-compat set NODE_TLS_REJECT_UNAUTHORIZED=0 process-wide (CodeQL
js/disabling-certificate-validation, high). A real stand has a valid
cert; only the local self-signed Tier-2 stack needs it, so the operator
now opts in via their own shell env, never in code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lays the Phase-3 substrate on top of the three tiers.
- schema-compat pre-flight (e2e/tools/schema-compat.mjs): introspects a
target backend's live GraphQL schema and validates every frontend
operation against it, so deploy skew (a renamed/removed field) fails
once, readably, instead of as dozens of red specs. Verified against the
live Tier-2 backend both ways: 105 operations pass, an injected bogus
field is caught with the exact location.
- trend aggregation (trend.mjs): turns a run's results.json into one JSONL
record (p50/p95 spec duration, slowest three, pass/flaky/fail) so slow
regressions are visible, not just green/red; CI appends it to a
90-day-retained artifact.
- diff-scoping (affected-routes.ts + affected.ts): maps a diff to the
manifest routes it touches via each route's owning sources — the
substrate for selective runs and for scoping the exploratory agent.
Pure mapping fn, unit-tested (backend-only → none; shared infra → all;
owned → that route).
- CI: an e2e-stand job (label-gated + a protected Environment whose
reviewers approve before secrets are exposed; schema-compat runs first),
a trend step, and an LLM-independent @stand smoke.
- docs: the stand tier, trend/affected tools, and the LLM advisory recipe
(playwright-mcp + init-agents + guardrails) — the recipe the
deterministic tiers plug into, not a bespoke bot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
18 baselines (9 manifest routes × light/dark) compared only inside
mcr.microsoft.com/playwright:v<version>-noble, so pixels are identical on
every machine and CI; the wrapper derives the tag from the installed
@playwright/test version, making the pin drift-proof, and a CI guard
fails loudly if the workflow's container tag falls behind.
macOS hosts cannot run the visual project directly (parallel darwin
baselines) nor mount their node_modules into the container (native vite
binaries): the wrapper builds dist on the host and the container serves
it with a dependency-free static server — route mocks intercept API
calls before the network, so no proxy is needed. The xterm canvas is
masked (SwiftShader pixels are driver-dependent). Determinism proven by
back-to-back container runs. The e2e-visual CI job is advisory, never a
required check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
specs/real/** now exercise the actual agent loop end to end: create flow
-> image/language/title -> tool-call-ID sampling -> subtask_list plan ->
done barrier -> subtask_patch refine -> report_result — with messages
streamed over the real GraphQL websocket and the flow settling in
Waiting. The mock LLM is ~150 lines of Node driven by a deterministic
first-match transcript; the custom provider env seam means zero backend
changes.
The stack is fully isolated from a developer machine:
- own compose project/network/ports (8444/5433), coexists with a dev stack
- --env-file /dev/null so the developer's .env (live keys, DOCKER_HOST,
config paths) never leaks into the e2e backend
- flow ids seeded from 90001: sandbox containers are named
pentagi-terminal-<flowId> on the shared docker daemon, so the range
keeps them clear of dev flows and makes runner cleanup unambiguous
(down -v cannot remove them — the backend spawns them outside compose)
- pentagi healthcheck via busybox wget (the alpine image has no curl),
since the base service has none and up --wait returns too early
Auth for live tiers moved to the canonical setup-project + storageState;
the localStorage seed is now mock-tier-only (a forged client session has
no cookie behind it and the first 401 wipes it). CI gains a nightly/
dispatch-only e2e-local job.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite's generative tests run with timeout 30000 — the new one missed
it and tripped vitest's 5s default on slower CI runners.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setup-go's cache:true already caches the build and module caches keyed
on go.sum; the extra actions/cache step restored the same read-only
~/go/pkg/mod on top and failed with "Cannot open: File exists" whenever
both caches hit. Also sets the workflow's GITHUB_TOKEN to contents:read.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
marked's cell splitter honors \| only after an odd backslash run and
truncates rows that split into extra columns, so escaping the pipe alone
in text like a\|b produced \\| — a live delimiter that dropped the
trailing cells on the next load. An odd run + pipe has no exact GFM
encoding: the serializer now pads the run by one backslash, trading a
one-character gain inside the cell for structural integrity, byte-stable
from the first save. Covered by direct round-trips and a random \/|
payload property test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the scenario-catalog coverage on the mock tier (48 specs).
- dashboard: analytics/overview tabs (lazy mount), period switch verified
on the wire, and the per-card degradation path — a failed stats query
renders "Couldn't load" in its own card while neighbours stay live
(first use of error-response cassette entries)
- settings: prompts (the largest typed cassette — 15 agent + 12 tool
prompt configs), providers empty state, create form opens
- resources: REST-seeded file tree, and the mkdir journey proving the
full chain — REST mutation raises a world flag, the flag releases a
resourceAdded frame, the subscription cache merge grows the tree
- e2e/routes.ts: route manifest (paths imported from src/lib/routes so
a rename breaks compile) driving a data-driven NAV sweep over 9
routes; entries carry owning source dirs for future diff-scoping
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The token-reveal dialog (and any dialog with an explicit Close/Cancel)
exposed two controls named "Close" — ambiguous for screen readers and
for role-based locators in strict mode. The shared X is now "Dismiss
dialog".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds on the mock gate: 26 new specs across flows, CRUD, and cross
concerns, all driven by typed cassettes against the production bundle.
- flows: no-duplicate and exact-set-disjoint message streaming across
concurrent flows (ID sets attached to the report), reconnect that
reconciles the missed delta exactly once, and the rename/finish/
favorite/delete lifecycle chained mutation->subscription-frame
- mock engine: streams move to one-driver-broadcast (flowUpdated is
opened by two providers at once; reconnect re-joins past the cursor,
delta-only) and frames can wait on world flags
- CRUD: templates, knowledges (required-validation proves no mutation
fired via the zero-unmatched teardown), api-tokens (inline create,
one-time secret reveal)
- cross: themes (seeded + toggled), responsive (Pixel 7 touch profile,
1279/1280 split boundary), a11y with a policy - critical/serious
floor plus a committed per-route allowlist
- terminal content is asserted through the xterm buffer (the WebGL
canvas has no DOM text): the hook now exposes the instance on its
host element
- app fixes surfaced by the sweep: the rows-per-page select had no
accessible name; the favorite star is useOptimistic and only
persists via the settingsUserUpdated frame - the spec now asserts
the real persistence path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The default tier runs the production bundle (vite build + preview)
against a cassette-driven mock of the whole API surface — GraphQL over
HTTP, graphql-transport-ws subscriptions, and REST — so the suite needs
no backend, no keys, and no secrets, and fork PRs can run it.
- mock engine: (operationName, variables) matching with sequenced
entries and world flags (login flips /info guest->user without
call-order coupling); unmatched calls answer 501 and fail the test,
so nothing leaks through the vite preview proxy to a live backend
- ws mock follows the graphql-transport-ws contract the app's client
needs: immediate ack, nothing before ack, streams stay open, delta
cursors survive reconnects, drops use retryable close codes
- clock and timezone pinned on the mock tier: formatDate branches on
isToday/isThisYear, so unpinned cassette dates rot within a day
- cassettes are TS modules typed against the generated GraphQL types,
so schema/operation drift fails the existing tsc gate at compile time
- CI: fork-safe e2e.yml (read-only token, no secrets, no write steps)
plus e2e-report.yml posting a sticky PR comment via workflow_run,
resolving fork PRs by head SHA
- smoke specs (login redirect, form login, authenticated /flows render)
green on the mock tier; e2e/ wired into tsc, eslint, and prettier
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
List pages and the flow detail page no longer replace live data with a
full-page error on a failed background refetch — the error/redirect branches
now gate on the absence of data, not on the error alone. On a cold-cache load
failure the flow detail page renders an in-page ErrorState + Retry instead of
silently redirecting to the list, while a genuine not-found still redirects.
Retrying after a failed initial load shows the spinner rather than flashing
the empty state.
Reconnect reconcile: catch the aggregate refetch rejection (the per-query
wrapper doesn't cover it) so a transient failure during the sweep can't
surface as an unhandledrejection, and re-hydrate the REST-backed resources
slot — which the observable-query sweep skips — via a ws:reconnected event.
The flow provider gates its blocking spinner and subscription teardown to the
initial load only, so a background reconcile no longer overlays the page or
bounces the live subscriptions.
Terminal: clear() cancels any in-flight chunked write so its trailing chunks
can't land after the buffer was cleared; the search Clear (X) resets only the
query and keeps the task/subtask filter.
Smaller fixes: role="alert" on ErrorState, a keyboard-operable per-agent Test
control, the correct prompt-validation Alert variant, and dashboard error
props. Shared LoadingState and DashboardError components collapse duplicated
loading/error blocks; terminal write cancellation and the Apollo cache
policies both gain test coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The GraphQL subscriptions are delta-only: the server registers a subscriber
for future events and never replays what it published while a client was
disconnected (and it drops events for a disconnected subscriber outright).
Nothing on the client refetched after the socket came back, so a websocket
drop during an active flow — a network blip, a laptop waking, a proxy timeout
— left a permanent hole in the streamed logs/messages/status until the user
manually reloaded the page.
Refetch the active queries on reconnect: graphql-ws hands `wasRetry` to the
`connected` handler, so on a retry (not the initial connect) call
`refetchObservableQueries()`, which re-runs the flow's queries and merges the
full current state back into the cache.
Verified live with a faithful drop (patched WebSocket, real `ws.close()` mid-
flow while the agent kept producing): before the fix the messages created
during the outage stayed missing after reconnect and only a reload recovered
them; with the fix they reappear automatically on reconnect, no reload. (A
CDP "offline" emulation does NOT reproduce this — it buffers the socket rather
than closing it, so use a real close when testing.) 1006 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round out the subscription coverage with createStreamingLink — the piece that
coalesces streamed assistant token updates. Export it (test-only) and drive it
with a controllable source plus a mocked clock.
Locks the three behaviors that make token streaming feel right: append parts
inside the 50ms throttle window are accumulated but not re-emitted (so the UI
isn't hammered), the next emission past the window carries the full running
message rather than the latest delta, and the final non-append part flushes the
accumulated total and clears the per-id cache so the same id can stream again.
Also confirms a non-assistant-log result passes straight through untouched.
1006 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The link that folds live subscription events into the Apollo cache
(updateCacheForSubscription) had no coverage — MockedProvider replaces the
whole link chain, so it can't exercise this. Export it and drive it directly
against a real InMemoryCache configured like production (flow-scoped list
fields keyed by flowId).
Locks the routing rules: *Added appends + de-dups by id, *Created prepends
(newest first — not re-sorted by id), *Deleted removes by id, *Updated merges
an entity's fields in place without reordering (and appends if it's not yet
cached). Also covers the two subtle bits: flowId variant isolation (an event
for one flow leaves another flow's list untouched) and type-tolerant id
de-dup (a numeric subscription id matches a string id from REST hydration).
Also exports createSubscriptionCacheLink for future frame-level tests. Pure
additive exports, no runtime change; app verified still loading. 1003 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The flow detail query runs with errorPolicy:'all', so a failed sibling field
(the flow's log lists are separate, nullable root query fields — terminalLogs,
messageLogs, etc.) surfaces an Apollo error while `flow` itself resolves fine.
Both the redirect effect and the provider's load-error toast keyed off "any
error present", so one flaky log resolver would kick the user back to /flows
and toast "Failed to load flow" over a flow that had actually loaded.
Gate both on the flow genuinely being absent (`!flowData?.flow`) instead of on
the presence of an error. A missing/invalid flow still redirects and toasts
(its non-null `flow` field propagates to a null result); a partial failure now
renders the flow and lets the affected panel show its own empty state.
Reachability confirmed from the schema (sibling log fields are `[X!]`, nullable;
`flow` is `Flow!`, non-null) — end-to-end confirmation folded into the P2-S5
live run. 996 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The terminal wrote incrementally by diffing on array length alone: if the
new logs were at least as long as what had been written, it appended the
tail from the old length onward. That assumes the on-screen prefix never
changes — but the same component is fed filtered subsets, so narrowing a
search then clearing it grew the array back past the filtered length and
appended the full-log tail on top of the still-visible filtered lines,
producing a scrambled, partly-duplicated buffer.
Track the exact lines last rendered and only take the append fast-path when
they are a true prefix of the new array; otherwise clear and rewrite. Pure
streaming stays incremental (no clear); any non-prefix change rewrites.
Verified with a repro test (red before, green after): full -> filtered ->
cleared restores the full set, streaming appends never clear, and a
different filter rewrites. 996 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resetting a prompt to its default on the prompts list swallowed a failed
delete into console.error — sonner wasn't even imported — so the row stayed
Custom with no hint anything went wrong. Toast it, matching how every other
mutation in the app reports failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The flows list did what the other lists used to: on a failed load the provider
fired toast.error with no id — so each repeated failure while the backend was
down stacked another copy — and the page, never reading the error, fell through
to its "No flows" empty state, indistinguishable from an empty account.
The provider already exposed flowsError; read it on the page and render the
shared ErrorState with a Try again button (refetch, now also exposed), and drop
the toast effect entirely. Flows now matches templates/knowledges/resources.
Verified live: three failed refetches leave zero stacked toasts and one error
state with the button (previously three toasts + a false empty state); clicking
Try again after recovery restores the list. 993 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Empty error state named the failure but left the user stranded — the only
way back was a full browser reload. Give ErrorState an optional onRetry that
renders a "Try again" button, and wire every load-error site to re-run its own
query: templates, knowledges (the active list-or-search query), resources, the
three settings lists, and the two settings detail pages. The templates and
knowledges providers now expose refetch for it; the rest already had one.
Verified live: failing the templates query shows the error with the button,
and clicking it after the backend recovers clears the error and renders the
list — no page reload. 993 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A failed delete/create/update on the provider and API-token lists parked a
persistent destructive Alert above the table, which lingered until the next
action and, for tokens, dumped the raw Postgres constraint text into a banner.
Every other mutation in the app reports failure with a toast; these two lists
were the exception.
Toast them too — the create/update handlers already swallowed the error into a
console.error, so this is the first real feedback they give — and drop the
banner, its error state (deleteErrorMessage plus the unread useMutation error
tuples), and now-unused ErrorAlert component.
Verified live: creating a token whose name already exists surfaces "Failed to
create token" as a toast (no second token written), with no standing banner
left behind. 993 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A failed list query showed a one-line destructive Alert stranded at the top of
an otherwise-empty page — it read as leftover chrome rather than the page's
state. The detail pages and the route error boundary already render a failure
as a centered Empty: a large icon, a title, the message. The lists now match.
Add ErrorState (the shared Empty-error the detail pages were inlining) and use
it for the load errors on templates, knowledges, resources, and the three
settings lists, plus the two detail pages that had inlined the same markup —
eight call sites, one shape.
Resources previously only toasted its load failure from the provider while the
page kept showing "No resources yet"; it now shows the error state like the
others, and the provider drops the toast (and the now-unused sonner import).
The settings mutation-error banners stay on the inline Alert — those sit above
rendered content, where a full Empty would be wrong; they're a separate change.
Verified live: a failed templates query (GraphQL) and a failed resources fetch
(REST) both render the centered Empty error instead of a banner or a false
empty state; the healthy paths are unchanged. 993 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The banner told the user "something failed" but left every card showing its
value — a down backend rendered Total Cost as $0 and each chart as "No data
for this period", numbers indistinguishable from a genuinely quiet account. And
when a single stat query failed while the other eleven succeeded, the banner
alarmed over an otherwise-healthy dashboard.
Give MetricCard and ChartCard an error state alongside their loading/empty ones
— a muted "—" / "Couldn't load", quiet rather than alarming — and wire each of
the dashboard's queries to its own card and table. A failed query now says so
in its own tile; the rest keep rendering their real data. The top-of-page
banner is gone.
Verified live: failing one period query leaves that chart as "Couldn't load"
while its neighbours draw real bars; a full stats outage fills every card and
table with the quiet error state and no lying zeros; the healthy path is
unchanged. 993 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Five copies of the same destructive Alert — same variant, same AlertCircle,
same title-and-message shape — sat inline across the settings pages, and the
preceding load-error fix had just added four more call sites of it.
Use the shared component at all nine, and rename it to ErrorAlert on the way:
the name it shipped with an hour ago, DataLoadError, was too narrow for two of
the five, which report a failed delete rather than a failed load.
Its `message` now also accepts null, because that is what the delete-error call
sites already had in hand and what the inline JSX happily rendered as nothing.
Left as they are: the prompt-validation dialog swaps variant between success
and error and carries a structured body, and the flow-files notice is a default
Alert — a warning, not an error. Neither is this shape.
No behaviour change. Verified by re-failing the providers and prompts queries
and confirming the same alert still renders and the page still recovers;
993 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A backend that was down rendered as a pristine empty account: every dashboard
chart said "No data for this period" and every metric card showed 0, while the
templates and knowledges lists showed their "nothing here yet" empty states —
all indistinguishable from a genuinely empty account, with no toast, no banner
and no other hint that anything had failed.
None of the three pages read `error` from their queries. The templates and
knowledges providers destructured only `{data, loading}` and never exposed it,
and every dashboard query dropped it on the floor. A failed query leaves `data`
undefined, which the empty-state branch cannot tell apart from an empty result.
Expose `error` from the two providers and render a load-error state distinct
from the empty state: a full-page alert on the lists, a banner above the cards
on both dashboard tabs. The settings pages already did exactly this, so the
shared `DataLoadError` gives those call sites one shape instead of four copies.
Templates also gained the loading state it never had — its empty state used to
show while the very first fetch was still in flight.
Verified by failing each query at the network layer and re-running the check
that caught it: templates and knowledges now surface "Error loading ..." rather
than their empty state, and the dashboard shows the banner on both tabs against
a cold cache. Happy path unchanged; 993 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The proxy was only declared under `server`, so `vite preview` served the
production build with no route to the backend: every /api/v1 call 404'd and
the built app could not get past the login screen.
That left the production bundle effectively untestable, which is where
chunking — and the bug that made every route download recharts — only ever
shows up; `vite dev` does not chunk at all.
Reuse the dev proxy for preview, on VITE_PORT+100 so it cannot collide with
the dev server. Verified: preview serves on 8100 and /api/v1/info returns 200
through the proxy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`cmd/installer/wizard/logger` opens log.json from its init(), so importing the
package is enough to create one in the process's working directory. Running
`go test ./cmd/installer/...` therefore drops a log.json into every package
directory it runs from — four of them were sitting untracked in the tree,
one step away from an accidental `git add -A`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Loading any page — /login included — fetched the `markdown` and `charts`
chunks, ~198KB gzip that the page never used.
Vite 8 bundles with rolldown, where `manualChunks` is a compatibility shim
over code-splitting groups, and a group captures its modules' dependencies
as well as the modules themselves. So `charts` (recharts) swallowed clsx and
`markdown` (react-markdown) swallowed react/jsx-runtime — both of which every
component needs, which dragged the two chunks into the entry graph and onto
index.html's modulepreload list. Returning 'react-vendor' from manualChunks
for those modules changed nothing: the group that captured them as
dependencies won regardless of the order the branches were written in.
Use rolldown's own `codeSplitting.groups` instead and rank the shared modules
above the heavy libraries that depend on them.
/settings/account now fetches 305KB of JS rather than 502KB; /dashboard 378KB
rather than 474KB, still loading `charts` because it draws charts. Verified
against the production build served locally: 10 routes render, console clean,
charts intact, 993 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Empty declares `flex-1 … justify-center` (empty.tsx:9), but the list pages wrapped
it in a plain `flex flex-col`, so it collapsed to its natural height and hugged the
header while api-tokens — same primitive, same "nothing yet + CTA" shape — centred
in its `flex-1` wrapper. At 1920 the CTA jumped ~350px between the two pages.
The five wrappers on knowledges, templates and flows (loading + empty states) now
carry flex-1 like the eight that already did. SidebarInset is `min-h-svh flex-1
flex-col`, so the height is there to fill. Measured at 1920: knowledges and
templates now report the same 564 centre / 24px offset as api-tokens.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The size-4 sweep stripped four icons the sweep had no right to touch, and they
rendered at lucide's default 24px: the avatar's UserIcon and the theme
switcher's Monitor/Sun/Moon in the sidebar user menu.
The sweep assumed every listed container sizes its icons at any depth. Only
Button/Toggle/CommandItem do, via [&_svg]. SidebarMenuButton and DropdownMenuItem
use [&>svg], which reaches a DIRECT child only — and these four sit deeper, inside
an AvatarFallback and inside TabsTrigger, so nothing sized them.
Found by measuring rendered icon width across every route with the menus open,
not by re-reading the selectors: the avatar icon was the only one a closed-menu
pass could see. Verified after: no icon inside a button or menu row renders over
16px except EmptyMedia's, which is meant to be large.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>