Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 22 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +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 insights # localhost Solid SPA over vx serve's /v1/* API
# — historical run flamegraphs, cache stats

vx serve # the unified backend — local OR Docker
vx serve --ui --open # unified backend + bundled insights SPA + open browser
# /v1/* JSON, SSE events, WS run protocol, CORS *
```

Expand Down Expand Up @@ -91,9 +88,10 @@ vx serve # the unified backend — local OR Doc
- **Self-host vx serve.** Same backend everywhere — laptop, Docker,
any container runtime. JSON `/v1/*` insights API + WebSocket run
protocol + SSE event stream + permissive CORS. One stack.
- **`vx insights`** — Solid SPA that talks to `vx serve` over HTTP.
Connection picker switches between local and hosted backends;
same UI for both. No DuckDB-WASM, no 30MB payload.
- **Insights dashboard built in.** `vx serve --ui` bundles 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.

Each lives behind a one-paragraph design doc under
`docs/design/*-2026-06.md`. Phase-by-phase implementation log:
Expand Down Expand Up @@ -174,23 +172,23 @@ traces · `vx cache prune` with TTL and size caps.

## How it compares

| | vx | Turborepo | Nx |
| ------------------------- | ------------------------------------------------------------------- | ------------------------------ | ------------------- |
| Fully cached, 100 pkgs¹ | **144 ms** | 279 ms | 583+ ms |
| Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
| Output ownership | **Strict** — wiped before exec AND restore | Additive (stale files survive) | Additive |
| Clean-tree hashing | **Zero reads** (git index OIDs) | git OIDs | re-hash / daemon |
| Daemon required for speed | **No** | Optional | Yes |
| Artifact signing | **Hard-fail** on unsigned | Soft | No |
| Per-task sandbox | **Yes** — kernel-level, opt-in | No | No |
| MCP server for AI agents | **Yes** (`vx mcp`, stdio) | No | No |
| Distributed CI execution | **Yes** — OSS, self-hostable (`vx coordinator` + `vx run --worker`) | No | Paid (Nx Cloud DTE) |
| Dashboard SPA | **Yes** (`vx insights`, Solid, talks HTTP to `vx serve`) | No | Paid |
| Self-hosted cloud | **Yes** — same `vx serve` in Docker; one stack | Vercel-only | No (proprietary) |
| Plugin API | **Yes** — Vite-style lifecycle hooks | No | Yes (TS-tied) |
| Predictive scheduling | **Yes** (opt-in: `predictive: true`) | No | No |
| OTel CI/CD spans | **Yes** (`OTEL_EXPORTER_OTLP_ENDPOINT`) | No | Paid |
| Install | **Single binary** — 1 curl line | npm + Node | npm + Node |
| | vx | Turborepo | Nx |
| ------------------------- | -------------------------------------------------------------------- | ------------------------------ | ------------------- |
| Fully cached, 100 pkgs¹ | **144 ms** | 279 ms | 583+ ms |
| Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
| Output ownership | **Strict** — wiped before exec AND restore | Additive (stale files survive) | Additive |
| Clean-tree hashing | **Zero reads** (git index OIDs) | git OIDs | re-hash / daemon |
| Daemon required for speed | **No** | Optional | Yes |
| Artifact signing | **Hard-fail** on unsigned | Soft | No |
| Per-task sandbox | **Yes** — kernel-level, opt-in | No | No |
| MCP server for AI agents | **Yes** (`vx mcp`, stdio) | No | No |
| Distributed CI execution | **Yes** — OSS, self-hostable (`vx coordinator` + `vx run --worker`) | No | Paid (Nx Cloud DTE) |
| Dashboard SPA | **Yes** — bundled into `vx serve --ui`, Solid + p50/p99 + sparklines | No | Paid |
| Self-hosted cloud | **Yes** — same `vx serve` in Docker; one stack | Vercel-only | No (proprietary) |
| Plugin API | **Yes** — Vite-style lifecycle hooks | No | Yes (TS-tied) |
| Predictive scheduling | **Yes** (opt-in: `predictive: true`) | No | No |
| OTel CI/CD spans | **Yes** (`OTEL_EXPORTER_OTLP_ENDPOINT`) | No | Paid |
| Install | **Single binary** — 1 curl line | npm + Node | npm + Node |

¹ Wall-clock, direct binaries, same machine and workspace — full
methodology and more scenarios in
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'vx insights — local & hosted dashboard', link: '/guides/insights/' },
{ label: 'Insights dashboard', link: '/guides/insights/' },
{ 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/' },
Expand Down
8 changes: 4 additions & 4 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"type": "module",
"dependencies": {
"@astrojs/starlight": "^0.40.0",
"astro": "^6.4.6",
"mermaid": "^11.6.0",
"sharp": "^0.34.0",
"unist-util-visit": "^5.0.0"
"astro": "^6.4.8",
"mermaid": "^11.15.0",
"sharp": "^0.35.2",
"unist-util-visit": "^5.1.0"
}
}
100 changes: 57 additions & 43 deletions apps/docs/src/content/docs/guides/insights.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,56 @@
---
title: vx insights — local & hosted dashboard
description: A Solid SPA that talks to vx serve over HTTP. Same UI locally or against a remote server. Run history, flamegraphs, cache stats, no daemon.
title: Insights dashboard
description: A Solid SPA bundled into vx serve. Run history, per-task averages, cache stats. One flag, no daemon.
---

`vx insights` opens a dashboard backed by `vx serve` — the same
unified backend used everywhere. Run it locally for a private,
client-side view of your workspace; or point a hosted copy of the
SPA at any reachable `vx serve` for a team view. One platform.
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.

## Quick start

```sh
cd your-workspace
vx insights
vx serve --ui --open
```

This boots two foreground processes:
That:

- **`vx serve`** on a kernel-assigned port, exposing the JSON `/v1/*`
insights API + the WebSocket run-submission protocol.
- **The SPA** (Vite dev server) on `http://127.0.0.1:5290`.
1. boots `vx serve` on a kernel-assigned port,
2. serves the bundled insights SPA at `/`,
3. opens your default browser at the same origin.

`Ctrl-C` stops both. Override the SPA port with `--port`.
`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`:

```sh
vx serve --ui --port 4321
```

## What you see

- **Overview** — cache stats cards (entries, total bytes, 24h hit
rate, runs/24h) plus a list of recent invocations sorted by start
time. Click a row → run detail.
- **Run detail** — per-task timeline (flamegraph), one lane per
project, bars colored by status / cache source, plus the task
table.
- **Overview** — hero cards (time saved 24h, hit rate, entries,
total time saved), Top time-burners + Recent failures, cache
bytes per project, recent invocations.
- **Tasks** — sortable table over every `(project, task)`: runs,
success rate, hit rate, avg, p50, p99, total time, last run;
substring filter; failure-mode dot.
- **Task detail** — 10 stat cards (min/avg/p50/p99/max, success/hit
rate, failure mode, last run), duration sparkline of the last 100
runs with cache hits marked, latest cache-entry card, full
recent-run table with CPU + peak RSS + hash.
- **Cache** — sortable entries table (largest/newest/recently-
accessed/slowest), by-project bytes breakdown.
- **Run detail** — per-task timeline flamegraph, lane per project,
bars colored by status / cache source.

A **connection picker** in the top-right shows the current server
origin and a status dot. Click to paste a different URL and the
origin and a status dot. Click it to paste a different URL and the
SPA reconnects — `http://localhost:4321` for another local server,
or `https://vx.your-company.com` for a hosted one.
or `https://vx.your-company.com` for a hosted one. Useful when you
host the SPA once and let everyone aim it at their own backend.

## How it works

Expand All @@ -50,8 +65,9 @@ or `https://vx.your-company.com` for a hosted one.
│ WS / (delegated run submission)
┌─────────────────────────────┐
│ vx serve (Bun.serve)
│ vx serve --ui (Bun.serve) │
│ • /v1/* JSON over cache.db │
│ • SPA static at / │
│ • CORS * │
│ • SSE event stream │
│ • WS run protocol │
Expand All @@ -64,12 +80,9 @@ or `https://vx.your-company.com` for a hosted one.
└─────────────────────────────┘
```

The SPA is platform-agnostic — every read is an HTTP call to a
configurable origin. Same JSON shape locally or hosted.

## HTTP surface

`vx serve` exposes a small JSON API:
`vx serve` exposes:

| Path | Returns |
| --- | --- |
Expand All @@ -78,31 +91,32 @@ configurable origin. Same JSON shape locally or hosted.
| `GET /v1/runs?project=&task=&runId=&limit=` | per-task run rows |
| `GET /v1/invocations?limit=` | grouped per `runId` |
| `GET /v1/runs/:runId` | full run detail + tasks |
| `GET /v1/tasks/:taskId` | per-task aggregate + recent + latest entry |
| `GET /v1/top-tasks?limit=` | biggest time-burners |
| `GET /v1/failures?limit=` | recent failed runs |
| `GET /v1/history?project=&task=&limit=` | rollups w/ p50/p99 |
| `GET /v1/cache/stats` | entry count, size, 24h hit rate |
| `GET /v1/history?project=&task=&limit=` | per-task rollups + p50/p99 |
| `GET /v1/explain/:taskId` | latest cache-key entry for a task |
| `GET /v1/cache/savings` | estimated time the cache saved you |
| `GET /v1/cache/breakdown?limit=` | bytes per project |
| `GET /v1/cache/entries?orderBy=size_bytes\|created_at\|accessed_at\|duration_ms&project=&limit=` | entries table |
| `GET /v1/explain/:taskId` | latest cache-key entry |
| `GET /v1/why/:runId/:taskId` | compare hash with previous run |
| `GET /events`, `GET /v1/events` | SSE stream of run events |

All routes ship `Access-Control-Allow-Origin: *` — the hosted SPA
must be able to reach localhost from a foreign origin.
All routes ship `Access-Control-Allow-Origin: *`.

## Host the SPA once, point it anywhere

Build `apps/insights/` once, deploy the static `dist/` to any
host. Users open it, paste their `vx serve` origin into the
connection picker, and the SPA does its thing. Browsers allow
HTTPS pages to call `http://localhost:*` per the Secure Context
exception, so a hosted `https://insights.example.com` can read
from a local `vx serve` running on `http://localhost:4321`.

This is the "Cloud" model — but the cloud is just a deployment
of `vx serve` (in Docker, on a VM, anywhere). No separate stack,
no Cloudflare Workers, no D1.
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.

## Privacy

When you run `vx insights` locally, nothing leaves your machine.
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.
Expand All @@ -112,9 +126,9 @@ your machine, not a third party.
- **No real-time view yet.** SSE event streaming exists on the
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.
- **No auth.** `vx serve` binds to localhost by default; trust is
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/).
6 changes: 3 additions & 3 deletions apps/docs/src/content/docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ 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.
- **[vx insights](../guides/insights/)** — Solid SPA over `vx serve`.
Run history, flamegraphs, cache stats. Connection picker switches
between local and hosted servers.
- **[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.
- **[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
Expand Down
16 changes: 8 additions & 8 deletions apps/insights/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
"preview": "vite preview"
},
"devDependencies": {
"@solidjs/router": "^0.15.0",
"@unocss/preset-icons": "^0.65.0",
"@unocss/preset-uno": "^0.65.0",
"@unocss/transformer-variant-group": "^0.65.0",
"solid-js": "^1.9.0",
"unocss": "^0.65.0",
"vite": "^6.0.0",
"vite-plugin-solid": "^2.11.0"
"@solidjs/router": "^0.16.1",
"@unocss/preset-icons": "^66.7.2",
"@unocss/preset-uno": "^66.7.2",
"@unocss/transformer-variant-group": "^66.7.2",
"solid-js": "^1.9.13",
"unocss": "^66.7.2",
"vite": "^8.0.16",
"vite-plugin-solid": "^2.11.12"
}
}
Loading
Loading