From c69936dff340caf92aeb9de39202a140e84a0bb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 20:37:21 +0000 Subject: [PATCH] Embed dashboard in the binary; drop "insights" naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vx serve --ui failed from a compiled binary ("--ui requires apps/insights/dist") because it resolved the SPA from disk. A binary must be self-contained — so the dashboard is now embedded. Embedding: - apps/ui builds to a single self-contained dist/index.html (JS + CSS inlined via a small generateBundle plugin in vite.config.ts). - src/cli/ui-asset.ts imports it with `with { type: 'file' }`, so `bun build --compile` embeds the bytes; the import resolves to a bunfs path Bun.file() reads. Dynamically imported only on --ui, so a source checkout that hasn't built the UI doesn't break `vx run`. - vx serve --ui serves that one file for every non-API GET (the SPA is a hash router). Drops the old on-disk dir walk + VX_INSIGHTS_DIST. - build.ui task (cd apps/ui && bun run build, boundary-free workspaceFiles I/O) builds the SPA; each build.bun.* depends on it so the binary embeds a fresh UI and a UI change cascades into the binary cache key. Same-project dep keeps test/CI unpolluted. Verified: compiled a --minify --bytecode binary, removed apps/ui/dist entirely, vx serve --ui still served the embedded dashboard. Rename (insights → ui / metrics / dashboard): - apps/insights → apps/ui; @vzn/vx-insights → @vzn/vx-ui - src/orchestrator/insights-queries.ts → metrics.ts (+ test) - guide insights.md → dashboard.md; brand, localStorage key, help, README, cli, landing, introduction copy - VX_INSIGHTS_DIST removed (nothing on disk to point at) module-boundaries test gains a .html asset carve-out (mirrors .json). No CACHE_VERSION impact. Full gate green (925 tests). --- README.md | 12 +- apps/docs/astro.config.mjs | 2 +- .../docs/guides/{insights.md => dashboard.md} | 45 ++++--- .../src/content/docs/guides/self-hosting.md | 23 ++-- apps/docs/src/content/docs/introduction.md | 7 +- apps/docs/src/pages/index.astro | 2 +- apps/insights/README.md | 54 -------- apps/insights/vite.config.ts | 14 -- apps/ui/README.md | 47 +++++++ apps/{insights => ui}/index.html | 2 +- apps/{insights => ui}/package.json | 2 +- apps/{insights => ui}/src/api.ts | 10 +- .../src/components/Flamegraph.tsx | 0 .../{insights => ui}/src/components/Shell.tsx | 2 +- .../src/components/Sparkline.tsx | 0 .../{insights => ui}/src/flamegraph-layout.ts | 0 apps/{insights => ui}/src/format.ts | 0 apps/{insights => ui}/src/main.tsx | 0 apps/{insights => ui}/src/pages/CachePage.tsx | 0 apps/{insights => ui}/src/pages/Overview.tsx | 0 apps/{insights => ui}/src/pages/RunDetail.tsx | 0 .../{insights => ui}/src/pages/TaskDetail.tsx | 0 apps/{insights => ui}/src/pages/Tasks.tsx | 0 apps/{insights => ui}/tsconfig.json | 0 apps/{insights => ui}/uno.config.ts | 0 apps/ui/vite.config.ts | 44 +++++++ apps/{insights => ui}/vx.config.ts | 0 bun.lock | 6 +- docs/cli.md | 14 +- docs/progress/implementation-log-2026-06.md | 34 +++++ src/cli/help.ts | 4 +- src/cli/serve.ts | 120 +++++++----------- src/cli/ui-asset.ts | 19 +++ src/orchestrator/index.ts | 4 +- .../{insights-queries.ts => metrics.ts} | 9 +- ...sights-queries.test.ts => metrics.test.ts} | 2 +- tests/module-boundaries.test.ts | 1 + tests/serve.test.ts | 47 +++---- vx.config.ts | 36 +++++- 39 files changed, 318 insertions(+), 244 deletions(-) rename apps/docs/src/content/docs/guides/{insights.md => dashboard.md} (76%) delete mode 100644 apps/insights/README.md delete mode 100644 apps/insights/vite.config.ts create mode 100644 apps/ui/README.md rename apps/{insights => ui}/index.html (91%) rename apps/{insights => ui}/package.json (93%) rename apps/{insights => ui}/src/api.ts (96%) rename apps/{insights => ui}/src/components/Flamegraph.tsx (100%) rename apps/{insights => ui}/src/components/Shell.tsx (99%) rename apps/{insights => ui}/src/components/Sparkline.tsx (100%) rename apps/{insights => ui}/src/flamegraph-layout.ts (100%) rename apps/{insights => ui}/src/format.ts (100%) rename apps/{insights => ui}/src/main.tsx (100%) rename apps/{insights => ui}/src/pages/CachePage.tsx (100%) rename apps/{insights => ui}/src/pages/Overview.tsx (100%) rename apps/{insights => ui}/src/pages/RunDetail.tsx (100%) rename apps/{insights => ui}/src/pages/TaskDetail.tsx (100%) rename apps/{insights => ui}/src/pages/Tasks.tsx (100%) rename apps/{insights => ui}/tsconfig.json (100%) rename apps/{insights => ui}/uno.config.ts (100%) create mode 100644 apps/ui/vite.config.ts rename apps/{insights => ui}/vx.config.ts (100%) create mode 100644 src/cli/ui-asset.ts rename src/orchestrator/{insights-queries.ts => metrics.ts} (98%) rename tests/{insights-queries.test.ts => metrics.test.ts} (99%) diff --git a/README.md b/README.md index 3657314..aae51fc 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ vx mcp # Model Context Protocol server (stdio vx coordinator build test --workers 4 # start a distributed-CI coordinator vx run --worker ws://coord:5180 # pull tasks from a coordinator and execute them -vx serve --ui --open # unified backend + bundled insights SPA + open browser +vx serve --ui --open # unified backend + embedded dashboard + open browser # /v1/* JSON, SSE events, WS run protocol, CORS * ``` @@ -86,12 +86,12 @@ vx serve --ui --open # unified backend + bundled insights S natively; every event flows to Grafana / Honeycomb / Datadog / Tempo with zero bridge package. - **Self-host vx serve.** Same backend everywhere — laptop, Docker, - any container runtime. JSON `/v1/*` insights API + WebSocket run + any container runtime. JSON `/v1/*` metrics API + WebSocket run protocol + SSE event stream + permissive CORS. One stack. -- **Insights dashboard built in.** `vx serve --ui` bundles a Solid +- **Dashboard embedded in the binary.** `vx serve --ui` serves a Solid SPA at `/` — task averages, p50/p99, cache savings, recent runs, - flamegraphs. Connection picker switches between local and hosted - backends; same UI for both. + flamegraphs — compiled into `vx` itself, nothing to install. + Connection picker switches between local and hosted backends. Each lives behind a one-paragraph design doc under `docs/design/*-2026-06.md`. Phase-by-phase implementation log: @@ -309,7 +309,7 @@ Production readiness for the **2026-06 platform layer**: | `vx coordinator` + `vx run --worker` | **shippable for self-hosted CI** | content-addressed assignment, disconnect recovery | | Plugin API | **shippable** | crash-isolated, lifecycle hooks fire end-to-end | | Predictive scheduling | **shippable as opt-in** | gated on `predictive: true` + observed data | -| `apps/insights/` (Solid SPA → vx serve HTTP) | **scaffold** | connection picker, HTTP /v1/\* reads; pages need real-world iteration | +| `apps/ui/` (Solid dashboard, embedded in binary) | **scaffold** | connection picker, HTTP /v1/\* reads; pages need real-world iteration | | OTel native emit (`src/orchestrator/otel-emit.ts`) | **shippable** | env-var auto-attach in `run()`; ships event stream to any OTLP backend | ## Development diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs index b837532..2098e08 100644 --- a/apps/docs/astro.config.mjs +++ b/apps/docs/astro.config.mjs @@ -70,7 +70,7 @@ export default defineConfig({ { label: 'Distributed CI execution', link: '/guides/distributed-ci/' }, { label: 'Writing a vx plugin', link: '/guides/plugins/' }, { label: 'Predictive scheduling', link: '/guides/predictive-scheduling/' }, - { label: 'Insights dashboard', link: '/guides/insights/' }, + { label: 'Dashboard', link: '/guides/dashboard/' }, { label: 'Self-host vx serve', link: '/guides/self-hosting/' }, { label: 'OpenTelemetry CI/CD spans', link: '/guides/otel-bridge/' }, { label: 'vx serve wire protocol', link: '/guides/wire-protocol/' }, diff --git a/apps/docs/src/content/docs/guides/insights.md b/apps/docs/src/content/docs/guides/dashboard.md similarity index 76% rename from apps/docs/src/content/docs/guides/insights.md rename to apps/docs/src/content/docs/guides/dashboard.md index 0d6946c..2b2ebde 100644 --- a/apps/docs/src/content/docs/guides/insights.md +++ b/apps/docs/src/content/docs/guides/dashboard.md @@ -1,11 +1,12 @@ --- -title: Insights dashboard -description: A Solid SPA bundled into vx serve. Run history, per-task averages, cache stats. One flag, no daemon. +title: Dashboard +description: A Solid SPA bundled into the vx binary. Run history, per-task averages, cache stats. One flag, no daemon, nothing to install. --- -The insights dashboard ships inside `vx serve` itself. Pass `--ui` -and the same backend that drives `vx run` delegation also serves -the SPA at `/`. One process, one stack. +The dashboard ships **inside the `vx` binary**. Pass `--ui` to +`vx serve` and the same backend that drives `vx run` delegation also +serves the dashboard at `/`. No separate install, no asset directory +on disk — the SPA is embedded in the executable. ## Quick start @@ -17,11 +18,11 @@ vx serve --ui --open That: 1. boots `vx serve` on a kernel-assigned port, -2. serves the bundled insights SPA at `/`, +2. serves the embedded dashboard at `/`, 3. opens your default browser at the same origin. -`Ctrl-C` stops it. Drop `--open` if you'd rather not auto-launch -a browser; drop `--ui` to get just the JSON API + WS + SSE. +`Ctrl-C` stops it. Drop `--open` if you'd rather not auto-launch a +browser; drop `--ui` to get just the JSON API + WS + SSE. Pin a port with `--port`: @@ -57,7 +58,7 @@ host the SPA once and let everyone aim it at their own backend. ``` Browser ┌─────────────────────────────┐ - │ apps/insights SPA (Solid) │ + │ apps/ui SPA (Solid) │ │ • connection picker │ │ • fetch over HTTP │ └────────┬────────────────────┘ @@ -67,7 +68,7 @@ host the SPA once and let everyone aim it at their own backend. ┌─────────────────────────────┐ │ vx serve --ui (Bun.serve) │ │ • /v1/* JSON over cache.db │ - │ • SPA static at / │ + │ • SPA embedded in binary │ │ • CORS * │ │ • SSE event stream │ │ • WS run protocol │ @@ -80,6 +81,10 @@ host the SPA once and let everyone aim it at their own backend. └─────────────────────────────┘ ``` +The dashboard builds to a single self-contained `index.html` (JS + +CSS inlined), which the binary embeds via Bun's `with { type: 'file' }`. +A compiled `vx` carries it with nothing else on disk. + ## HTTP surface `vx serve` exposes: @@ -107,19 +112,19 @@ All routes ship `Access-Control-Allow-Origin: *`. ## Host the SPA once, point it anywhere -You can also build `apps/insights/dist/` and deploy it to any static -host. The connection picker means the same hosted bundle works -against any reachable `vx serve` — browsers allow HTTPS pages to -call `http://localhost:*` per the Secure Context exception, so a -hosted `https://insights.example.com` reading from a local -`http://localhost:4321` Just Works. +You can also build `apps/ui/dist/` and deploy the single +`index.html` to any static host. The connection picker means the +same hosted bundle works against any reachable `vx serve` — browsers +allow HTTPS pages to call `http://localhost:*` per the Secure +Context exception, so a hosted `https://dash.example.com` reading +from a local `http://localhost:4321` Just Works. ## Privacy When you run `vx serve --ui` locally, nothing leaves your machine. A hosted SPA pointed at `http://localhost:*` is also entirely -local — the picker is just configuration; the page reads from -your machine, not a third party. +local — the picker is just configuration; the page reads from your +machine, not a third party. ## Known limits @@ -127,8 +132,8 @@ your machine, not a third party. server (`/v1/events`) but the SPA doesn't subscribe yet. Reload to see new runs. - **No auth.** `vx serve` binds to localhost by default; trust is - by network reachability. Add a reverse proxy with auth for - hosted deployments. + by network reachability. Add a reverse proxy with auth for hosted + deployments. See also: [`Self-hosting`](/vx/guides/self-hosting/), [`Wire protocol`](/vx/guides/wire-protocol/). diff --git a/apps/docs/src/content/docs/guides/self-hosting.md b/apps/docs/src/content/docs/guides/self-hosting.md index 4514990..88abc8a 100644 --- a/apps/docs/src/content/docs/guides/self-hosting.md +++ b/apps/docs/src/content/docs/guides/self-hosting.md @@ -84,25 +84,28 @@ vx.example.com { } ``` -## Point the SPA at it +## Dashboard -Build `apps/insights/` once and host the resulting `dist/`. Users -open it, paste the server origin into the connection picker, and -the SPA reads via `/v1/*`. No build step per user, no per-user -config — same SPA, any backend. +`vx serve --ui` serves the dashboard at `/` directly from the binary +(it's embedded — no asset directory to deploy). For a hosted +deployment behind a proxy, that's all you need. + +If you'd rather host the dashboard separately, build the single-file +bundle and drop it on any static host. The connection picker means +the same bundle works against any reachable `vx serve`: ```sh -cd apps/insights +cd apps/ui bun install bun run build -# Deploy dist/ to any static host (S3 + CloudFront, Vercel, GitHub -# Pages, your own nginx — anywhere). +# Deploy the single dist/index.html anywhere (S3 + CloudFront, Vercel, +# GitHub Pages, your own nginx). ``` ## Browser → localhost gotcha The Secure Context exception in WHATWG lets HTTPS pages call -`http://localhost:*`. So a hosted `https://insights.example.com` +`http://localhost:*`. So a hosted `https://dash.example.com` can read from `http://localhost:4321` without breaking the mixed- content rule — that's intentional, and what the connection picker exploits. @@ -120,5 +123,5 @@ story. We unified on `vx serve`: - The hosted SPA sees the same `/v1/*` shape locally or against a multi-tenant deployment — no shimming. -See also: [`insights`](/vx/guides/insights/), +See also: [`dashboard`](/vx/guides/dashboard/), [`wire protocol`](/vx/guides/wire-protocol/). diff --git a/apps/docs/src/content/docs/introduction.md b/apps/docs/src/content/docs/introduction.md index cc674ec..564133e 100644 --- a/apps/docs/src/content/docs/introduction.md +++ b/apps/docs/src/content/docs/introduction.md @@ -75,9 +75,10 @@ require additional services: - **[Self-host vx serve](../guides/self-hosting/)** — drop the binary in Docker, get a hosted backend with the same JSON `/v1/*` shape the local one has. One stack, no separate cloud project. -- **[Insights dashboard](../guides/insights/)** — Solid SPA bundled - into `vx serve --ui`. Run history, per-task averages, cache stats. - Connection picker switches between local and hosted servers. +- **[Dashboard](../guides/dashboard/)** — Solid SPA embedded in the + `vx` binary, served by `vx serve --ui`. Run history, per-task + averages, cache stats. Connection picker switches between local and + hosted servers. - **[OpenTelemetry CI/CD spans](../guides/otel-bridge/)** — set `OTEL_EXPORTER_OTLP_ENDPOINT`, install the three OTel peers; every event lands in Grafana / Honeycomb / Datadog / Tempo natively, no diff --git a/apps/docs/src/pages/index.astro b/apps/docs/src/pages/index.astro index 03ba8a6..95f8ab5 100644 --- a/apps/docs/src/pages/index.astro +++ b/apps/docs/src/pages/index.astro @@ -103,7 +103,7 @@ const platform = [ icon: 'cloud', tone: 'var(--plasma)', title: 'Self-hostable, Docker-ready', - body: 'vx serve runs locally or in Docker — same backend everywhere. The hosted insights SPA points at any reachable origin via a connection picker. No separate cloud stack, no CF lock-in.', + body: 'vx serve runs locally or in Docker — same backend everywhere. The dashboard is embedded in the binary and points at any reachable origin via a connection picker. No separate cloud stack, no CF lock-in.', }, { icon: 'brain', diff --git a/apps/insights/README.md b/apps/insights/README.md deleted file mode 100644 index ff59d13..0000000 --- a/apps/insights/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# @vzn/vx-insights - -Local SPA over `cache.db` — read-only analytics for your vx runs. - -## What this is - -A Solid + UnoCSS + Vite app that loads DuckDB-WASM in the browser, ATTACHes -the local SQLite `cache.db` via the `sqlite_scanner` extension, and runs -typed queries against it for the run history and per-task flamegraphs. - -No backend. No ETL. Pure client-side analytics over the same database `vx -run` writes to. - -## Run it - -The intended entry point is the CLI: - -```bash -vx insights serve -``` - -which boots both this SPA's dev server (port 5290 by default) and a tiny -static file server that exposes the workspace's `cache.db` to the -browser via the `VITE_CACHE_DB_URL` env var. - -Standalone (for development of the SPA itself): - -```bash -bun --cwd apps/insights install -bun --cwd apps/insights run dev -``` - -The SPA will look for `cache.db` at the path in `VITE_CACHE_DB_URL`, or -fall back to `/cache.db` on the same origin. - -## Build - -```bash -bun --cwd apps/insights run build -# dist/ is the static bundle -``` - -## Architecture notes - -- **DuckDB-WASM is large (~30MB).** It loads lazily on the first query. - The Overview page shows a loading state during the bootstrap. -- **DuckDB reads SQLite directly** via the `sqlite_scanner` extension — - no ETL, no schema rewrites. The same DB `vx run` writes to is the DB - the SPA reads. -- **Read-only.** The SPA never writes to `cache.db`. The SQLite handle - on the browser side never opens with a write flag. -- **No build step in the orchestrator's hot path.** `vx insights serve` - fails loud with a build hint if `apps/insights/dist/` is missing - rather than silently falling back to something else. diff --git a/apps/insights/vite.config.ts b/apps/insights/vite.config.ts deleted file mode 100644 index f881aa7..0000000 --- a/apps/insights/vite.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'vite' -import solid from 'vite-plugin-solid' -import unocss from 'unocss/vite' - -export default defineConfig({ - plugins: [unocss(), solid()], - server: { - port: 5290, - strictPort: false, - }, - preview: { - port: 5290, - }, -}) diff --git a/apps/ui/README.md b/apps/ui/README.md new file mode 100644 index 0000000..fc836b5 --- /dev/null +++ b/apps/ui/README.md @@ -0,0 +1,47 @@ +# @vzn/vx-ui + +The vx dashboard — a Solid SPA that reads a `vx serve` over HTTP. + +It builds to a **single self-contained `dist/index.html`** (JS + CSS +inlined; see `vite.config.ts`). `vx serve --ui` embeds that one file +into the `vx` binary via `with { type: 'file' }`, so the dashboard +ships inside `vx` with nothing else on disk. + +## Run it + +From a built `vx`: + +```bash +vx serve --ui --open +``` + +## Develop + +```bash +bun run --filter @vzn/vx-ui dev # Vite dev server +bun run --filter @vzn/vx-ui build # produce the single-file dist/index.html +# or, via vx (what the binary build uses): +vx run build.ui +``` + +`VITE_DEFAULT_ORIGIN` seeds the dev server's connection target; the +header's connection picker overrides it at runtime. The SPA is +platform-agnostic — every read is an HTTP call to a configurable +origin, so the same UI works against a local or hosted `vx serve`. + +## Pages + +- **Overview** — time saved, hit rate, top time-burners, recent + failures, cache-by-project, recent invocations. +- **Tasks** — sortable per-(project, task) table (runs, success/hit + rate, avg/p50/p99, total time, last run). +- **Task detail** — full stats, duration sparkline, latest cache + entry, recent-run table with CPU + peak RSS. +- **Cache** — entries table (sortable) + by-project bytes breakdown. +- **Run detail** — per-task flamegraph. + +## Data source + +Everything comes from `vx serve`'s `/v1/*` JSON routes over the +workspace's `cache.db`. No DuckDB, no direct file reads, no build +step in the runner's hot path. diff --git a/apps/insights/index.html b/apps/ui/index.html similarity index 91% rename from apps/insights/index.html rename to apps/ui/index.html index 8f774af..3b68822 100644 --- a/apps/insights/index.html +++ b/apps/ui/index.html @@ -4,7 +4,7 @@ - vx insights + vx dashboard
diff --git a/apps/insights/package.json b/apps/ui/package.json similarity index 93% rename from apps/insights/package.json rename to apps/ui/package.json index 8af922b..e1ad872 100644 --- a/apps/insights/package.json +++ b/apps/ui/package.json @@ -1,5 +1,5 @@ { - "name": "@vzn/vx-insights", + "name": "@vzn/vx-ui", "version": "0.0.0", "private": true, "type": "module", diff --git a/apps/insights/src/api.ts b/apps/ui/src/api.ts similarity index 96% rename from apps/insights/src/api.ts rename to apps/ui/src/api.ts index a52c4ac..4c132d8 100644 --- a/apps/insights/src/api.ts +++ b/apps/ui/src/api.ts @@ -1,4 +1,4 @@ -// HTTP client for the vx serve insights API. Same shape locally or +// HTTP client for the vx serve metrics API. Same shape locally or // against a remote/hosted vx serve — the SPA is platform-agnostic. // // The base URL is resolved from the connection store; that store @@ -9,11 +9,11 @@ import { createSignal } from 'solid-js' -const STORAGE_KEY = 'vx-insights:origin' +const STORAGE_KEY = 'vx-ui:origin' function defaultOrigin(): string { - // `vx insights` injects this at dev time; the hosted build falls back - // to the user choosing via the connection picker. + // The dev server injects this; the hosted build falls back to the user + // choosing via the connection picker. const injected = import.meta.env.VITE_DEFAULT_ORIGIN if (typeof injected === 'string' && injected.length > 0) return injected return 'http://localhost:4321' @@ -52,7 +52,7 @@ async function getJson(pathname: string): Promise { } // --------------------------------------------------------------------------- -// Types — mirror src/orchestrator/insights-queries.ts return shapes. +// Types — mirror src/orchestrator/metrics.ts return shapes. // --------------------------------------------------------------------------- export interface RunSummaryRow { diff --git a/apps/insights/src/components/Flamegraph.tsx b/apps/ui/src/components/Flamegraph.tsx similarity index 100% rename from apps/insights/src/components/Flamegraph.tsx rename to apps/ui/src/components/Flamegraph.tsx diff --git a/apps/insights/src/components/Shell.tsx b/apps/ui/src/components/Shell.tsx similarity index 99% rename from apps/insights/src/components/Shell.tsx rename to apps/ui/src/components/Shell.tsx index 0da6b58..37afd33 100644 --- a/apps/insights/src/components/Shell.tsx +++ b/apps/ui/src/components/Shell.tsx @@ -25,7 +25,7 @@ export const Shell: ParentComponent = (props) => {
- vx insights + vx dashboard