DM-55470: Add execution_error to Times Square client schemas - #631
DM-55470: Add execution_error to Times Square client schemas#631jonathansick wants to merge 13 commits into
Conversation
Adopt the additive DM-55470 `execution_error` contract in `@lsst-sqre/times-square-client`: a new `ExecutionErrorSchema`, the field on `HtmlStatusSchema` and `HtmlEventSchema`, and failed-state mock fixtures for the slices that build the terminal UI on top. Key decisions: - `code` is parsed as `z.string()`, not an enum, so codes introduced by newer Times Square deployments still validate; the build-time-known values ship as the exported `EXECUTION_ERROR_CODES` const plus a `KnownExecutionErrorCode` type. - `execution_error` uses `.nullable().default(null)`, which accepts the key present, explicitly null, or absent and normalizes all three to a non-optional `ExecutionError | null` on the output type. Consumers therefore never see `undefined`, and pre-DM-55470 payloads parse unchanged. - Because the output type is non-optional, every existing `HtmlStatus` and `HtmlEvent` mock literal now carries `execution_error: null`. - Failed fixtures follow the server contract: `available: false` and `html_hash: null` alongside a non-null `execution_error`. Next-iteration notes: - The changeset `.changeset/times-square-execution-error.md` is the package's entry for this branch; later slices should extend that file rather than adding a second `@lsst-sqre/times-square-client` entry. The app needs its own `squareone` changeset. - `packages/times-square-client/openapi.json` is still the pre-DM-55470 vendored spec — re-vendoring is a separate task on this PRD. - `mockHtmlStatusFailed` / `mockHtmlEventFailed` / `mockExecutionError` are exported from the package root, ready for the `useHtmlStatus` terminal-polling and SSE abort-on-failure slices. - Local-only: `.playwright-mcp/*.yml` scratch artifacts make `pnpm run prettier:yaml` fail in this working copy. They are gitignored and unrelated to this branch. Closes #621
🦋 Changeset detectedLatest commit: 2ccb629 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Extend the Times Square SSE transport with optional `maxReconnectAttempts` and `reconnectBackoffMs` options so a subscription can stop retrying a persistently unreachable endpoint, aborting the stream and reporting a terminal `SseConnectionFailedError` through `onError`. This replicates in the package the bounded-reconnect behavior PR #610 added inline to `TimesSquareHtmlEventsProviderClient`, ahead of that provider migrating onto this transport. Key decisions: - The terminal signal is a dedicated `SseConnectionFailedError` subtype delivered through the existing `onError` callback (rather than a new callback), so a consumer distinguishes it by `instanceof` alongside the non-fatal `SseInvalidEventError`. It carries the last underlying error as `cause` and the failure count as `attempts`. - The per-attempt connection `Error` is still reported before the terminal error, so default behavior is unchanged and a consumer that throttles capture to the first error keeps seeing the raw cause. - Termination throws from `onerror` after aborting: fetch-event-source reschedules a reconnect on any returned value, and only a throw stops it. The resulting promise rejection is swallowed via a `connectionFailed` flag so the failure is not reported twice. - The failure counter resets on a successful `onopen`, making `maxReconnectAttempts` a bound on *consecutive* failures rather than on the subscription's lifetime. - Both options default to unset: no bounding and no backoff, so the transport's existing retry behavior and all existing consumers are unaffected. Next-iteration notes: - #626 migrates `TimesSquareHtmlEventsProviderClient` onto this transport with `{ maxReconnectAttempts: 5, reconnectBackoffMs: 1000 }`, which reproduces its current 5-attempt / 1s-linear-backoff behavior; drive `connectionFailed` off `error instanceof SseConnectionFailedError` and keep the once-per-subscription Sentry capture on the first error. - Extend `.changeset/times-square-execution-error.md` rather than adding a second `@lsst-sqre/times-square-client` changeset entry. Closes #622
Both html-status query option builders now derive `refetchInterval` from
the cached status, returning `false` once `execution_error` is non-null so
a failed notebook no longer polls Times Square once per second forever.
`useHtmlStatus` surfaces the failure as a new `executionError` field.
Key decisions:
- The interval helper takes a structural `{ state: { data } }` snapshot
rather than TanStack's `Query`, so one helper serves both builders
despite their different query key types.
- Recovery is left to cache invalidation: a re-run (later slice)
invalidates the query, which clears the cached error and resumes the
unchanged 1 s cadence.
- The new hook test asserts request counts against real timers (~2.5 s
observation window) instead of fake timers, which react-query's
interval plumbing and RTL's `waitFor` do not compose cleanly with.
Next-iteration notes:
- `useHtmlStatus` consumers in `apps/squareone` do not yet read
`executionError`; wiring the failed-state UI is a later slice.
- Changeset entry `.changeset/times-square-execution-error.md` extended
in place; keep appending there for further client changes.
Closes #623
Extend the auto-abort condition in subscribeToHtmlEvents so a validated event carrying a non-null execution_error also completes the subscription. A failed run reports execution_status 'complete' with no html_hash, so the previous success-only condition left the SSE stream open indefinitely after a terminal failure. Key decisions: - The terminal event is delivered to onEvent before onComplete fires and the stream aborts, so a consumer can read the failure off that event rather than needing a separate signal. - autoAbortOnComplete: false disables both terminal conditions (success and failure), keeping the opt-out a single all-or-nothing switch. - Failure is keyed on execution_error being non-null alone, not on execution_status, since the status is 'complete' for failed runs and the error object is the authoritative terminal signal. Next-iteration notes: - None. The changeset .changeset/times-square-execution-error.md gained a paragraph for this change; keep extending that same file for further @lsst-sqre/times-square-client work on this branch. Closes #624
Adds the `DELETE /v1/pages/{page}/html` client functions and the package's
first react-query mutation, so a terminal `execution_error` can be cleared
by requesting a fresh notebook execution.
Key decisions:
- Two client call shapes, `deletePageHtml(baseUrl, pageName, params)` and
`deleteHtmlByUrl(htmlUrl, params)`, mirroring
`fetchHtmlStatus`/`fetchHtmlStatusByUrl`. ExecStats holds a fully-formed
`html_url` rather than a page name, so the by-URL variant lets it drop
its raw `fetch(..., { method: 'DELETE' })` without reconstructing a page
name and base URL.
- Mutation options live in a new `mutation-options.ts` next to
`query-options.ts`, and take the `QueryClient` as their first argument
(a mutation's cache side effects need a client, and the factory has no
React context to read one from). This is the pattern for future
mutations in the monorepo.
- `onSuccess` invalidates the whole `['times-square', 'html-status']` key
prefix rather than one exact key. That covers both `htmlStatusForPage`
and `htmlStatusByUrl` shapes, which matters because a consumer holding
an `html_url` cannot reconstruct the exact key of the html-status query
it needs to refresh. The over-invalidation is bounded — html-status
queries poll every second anyway — and the alternative (deriving the
status URL from the html URL by string surgery) is fragile.
Next-iteration notes:
- Consumers should use the `useRerunPage(options?)` hook from
`@lsst-sqre/times-square-client`: it returns `{ rerunPage,
rerunPageAsync, isPending, isError, error, reset }`, where `rerunPage`
takes either `{ pageName, params?, baseUrl? }` or `{ htmlUrl, params? }`
(the `RerunPageVariables` union). The hook fills in the
repertoire-discovered base URL, so `baseUrl` is only needed to override.
- #627 (ExecStats Recompute): call `rerunPage({ htmlUrl:
htmlEvent.htmlUrl })` and drive the existing failure UI from `isError`
rather than checking `response.ok` by hand.
- #628 (viewer Re-run button): the viewer polls by
`htmlStatusUrl`, and the mutation's prefix invalidation reaches that key
shape too, so no extra wiring is needed to resume polling.
- `timesSquareKeys.rerunPage()` is the mutation key if a consumer needs
`useIsMutating` instead of a local `isPending`.
Closes #625
TimesSquareHtmlEventsProviderClient no longer carries its own SSE transport: it subscribes through the package's subscribeToHtmlEvents with the bounded-reconnect options and builds its URL with createHtmlEventsUrl, so events are schema-validated by the package and the context can expose the DM-55470 executionError. Key decisions: - connectionFailed is driven by the package's terminal SseConnectionFailedError; the Sentry capture stays gated on the first error of a subscription (the raw connection error, i.e. the wrapper's cause) so the terminal signal doesn't produce a second, duplicate event. - The page query string is turned back into a params record for createHtmlEventsUrl. An empty query string now yields a bare URL rather than a trailing "?", which is the only URL-shape change. - MAX_SSE_RECONNECT_ATTEMPTS is no longer exported; the provider tests assert the 5-attempt / 1000 ms options passed to the transport instead. - @microsoft/fetch-event-source is dropped from the squareone app's dependencies (the package owns it now). The lockfile edit is surgical — only the app importer entry — because a full `pnpm install` here rewrote unrelated `libc:` fields; `pnpm install --frozen-lockfile` passes. Next-iteration notes: - TimesSquareHtmlEventsContextValue now requires executionError (ExecutionError | null); any new fake context value must supply it (the dynamic-import loading fallback and ExecStats.test.tsx already do). - ExecStats.stories.tsx declares its own local HtmlEventContextProps that omits connectionFailed/executionError; it type-checks today but should be switched to TimesSquareHtmlEventsContextValue if a story needs either. - #628 consumes executionError from useHtmlStatus, not from this context; the context field is for the panel/exec-stats path. Closes #626
ExecStats no longer issues its own raw `DELETE fetch` against the SSE
event's `html_url`; it calls `useRerunPage`'s `rerunPageAsync` with the
by-URL variables shape, so the panel's Recompute and the execution-error
re-run path share one transport, one credentialed request, and one
html-status cache-invalidation policy. This removes the last raw DELETE
call in the app.
Key decisions:
- Used `rerunPageAsync` in a try/catch rather than an effect on the
hook's `error`, keeping the Sentry capture (`site:
'times-square-recompute'`) at the call site and guaranteeing exactly
one report per failed attempt.
- Dropped the local `recomputeFailed` state in favour of the mutation's
`isError`; a new attempt resets it, so a successful retry clears the
alert without any explicit bookkeeping (covered by a new test).
- Called `useRerunPage()` with no options: the by-URL shape never
consults the discovered base URL, and the hook's discovery query is
disabled without a repertoire URL, so threading one through would add
a ConfigProvider dependency for no behavioral gain.
- Disabled the button while the request is in flight, now that pending
state is available, to prevent overlapping soft-deletes.
Next-iteration notes:
- `ExecStats` now requires a `QueryClientProvider` ancestor; the app has
one at the root layout and Storybook has one in its preview decorator,
and `ExecStats.test.tsx` wraps its renders in a per-test QueryClient
with `mutations: { retry: false }`.
- Package soft-delete responses are schema-parsed, so any new test
stubbing a successful DELETE must return a JSON body with `html_url`
and `html_events_url` — a bare 202 with no body now fails.
- `ExecStats.stories.tsx` still declares its local `HtmlEventContextProps`
omitting `connectionFailed`/`executionError`; untouched here because no
story needs either field yet.
Closes #627
A notebook that fails to execute no longer leaves the viewer stuck on "Loading..." forever: when `useHtmlStatus` reports a terminal `execution_error`, `TimesSquareNotebookViewerClient` renders a new `NotebookExecutionError` panel carrying the API's own title and message, with a Re-run button that soft-deletes the page instance through `useRerunPage` so polling resumes and a subsequent render displays HTML. Key decisions: - The panel renders the API's `title`/`message` verbatim and authors no per-code copy; `code` only picks the lucide icon and accent tone, with an unrecognized code (from a newer Times Square) falling back to the generic red treatment rather than rendering nothing. - Re-ran by URL using the *page metadata's* parameter-free `html_url` plus the viewer's own params, mirroring exactly how the same metadata's `html_status_url` is parameterized for polling. Using the status response's `html_url` instead would have risked duplicated query parameters, since the package appends rather than replaces them. - Mirrored ExecStats' `rerunPageAsync` + try/catch shape so the Sentry capture stays at the call site (tagged `site: times-square-rerun`), and reused the mutation's own `isError`/`isPending` instead of local state. - Put `role="alert"` on the panel container (house style, cf. TokenCreationErrorDisplay) and left the re-run failure message as a plain paragraph inside it, avoiding a nested live region. - Memoized the params object so the re-run callback keeps a stable identity; the query key is hashed structurally, so polling is unaffected. Next-iteration notes: - `TimesSquareNotebookViewerClient` now needs a `QueryClientProvider` ancestor for the mutation as well as the queries; the app root has one and the new test supplies its own. - The test drives the real hooks against a stubbed `global.fetch` routing page metadata / htmlstatus / DELETE. Any new case stubbing a successful DELETE must return the `DeleteHtmlResponse` JSON body. - The polling-stop assertion waits 1.5 s of real time (the package's poll cadence is 1 s); the legacy-shape test needs a 3 s `findBy` timeout for the same reason. - Dev mock API routes still return the pre-DM-55470 shape, so the failure state is not yet exercisable in `pnpm dev` — that remains the dev-route task's job. Closes #628
Extend the development-mode Times Square mocks so the failure → error panel → re-run → loading flow can be driven without a live Times Square: `?a=3` now reports a terminal `timeout` execution error from both the htmlstatus and htmlevents routes, and the mock html route answers DELETE with the spec's `DeleteHtmlResponse` instead of a 405. Key decisions: - A mocked re-run is stateful: the DELETE records the page instance in a small in-memory store (`timesSquareExecutionStore`) that reports it as re-executing for 15 s, so the viewer really does drop back to its loading state and resume polling before the instance settles back to its `?a=`-driven outcome. A stateless DELETE would return the error immediately and never exercise the loading half of the flow. - Browser validation surfaced that the dev page-metadata mocks (`/v1/pages/:page`, `/v1/github/:path`, PR preview) had drifted from the `Page` schema — string `description`, missing `date_added`, `uploader_username`, `html_events_url`, `github` — so the schema-parsing client rejected them and the dev notebook viewer was stuck on "Loading…" regardless of `?a=`. They now share one `buildMockPage` builder that emits a schema-valid payload, without which none of this task's acceptance criteria are observable. - Existing magic values keep their behavior: `a=1` succeeds, `a=2` stays pending. Next-iteration notes: - The re-run window is 15 s (`RERUN_WINDOW_MS`); dev-only, in-memory, and reset per server restart. - The Times Square dev page renders end-to-end again; use `/times-square/github/lsst-sqre/times-square-demo/demo?a=3` to see the execution-error panel. Closes #629
Refresh packages/times-square-client/openapi.json from the Times Square 0.25.0 release, the first tagged release carrying the DM-55470 execution_error contract, so the vendored spec documents the API surface the client's Zod schemas already model. Key decisions: - Fetched from the versioned docs URL (https://times-square.lsst.io/v/0.25.0/_static/openapi.json) rather than via `pnpm fetch-openapi`: the latest docs build currently serves a dev-versioned spec (0.24.2.devNN+g...) from a docs-tagging quirk. The package's fetch-openapi script is deliberately left unchanged — the drift checker derives the live URL from it, and it should keep pointing at the latest docs build. - The 0.25.0 spec is byte-identical to the current latest docs spec apart from info.version, which the drift checker excludes from comparison, so times-square-client now reports "version-only" (green) instead of "drift". - Only the htmlstatus response changed: HtmlStatus gained an optional-nullable execution_error backed by the new HtmlExecutionError and NotebookExecutionErrorCode schemas, matching ExecutionErrorSchema and EXECUTION_ERROR_CODES exactly. No client code change is needed. - Extended the existing times-square-execution-error changeset rather than adding a new one; the re-vendoring is part of the same execution_error adoption already described there. Next-iteration notes: - The SSE html/events payload is still not schema-modeled upstream (the endpoint declares an untyped text/event-stream response), so HtmlEventSchema's execution_error remains client-side only. PRD acceptance criterion "execution_error on ... the SSE event schemas" cannot be satisfied from the vendored spec. - `pnpm run check-openapi-drift` still exits 1 on semaphore-client, a pre-existing failure unrelated to this task: the committed and live specs differ only in the element order of two `examples` arrays (["58","57","56","59"] vs ["58","56","57","59"]), which look like a serialized Python set. Needs either a re-vendor or an order-insensitive comparison for examples arrays; tracked separately. Closes #630
jonathansick
left a comment
There was a problem hiding this comment.
Stoker review — PR #631 (review pass 1)
Well-structured adoption of the terminal execution_error contract with strong backward-compatibility handling and thorough tests across the package transport, app providers, viewer UI, and dev mocks; verified against the vendored fetch-event-source semantics. One edge-case robustness bug in the error panel's code-to-presentation lookup and two informational nits — nothing blocking.
Findings
-
[warning / correctness]
apps/squareone/src/components/TimesSquareNotebookViewer/NotebookExecutionError.tsx(L45–63): The presentation lookuppresentations[executionError.code] ?? genericPresentationuses a plain object literal, so a code equal to anObject.prototypeproperty name (e.g.constructor,toString,valueOf) resolves to the inherited value rather thanundefined. The??fallback is bypassed,Iconandtonedestructure toundefined, and rendering<Icon />throws — defeating the component's own unknown-code fallback for exactly the forward-compatibility case it documents. The code value is API-supplied, so this is unlikely but real.
Suggested fix: guard withObject.hasOwn(presentations, executionError.code)(or a null-prototype record /Map), and add a test/story with codeconstructorto lock in the fallback. -
[info / maintainability]
apps/squareone/src/lib/mocks/timesSquareExecutionStore.ts(L25–37):mockExecutionErrorduplicates, verbatim, the fixture the package already exports from@lsst-sqre/times-square-clientmock-data (the file even imports theExecutionErrortype from the package). The two copies can silently drift.
Suggested fix: import the package'smockExecutionErrorinstead of redefining it, or intentionally diverge and note why. -
[info / compatibility]
packages/times-square-client/src/mutation-options.ts(L10–14):mutationOptionsis imported from@tanstack/react-query, but that helper was added partway through the v5 line (present in the installed 5.90.20). The package's peerDependencies still declare^5, so a consumer on an early 5.x would fail at import.
Suggested fix: verify the release that introducedmutationOptionsand raise the peer range floor accordingly (e.g.^5.83.0).
{
"stoker_review_version": 1,
"pr_number": 631,
"blocking": false,
"summary": "Well-structured adoption of the terminal execution_error contract with strong backward-compatibility handling and thorough tests across the package transport, app providers, viewer UI, and dev mocks; verified against the vendored fetch-event-source semantics. One edge-case robustness bug in the error panel's code-to-presentation lookup and two informational nits — nothing blocking.",
"findings": [
{
"id": "f1",
"severity": "warning",
"category": "correctness",
"file": "apps/squareone/src/components/TimesSquareNotebookViewer/NotebookExecutionError.tsx",
"line_start": 45,
"line_end": 63,
"summary": "The presentation lookup `presentations[executionError.code] ?? genericPresentation` uses a plain object literal, so a code equal to an Object.prototype property name (e.g. 'constructor', 'toString', 'valueOf') resolves to the inherited value rather than undefined. The `??` fallback is bypassed, `Icon` and `tone` destructure to undefined, and rendering `<Icon />` throws — defeating the component's own unknown-code fallback for exactly the forward-compatibility case it documents. The code value is API-supplied, so this is unlikely but real.",
"suggested_fix": "Guard the lookup with an own-property check, e.g. `const presentation = Object.hasOwn(presentations, executionError.code) ? presentations[executionError.code] : genericPresentation;`, or build `presentations` as a `Map` / `Object.create(null)`-based record. Consider adding a story/test case with code 'constructor' to lock in the fallback."
},
{
"id": "f2",
"severity": "info",
"category": "maintainability",
"file": "apps/squareone/src/lib/mocks/timesSquareExecutionStore.ts",
"line_start": 25,
"line_end": 37,
"summary": "`mockExecutionError` here duplicates, verbatim, the fixture the package already exports from `@lsst-sqre/times-square-client` mock-data (the file even imports the ExecutionError type from the package). The two copies can silently drift.",
"suggested_fix": "Import and re-export the package's `mockExecutionError` instead of redefining it, or intentionally diverge the dev-mock copy and note why."
},
{
"id": "f3",
"severity": "info",
"category": "compatibility",
"file": "packages/times-square-client/src/mutation-options.ts",
"line_start": 10,
"line_end": 14,
"summary": "`mutationOptions` is imported from `@tanstack/react-query`, but that helper was only added partway through the v5 line (present in the installed 5.90.20; believed introduced around 5.83 — needs verification). The package's peerDependencies still declare `^5`, so a consumer on an early 5.x would fail at import. Low stakes while squareone (pinned ^5.90.20) is the only consumer.",
"suggested_fix": "Verify the release that introduced `mutationOptions` and raise the `@tanstack/react-query` peer range floor accordingly (e.g. `^5.83.0`)."
}
]
}- Guard the NotebookExecutionError presentation lookup with Object.hasOwn so an API-supplied code matching an Object.prototype member (constructor, toString, ...) takes the generic fallback instead of an inherited value, with a unit test locking in the fallback. - Re-export the package's mockExecutionError fixture from the dev execution store instead of duplicating it verbatim. - Raise the @tanstack/react-query peer floor to ^5.82.0, the first release exporting mutationOptions (absent in 5.81.5), and note it in the changeset.
jonathansick
left a comment
There was a problem hiding this comment.
Stoker review — PR #631 (review pass 2, scoped to e8ade4f)
The fixup commit correctly addresses all three findings from the first review: the Object.hasOwn presentation-lookup guard lands with thorough prototype-name tests (constructor, toString, valueOf, __proto__, plus a consistency assertion against ordinary unknown codes); the dev-mock mockExecutionError is deduplicated onto the package's identical fixture export; and the @tanstack/react-query peer floor is raised to ^5.82.0 with a matching devDependency, lockfile entry, and changeset note. No new defects introduced.
{
"stoker_review_version": 1,
"pr_number": 631,
"blocking": false,
"summary": "The fixup commit correctly addresses all three findings from the first review: the Object.hasOwn presentation-lookup guard lands with thorough prototype-name tests, the dev-mock mockExecutionError is deduplicated onto the package's identical fixture export, and the @tanstack/react-query peer floor is raised to ^5.82.0 with a matching devDependency, lockfile entry, and changeset note. No new defects introduced.",
"findings": []
}ExecStats reported a failed notebook run as a success — Times Square sends `execution_status: 'complete'` with a non-null `execution_error`, so the sidebar read "Computed … in 14.2 seconds." while the viewer showed the failure panel. It now reads the events context's `executionError` and renders a failure summary instead. Key decisions: - The `executionError` check precedes the `executionStatus === 'complete'` branch, so it is independent of that branch's `dateFinished` guard: a failed run that settles without a finish time still gets a summary, just without the timestamp line. - Copy follows NotebookExecutionError's policy — the API's `title` is rendered verbatim and Squareone authors no per-code wording. Nothing is keyed by `code` here, so no presentation lookup was needed. - Duration is dropped entirely in the failed state; it describes work that never produced a result. - The summary is plain text, not a live region: the viewer's failure panel already announces the same failure as an alert, and a second one would double-announce. The `recomputeFailed` alert is untouched. - ExecStats.stories.tsx now types its args as `TimesSquareHtmlEventsContextValue` rather than a hand-rolled subset, so a story cannot drift from the context contract. Next-iteration notes: - None. Closes #634
jonathansick
left a comment
There was a problem hiding this comment.
Stoker review — PR #631 (review pass 3, scoped to 6a65631)
The follow-up commit (task #634) is correct and well-tested: the executionError branch precedes the complete branch so a failed run is never reported as computed, the no-finish-time case degrades gracefully, the recompute path is preserved and covered from the failed state, and the stories are now typed against the real context contract. One non-blocking token-hygiene note; mergeable as-is.
Findings
- [info / clarity]
apps/squareone/src/components/TimesSquareGitHubPagePanel/ExecStats.module.css(L14–19): the new.failureclass usesvar(--rsd-color-red-900), but that custom property is not defined anywhere in the repo — the rubin-style-dictionary red scale tops out at 800, so thecolordeclaration silently falls back to the inherited text color. This mirrors the pre-existing.errorclass in the same file and other usages across the codebase (TokenDetailsView.module.css, squaredTextInput), so it is an established latent token gap rather than a regression introduced here.
Suggested follow-up (not required for this PR): either switch these usages to the defined--rsd-color-red-600("Dark red for solid backgrounds and text") or add a 900 shade to the red scale in rubin-style-dictionary, then sweep the existing red-900 usages.
{
"stoker_review_version": 1,
"pr_number": 631,
"blocking": false,
"summary": "The follow-up commit 6a656319 (task #634) is correct and well-tested: the executionError branch precedes the 'complete' branch so a failed run is never reported as computed, the no-finish-time case degrades gracefully, the recompute path is preserved and covered from the failed state, and the stories are now typed against the real context contract. One non-blocking token-hygiene note; mergeable as-is.",
"findings": [
{
"id": "f1",
"severity": "info",
"category": "clarity",
"file": "apps/squareone/src/components/TimesSquareGitHubPagePanel/ExecStats.module.css",
"line_start": 14,
"line_end": 19,
"summary": "The new .failure class uses var(--rsd-color-red-900), but that custom property is not defined anywhere in the repo — the rubin-style-dictionary red scale tops out at 800 (packages/rubin-style-dictionary/src/color/red.yaml), so the color declaration silently falls back to the inherited text color and the failure headline is not actually red. This mirrors the pre-existing .error class in the same file and other usages across the codebase (TokenDetailsView.module.css, squared TextInput), so it is an established latent token gap rather than a regression introduced here.",
"suggested_fix": "As a follow-up (not required for this PR): either switch these usages to the defined --rsd-color-red-600 (documented as 'Dark red for solid backgrounds and text') or add a 900 shade to the red scale in rubin-style-dictionary, then sweep the existing red-900 usages."
}
]
}Every source usage of --rsd-color-red-900 pointed at a custom property
the rubin-style-dictionary red scale never defines (it ends at 800), so
each of those color declarations was dropped and error text silently
inherited the surrounding body color. All nine usages now resolve to
--rsd-color-red-600 ("Dark red for solid backgrounds and text").
Key decisions:
- Adopted the existing documented token rather than adding a red-900
shade to rubin-style-dictionary; a richer red scale is a future
design-system decision, not a prerequisite for making these styles
render at all.
- Picked 600 over 700/800 because it is the scale's documented "solid
backgrounds and text" step and pairs with the red-500 borders and
red-100 backgrounds these rules already carry. Contrast checks:
7.17:1 on white and 6.16:1 on red-100, both clearing WCAG AA.
- Confirmed in Storybook that the TextArea error variant computes to
rgb(173, 25, 25) while --rsd-color-red-900 resolves to the empty
string, verifying both the bug and the fix.
Next-iteration notes:
- The only remaining "red-900" strings in the repo are in the historical
planning note .claude/tasks/2025-10-fix-css-variables.md, which is a
record of past work and not app or package source.
- Built storybook-static artifacts still embed the old value; they are
generated output and regenerate on the next build.
Closes #635
jonathansick
left a comment
There was a problem hiding this comment.
Stoker review — PR #631 (review pass 4, scoped to 2ccb629)
The red-900 → red-600 sweep (task #635) is complete and correct: all nine source usages of the undefined --rsd-color-red-900 are replaced with the defined --rsd-color-red-600 (#ad1919), a repo-wide grep finds no remaining source references (only the historical planning note and generated storybook-static output), only color declarations were touched (the adjacent red-500 borders and red-100 backgrounds are preserved), and the new @lsst-sqre/squared patch changeset plus the extended app changeset accurately describe the fix. No findings.
{
"stoker_review_version": 1,
"pr_number": 631,
"blocking": false,
"summary": "The red-900 to red-600 sweep (task #635) is complete and correct: all nine source usages of the undefined --rsd-color-red-900 are replaced with the defined --rsd-color-red-600 (#ad1919), no source references remain, only color declarations were touched (red-500 borders and red-100 backgrounds preserved), and the new squared patch changeset plus the extended app changeset accurately describe the fix.",
"findings": []
}
Summary
Adopt the additive Times Square
execution_errorcontract (DM-55470) in@lsst-sqre/times-square-client: a newExecutionErrorSchema(code,title,message) plus the field onHtmlStatusSchemaandHtmlEventSchema.Stay backward-compatible with pre-DM-55470 Times Square deployments —
execution_erroris optional-nullable and defaults tonull, so payloads that omit the key entirely still parse and normalize tonull.codeis parsed as a plain string rather than an enum for forward compatibility, with the build-time-known values (timeout,jupyter_error,unknown,result_unavailable) exported as theEXECUTION_ERROR_CODESconst.Add failed-state mock fixtures (
mockExecutionError,mockHtmlStatusFailed,mockHtmlEventFailed) for the later slices that build the terminal UI on top.Treat a reported
execution_erroras terminal in both data paths. The html-status query option builders deriverefetchIntervalfrom the cached status and stop polling once the error is non-null (useHtmlStatussurfaces it as a newexecutionErrorfield), andsubscribeToHtmlEventsnow auto-aborts on an event carrying a non-nullexecution_erroras well as on a successfulcompleteevent — a failed run reportscompletewith nohtml_hash, so the old success-only condition left the SSE stream open forever. The terminal event still reachesonEventbeforeonCompletefires, andautoAbortOnComplete: falsedisables both conditions.Give
subscribeToHtmlEventsoptional bounded-reconnect options (maxReconnectAttempts,reconnectBackoffMs): once the configured number of consecutive connection failures is reached the stream is aborted and a terminalSseConnectionFailedErroris reported throughonError, so a consumer can drive a connection-failed UI state and a once-only Sentry capture. Both options default to unset, leaving today's unbounded retry behavior unchanged.Add the way out of that terminal state:
deletePageHtml/deleteHtmlByUrlissue Times Square'sDELETE /v1/pages/{page}/htmlsoft delete, and the package's first react-query mutation (rerunPageMutationOptions, plus theuseRerunPagehook) wraps them. On success it invalidates the['times-square', 'html-status']key prefix — covering both thehtmlStatusForPageandhtmlStatusByUrlkey shapes — so the cachedexecution_erroris dropped and polling resumes at its normal cadence.Move Squareone's
TimesSquareHtmlEventsProvideronto that package transport: the client provider drops its inlineHtmlEventtype and directfetchEventSourceusage in favor ofsubscribeToHtmlEvents+createHtmlEventsUrlwith the bounded-reconnect options (5 attempts, 1 s linear backoff), so SSE events are schema-validated in one place. TheconnectionFailedalert banner and the once-per-subscription Sentry capture (taggedsite: times-square-sse) behave as before, now driven by the package's terminalSseConnectionFailedError, and the events context gains anexecutionErrorfield for the terminal-UI slices to consume.@microsoft/fetch-event-sourceis no longer an app dependency.Unify both re-run paths on that one mutation: the page panel's Recompute action drops its raw
DELETE fetchforuseRerunPage'srerunPageAsync, so it too sends a credentialed request and invalidates the html-status cache. The failure alert and its Sentry capture (taggedsite: times-square-recompute) are unchanged — the mutation is deliberately Sentry-agnostic, so the app reports from the call site — and the button is now disabled while a request is in flight. No raw DELETE calls remain in the app.Reach the terminal UI state the contract was added for: when
useHtmlStatusreports a non-nullexecution_error,TimesSquareNotebookViewerClient(which serves both the notebook viewer and the GitHub page-panel paths) stops showing an endless "Loading…" and renders a newNotebookExecutionErrorpanel carrying the API's owntitleandmessage. Squareone authors no per-code copy of its own —codeonly selects the panel's icon and accent tone, and a code from a newer Times Square falls back to a generic treatment. The panel's Re-run notebook button soft-deletes the page instance throughuseRerunPage, targeting the page'shtml_urlwith this instance's notebook parameters and display settings; the mutation's invalidation drops the cached failure, so the viewer returns to its loading state, polling resumes, and the next successful render displays the HTML. A failed re-run surfaces an inline message and is captured in Sentry (taggedsite: times-square-rerun).Make that whole flow exercisable in dev mode without a live Times Square: the mock
htmlstatusandhtmleventsroutes gain a failure case on the existing?a=magic-parameter convention (a=3→available: false,html_hash: null, and a realistictimeoutexecution_error;a=1anda=2keep their current behavior), and the mock html route answersDELETEwith the spec'sDeleteHtmlResponseinstead of a 405. A mocked re-run is stateful — it clears the instance's cached outcome for 15 s — so the viewer really does drop back to its loading state and resume polling before the failure returns. The dev page-metadata mocks (/v1/pages/:page,/v1/github/:path, PR preview) had drifted from thePageschema the client parses (stringdescription; missingdate_added,uploader_username,html_events_url,github), which left the dev notebook viewer stuck on "Loading…" regardless of?a=; they now share one schema-valid builder.Re-vendor
packages/times-square-client/openapi.jsonat the Times Square 0.25.0 release, the first tagged release carrying theexecution_errorcontract, so the vendored spec documents the API surface these schemas model. The only API-surface change is onhtmlstatus:HtmlStatusgains an optional-nullableexecution_errorbacked by the newHtmlExecutionError(code,title,message) andNotebookExecutionErrorCode(timeout,jupyter_error,unknown,result_unavailable) schemas — matchingExecutionErrorSchemaandEXECUTION_ERROR_CODESexactly, so no client code changes. The SSEhtml/eventspayload is not schema-modeled upstream (the endpoint declares an untypedtext/event-streamresponse), soHtmlEventSchema'sexecution_errorstays client-side only.Stop the page panel from reporting a failed run as a success. Times Square reports a failure as
execution_status: 'complete'with a non-nullexecution_error, soExecStatskept reading "Computed … in N seconds." in the sidebar while the viewer showed the failure panel. It now reads the events context'sexecutionErrorand, when it is non-null, renders a failure summary carrying the API's owntitle(the same verbatim-copy policy as the viewer panel — no Squareone-authored per-code wording), followed by when the run finished and no duration phrasing. The check precedes the completed branch and itsdateFinishedguard, so a failed run that settles without a finish time still gets a summary, just without the timestamp line. The Recompute button stays — it is the recovery path — and withexecutionError: nullthe summary is byte-identical to before.Make that error text actually render red. Every source usage of
--rsd-color-red-900— the newExecStatsfailure/error summaries, the SSE connection alert, the token detail page's delete-error and error panels, and squared'sTextInput/TextArea/Selecterror variants — pointed at a custom property rubin-style-dictionary never defines (its red scale ends at 800), so eachcolordeclaration was dropped and the text silently inherited the surrounding body color. All nine now use--rsd-color-red-600("Dark red for solid backgrounds and text"), which pairs with the red-500 borders and red-100 backgrounds those rules already carry and clears WCAG AA on both white (7.17:1) and red-100 (6.16:1). Adopting the documented token is the fix; adding a red-900 shade is left as a future design-system decision.Subsequent PRD #620 tasks append to this PR.
Validation steps
HtmlStatusSchemaand parse a payload withexecution_errorpresent, one with it explicitlynull, and one with the key absent — confirm all three parse and the absent case yieldsexecution_error: null.HtmlEventSchema.execution_errorcarrying acodevalue not inEXECUTION_ERROR_CODES(e.g.some_future_code) and confirm it is accepted rather than rejected.mockHtmlStatusFailed/mockHtmlEventFailedfrom the package root and confirm they reflect the server contract (available: false,html_hash: null).execution_error, watch the network panel and confirm the htmlstatus request stops repeating once the error arrives.html/eventsSSE connection closes once the failure event arrives instead of staying open, and that the failing event still reached the UI before teardown.{ autoAbortOnComplete: false }and confirm the stream stays open after both a failed and a successful terminal event.subscribeToHtmlEventsagainst an unreachable events URL with{ maxReconnectAttempts: 3, reconnectBackoffMs: 1000 }and confirm in devtools that the browser stops retrying after the third failure, with roughly 1 s / 2 s gaps between attempts, and thatonErrorreceives a finalSseConnectionFailedError.useRerunPage()'srerunPage({ pageName, params })on a parameterized page and confirm in the network panel that a singleDELETE /times-square/api/v1/pages/<page>/html?<params>goes out carrying that instance's parameters.rerunPage({ htmlUrl })with a fully-formedhtml_url(the shape ExecStats holds) and confirm the same DELETE is issued against that URL.execution_error: null, and the 1 s cadence resumes.isErrorwith aTimesSquareErrorrather than silently doing nothing.html/eventsrequest still carries the full query string (bothts_-prefixed display settings and notebook parameters), and that the exec-stats sidebar fills in as events arrive.site: times-square-sse.DELETEgoes out against the instance'shtml_url, that it carries credentials, that the button is disabled while it is in flight, and that the notebook re-executes.site: times-square-recomputeis captured; then restore the backend, click Recompute again, and confirm the alert clears on the successful retry.DELETE .../html?<params>goes out carrying this page instance's notebook parameters and display settings, the viewer drops back to the loading state, htmlstatus polling resumes, and the notebook renders once execution succeeds.site: times-square-rerunis captured.execution_errorkey), open a page that is still executing and one that renders successfully, and confirm the viewer behaves exactly as it does today.pnpm dev --filter squareone, open/times-square/github/lsst-sqre/times-square-demo/demo?a=3, and confirm the execution-error panel renders with the timeout title and message (the page metadata and notebook tree load normally).DELETE .../pages/demo/html?a=3&...returns 200, the viewer switches to "Loading…", htmlstatus polling resumes, and the error panel returns about 15 seconds later.?a=(success) and with?a=2(pending) and confirm the notebook iframe renders and the loading state persists, respectively — unchanged from before.pnpm run check-openapi-driftand confirmtimes-square-clientreports in sync (a version-only difference against the latest docs build, which currently serves a dev-versioned spec) rather than DRIFT.packages/times-square-client/openapi.jsonreportsinfo.version0.25.0and definesexecution_erroron theHtmlStatusschema.pnpm dev --filter squareone, open/times-square/github/lsst-sqre/times-square-demo/demo?a=3, and confirm the sidebar summary reads the failure title and when it failed — not "Computed … in N seconds."?a=1and confirm the sidebar still reads "Computed … in N seconds." and?a=2still shows "Computing…".?a=3page, inspect the sidebar failure summary and the SSE connection alert and confirm the text renders dark red (#ad1919) rather than the default body color.pnpm storybook --filter @lsst-sqre/squared), open the TextInput / TextArea validation-state stories and the Select "With Form Field Error" story and confirm the error variants' text is dark red, matching their red border, instead of ordinary body text.References