Migrate webapp to SvelteKit (Svelte 5 + adapter-node) #6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "worktree-svelte-migration"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #4.
Shape of the change
Two-codebase webapp → one SvelteKit app.
nexus/src/) + vanilla-TS SPA (nexus/web/), built intonexus/dist/{src,web}/, served together by Fastify with@fastify/static.nexus/build/, run vianode build. Same external behavior, same UI.Pure-logic modules (auth, crypto, db, services, providers) move under
src/lib/server/essentially untouched. The Fastify-only seams (server.ts,*routes.ts,guard.ts,types.d.ts,@fastify/cookieregistration) are deleted and replaced by SvelteKit endpoints +hooks.server.ts.API path changes
Auth grouped under
/api/auth/:Everything else is byte-for-byte identical. Public allowlist in
hooks.server.ts:Everything else under
/api/*requires a valid session cookie or returns401 {"error":"locked"}.Frontend routing
Nested SvelteKit routes per phase/view, with
+layout.server.tsredirecting bystate.phase:needs-setup/setupneeds-unlock/unlockneeds-claude-session/claude-sessionneeds-provider/providers(autofocus on)ready/,/new,/providers(free nav)The
refresh()callback is replaced byinvalidateAll()exposed viasetContext('refresh', ...). The old WeakMap signature-diff inworkers.ts/sessions.tsis gone — Svelte's keyed{#each}plusbind:valueform state preserves input naturally; the 10s poll lives in+page.svelte'sonMountand bumps atickrune that cascades.Cookie contract — explicit options matter
The issue body said "set cookies explicitly" without spelling out which defaults bite. Reassessed against SvelteKit ≥2 defaults — three options must be set, two are written for documentation:
path/api/auth/unlock, cookie never sent on/api/stateetc. Silent regression.securetrueif URL is HTTPS, else falseFORCE_SECURE_COOKIEenv knob.maxAgehttpOnlytruesameSite'lax''strict'. Strict is overkill for an internal tool with no cross-site entry points; lax matches the SvelteKit default.auth/lockmirrors thesecureflag on delete so browsers actually match and clear the cookie.Bodyless POSTs
/api/auth/lock,/api/workers/[id]/start,/api/workers/[id]/stopwork without the Fastify content-type-parser workaround — SvelteKit reads request bodies lazily.Migrations
Migration files are statically imported in
src/lib/server/db/migrate.tsso vite bundles them into the SSR build. adapter-node can't dynamic-import.tsfiles at runtime in production.runMigrationsruns in a SvelteKitServerInithook (added in kit ≥2.10) — at server start, before the first request, but not at build time.Per-session health probe
Per AGENTS.md fact #5: per-session probe in
checkSessionHealthstill matches the full--remote-control NAMEso one process doesn't satisfy every session's probe. Untouched.Test plan
pnpm installpnpm typecheck— 0 errorspnpm test— 88 passed (was 80, plus 8 newhooks.server.test.ts)pnpm lint— cleanpnpm build— succeeds, ~5.8snode buildstarts cleanly, runs migrations, serves:GET /api/state→ 200,phase: needs-setupGET /api/workers(no session) → 401,{"error":"locked"}POST /api/auth/lock(no session, no body) → 204 +Set-Cookie: nexus_sid=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Laxdocker compose down -v && docker compose up -d --build, walk the full phase flow (setup → unlock → claude-session bootstrap → providers → workspaces → new workspace → session create → session health → lock → re-unlock)./, type a name, wait 12s — value preserved across the 10s poll./— stays on/(cookie survives).Commit list
Add SvelteKit (Svelte 5 + adapter-node) build wiring alongside the existing Fastify code. Subsequent commits move logic under src/lib/server/, port endpoints to +server.ts, and replace the SPA with Svelte 5 components. - package.json: drop fastify/@fastify/cookie/@fastify/static, add @sveltejs/{kit,adapter-node,vite-plugin-svelte}, svelte 5, svelte-check, eslint-plugin-svelte. Scripts collapse to dev/ build/start/typecheck. - svelte.config.js: adapter-node + $server alias to src/lib/server. - vite.config.ts: just the sveltekit() plugin. - tsconfig.json: extends .svelte-kit/tsconfig.json. - eslint.config.js: add svelte plugin + ignore build/.svelte-kit. - vitest.config.ts: drop coverage exclusions for files that move. - src/app.html, src/app.d.ts: SvelteKit shell + App.Locals.masterKey. - web/styles.css → src/app.css. - web/logo.svg → static/logo.svg. - Remove tsconfig.web.json (the SPA-only config). - .gitignore/.dockerignore: ignore build/ and .svelte-kit/. Refs issue #4.The Fastify-only seams (server.ts, *routes.ts, guard.ts, types.d.ts) get deleted entirely. Everything else moves as one subtree under src/lib/server/, so internal relative imports stay valid without code changes. SvelteKit endpoints in the next commit import from $server/... - src/{auth,crypto,db,connections,workers,sessions,providers,testing} → src/lib/server/<same> - src/lib/{cache,shell,logger}.ts → src/lib/server/lib/ - src/config.ts → src/lib/server/config.ts - DELETE: src/server.ts, src/types.d.ts, src/auth/{guard,routes}.ts, src/{connections,workers,sessions,state}/routes.ts - ADD: src/lib/server/singletons.ts — db + docker + sessions, runs migrations on module init, registers SIGTERM/SIGINT (adapter-node has no built-in shutdown hook). - db/client.ts: migrationsDir() now resolves from CWD (or the MIGRATIONS_DIR env var) since adapter-node bundles modules and import.meta.dirname is no longer stable. Refs issue #4.The auth gate moves from per-route Fastify preHandler hooks to a single handle in hooks.server.ts with a path-prefix check on /api/* plus a public allowlist: - /api/state (reads cookie itself; phase varies on session) - /api/auth/{setup,unlock,lock} Auth API paths are grouped under /api/auth/ (was /api/setup, /api/unlock, /api/lock at the Fastify root). Cookie set in /api/auth/unlock spells out path/maxAge/secure explicitly. SvelteKit's defaults bite otherwise: path defaults to the request URL pathname (would scope the cookie to /api/auth/unlock only), secure defaults to URL-scheme detection (we want the existing FORCE_SECURE_COOKIE env knob), and maxAge unset gives a session-only cookie. sameSite loosens from 'strict' to 'lax' — strict is overkill for an internal tool with no cross-site entry points, lax is the SvelteKit default and avoids edge cases on redirect chains. Per-method endpoints under src/routes/api/: state, auth/{setup,unlock,lock}, claude-session/recheck, connections/{,[id]/,[id]/validate,[id]/repos}, workers/{,[id]/,[id]/start,[id]/stop,[id]/health,[id]/logs, [id]/sessions/{,[sessionId]/,[sessionId]/health}}. Each handler is a body-for-body translation of the corresponding Fastify route: same status codes, same payload shapes, same Zod validation. req.server.* → imports from $server/singletons; req.masterKey → event.locals.masterKey. Bodyless POSTs (auth/lock, workers/[id]/{start,stop}) work without the Fastify content-type-parser workaround — SvelteKit reads request bodies lazily. testing/db.ts: bump migrations import paths after the move. Refs issue #4.The vanilla-TS SPA in web/ is replaced by a SvelteKit app with one nested route per phase/view. +layout.server.ts loads /api/state and redirects by phase so each page only renders when it's allowed: needs-setup → /setup needs-unlock → /unlock needs-claude-session → /claude-session needs-provider → /providers (autofocus on) ready → /, /new, /providers (free nav) Layout exposes invalidateAll via setContext('refresh', ...) so child components don't prop-drill the refresh callback. The diff-render contortions in workers.ts and sessions.ts go away — keyed {#each list as item (item.id)} plus form state held in component runes (bind:value) replaces the WeakMap signature cache. The 10s poll lives in +page.svelte's onMount and bumps a `tick` rune that cascades into Workers/Sessions; the typing-skip check is preserved. Components ported: Setup, Unlock, ClaudeSession, Connections (with optional autofocus prop for the dual-purpose case), Workers, NewWorker, Sessions, Toast, Header, LogsModal. Build infra: - migrations are statically imported in src/lib/server/db/migrate.ts (`import * as m0001 from '../../../../migrations/0001_init.js'`) so vite bundles them — adapter-node can't dynamic-import .ts files at runtime in production. - runMigrations moves out of singletons top-level (which fired during the build) into a SvelteKit ServerInit hook in hooks.server.ts. DELETE: nexus/web/* (the old SPA). Refs issue #4.