feat(web)!: migrate to SvelteKit 3 and Vite 8 with build-time DB driver#115
Warning
🤖 Claude Opus 5.5 on behalf of Oliver. This moves the dashboard to a prerelease SvelteKit (3.0.0-next.27). That is intentional.
ELI5
This moves the dashboard to the next major version of its web framework. It also picks the database (a local SQLite file or Cloudflare D1) when the app is built, instead of on every request.
Why
Alchemy's SvelteKit support needs kit ≥ 3.0.0-next.27 and Vite 7 or 8. SvelteKit 3 also removes platform.env on Cloudflare, so D1 has to come from cloudflare:workers. That module can't be imported in a Node build, so the runtime platform?.env.DB check had to go.
Implementation
- Versions:
- SvelteKit
3.0.0-next.27and adapter-cloudflare8.0.0-next.7, pinned exactly. - Vite 8,
vite-plugin-svelte7, TypeScript 6 and vitest 5. - Node 24 in
shell.nixand.node-version, because kit 3 needs Node ≥ 22.17 and nixpkgs only has 22.16. The Node version pins across the stack are reconciled in #117.
- SvelteKit
- Config:
svelte.config.jsis gone. Its settings now live insveltekit({...})invite.config.ts.$libimports are now#libsubpath imports with file extensions, and env vars come from$app/env/private. - Build-time DB driver:
#dbresolves todb/d1.tsunder a customatlantis-d1condition, and todb/sqlite.tsotherwise.ATLANTIS_DB_DRIVERis read only from the shell, never from.env. A stray.envvalue can't turn a deploy build into a SQLite worker.vite builddefaults to D1 andvite devto SQLite. Routes no longer receiveplatform.
- runed removed: URL state is read from
page.urlthrough the zod schema. All writes go through oneupdateSearch()called from event handlers, usinggoto(…, { reset: false }). The group-by clamp is a$derived, and URL sync no longer writes state from an effect. - Drill-down: Traffic Overview, the IP, protocol and spectrum breakdowns, Flow Characteristics and Unique Ports now each report a drill-down as one
{ groupBy, startDate, endDate }payload. The page applies it in a singleupdateSearch(). Previously the new group-by was sent before the new dates, so it was clamped against the old range.
Supported build and preview pairs
vite preview runs the server in Node, so it can only serve the SQLite build.
| Goal | Command | Result |
|---|---|---|
| Local dev on SQLite | bun run dev:web | Vite dev server, no workerd |
| Local dev on D1 | ATLANTIS_DB_DRIVER=d1 bun run dev:web | Dev through the Workers runtime |
| Preview a production build | bun run --cwd apps/web preview | Always rebuilds with SQLite, then runs vite preview |
| Serve the deploy build | bun run build:web, then cd apps/web && bunx wrangler dev | D1 worker under wrangler |
vite preview also ignores ATLANTIS_DB_DRIVER and always resolves SQLite. Running build:web and then preview used to load the Cloudflare bundle in Node and return 500. It now works.
Flows to exercise
Setup: a local SQLite dataset covering at least 90 days, with daily rows.
- Drill down from a wide daily range (Traffic Overview). Open
/datasets/<id>?startDate=2025-01-01&endDate=2025-03-31&groupBy=dateand click a daily point.- Expected: the URL becomes about a month around the clicked day with
groupBy=hour. - Expected: the next
/api/netflow/statsrequest hasgroupBy=hour. - Before this fix: the dates changed but
groupBystayed atdate.
- Expected: the URL becomes about a month around the clicked day with
- Drill down in the IP, protocol or spectrum breakdown. Use the same URL and click a daily point.
- Expected:
groupBy=hourin the URL. - Expected: the next
/api/ip/stats(or protocol/spectrum) request hasgranularity=1h.
- Expected:
- Clamping and history. Change the dates and the group-by, then pick a range longer than 62 days.
- Expected: Day is forced and the finer options are disabled.
- Expected: back and forward restore both the URL and the charts.
- Preview after a deploy build. Run
bun run build:web, thenbun run --cwd apps/web preview.- Expected: the dashboard serves local SQLite data. Before this fix it returned 500.
- Dev mode. Run
bun run dev:web.- Expected: it reads local SQLite without starting workerd.
Edge cases and decisions
- A URL with an invalid
groupByis no longer rewritten on load. The clamped value is shown, and the URL is corrected on the next filter change. The old effect pushed an extra history entry. - A start date that forces a clamp now sends one stats request instead of a stale request followed by a correct one.
- A drill-down whose new range still allows the requested granularity keeps it. Clamping only applies when the target range is too wide.
previewalways builds before serving, so it is slower than a barevite preview. In exchange, it can't serve a stale or mismatched build.- The shadcn-svelte
components.jsonstill generates$libimports. Fix them by hand when adding components.
Verification
Automated (all pass):
bun run format,lint,typecheckandtest:web.bun run test:e2e(8 tests). It includes the newdashboard-drilldown.spec.ts, which checks both the URLgroupByand the API granularity for Traffic Overview and the IP breakdown. Both tests fail without the fix.- The Playwright web server builds the default D1 worker first and then runs
preview, so every e2e run covers build → preview. With the oldpreviewscript, all 8 tests fail. build:web, which the e2e web server runs.
Checked manually on this branch before the fixes:
- All 12 API responses are byte-identical to #114 on a production-sized dataset.
- Playwright URL and history parity against #114.
- The Cloudflare build contains no
better-sqlite3, and the Node build contains nocloudflare:workers. - Local D1 under
wrangler devreturns 200.
Remaining manual checks:
- Drill-downs on the protocol and spectrum charts against real data.
wrangler devagainst local D1 afterbuild:web.
Claude Opus 5.5 · Claude Code (T3 Code)
Rebased onto the merged MAAD stack
$lib→#libapplied to all MAAD-stack files.- The SQLite driver uses the MAAD stack's schema gate, which skips obsolete databases. Both drivers return
has_locality. - URL state (the runed replacement) keeps
direction,ipVersion,measureand 10-minute grouping. The single drill-down callback also covers the MAAD Dimensions card. - The D1 driver interface is unchanged (four functions); the new MAAD queries are shared code. A new parity test runs 7 routes against both drivers, with D1 returning MAAD blobs as
number[]. Breaking that decode fails 4 of the tests. - New e2e test for Dimensions drill-down. Local checks: 197 web tests and 17/17 e2e pass.
Checks
- SuccessfulPlaywright E2EGitHub
- SuccessfulRust Pipeline ChecksGitHub
- SuccessfulWeb ChecksGitHub
Added 7 commits
- feat(web)!: migrate to SvelteKit 3 and Vite 8 with build-time DB driver1576b0d
- refactor(web): replace runed with SvelteKit URL state3dffdaa
- fix(web): build with SQLite before vite preview70bb59b
- fix(web): apply dashboard drill-down grouping and dates in one URL updatebe736a3
- test(web): check SQLite and D1 driver parity for MAAD routesf49f384
- test(web): cover the MAAD dimensions drill-down in one URL update5d4d2c3
- test(web): hoist the cloudflare:workers mock in the driver parity testb72af58