Redesign the Pulse UI onto the LotusGuild Terminal Design System #35

Merged
jared merged 4 commits from feature/tds-redesign into main 2026-09-08 22:23:24 -04:00
Owner

Summary

Pulse's UI was a single hand-rolled static page — public/index.html, 3161 lines, ~830 lines of
inline <style>, ~2000 lines of inline JS and 58 inline onclick/onchange handlers — served by
express.static('public'), with public/base.js a git symlink into a sibling web_template
checkout that only happens to exist on CT122.

This PR replaces it with six server-rendered EJS pages inside the shared LotusGuild chrome, a
vendored copy of the design system, a strict nonce-based CSP, and a small shared frontend runtime
(window.Pulse) that page modules plug into. No API behaviour changes: /api/* responses are
byte-identical to main (verified, see below), and the commandHistory / pulse_executionView
localStorage keys are preserved.

Feature parity was mandatory and is complete; the redesign also picks up a set of behavioural
improvements that fall out of the design system (see Behavioural notes).

What changed by area

Server (server.js, lib/)

  • helmet with a strict nonce CSP: default-src 'self', script-src 'self' 'nonce-…',
    script-src-attr 'none', object-src 'none', frame-ancestors 'none';
    PULSE_CSP_REPORT_ONLY=1 switches to report-only with report-uri /csp-report.
  • Static mounts moved to /web_template and /assets (the root public/ mount is gone), with
    maxAge: 1h and ?v=<pkg version>-<web_template sha> cache busting.
  • Six page routes driven by a PAGES table in lib/nav.js; lib/render.js does two-step EJS
    rendering (page view → bodylayout.ejs) and renders themed HTML error pages for 401/403/404.
  • lib/pageauth.js: the users upsert extracted out of authenticateSSO and reused by page auth,
    which returns themed HTML (not JSON) for 401/403. /api/* auth is untouched.
  • PULSE_DEV_READONLY=1 (alias DISABLE_BACKGROUND_JOBS=1) disables every background writer —
    stale-execution recovery, execution cleanup, the scheduled-command processor, the stale-worker
    sweep — so a dev instance can safely point at the production database. Each skip is logged.
  • package.json: "start": "node server.js", "main" corrected to server.js; ejs 3.1.10 and
    helmet 8.1.0 pinned in dependencies; lockfile committed.

Design system (public/web_template/)

  • base.css, base.js and a VERSION file are now vendored as real files (no symlink, no
    runtime dependency on a sibling checkout). scripts/sync-web-template.sh <checkout> refreshes
    them and rewrites VERSION. .eslintignore excludes the vendored tree.
  • The vendored layout.ejs fixes upstream's en-dash comment delimiters (which make the upstream
    file unrenderable) and adds a skip link, mobile drawer, theme button, WS status dot,
    "Refreshed: HH:MM:SS" stamp, command palette and keyboard-shortcut modal.

Shared frontend (public/assets/app.js, app.css)

  • window.Pulse: action registry + delegated data-action / data-change-action /
    data-input-action / data-submit-action listeners, event bus, Pulse.confirm /
    Pulse.alertModal (themed, promise-based), formatters, escaping, a 401→reload fetch wrapper,
    the single WebSocket connection, and the single lt.autoRefresh registration (30 s).
  • Page modules register with Pulse.registerPage({ name, init, refresh, onEvent }) and never own
    timers, listeners or the WebSocket.

Pages (views/pages/*.ejs, public/assets/pages/*.js|css)

  • Dashboard, Workers, Workflows, Executions, Quick Command, Scheduler — each an EJS view plus a
    page module confined to its own action prefix (dash:, wk:, wf:, ex:, qc:, sc:) and
    DOM id prefix. Every dynamic string goes through Pulse.esc.

Integration/cleanup (this work package)

  • lib/render.js error page used .lt-alert-danger, which does not exist in base.css → fixed to
    .lt-alert--error (tree-wide grep for single-dash alert classes is now clean).
  • .lt-modal-lg was used by three pages but defined by none (Executions faked it with an
    id-scoped width rule). Added one shared rule in app.css, mirroring base.css's sizing
    conventions and re-capping at its two breakpoints; removed the id-scoped duplicate.
  • .lt-field-error was defined page-locally in workflows.css; moved it plus an .is-invalid
    input state into app.css so every page shares them (wf-input-invalidis-invalid).
  • views/pages/quick.ejs used the non-existent .lt-field-hint.lt-form-hint.
  • Dashboard load failures now surface a toast (once per outage) instead of only a console error.
  • Deleted public/index.html and the public/base.js symlink.
  • README: rewritten Styling & Layout section (vendored design system, sync script, VERSION), a new
    Web UI section (routes, the Pulse.registerPage contract, the CSP rule for contributors), the
    new env vars, and a "Local development against a real database" note.

Screenshots

None — server-rendered pages, verified via curl/jsdom.

Verification performed

Local instance against the production MariaDB with PULSE_DEV_READONLY=1 PORT=8099 HOST=127.0.0.1.
Startup log confirms all four background writers skipped.

Static / CI

Check Result
npx eslint --ext .js . 0 errors (3 pre-existing warnings in server.js)
npx jest 31/31 passed, 1 suite
grep -rnE 'on(click|change|submit|input|keydown)="' views/ public/assets/ empty
grep -rn '<script' views/ | grep -v nonce= empty
grep -rn autoRefresh public/assets/pages/ empty
grep -rnE '\b(confirm|alert)\(' public/assets/pages/ public/assets/app.js empty (one comment)
grep -rnE 'lt-alert-(danger|warning|success|info)' empty

Route matrix — six routes × {admin, employee} = 12 requests, all:
200, exactly 4 × nonce=", exactly one .lt-nav-link active and it points at the current
route, and a content-security-policy header containing script-src-attr 'none'.

/ /workers /workflows /executions /quick /scheduler
admin 200 ✓ 200 ✓ 200 ✓ 200 ✓ 200 ✓ 200 ✓
employee 200 ✓ 200 ✓ 200 ✓ 200 ✓ 200 ✓ 200 ✓

Also: no auth headers → 401 HTML; Remote-Groups: guest403 HTML (themed page, now
rendering the corrected .lt-alert--error); /index.html404; /base.js404;
/api/nope404 JSON ({"error":"Not found"}); all 18 /assets/** and /web_template/*
URLs → 200. With PULSE_CSP_REPORT_ONLY=1: Content-Security-Policy-Report-Only present with
report-uri /csp-report, and no enforcing Content-Security-Policy header.

API byte-compare vs mainmain checked out in a throwaway worktree with the same .env,
started on :8098 and stopped within seconds (before its 60 s scheduler/stale-worker intervals
could fire), responses captured from both ports back-to-back with identical headers:

Endpoint Result
/api/workers identical (371 B)
/api/workflows identical (50 936 B)
/api/executions?limit=5 identical (1 180 B)
/api/scheduled-commands identical (2 B)
/api/user identical (103 B)

Byte-identical, not merely equivalent — no timestamp drift to explain away.

Behavioural QA (jsdom) — each page's live-rendered HTML plus base.js + app.js + its page
module, with fetch stubbed from JSON captured off the running instance:

Suite Result
Integration (6 pages + shell) 40/40
Dashboard + Workers (WP-C) ALL PASS
Workflows (WP-D) 16/17 — the one failure is jsdom refusing location.href navigation, not app code
Executions (WP-E) 56/56 + 6/6 for the ?open= deep link
Quick (WP-F) 26/26
Scheduler (WP-F) 19/19

Every page loads with a clean console and renders its core content from real data. Shell coverage:
ESC closes an open modal, the theme button flips data-theme (and back), Ctrl+K opens the command
palette (ESC closes it), and Pulse.confirm renders a themed dialog, escapes its message and
resolves false on ESC.

WebSocketlt.ws.connect is called with maxRetries: Number.MAX_SAFE_INTEGER and
reconnectDelay: 2000 (upstream's default of 10 retries would abandon live updates permanently
after ~5 minutes of downtime), with a raw-WebSocket fallback if lt.ws is missing. Disconnects
set the header status dot to Disconnected; a reconnect toasts "Live updates reconnected" and
triggers a refresh.

Not verified: no headless browser exists on this machine (chromium, chromium-browser,
google-chrome all absent; no puppeteer/playwright installed), so CSP enforcement was verified
from response headers and by static audit rather than by a real browser refusing a resource. A
manual click-through under Chrome after deploy is worth one pass.

Behavioural notes / improvements

  • Run modal for every workflow. Execute always opens a modal — parameter inputs generated from
    definition.params (with required-field validation and a Dry Run checkbox) when the workflow
    takes parameters, and a plain confirmation when it does not. The old flow used a native
    confirm().
  • Server-driven Manual/Automated tabs on Executions, with live counts, search over
    command/id/workflow, status filter, "Showing X of Y" and Load More.
  • ?open=<execution-id> deep link opens the detail modal directly and strips the parameter
    from the URL — used by cross-page navigation from the Dashboard, Quick Command and Workflows.
  • WebSocket status dot in the header (connecting/connected/disconnected) plus a
    "Refreshed: HH:MM:SS" stamp, so a stale page is now obvious.
  • Themed confirm dialogs everywhere: zero confirm()/alert() calls remain in the frontend.
  • Responsive layout comes from base.css — the tables collapse to labelled cards, the nav
    becomes a drawer, and modals go full-bleed on small screens.
  • Reduced motion respected: the design system's prefers-reduced-motion blocks apply, and
    Pulse's own running-row pulse animation opts out under the same query.

These substantially close #24 (mobile), #25 (alert/confirm consistency, WS indicator) and
#32 (reduced motion).

Follow-ups

  • Upstream issue for web_template (drafted separately): unrenderable node/layout.ejs (en-dash
    comment delimiters), README vs CSS mismatches (.lt-modal-backdrop/.lt-modal-overlay,
    .lt-alert-warning/.lt-alert--warning, .lt-field-hint/.lt-form-hint), missing
    .lt-field-error and .lt-modal-lg, lt.pagination option names, lt.init silently ignoring
    csrf/skipBoot, and the lt.ws.connect give-up-after-10-retries default.
  • .lt-modal-lg, .lt-field-error and .is-invalid live in public/assets/app.css only because
    base.css lacks them; delete them here once they land upstream.
  • Post-merge: watch the Gitea Action + deploy webhook, then curl the prod routes through Authelia
    and tail journalctl -u pulse for CSP violations or render errors.

Closes #2
Relates to #24, #25, #32

🤖 Generated with Claude Code

https://claude.ai/code/session_01HamVMDrA8RqhyxmUHgiqRp

## Summary Pulse's UI was a single hand-rolled static page — `public/index.html`, 3161 lines, ~830 lines of inline `<style>`, ~2000 lines of inline JS and 58 inline `onclick`/`onchange` handlers — served by `express.static('public')`, with `public/base.js` a git **symlink** into a sibling `web_template` checkout that only happens to exist on CT122. This PR replaces it with six server-rendered EJS pages inside the shared LotusGuild chrome, a vendored copy of the design system, a strict nonce-based CSP, and a small shared frontend runtime (`window.Pulse`) that page modules plug into. **No API behaviour changes**: `/api/*` responses are byte-identical to `main` (verified, see below), and the `commandHistory` / `pulse_executionView` localStorage keys are preserved. Feature parity was mandatory and is complete; the redesign also picks up a set of behavioural improvements that fall out of the design system (see *Behavioural notes*). ## What changed by area **Server (`server.js`, `lib/`)** - helmet with a strict nonce CSP: `default-src 'self'`, `script-src 'self' 'nonce-…'`, `script-src-attr 'none'`, `object-src 'none'`, `frame-ancestors 'none'`; `PULSE_CSP_REPORT_ONLY=1` switches to report-only with `report-uri /csp-report`. - Static mounts moved to `/web_template` and `/assets` (the root `public/` mount is gone), with `maxAge: 1h` and `?v=<pkg version>-<web_template sha>` cache busting. - Six page routes driven by a `PAGES` table in `lib/nav.js`; `lib/render.js` does two-step EJS rendering (page view → `body` → `layout.ejs`) and renders themed HTML error pages for 401/403/404. - `lib/pageauth.js`: the users upsert extracted out of `authenticateSSO` and reused by page auth, which returns themed HTML (not JSON) for 401/403. `/api/*` auth is untouched. - `PULSE_DEV_READONLY=1` (alias `DISABLE_BACKGROUND_JOBS=1`) disables every background writer — stale-execution recovery, execution cleanup, the scheduled-command processor, the stale-worker sweep — so a dev instance can safely point at the production database. Each skip is logged. - `package.json`: `"start": "node server.js"`, `"main"` corrected to `server.js`; `ejs 3.1.10` and `helmet 8.1.0` pinned in `dependencies`; lockfile committed. **Design system (`public/web_template/`)** - `base.css`, `base.js` and a `VERSION` file are now **vendored as real files** (no symlink, no runtime dependency on a sibling checkout). `scripts/sync-web-template.sh <checkout>` refreshes them and rewrites `VERSION`. `.eslintignore` excludes the vendored tree. - The vendored `layout.ejs` fixes upstream's en-dash comment delimiters (which make the upstream file unrenderable) and adds a skip link, mobile drawer, theme button, WS status dot, "Refreshed: HH:MM:SS" stamp, command palette and keyboard-shortcut modal. **Shared frontend (`public/assets/app.js`, `app.css`)** - `window.Pulse`: action registry + delegated `data-action` / `data-change-action` / `data-input-action` / `data-submit-action` listeners, event bus, `Pulse.confirm` / `Pulse.alertModal` (themed, promise-based), formatters, escaping, a 401→reload fetch wrapper, the single WebSocket connection, and the **single** `lt.autoRefresh` registration (30 s). - Page modules register with `Pulse.registerPage({ name, init, refresh, onEvent })` and never own timers, listeners or the WebSocket. **Pages (`views/pages/*.ejs`, `public/assets/pages/*.js|css`)** - Dashboard, Workers, Workflows, Executions, Quick Command, Scheduler — each an EJS view plus a page module confined to its own action prefix (`dash:`, `wk:`, `wf:`, `ex:`, `qc:`, `sc:`) and DOM id prefix. Every dynamic string goes through `Pulse.esc`. **Integration/cleanup (this work package)** - `lib/render.js` error page used `.lt-alert-danger`, which does not exist in `base.css` → fixed to `.lt-alert--error` (tree-wide grep for single-dash alert classes is now clean). - `.lt-modal-lg` was used by three pages but defined by none (Executions faked it with an id-scoped width rule). Added one shared rule in `app.css`, mirroring base.css's sizing conventions and re-capping at its two breakpoints; removed the id-scoped duplicate. - `.lt-field-error` was defined page-locally in `workflows.css`; moved it plus an `.is-invalid` input state into `app.css` so every page shares them (`wf-input-invalid` → `is-invalid`). - `views/pages/quick.ejs` used the non-existent `.lt-field-hint` → `.lt-form-hint`. - Dashboard load failures now surface a toast (once per outage) instead of only a console error. - Deleted `public/index.html` and the `public/base.js` symlink. - README: rewritten Styling & Layout section (vendored design system, sync script, VERSION), a new Web UI section (routes, the `Pulse.registerPage` contract, the CSP rule for contributors), the new env vars, and a "Local development against a real database" note. ## Screenshots None — server-rendered pages, verified via curl/jsdom. ## Verification performed Local instance against the production MariaDB with `PULSE_DEV_READONLY=1 PORT=8099 HOST=127.0.0.1`. Startup log confirms all four background writers skipped. **Static / CI** | Check | Result | |---|---| | `npx eslint --ext .js .` | **0 errors** (3 pre-existing warnings in `server.js`) | | `npx jest` | **31/31 passed**, 1 suite | | `grep -rnE 'on(click\|change\|submit\|input\|keydown)="' views/ public/assets/` | empty | | `grep -rn '<script' views/ \| grep -v nonce=` | empty | | `grep -rn autoRefresh public/assets/pages/` | empty | | `grep -rnE '\b(confirm\|alert)\(' public/assets/pages/ public/assets/app.js` | empty (one comment) | | `grep -rnE 'lt-alert-(danger\|warning\|success\|info)'` | empty | **Route matrix** — six routes × {admin, employee} = 12 requests, all: `200`, exactly `4` × `nonce="`, exactly one `.lt-nav-link active` and it points at the current route, and a `content-security-policy` header containing `script-src-attr 'none'`. | | / | /workers | /workflows | /executions | /quick | /scheduler | |---|---|---|---|---|---|---| | admin | 200 ✓ | 200 ✓ | 200 ✓ | 200 ✓ | 200 ✓ | 200 ✓ | | employee | 200 ✓ | 200 ✓ | 200 ✓ | 200 ✓ | 200 ✓ | 200 ✓ | Also: no auth headers → **401 HTML**; `Remote-Groups: guest` → **403 HTML** (themed page, now rendering the corrected `.lt-alert--error`); `/index.html` → **404**; `/base.js` → **404**; `/api/nope` → **404 JSON** (`{"error":"Not found"}`); all 18 `/assets/**` and `/web_template/*` URLs → **200**. With `PULSE_CSP_REPORT_ONLY=1`: `Content-Security-Policy-Report-Only` present with `report-uri /csp-report`, and no enforcing `Content-Security-Policy` header. **API byte-compare vs `main`** — `main` checked out in a throwaway worktree with the same `.env`, started on `:8098` and stopped within seconds (before its 60 s scheduler/stale-worker intervals could fire), responses captured from both ports back-to-back with identical headers: | Endpoint | Result | |---|---| | `/api/workers` | identical (371 B) | | `/api/workflows` | identical (50 936 B) | | `/api/executions?limit=5` | identical (1 180 B) | | `/api/scheduled-commands` | identical (2 B) | | `/api/user` | identical (103 B) | Byte-identical, not merely equivalent — no timestamp drift to explain away. **Behavioural QA (jsdom)** — each page's *live-rendered* HTML plus `base.js` + `app.js` + its page module, with `fetch` stubbed from JSON captured off the running instance: | Suite | Result | |---|---| | Integration (6 pages + shell) | **40/40** | | Dashboard + Workers (WP-C) | ALL PASS | | Workflows (WP-D) | 16/17 — the one failure is jsdom refusing `location.href` navigation, not app code | | Executions (WP-E) | **56/56** + **6/6** for the `?open=` deep link | | Quick (WP-F) | **26/26** | | Scheduler (WP-F) | **19/19** | Every page loads with a clean console and renders its core content from real data. Shell coverage: ESC closes an open modal, the theme button flips `data-theme` (and back), Ctrl+K opens the command palette (ESC closes it), and `Pulse.confirm` renders a themed dialog, escapes its message and resolves `false` on ESC. **WebSocket** — `lt.ws.connect` is called with `maxRetries: Number.MAX_SAFE_INTEGER` and `reconnectDelay: 2000` (upstream's default of 10 retries would abandon live updates permanently after ~5 minutes of downtime), with a raw-`WebSocket` fallback if `lt.ws` is missing. Disconnects set the header status dot to *Disconnected*; a reconnect toasts "Live updates reconnected" and triggers a refresh. **Not verified:** no headless browser exists on this machine (`chromium`, `chromium-browser`, `google-chrome` all absent; no `puppeteer`/`playwright` installed), so CSP enforcement was verified from response headers and by static audit rather than by a real browser refusing a resource. A manual click-through under Chrome after deploy is worth one pass. ## Behavioural notes / improvements - **Run modal for every workflow.** Execute always opens a modal — parameter inputs generated from `definition.params` (with required-field validation and a Dry Run checkbox) when the workflow takes parameters, and a plain confirmation when it does not. The old flow used a native `confirm()`. - **Server-driven Manual/Automated tabs** on Executions, with live counts, search over command/id/workflow, status filter, "Showing X of Y" and Load More. - **`?open=<execution-id>` deep link** opens the detail modal directly and strips the parameter from the URL — used by cross-page navigation from the Dashboard, Quick Command and Workflows. - **WebSocket status dot** in the header (connecting/connected/disconnected) plus a "Refreshed: HH:MM:SS" stamp, so a stale page is now obvious. - **Themed confirm dialogs** everywhere: zero `confirm()`/`alert()` calls remain in the frontend. - **Responsive layout** comes from base.css — the tables collapse to labelled cards, the nav becomes a drawer, and modals go full-bleed on small screens. - **Reduced motion respected**: the design system's `prefers-reduced-motion` blocks apply, and Pulse's own running-row pulse animation opts out under the same query. These substantially close **#24** (mobile), **#25** (alert/confirm consistency, WS indicator) and **#32** (reduced motion). ## Follow-ups - Upstream issue for `web_template` (drafted separately): unrenderable `node/layout.ejs` (en-dash comment delimiters), README vs CSS mismatches (`.lt-modal-backdrop`/`.lt-modal-overlay`, `.lt-alert-warning`/`.lt-alert--warning`, `.lt-field-hint`/`.lt-form-hint`), missing `.lt-field-error` and `.lt-modal-lg`, `lt.pagination` option names, `lt.init` silently ignoring `csrf`/`skipBoot`, and the `lt.ws.connect` give-up-after-10-retries default. - `.lt-modal-lg`, `.lt-field-error` and `.is-invalid` live in `public/assets/app.css` only because base.css lacks them; delete them here once they land upstream. - Post-merge: watch the Gitea Action + deploy webhook, then curl the prod routes through Authelia and tail `journalctl -u pulse` for CSP violations or render errors. Closes #2 Relates to #24, #25, #32 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01HamVMDrA8RqhyxmUHgiqRp
jared added 3 commits 2026-09-08 22:17:16 -04:00
- Vendor web_template base.css/base.js (v1.2 bbec859) under public/web_template with sync script
- Add views/layout.ejs (fixed upstream EJS comment delimiters, mobile drawer, cmd palette,
  keys help, WS status, theme toggle) and lib/render.js two-step renderer
- Add page routes for /, /workers, /workflows, /executions, /quick, /scheduler (stub views)
- helmet with strict nonce CSP (script-src-attr 'none'), /csp-report, report-only toggle
- lib/pageauth.js: shared users upsert + HTML 401/403 for page routes
- PULSE_DEV_READONLY guard disabling background writers for local testing
- public/assets/app.js shared runtime (Pulse namespace, delegated actions, WS singleton,
  themed confirm modal) and app.css
- eslint globals for browser files; ignore vendored assets

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HamVMDrA8RqhyxmUHgiqRp
- Dashboard, Workers, Workflows, Executions, Quick Command, Scheduler pages on the
  shared layout, all behaviour via Pulse delegated actions (no inline handlers)
- Executions: server-driven Manual/Automated views, compare mode, detail modal with all
  log types, ?open= deep link, cross-page re-run via sessionStorage
- Workflows: run modal (dry-run available for every workflow), create modal pre-filled
  with the example definition
- Fix EJS comment in page-header partial that broke rendering

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HamVMDrA8RqhyxmUHgiqRp
feat(ui): TDS migration stage 3 — integration fixes, legacy removal, docs
Lint / JS (eslint) (pull_request) Successful in 10s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / JS Security (npm audit) (pull_request) Failing after 10s
Test / JS Tests (jest) (pull_request) Successful in 14s
Lint / JS (eslint) (push) Successful in 11s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / JS Security (npm audit) (push) Failing after 11s
Test / JS Tests (jest) (push) Successful in 10s
eca5cb45b1
- Fix design-system class names that do not exist in base.css (.lt-alert--error,
  .lt-form-hint); add shared .lt-modal-lg, .lt-field-error and .is-invalid rules
- Remove public/index.html and the public/base.js symlink
- package.json: start script, correct main
- README: vendored design system, Web UI section, new env vars, read-only dev mode

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HamVMDrA8RqhyxmUHgiqRp
jared added 1 commit 2026-09-08 22:21:48 -04:00
chore(deps): npm audit fix — ws 8.21.3, mysql2 3.24.4, qs 6.16.0 (0 vulnerabilities)
Lint / JS (eslint) (push) Successful in 10s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / JS (eslint) (pull_request) Successful in 10s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / JS Security (npm audit) (push) Successful in 10s
Test / JS Tests (jest) (push) Successful in 10s
Security / JS Security (npm audit) (pull_request) Successful in 10s
Test / JS Tests (jest) (pull_request) Successful in 11s
1c3a2777c8
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HamVMDrA8RqhyxmUHgiqRp
jared merged commit e13bdd00d1 into main 2026-09-08 22:23:24 -04:00
Sign in to join this conversation.