Skip to content

feat(dashboard): full AWS UI coverage + hosted state-viewer deployment - #1022

Open
DavidJFelix wants to merge 115 commits into
alchemy-run:mainfrom
DavidJFelix:claude/alchemy-aws-ui-jhzbam
Open

feat(dashboard): full AWS UI coverage + hosted state-viewer deployment#1022
DavidJFelix wants to merge 115 commits into
alchemy-run:mainfrom
DavidJFelix:claude/alchemy-aws-ui-jhzbam

Conversation

@DavidJFelix

@DavidJFelix DavidJFelix commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Completes two gaps in the dashboard and makes it deployable: every AWS resource now renders in the dashboard, and the dashboard runs as a hosted state viewer — the same SPA the CLI serves for deploy --ui, deployed against a state store with no CLI process involved.

  • AWS UI coverage goes from 135 to 691 resources across all 193 service directories (parity with Cloudflare). Per-service providers live in AWS/{Service}/ui.ts; AWS/UI.ts is regenerated with nested Layer.mergeAll groups.
  • New alchemy/Dashboard/Viewer: the read-only half of the dashboard API as a platform-free handler (no FileSystem/Path/process) over any StateService — document snapshots, SSE, deployment history + journal folds, projections. sse: "stream" | "poll" adapts the transport to hosts that buffer responses.
  • Registry test suites in packages/dashboard/test/registry.test.ts: a conformance sweep executing all 940+ UI providers against undeployed/empty state (callbacks total, icons are real lucide names, no URL interpolates a missing value), and a coverage gate asserting every Resource<>("Cloud.Type") in a registered cloud has a UIProvider — the 691/691 census as a permanent check (it already caught GitHub.Environment, Docker.Context, Docker.Swarm).
  • Merge maintenance: main is merged into the branch (Plan.destroy, aggregated DestroyError, provider modes reconciled with the deployment journal; on a successful destroy, state.deleteStack runs after the journal closes since the journal lives inside the stage directory).

Hosted viewer: host x state-backend matrix

viewer({ state }) takes any StateService, so the host and the backend are independent axes. The backend choice is one expression in each example; the Effect R-channel enforces which combinations a host can satisfy.

viewer host \ state backend Cloudflare HTTP store AWS S3 store local FS
Cloudflare Worker examples/dashboard-viewer possible, not shipped (see below) not possible
AWS Lambda + CloudFront config swap of the AWS example (see below) examples/dashboard-viewer-aws not possible
CLI (alchemy dashboard / deploy --ui) yes yes yes — this is the local cell

Cloudflare Worker + HTTP store (viewer.ts): SPA as Worker assets (runWorkerFirst: ["/api/*"], SPA fallback), state over makeHttpStateStore. Same-zone worker-to-worker fetch is blocked (error 1042), so the state API rides a service binding to alchemy-state-store (FetchHttpClient.Fetch override); plain fetch remains the cross-zone fallback. A bare bun run deploy self-configures from the credentials Cloudflare.state() caches at ~/.alchemy/credentials/{profile}/cloudflare-state-store.json.

Lambda + S3 store (viewer-function.ts, alchemy.run.ts): the S3 store has no server, so the viewer Lambda is the reader — its execution role gets s3:GetObject/s3:ListBucket on the state bucket plus kms:Decrypt; no long-lived credentials exist. One AWS.Website.Router distribution serves the SPA from S3 and routes /api/* to the function URL. Lambda URLs buffer responses, so this host uses sse: "poll" (one snapshot frame + retry: hint, then close; EventSource auto-reconnect makes it polling).

Lambda + Cloudflare store (no example needed): swap AWS.makeS3State(...) for makeHttpStateStore({ url, authToken }) in the Lambda and drop the IAM statements — plain HTTPS to the store worker, no same-zone constraint.

Worker + S3 store (possible, deliberately not shipped): makeS3State runs on workerd (distilled AWS is fetch-based), but the Worker would need long-lived IAM user keys stored as worker secrets plus kms:Decrypt — the one cell where credential posture degrades instead of improving (every other cell uses a scoped bearer token or an ephemeral execution role). Anyone who wants it can compose it; the examples don't encourage it.

Local FS: hosted viewers cannot reach a developer's .alchemy/state by construction (makeLocalState requires FileSystem, which no remote host provides — it fails to compile, not at runtime). The CLI dashboard is that cell; alchemy state sync bridges local state into a hosted store.

In every cell the viewer exposes whatever its credential can read (props/attrs with secrets redacted by the state encoding, journals, outputs) — hosted deployments should sit behind an access layer (Cloudflare Access, CloudFront auth) before sharing.

sam-goodwin and others added 30 commits July 1, 2026 18:14
- UIProvider.succeed/effect mirror Provider: Context.Service tags keyed
  UI(<type>), aggregated per-service -> per-cloud -> app registry
- alchemy dashboard [main] serves a JSON API over the state store plus
  the @alchemy.run/dashboard Vite SPA (React Flow canvas, inspector,
  list view); graph edges derived from downstream + binding sids
- per-service ui.ts stubs + per-cloud UI.ts aggregators (generated)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
399 resource UI providers across AWS (26 services), Cloudflare (91
services), GitHub, Neon, PlanetScale, Axiom, and Docker — icons,
categories, brand colors, summaries, facts, links, and console deep
links, validated against real Resource/Platform type tags, lucide icon
names, and browser-safe imports (packages/dashboard/scripts/validate-ui.ts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fitView on mount saw every node at (0,0); re-fit when positions land
and cap fit zoom at 1.25 so small graphs don't over-zoom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/replace/delete

- /api/plan runs Plan.make with the same layers as `alchemy plan`;
  JSON projection carries identity + action only (plan props are
  unresolved Output proxies and are never serialized)
- SPA merges plan into the graph: action badges, dashed pending nodes
  for not-yet-deployed resources, red orphans, top-bar summary chips
  ("1 create · 1 update · 1 delete" or "✓ in sync")
- degrades to state-only view when the plan can't be computed

Verified against a real deployed Cloudflare stack (Worker + KV + R2,
testing profile, local state): live attrs/URLs render, and mutating the
stack file surfaces create/update/delete annotations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dashboard observes deploys through the same pluggable CLIService the
TUI uses. withDashboardReporter(selectCli()) tees every apply session:

- Discovery: `alchemy dashboard` advertises in .alchemy/dashboard.json
  (project-scoped, ~250ms health-check; no dashboard = pure pass-through)
- Reporter: posts apply/start with the plan JSON, tees each ApplyEvent
  through a serialized queue (order preserved, failures swallowed —
  never fails or stalls a deploy), flushes on done with a 3s budget
- Server: in-memory session + PubSub; GET /api/events is SSE with
  snapshot replay for browsers connecting mid-deploy
- SPA: live node statuses (pulsing in-flight dots), apply-plan overlay,
  activity feed with annotate notes, state+plan refetch on done

Verified live: `bun alchemy deploy` of the real Cloudflare demo stack
streamed create/update/delete into the canvas; terminal output unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- /api/plan?stage=X re-runs the stack effect under Layer.succeed(Stage, X)
  (same per-stage service layers as `alchemy plan`) — physical names,
  providers, and the compiled graph are functions of the stage
- top-bar StageSelect lists stages known to the state store and accepts
  free text; a never-deployed stage previews the whole stack as dashed
  `+ create` nodes
- graph/plan/inspector all follow the selected stage

Verified live: dev (deployed state + real URLs) ⇄ "staging" (never
deployed, plans as 3 creates) round-trips cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r dep

Packaging: @alchemy.run/dashboard publishes prebuilt static assets only
(files: ["dist"], zero runtime dependencies — React/Vite are build-time
devDependencies). alchemy declares it as an optional peerDependency, so
the UI is opt-in: `bun add -D @alchemy.run/dashboard`. When missing, UI
commands print install instructions and exit cleanly before doing any
real work.

--ui on deploy/destroy/plan:
- reuses a healthy already-running dashboard for the project (Discovery)
- otherwise forks Dashboard.launchDashboard in-process on a random port
  (Deferred readiness, 30s budget, continues without UI on timeout)
- the run streams in via the existing DashboardReporter tee; after the
  run the CLI keeps serving ("press Ctrl+C to exit")

Also: SSE heartbeat (:ping every 8s) so Bun.serve's 10s idle timeout no
longer kills the event stream; `alchemy dashboard` command now delegates
to the shared Dashboard/Launch.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- LogEvent joins the ApplyEvent union; apply() injects a per-resource
  Logger at instrumentLifecycle (the single lifecycle dispatch point), so
  every Effect.log* inside a provider op is attributed to its resource
  and flows through the same Cli seam (LoggingCli/TUI ignore log events —
  the merged default logger already prints to the terminal)
- SPA: in-flight nodes render a spinner with the latest annotate note
  inline on the card; inspector gains a "Deploy logs" section (per
  resource, level-colored, capped at 500 lines); activity feed shows log
  lines dimmed
- fix deploy --ui starving the page: /api/plan returns the live session's
  plan during an active apply instead of re-evaluating the whole stack in
  the deploying process, and the canvas renders from the session plan
  without waiting for the graph fetch

Verified live: Worker provider's reconcile logInfo lines land in the api
node's Deploy logs section and dimmed in the feed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three compounding causes:
- the empty-state branch unmounted <Canvas> whenever the merged node list
  transiently hit zero during plan/graph/live handoffs — the whole scene
  popped in and out; the hint is now an overlay and the canvas stays
  mounted
- ELK relayout + fitView re-ran on every SSE event (each status/note
  changes the graph prop identity); both are now keyed to a structure key
  (node fqns + edge pairs) so data-only updates never move the viewport
  or re-place nodes, and layout results merge into previous positions
- after apply-done, effectivePlan fell back to the stale pre-deploy plan
  while the post-apply refetch (a full stack re-eval) was still running,
  re-badging everything; the live session's plan now stays authoritative
  until the fresh plan lands (planStale)

Verified with a simulated from-scratch session dripped through the
ingest API: node positions identical across every frame, spinners and
inline notes render mid-apply, no popping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation

The partial canvas after deploying examples/cloudflare-worker had a root
cause beyond the earlier flicker fix: every page load hit /api/plan,
which re-evaluates the whole stack (bundling three workers) inside the
serving process — and Bun's 10s idleTimeout killed the starved
/api/graph//api/meta requests, so the SPA died with "Failed to fetch"
or rendered a partial scene. Fixed at three layers:

- plan computation is cached per stage (30s TTL, concurrent requests
  deduped via Effect.cachedWithTTL; invalidated on apply-done)
- the dashboard's HTTP server sets idleTimeout: 240 (httpServer() now
  accepts the option; Bun-only, Node unaffected)
- SPA fetches retry with backoff instead of failing the whole app

What the apply DID is now first-class on the canvas:
- terminal session statuses become filled result chips per node —
  ✓ created (green), ✓ updated (amber), ↻ replaced (purple),
  − deleted (red), ✗ failed (red) — distinct from the outline plan
  badges (what a deploy WOULD do)
- resources deleted by the apply stay visible as dashed red ghost nodes
  (synthesized from the session, since they vanish from state) until the
  deploy feed is dismissed; dismissal now lives in App and clears the
  whole result overlay
- inspector shows a "Last deploy: …" callout; list view shows result
  chips

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A leftover query in the filter box silently hides most of the graph,
which reads as "the dashboard lost my resources" — especially mid-apply.
The input now shows an amber border + inline ✕ when non-empty, and a
prominent "filtered: N of M shown ✕" chip sits next to it; both clear
the filter on click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…feed

- deploy/destroy --ui without --yes now waits for approval in the
  dashboard: the plan renders on the canvas with an approve/reject
  banner; the terminal just points at the URL (falls back to the
  terminal prompt if the dashboard is unreachable)
- destroy plans (empty resources, everything in deletions) now
  synthesize nodes and edges from the deletion list, so the whole
  architecture stays visible through teardown: red spinner while
  deleting, then a gray dashed "dead" ghost with a − deleted chip —
  instead of the scene glitching out as state emptied
- deletions in PlanJson carry their real binding sids so destroy
  edges render
- the activity feed starts minimized: a one-line pill showing the
  latest event, or "Deployment complete/failed" when done; click to
  expand the full log, ✕ still dismisses the session overlay

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SPA was stitching four async sources (state graph fetch, plan fetch,
SSE session, client-synthesized ghosts) with precedence rules, joined on
inconsistent keys (events carry logicalId, nodes carry fqn). Every
rendering bug traced back to that: stale result overlays bleeding into
the next operation's approval view, missing nodes on from-scratch
deploys, misjoined namespaced resources.

Now the server assembles the scene (Dashboard/Scene.ts): state + plan +
live session folded in one place, with one logicalId->fqn join, and
explicit session lifecycle — approval-request and apply-start retire the
previous session's overlay. The scene streams over SSE as versioned full
snapshots; /api/scene serves it per stage. State reads are cached (10s
TTL, forced refresh after apply); plans compute on a server-scoped
worker queue (never eagerly at startup — a stack re-evaluation in the
serving process can starve the event loop and hang the first page load).

The SPA drops api.ts/live.ts/mergePlan entirely: one useScene() hook,
render what the scene says. Canvas/Inspector/ListView unchanged (scene
nodes carry the same fields).

Also: killed zombie SO_REUSEPORT listeners sharing the port caused
hanging/white page loads — restart guidance is one server per project.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Click a resource → "Deploy logs" now shows its full deployment timeline:
status transitions, annotate notes, and captured Effect.log* lines
interleaved (level-colored). Timelines live server-side and survive the
session ending — a resource's timeline resets only when its NEXT
lifecycle operation begins, so you can inspect what the last deploy did
to any resource long after it finished.

An empty stage no longer renders a blank "No resources" screen: the
server runs a fast structure pass (evaluate the stack file, register
resources + binding sids — no planning, no credentials, seconds) and
the scene synthesizes dashed "not deployed" ghosts with binding edges;
plan actions upgrade them when the plan lands. While evaluating, the
canvas says "Evaluating stack…" instead of telling you to deploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e reads

The structure/plan worker was only triggered by /api/scene requests, but
the SPA's default view loads exclusively over SSE — a fresh dashboard
never evaluated anything and sat on "Evaluating stack…" forever.
Evaluation is now queued when an SSE stream connects (after the snapshot
is served, so page load stays instant).

State-store reads are also bounded (20s timeout per call) and surface as
scene.stateError with a visible banner, instead of a silently empty
canvas when the backend hangs or auth fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re on all four state backends

Adds an optional deployments sub-interface to StateService (begin/appendEvents/
heartbeat/end/list/get/readEvents) plus batch getAll, with typed
DeploymentInProgress/DeploymentTokenInvalid/DeploymentNotFound errors.

- Monotonic per-(stack,stage) versions, atomically allocated: O_EXCL record
  claim (local), If-None-Match:"*" PUT (S3, distilled patch for the typed
  412), storage.transaction (Cloudflare DO), counter (in-memory)
- Crash-safe without end(): heartbeated open markers; next begin reconciles
  TTL-expired opens as abandoned; end after abandonment records
  completed-late; appendEvents is seq-idempotent
- Concurrent deploys of the same stack/stage now fail fast with typed
  DeploymentInProgress carrying the holder
- CF: events as SQLite rows in the stack DO, alarm-based stale-open expiry,
  AES-CTR + SHA-256 integrity (no silent-undefined for history), tokens
  stored as hashes, STATE_STORE_VERSION 7 -> 8, DeploymentNotFound as HTTP
  410 so the 404 edge-propagation retry can never eat it
- Shared 16-case conformance suite runs unconditionally against in-memory +
  local; live S3/CF runs are gated behind ALCHEMY_TEST_STATE_{S3,CF}=1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every deploy/destroy now opens a versioned deployment (DeploymentSession)
inside apply() — the single seam shared by the CLI, programmatic
Deploy/Destroy, and the test harness:

- ApplyEvent envelope v2: ts stamped at emission (Clock), fqn on every
  event, new op-start/op-end/annotation kinds; renderers tolerate unknown
  kinds
- instrumentLifecycle brackets every provider lifecycle dispatch with
  op-start/op-end (opId, phase execute/gc/converge) — wait/run timing for
  the dashboard's Table and Waterfall views
- State facade journals state-set/state-delete/output-set (status-only
  before images) alongside untouched head writes; collectGarbage re-provided
- Exit-mapped end: succeeded (per-action counts) / failed (Cause digest) /
  interrupted via scope-finalizer backstop; 10s heartbeat fiber; batched
  fire-and-forget appends that can never fail or stall a deploy
- DeploymentInProgress surfaces as a clean CLI error (holder, since-when,
  ~60s auto-expiry); stores without deployments feature-detect to a no-op
- Crash drill verified: SIGKILL mid-create leaves a durable open record;
  next begin reconciles it to abandoned and the next deploy proceeds

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y endpoints

Layer B of the frontend-agnostic stack: a pure, framework-free
DeploymentDocument fold + Schema-typed patch wire contract + view
projections, hosted per-stage by the server and streamed as
snapshot-then-patches over SSE.

- Document.ts: O(1) incremental fold (states/plan/structure/events/journal),
  union-graph structure whose structuralHash moves only when the fqn/edge
  sets move, opSpans (wait/run segments from pending->op-start->op-end),
  minimal-patch derivation with a proven patch-stream-equivalence property
- DocumentPatch.ts: the public patch union + client applyPatch reducer
- Projections.ts: summaryOf/listGroupsOf/tableRowsOf/waterfallSpansOf/
  annotationsOf — pure functions any frontend (SPA, TUI, native) renders
- DocumentHost.ts: per-stage host — hydrates from getAll + newest deployment
  record + full journal re-fold (mid-deploy server restart catches up from
  the store), debounced ~40ms typed patch broadcast
- Server.ts (additive, v1 untouched): /api/v2/document, /api/v2/events
  (SSE snapshot-then-patches), /api/v2/deployments[/:version] (history +
  server-computed projections), /api/v2/projections

Verified e2e: 2 deploys + destroy journaled to a temp local store, real
server over that store — history lists 3 versions, per-version waterfall
has real durations, live v1 tee produces contiguous v2 patches, v1
/api/scene regression green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… canvas, six views + history

Rewrites the SPA onto the v2 document/patch protocol; the v1 scene
protocol client (scene.ts/plan.ts) is deleted.

- store.ts: one zustand vanilla store mirroring DeploymentDocument via
  fromSnapshot/applyPatch (imported from alchemy/Dashboard/*), per-fqn
  identity-guarded selectors, history overlay (decorations swap, structure
  stays live), positionsByHash LRU
- ingest.ts: SSE snapshot-then-patches, revision-gap re-snapshot,
  capped-backoff reconnect, stage switching, deployment history loading
- Canvas: propless, always mounted; ELK in a Web Worker keyed by
  structuralHash (main-thread fallback); data:{fqn}-only memoized nodes;
  fitView only on first layout of a new hash or the explicit Fit button;
  O(E) mergeEdges
- Views: Summary, List, Table (sortable, wait/run ms), Waterfall (CSS
  bars, wait vs run segments), Annotations + DeploymentPicker with
  'viewing vN (historical)' pill that recolors the same unmoving graph
- By construction: one decorate patch re-renders exactly one node;
  selection re-renders 2; filter keystrokes never touch layout

Verified: typecheck, vite build, validate-ui (771 providers), 17/17
server smoke against a journaled temp local store (bundle references
/api/v2/events; / serves built index.html)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r React Flow

- Move applyStates into server-only DocumentStates.ts: its toGraph ->
  encodeState -> Resource chain dragged the whole engine (Cli/Auth/node:os)
  into the SPA module graph, which broke vite dev outright and bloated the
  bundle. Document.ts now value-imports nothing from the engine.
- Give canvas node objects fixed width/height (the ResourceNode constants):
  the rebuild-on-position-change effect replaces node objects, which wiped
  React Flow's measured dimensions — nodesInitialized never turned true and
  edges silently never rendered.

Verified live against a journaled 5-deployment local-state demo: canvas
edges render, Waterfall shows real wait/run segments, history picker
overlays v4's failed Summary (counts + error rollup) and returns to Live
with node positions byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rest themes, brand fonts, yantra mark

Restyles the SPA onto the website's --alc-* token system (tokens.css
mirrored into the package; kept in sync with website/src/styles/tokens.css):

- Light (default, honors prefers-color-scheme): warm cream parchment
  surfaces, moss accent, terracotta secondary, walnut text scale. Dark:
  night-forest walnut page, cream text, lifted moss/terracotta.
- Pre-paint data-theme script (no flash), alchemy-dashboard-theme storage,
  light/dark/auto toggle in the TopBar, meta theme-color per theme.
- Fonts: Inter UI, Source Serif 4 identity headings (deployment header,
  wordmark, empty states), JetBrains Mono for fqns/durations/logs and
  eyebrow section labels (APPLIED / FAILURES / OUTPUTS).
- Sri-Yantra brand mark in the TopBar and BootScreen (bindu flips to
  terracotta in dark); 4%-opacity yantra watermark on empty states.
- Semantics via tokens everywhere: moss create/success, honey update,
  brick delete/replace/fail, slate-teal info; chips are color-mix soft
  washes; hairline borders; walnut code surfaces for JSON/logs/errors;
  binding edges terracotta, dependency edges hairline.
- Theme flips are pure CSS: status/plan/result colors are var() strings in
  identity-cached style helpers, edge/marker constants stay hoisted — a
  toggle re-renders only React Flow's colorMode consumer, zero nodes.

Verified live in both themes against the journaled demo (canvas, Summary
failure rollup, history overlay); typecheck, vite build, validate-ui (771
providers), store simulation all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le, no fit-flick, tab reuse

- Contrast (light + dark): chips 16%→22% wash, node cards on elev-2 with
  hairline-2 borders (hover hairline-3), dependency edges/markers/handles
  from hairline-3 to fg-4, type captions fg-4→fg-3
- Theme toggle is now a binary flip on the RESOLVED theme — the old
  light→dark→auto cycle made the first click a visual no-op whenever auto
  already matched, reading as 'two clicks to switch'
- Cursors: pointer on all enabled buttons/selects/summaries (Tailwind v4
  preflight default) and on canvas nodes (matching xyflow's .draggable
  specificity); grabbing while dragging
- No first-render zoom flick: the canvas is opacity-0 until the rendered
  nodes carry the final ELK positions AND fitView has applied — the fit
  runs synchronously when the store is in sync (no requestAnimationFrame,
  which never fires in hidden tabs)
- alchemy dashboard no longer opens a duplicate tab: the server signals
  the launcher on first SSE attach; the launcher waits 2.5s for a
  previously-open tab to reconnect before calling open

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…removes rows in List/Table

- Stage picker: union the server's listStages with every stage this
  client has ever seen (persisted per stack in localStorage) — switching
  to a new stage can never hide the one you came from; the launch stage
  (--stage / default) is always remembered from the first snapshot
- Viewport: the user's framing is per (stack, stage) and survives
  reloads — persisted on real gestures only (programmatic fits never
  count), restored exactly on load/stage-return via a restore epoch the
  Canvas consumes without remounting
- Fixed the stage-switch misframe: the shell's hydration gate remounts
  the Canvas (React Flow resets to the identity viewport), but the
  module-level fitted-hash set remembered the structure and skipped the
  re-fit, stranding the graph top-left at zoom 1. The fitted set is now
  per-mount, so every remount re-centers untouched stages while user
  framing still wins
- Auto-fit now re-centers on every structural change (ghosts -> plan)
  until the user pans that stage; the explicit Fit button resets the
  saved framing
- Filtering REMOVES rows in List (with group counts + empty groups
  collapsing) and Table ('No resources match the filter' empty state);
  the canvas keeps dimming — nodes have spatial identity worth keeping

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ette; filter scoped to List

- Views trimmed to Status (né Summary), List, Graph (né Canvas) — Table,
  Annotations, and Waterfall views removed (projections + timing data
  remain in the document core; the views are one revert away)
- Command palette (⌘K / Ctrl+K, or the TopBar Search pill): type a
  resource name to jump to it (icon + logical id + path + friendly type,
  rendered like a List row; Enter opens its Inspector) or a view name for
  'Go to: Graph' actions; arrow keys + Enter + Esc
- The resource filter now lives ONLY on the List page (removes rows,
  collapses empty groups, shows 'N of M'); the Graph never dims or
  filters, and the TopBar filter box is gone

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Typing any text in ⌘K always offers 'Select stage: <typed>' (plus any
  known stages the query matches) — Cmd+K → 'prod' → Enter switches stage
  in one keystroke when nothing else matches
- New 'Go to Stage…' command (shown on empty query, matches 'stage'):
  flips the palette into the stage prompt — same selector semantics as
  the TopBar picker (known stages from server ∪ seen, current checked,
  free-typed names preview fresh). Esc or Backspace-on-empty returns to
  the root palette

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The TopBar chrome washed out on the parchment nav surface — everything
sat on 8-14% hairlines with fg-3/fg-4 labels. Now:

- Search pill: elev-2 paper surface + hairline-3 border + shadow-sm
  (was sunk-on-nav, nearly invisible); label/icon/⌘K kbd up one step
- View tabs: bordered elev-1 segmented container with shadow; inactive
  tab labels fg-3 → fg-2
- HAIRLINE_BUTTON (history/fit/theme): hairline-3 border, fg-2 resting
  text, fg-4 border on hover
- Stage pill + in-sync chip: elev-1 surface + hairline-3 border

All via tokens, so dark mode picks up the same (cream-side) lift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pending-create and structure-ghost cards were triple-faded: 0.55 card
opacity × fg-4 titles × 14%-hairline dashed border — the whole graph
read as washed out on a fresh stage in both themes.

'Doesn't exist (yet/anymore)' is now carried by the dashed border alone:
- no opacity fade (only the explicit server-side hidden flag still dims)
- ghost titles render fg-1 like live nodes (deleted stays danger-tinted)
- dashed border strengthened from hairline-2 to solid walnut fg-4, with
  the same shadow as solid cards

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude added 2 commits July 31, 2026 21:47
Same-zone worker-to-worker fetch is blocked by Cloudflare (error 1042,
surfaced as 404), so a viewer on the same account as alchemy-state-store
cannot reach it over plain HTTP. Register a service binding to the
state-store worker at deploy time (ALCHEMY_STATE_SERVICE, default
alchemy-state-store; "" disables) and route the HTTP state client
through the binding's fetch via FetchHttpClient.Fetch. URL and bearer
token are unchanged in both modes; plain fetch remains the cross-zone
fallback.
…viewer

dashboard-viewer (Cloudflare): a bare `bun run deploy` now works — the
worker's init resolves the state store endpoint/token from
~/.alchemy/credentials/{profile}/cloudflare-state-store.json (the cache
Cloudflare.state() maintains) when ALCHEMY_STATE_URL/TOKEN are unset,
and registers them as plain_text/secret_text bindings alongside the
service binding.

dashboard-viewer-aws: the same dashboard hosted on AWS. The S3 state
store has no server, so the viewer Lambda IS the reader: its execution
role gets s3:GetObject/ListBucket on the state bucket plus kms:Decrypt
for envelope-encrypted secrets. One CloudFront distribution
(AWS.Website.Router) serves the SPA from S3 and routes /api/* to the
Lambda's function URL.

Viewer: new `sse: "stream" | "poll"` option — poll sends one snapshot
frame plus a retry hint and closes, so buffered hosts (Lambda URLs in
BUFFERED invoke mode) still deliver; EventSource auto-reconnect turns
it into snapshot polling.
sam-goodwin and others added 4 commits July 31, 2026 22:44
Two new suites in packages/dashboard/test/registry.test.ts:

- Conformance: builds the full merged registry (all clouds, 940+
  providers) and executes every provider against the states the
  dashboard actually renders pre-deploy (attrs/props undefined and {}):
  callbacks must be total, icons must be real lucide names (unknown
  names silently render the box fallback today), colors #rrggbb,
  categories in the UICategory union, links/consoleUrls must never
  interpolate a missing value.
- Coverage: scans Resource<>("Cloud.Type") declarations across the
  registered clouds and asserts each has a UIProvider — the 691/691 AWS
  census becomes a permanent gate, so a resource cannot land without
  dashboard UI.

The gate immediately caught three gaps, fixed here: GitHub.Environment
(new on main via alchemy-run#845), Docker.Context, Docker.Swarm.
@sam-goodwin

Copy link
Copy Markdown
Contributor

Can you include some screenshots or video?

claude added 9 commits August 3, 2026 02:08
…in viewer

The hosted viewer swallowed every store failure into the same 'no
stack/stage found' 404, making a broken transport (blocked same-zone
fetch, missing binding, bad URL) indistinguishable from a store with
nothing deployed. Store errors now surface as 502 'state store
unreachable: <message>', /api/health probes the store and reports the
error verbatim, and hosts can attach a diagnostics payload (the
Cloudflare example reports which transport the state client rides and
the configured store host). Also suffix the example's self-bind sid so
it can never collide with another self-bind on the worker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
main moved HelmChart and Manifest to the cluster-agnostic Kubernetes
namespace, which broke the type-check: AWS/EKS/ui.ts still imported
./HelmChart.ts and ./Manifest.ts.

    error TS2307: Cannot find module './HelmChart.ts'
    error TS2307: Cannot find module './Manifest.ts'

Move both UI providers to Kubernetes/UI.ts under their canonical
Kubernetes.* types, wire the new cloud into the dashboard registry, and
add Kubernetes to the coverage gate's registered clouds so resources
added there stay covered. Facts follow the new attribute shape --
clusterName is gone, so the cluster is surfaced from connection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
… binding

The viewer reached the state store over plain fetch even though the
service binding was attached, so Cloudflare's same-zone worker-to-worker
block (1042) turned every store read into a 404:

    {"store":{"ok":false,"error":"Decode error (404 GET .../state/stacks)"},
     "diagnostics":{"transport":"service-binding"}}

The store never answers 404 itself — unmatched routes there return 500 —
so the request was not reaching it. Swapping FetchHttpClient's `Fetch`
reference underneath the stock layer was too indirect to rely on inside
workerd; build the HttpClient directly on `binding.fetch` instead, which
is the shape the Svelte state viewer already uses in production.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
A viewer pinned to a stack that no longer has any stage reported the
generic "no stack/stage found in the state store", which reads as an
empty store and sends you looking in the wrong place. Carry the reason
on the empty result so the response says which stack came up short.

    {"error":"stack \"CloudflareWorker\" has no stages in the state store — or pass ?stack= and ?stage="}

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
The SPA was single-target: every fetch went through a helper that only
ever appended `?stage=`, so a hosted viewer could show exactly one stack
(pinned via ALCHEMY_VIEWER_STACK) and `/api/stacks` had no consumer.

    -const stageQuery = (stage) => `?stage=${...}`
    +const targetQuery = (target: Target) => // ?stack=&stage=

- `Target = { stack, stage }` threads through the SSE stream, document
  snapshots, deployment history and the scene fetch; `setStack` resets the
  stage (stage names are per-stack)
- the target lives in the URL, so a viewer link is shareable and survives
  reload; a bare `/` still means "whatever the server picks", which is
  what the CLI dashboard sends
- StackSelect renders only when the store knows more than one stack, so
  the CLI dashboard (which 404s /api/stacks) is visually unchanged
- the stage list now unions the catalog's stages for the selected stack

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
…acks

Two bugs in the target picker.

Stage names are per-stack, but `layout.stagesSeen` survived a stack
switch and `rememberStages` folded that in-memory set into the NEW
stack's persisted set — so the picker offered stages from the stack you
came from, and selecting one 404s. `resetForTarget` now reloads the seen
set for the incoming stack, and `rememberStages` merges only that stack's
own persisted stages.

A stack whose stages have all been destroyed stays registered in the
store, so it appeared in the picker and selecting it could only fail. It
now renders disabled with a "no stages" hint rather than vanishing —
hiding it would make the list disagree with the store.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
`/` guessed a stack (first one the server sorted) instead of offering a
choice, and the target lived in a query string.

    /                        index — pick a stack
    /stacks/:stack           a stack, server-default stage
    /stacks/:stack/:stage    a stack + stage

- the index is hosted-only: with one stack (or none, which is the CLI
  dashboard — it serves no /api/stacks) boot redirects straight to the
  graph, so `deploy --ui` is unchanged
- `navigate` pushes real history, so back/forward walk the stacks you
  visited; the transport re-points off the route, which now also covers
  the ⌘K palette's stage switch
- legacy `?stack=&stage=` links still resolve and are rewritten to their
  path form in place
- both hosts already SPA-fallback unknown paths (Server.ts, and the
  Worker's notFoundHandling), so deep links load with no server change

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
The `?stack=&stage=` form only ever existed in this branch, so there are
no links to keep working.

`parseRoute` takes a pathname and nothing else; boot no longer
canonicalizes a query link before connecting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
@DavidJFelix
DavidJFelix marked this pull request as ready for review August 3, 2026 23:08
@DavidJFelix

Copy link
Copy Markdown
Contributor Author

I think i'm at a pause point looking for either a merge or direction.

Hosted view

ui starts at ~0:45. This showcases a couple of my stacks unrelated to this pr and navigating around them as expamples

Screen.Recording.2026-08-03.at.5.58.09.PM.mov

CLI view with approve/deny

this existed already but just showing it in action

Screen.Recording.2026-08-03.at.6.07.21.PM.mov

Next steps

  • Small UX improvements to hierarchy of the page and naviagation
  • UI nits i have that I tried not to focus on
  • Ensuring the example provides a suggestion for auth

Long goals

  • Some idea of what hosted approval looks like if at all?

Comment thread .demo-live/alchemy.run.ts Outdated
Comment thread packages/alchemy/scripts/upgrade-cf-state-store.ts Outdated
@DavidJFelix DavidJFelix changed the title wip: feat(dashboard): full AWS UI coverage + hosted state-viewer deployment feat(dashboard): full AWS UI coverage + hosted state-viewer deployment Aug 4, 2026
@sam-goodwin

Copy link
Copy Markdown
Contributor

Nice, took a look at the videos. I like how i can be deployed and how i can just see the stages with all the pr-* stages :)

claude added 6 commits August 5, 2026 02:02
…ui-jhzbam

# Conflicts:
#	bun.lock
#	packages/alchemy/src/Apply.ts
#	packages/alchemy/src/Cli/Event.ts
#	packages/alchemy/src/Cli/LoggingCli.ts
#	packages/alchemy/src/State/index.ts
#	packages/alchemy/src/Util/PlatformServices.ts
…cratch

main added nine SES resources and Lambda Version, which the dashboard's
UI-coverage gate requires providers for.

Also removes demo debris that was never part of the feature: the
`.demo-live/` scratch stack, a stray `Api.ts.notouch` artifact, and a
local-state hack in the cloudflare-worker example. That example keeps the
optional `@alchemy.run/dashboard` peer and gains a `dashboard` script, so
the CLI preview runs against a regular example instead of a hidden folder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
…ui-jhzbam

# Conflicts:
#	packages/alchemy/src/Apply.ts
#	packages/alchemy/src/State/LocalState.ts
…e version

Deployment history and write fencing had pushed STATE_STORE_VERSION to 10
(and the HTTP contract to 6). A bump is breaking for everyone with a
deployed store: the next deploy stops to upgrade the worker in place
before it can run. Revert both constants to the values on main and let
the additive endpoints advertise themselves instead.

```ts
// /version, additive and optional — absent on an older worker
capabilities: Schema.optional(Schema.Array(Schema.String))
```

The Cloudflare path already fetches /version before building the client,
so it passes the observed list straight through; no extra round-trip.
An older store reports nothing, `deployments` comes back undefined, and
`openDeploymentSession` takes the no-op session it already had. Fencing
needs nothing — `fence` is already optional on the wire, so a write to a
store that never had fencing simply goes unfenced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
…ui-jhzbam

# Conflicts:
#	packages/alchemy/src/Apply.ts
It existed to force a deployed store up to this branch's bumped
STATE_STORE_VERSION. With the versions rolled back to main's values
there is no gap to close, and `ensureLatest` in State.ts performs the
same in-place upgrade automatically on any genuine future bump.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njd6sRFqot6LLDvzaHYcU
@DavidJFelix

Copy link
Copy Markdown
Contributor Author

Ok @sam-goodwin I removed the store versioning and the demo folder. I think instead demoing from the examples using --ui is the correct way. I think this is ready to merge and I'll follow up with any UI nits and maybe docs around securing this (with access?).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants