Devtools UI

Walkthrough of the local devtools dashboard's Migrations tab — drift banner, pending plan card, and history timeline. What's clickable, what's not, and when the Apply button appears.

The local devtools dashboard (http://localhost:5179) ships a per-app Migrations tab that mounts the shared MigrationsPage component from @voltro/devtools-ui. The cloud dashboard mounts the same component over different transport — see Cloud UI.

To reach it: voltro dev boot launches the dashboard automatically, pick an app from the sidebar, click the Migrations tab between Database and Data.

Layout

┌──────────────────────────────────────────────────────────────┐
│ Migrations                                                   │
│ App: myApi · http://localhost:4000                           │
├──────────────────────────────────────────────────────────────┤
│ ⚠ Schema drift detected                                      │ ← drift banner
│   last applied: 8f507ba1e1aadad5 at 2026-06-15 14:32:00     │   only when drifted
│   live now:     d32149b280101693                             │
│   → Run `voltro db plan` to see what your code expects.      │
├──────────────────────────────────────────────────────────────┤
│ Pending plan — 3 ops                                         │ ← pending card
│   ✓ ALTER TABLE users ADD COLUMN bio text                    │   classifications +
│   ⊕ ALTER TABLE users ADD COLUMN email text                  │   fix hints
│     # NOT NULL, declared backfill: sql`'unknown-' || id`     │
│   ⊕ UPDATE users SET email = ...                             │
│                                                              │
│   from 8f507ba1e1aadad5 → to a8f2c9d10b3f4e62                │
├──────────────────────────────────────────────────────────────┤
│ Applied history — up to 20 plans                             │ ← history timeline
│   plan_mig_5k78  fp=d3214928  env=dev  src=auto-diff  ...    │
│   plan_mig_5k77  fp=b414a413  env=dev  src=auto-diff  ...    │
│   plan_mig_5k76  fp=bd27c13e  env=dev  src=auto-diff  ...    │
│   ...                                                        │
└──────────────────────────────────────────────────────────────┘

Auto-refreshes every 10 seconds. The transport is HTTP — the page fetches GET <app-url>/_voltro/inspect/migrations directly + renders the JSON.

Drift banner

Renders at the top in rose when drift.isDrifted: true. Surfaces:

  • last applied fingerprint + the time it was applied
  • live fingerprint computed from the current introspection
  • A pointer at the CLI command to investigate

The banner is informational — it doesn't block anything. The Apply button below it (when present) still works; the plan diff will include reconciling changes to bring live back to declared.

Common drift causes:

  • A psql (or equivalent) session ran DDL outside the planner
  • A team member applied a plan but the row didn't make it into _voltro_migration_plans (rare; would mean the applier crashed after DDL but before writing the row)
  • The _voltro_migration_plans table itself got truncated / restored from a backup

For non-trivial drift, run voltro db drift in the shell — same diagnosis, with a copy-pasteable fix path.

Pending plan card

Renders the MigrationPlan from the planner.

  • One line per PlannedOperation with a color-coded badge for its OperationClass:
    • safe / needs-default → emerald
    • needs-backfill / needs-rename-annotation → amber
    • lossy → rose
    • online-required → cyan
    • multi-step → fuchsia
  • The op description follows the badge — e.g. ALTER TABLE users ADD COLUMN email text NOT NULL
  • A dim-text reason follows: # backfill declared (kind=sql) — applier will run 3-step add/update/set-not-null
  • For blocked ops, a red ! fix: hint underneath: ! fix: declare email: text().backfill(sql...) ...

Below the op list, a chip row summarises counts per class. Below that, the from/to fingerprint short forms.

The Apply button

The shared MigrationsPage component renders an "Apply plan" button only when BOTH the caller's canApplyMigration capability is set AND the host wires an optional useApplyPlan hook (and the pending plan has zero blocked ops). The local devtools is single-user, single-machine, so it can wire that hook; the cloud dashboard never does (see Cloud UI).

When the Apply button isn't wired, the page surfaces the next-best thing: run voltro db apply from the project root + the page auto-refreshes within 10 seconds to show the post-apply state. That CLI path carries --note '...', runs with whatever credentials are in the operator's shell, and exits with a status code CI/CD can act on — and it's the only path that ever reaches production, since voltro db apply refuses on NODE_ENV=production.

History timeline

Lists rows from _voltro_migration_plans newest-first, capped at 20.

Per-row:

  • plan idplan_mig_5k78, the typeid from the row
  • fp — 16-char short fingerprint of the post-apply state
  • env — dev / staging / prod (color-coded chip)
  • src — auto-diff (planner) or file (file-based migration)
  • op count — how many ops were in that plan
  • duration — milliseconds the apply took
  • applied at — ISO timestamp
  • applied by — CLI user / boot:dev / service principal
  • notes — the freeform --note string if provided, dim italic

Click a row to expand → shows the operations JSON (the full PlannedOperation[] that ran). Useful for "why is users.legacy gone?" — find the plan that dropped it, see exactly what executed.

No per-row Rollback button

History rows are read-only — there is NO Rollback button on a plan row. That matches the runtime: planner-applied plans have no auto-rollback (see Rollback). The page DOES surface a separate restore control for soft-dropped columns (when a plan was applied with VOLTRO_SOFT_DROP=1, the sidecar columns get a per-plan "Restore" button driving voltro db restore-snapshot). Reversing a file-based migration's down body is a CLI-only action (voltro db rollback-file <id>).

Loading + error states

The page wraps the data fetch in a DataSource<MigrationsStatus>:

  • Pending: shows a loading card while the first fetch is in flight
  • Error: renders the error message in rose
  • Data + error both undefined → page is in initial state, no flicker

A common error: the framework's inspect endpoint isn't reachable. Verify with curl -sS http://localhost:4000/_voltro/inspect/migrations — should return JSON. If not, check that the app is running + VOLTRO_INSPECT isn't off.

Multi-app

The dashboard's app picker shows every running voltro process (from ~/.voltro/runtime-registry.json). Each app gets its own Migrations tab; the data is per-app + the URL includes the app id.

If multiple apps target the same database, they're showing the same _voltro_migration_plans rows — applied plans are global. Drift detection runs against the live DB per-app inspect, so if one app is talking to a different db (env var override etc.) you might see drift on one but not the other.

Source code

The shared component lives in voltro/packages/devtools-ui/src/pages/MigrationsPage.tsx. The local devtools wiring is in voltro-devtools/apps/dashboard/src/pages/apps/[appId]/migrations.tsx. Both repos are open to extension.