feat(dashboard): full AWS UI coverage + hosted state-viewer deployment - #1022
Open
DavidJFelix wants to merge 115 commits into
Open
feat(dashboard): full AWS UI coverage + hosted state-viewer deployment#1022DavidJFelix wants to merge 115 commits into
DavidJFelix wants to merge 115 commits into
Conversation
- 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>
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.
…key, KMS on S3) (alchemy-run#984)" This reverts commit 0378bf8.
…after encryption revert
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.
…ui-jhzbam # Conflicts: # bun.lock
Contributor
|
Can you include some screenshots or video? |
…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
marked this pull request as ready for review
August 3, 2026 23:08
Contributor
Author
|
I think i'm at a pause point looking for either a merge or direction. Hosted viewui 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.movCLI view with approve/denythis existed already but just showing it in action Screen.Recording.2026-08-03.at.6.07.21.PM.movNext steps
Long goals
|
DavidJFelix
commented
Aug 3, 2026
DavidJFelix
commented
Aug 3, 2026
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 :) |
…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
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?). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/{Service}/ui.ts;AWS/UI.tsis regenerated with nestedLayer.mergeAllgroups.alchemy/Dashboard/Viewer: the read-only half of the dashboard API as a platform-free handler (no FileSystem/Path/process) over anyStateService— document snapshots, SSE, deployment history + journal folds, projections.sse: "stream" | "poll"adapts the transport to hosts that buffer responses.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 everyResource<>("Cloud.Type")in a registered cloud has a UIProvider — the 691/691 census as a permanent check (it already caughtGitHub.Environment,Docker.Context,Docker.Swarm).Plan.destroy, aggregatedDestroyError, provider modes reconciled with the deployment journal; on a successful destroy,state.deleteStackruns after the journal closes since the journal lives inside the stage directory).Hosted viewer: host x state-backend matrix
viewer({ state })takes anyStateService, 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.examples/dashboard-viewerexamples/dashboard-viewer-awsalchemy dashboard/deploy --ui)Cloudflare Worker + HTTP store (
viewer.ts): SPA as Worker assets (runWorkerFirst: ["/api/*"], SPA fallback), state overmakeHttpStateStore. Same-zone worker-to-workerfetchis blocked (error 1042), so the state API rides a service binding toalchemy-state-store(FetchHttpClient.Fetchoverride); plain fetch remains the cross-zone fallback. A barebun run deployself-configures from the credentialsCloudflare.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 getss3:GetObject/s3:ListBucketon the state bucket pluskms:Decrypt; no long-lived credentials exist. OneAWS.Website.Routerdistribution serves the SPA from S3 and routes/api/*to the function URL. Lambda URLs buffer responses, so this host usessse: "poll"(one snapshot frame +retry:hint, then close; EventSource auto-reconnect makes it polling).Lambda + Cloudflare store (no example needed): swap
AWS.makeS3State(...)formakeHttpStateStore({ 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):
makeS3Stateruns on workerd (distilled AWS is fetch-based), but the Worker would need long-lived IAM user keys stored as worker secrets pluskms: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/stateby construction (makeLocalStaterequiresFileSystem, which no remote host provides — it fails to compile, not at runtime). The CLI dashboard is that cell;alchemy state syncbridges 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.