# feat\(web\)\!: migrate to SvelteKit 3 and Vite 8 with build-time DB driver · PR \#115 · flambo/atlantis

[View on GitCafe](https://git.cafe/flambo/atlantis/pull/115)

Repository: [flambo/atlantis](https://git.cafe/flambo/atlantis)

Visibility: public

State: merged

Source repository: flambo/atlantis

Source branch: stack/sveltekit-3

Target branch: stack/sveltekit-2-70

Head commit: b72af5856c99a520a25c9c1a604dbd39e56a4153

[Discussion and reviews](https://git.cafe/flambo/atlantis/pull/115)

[Commits](https://git.cafe/flambo/atlantis/pull/115/commits?format=markdown)

[Files changed](https://git.cafe/flambo/atlantis/pull/115/diffs?format=markdown)

## Description

---

> [!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.27` and adapter-cloudflare `8.0.0-next.7`, pinned exactly.
  - Vite 8, `vite-plugin-svelte` 7, TypeScript 6 and vitest 5.
  - Node 24 in `shell.nix` and `.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.
- **Config:** `svelte.config.js` is gone. Its settings now live in `sveltekit({...})` in `vite.config.ts`. `$lib` imports are now `#lib` subpath imports with file extensions, and env vars come from `$app/env/private`.
- **Build-time DB driver:**
  - `#db` resolves to `db/d1.ts` under a custom `atlantis-d1` condition, and to `db/sqlite.ts` otherwise.
  - `ATLANTIS_DB_DRIVER` is read only from the shell, never from `.env`. A stray `.env` value can't turn a deploy build into a SQLite worker.
  - `vite build` defaults to D1 and `vite dev` to SQLite. Routes no longer receive `platform`.
- **runed removed:** URL state is read from `page.url` through the zod schema. All writes go through one `updateSearch()` called from event handlers, using `goto(…, { 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 single `updateSearch()`. 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.

1. **Drill down from a wide daily range (Traffic Overview).** Open `/datasets/<id>?startDate=2025-01-01&endDate=2025-03-31&groupBy=date` and click a daily point.
   - **Expected:** the URL becomes about a month around the clicked day with `groupBy=hour`.
   - **Expected:** the next `/api/netflow/stats` request has `groupBy=hour`.
   - **Before this fix:** the dates changed but `groupBy` stayed at `date`.
2. **Drill down in the IP, protocol or spectrum breakdown.** Use the same URL and click a daily point.
   - **Expected:** `groupBy=hour` in the URL.
   - **Expected:** the next `/api/ip/stats` (or protocol/spectrum) request has `granularity=1h`.
3. **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.
4. **Preview after a deploy build.** Run `bun run build:web`, then `bun run --cwd apps/web preview`.
   - **Expected:** the dashboard serves local SQLite data. Before this fix it returned 500.
5. **Dev mode.** Run `bun run dev:web`.
   - **Expected:** it reads local SQLite without starting workerd.

## Edge cases and decisions
- A URL with an invalid `groupBy` is 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.
- `preview` always builds before serving, so it is slower than a bare `vite preview`. In exchange, it can't serve a stale or mismatched build.
- The shadcn-svelte `components.json` still generates `$lib` imports. Fix them by hand when adding components.

## Verification
**Automated (all pass):**
- `bun run format`, `lint`, `typecheck` and `test:web`.
- `bun run test:e2e` (8 tests). It includes the new `dashboard-drilldown.spec.ts`, which checks both the URL `groupBy` and 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 old `preview` script, 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 no `cloudflare:workers`.
- Local D1 under `wrangler dev` returns 200.

**Remaining manual checks:**
- Drill-downs on the protocol and spectrum charts against real data.
- `wrangler dev` against local D1 after `build:web`.

---
Claude Opus 5.5 · Claude Code (T3 Code)



## Rebased onto the merged MAAD stack

- `$lib` → `#lib` applied 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`, `measure` and 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.

