Utilize SvelteKit for Oz #59
Labels
No labels
blocked
bug
duplicate
effort
high
effort
low
effort
medium
effort
minimal
enhancement
help wanted
invalid
pr
feedback
pr
reviewing
question
status
backlog
status
document
status
done
status
implement
status
pending
status
planning
status
reviewing
waiting
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tepichord/milner#59
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Naina:
Updated on 2026-09-15 based on maintainer feedback.
Overview
Rewrite
oz/as a SvelteKit 2.x app (@sveltejs/kit2.70.3 +@sveltejs/adapter-node5.5.7, Svelte 5, bits-ui 2.19.2) preserving OIDC login, the preview table, the three actions, CSRF, preview-mode simulation, and the no-delete-verb boundary. SvelteKit replaces the Astro plan: Astro's shadcn islands pulled in React, and the maintainer selected SvelteKit's leaner runtime under the 256Mi limit.router/andpreviewlib/k8s.pystay Python; the image stays a two-stage Nix build that bakes/app/PREVIEW-BUILDfrom--build-arg OZ_PREVIEW=1.Technical Approach
Add
oz/flake.nix+oz/flake.lock; addnodejs_22to the root dev shell.Root
flake.nix: addnodejs_22tobuildInputs(22.23.2 at the pinned rev4).Scaffold SvelteKit in place, then add the runtime deps.
oz/svelte.config.js+oz/vite.config.ts. adapter-node is standalone;build/index.jsis the entry point and readsHOST/PORT(neverHOSTNAME)99b.Port the Kubernetes client to
oz/src/lib/k8s.ts— same surface aspreviewlib/k8s.py, same injected-transport seam (the Python transport takes a fifthtimeoutargument11b).Default transport:
undiciAgent({ connect: { ca: readFileSync(${SA_DIR}/ca.crt) } })— Node's globalfetchhas no per-requestca.Port the pure preview logic to
oz/src/lib/preview.ts— labels and patch shapes unchanged fromoz/cluster.py22b.Port
summarise,previewState,mergeRows,describeExpiry,validateBranch, andPreviewStore(TTL frompreview-config).Port auth to
oz/src/lib/auth.ts—openid-clientv6 authorization-code + PKCE,joseJWKS verify,node:cryptoHMAC10.Keep the
payload.signatureHMAC-SHA256 cookie (oz_session/oz_login),allowed(), andcsrfMatches()viatimingSafeEqual.oz_loginsetssameSite: "lax"(cross-site OAuth redirect).Route handlers (explicit CSRF; no form actions).
+server.tskeeps the existing URL table; the UI posts form-encoded forms like the Python console33b.oz/src/hooks.server.ts— onehandlegates everything and populatesevent.locals.user.Preview mode
oz/src/lib/preview-mode.ts— same marker, fixtures, and state machine asoz/app.py; never an environment variable88b.Port
previewFixtureRows,applySimulated,simulatedRows; actions returnredirect(303, "/?preview=<action>&pr=<n>")and log one line.UI:
oz/src/routes/+page.svelte+oz/src/app.css. SSR table; bits-uiButton/AlertDialogfor the interactive primitives; table/badge markup hand-styled (bits-ui ships no Table/Badge/Card).app.csscarries the shadcn-svelte:root/.darkvariable block plus rules on[data-button-root],[data-alert-dialog-content], etc. — no Tailwind.Rewrite
oz/Dockerfileas a two-stage Nix build.adapter-node's output is not self-contained:
build/index.js+build/client/**+build/server/**+ productionnode_modules.Update both Deployments (
gitops/clusters/milner/preview-system/oz.yaml,apps/preview/oz.yaml).Drop
OZ_PORT,PYTHONDONTWRITEBYTECODE,PYTHONUNBUFFERED55b. Keep the/tmpemptyDir,OZ_*config,envFrom: oz-secrets, probes and limits.Update the pipeline and static guards (
.woodpecker/gates.yaml,tests/test_static.py).test_static.py: retarget theoz/app.pyassertions tooz/src/lib/preview-mode.ts, replaceenv["PYTHONUNBUFFERED"] == "1"withenv["PORT"] == "8080", replace theoz.app.PREVIEW_BUILDsmoke assertion with aPREVIEW-BUILDgrep plus the/healthzsmoke. Deletetests/test_preview_console.py(its spec moves to Vitest).Docs — PLAN.md Phase 7 (
ozis SvelteKit,router/stays Python) and the AGENTS.md repo-map row foroz/.Implementation Details
oz/{package.json,package-lock.json,svelte.config.js,vite.config.ts,tsconfig.json}oz/flake.nix,oz/flake.lockoz/src/routes/**,oz/src/lib/**,oz/src/hooks.server.ts,oz/src/app.css,oz/src/app.d.tsoz/src/**/*.test.tsoz/Dockerfilebuild/+ prodnode_modules; marker preserved).dockerignoreoz/node_modules,oz/build,oz/.svelte-kit)oz/{app,auth,cluster,forge,__main__,__init__}.pyflake.nixnodejs_22gitops/clusters/milner/preview-system/oz.yaml,apps/preview/oz.yamlHOST/PORT/ORIGIN/NODE_OPTIONS).woodpecker/gates.yamlteststep66btests/test_static.pyoz/app.pyguards77btests/test_preview_console.pyPLAN.md,AGENTS.mdDependencies. Runtime:
@sveltejs/kit@2.70.3,@sveltejs/adapter-node@5.5.7,svelte@5,bits-ui@2.19.2,openid-client@^6,jose@^6,undici. Dev:vite,vitest,typescript,svelte-check.Constraints. arm64 only; buildah
vfs+--isolation=chrootwithsandbox = false;runAsUser: 65535,readOnlyRootFilesystem, all caps dropped, no privilege escalation; 50m–300m CPU, 64Mi–256Mi memory; images pinned by tag (policy/k8s.rego); one Ingress per hostname (unchanged); the console still holdsnamespaces get/list/watch/patchand no delete verb.Out-of-scope notes.
router/andpreviewlib/k8s.pyunchanged. No registry/retention, hostname, Vault secret orci/preview-pr.shchanges. Preview mode gains no new behaviour. Flagged risk: the undiciAgent({ connect: { ca } })in-cluster TLS path is runtime-agnostic — if bundling does not preserve it, fall back toNODE_EXTRA_CA_CERTS, thentls.setDefaultCACertificates.Acceptance Criteria
oz.faun-snapper.ts.netas acluster_adminsmember renders the PR/preview table; a non-member gets 403 at/callbackmilner.dev/preview-active=trueand extends its expiry; Delete patches the expiry to now; Build triggers Woodpecker withPREVIEW_PR; noDELETEhandler or request exists in the code/healthzreturns 200 without a session--build-arg OZ_PREVIEW=1yields/app/PREVIEW-BUILD; that image renders fixtures, logs each action and redirects, needs no credential, and no pod-spec value can toggle the model3-build-ozbuilds, pushes, verifies and smoke-tests the SvelteKit image, and L3 reaches Kustomization Ready with the image's root filesystem read-onlyapp.csscd oz && npm testandpython3 -m pytest tests/passTesting Plan
csrfMatches,allowed, PKCE S256, ID-tokeniss/aud/nonce/exprejection against a fixture JWKS;validatePr/validateBranch,activationPatch/deletePatch,summarise,describeExpiry,mergeRowsorphans;openPullsandtriggerthrough a fake transport; preview fixtures and simulated transitions.RequestEvent+ injected transport):/healthz→ 200;/unauthenticated → 302/login; non-admin → 403; bad CSRF → 403; preview mode → fixture rows + log line + 303 redirect.tests/test_static.pypasses;tests/test_preview_console.pyremoved.l3-build-ozsmoke passes; the L3 preview pod becomes Ready and serves the fixture console.preview.faun-snapper.ts.net.Related Issues/PRs
l3-build-ozsmoke test this plan editsrouter/+previewlib/k8s.py— shared Python, unchanged by this issueResearch Findings
adapter-nodereadsHOST/PORT(neverHOSTNAME) and needsORIGINbehind the ingress, or legitimate form posts fail withCross-site POST form submissions are forbidden.kit.csrf.checkOriginonly guards form content types and is bypassed byapplication/json, so the explicit per-session token check must stay.ButtonandAlertDialogbut no Table, Badge or Card, so that markup is hand-styled withapp.cssanddata-*rules.fetchhas no per-requestca; the supported in-cluster path isundiciAgent({ connect: { ca } })via thedispatcheroption (undici #2200 remains open).openid-clientv6 sets a custom fetch via the exportedcustomFetchsymbol on theConfiguration, andjosev6 verifies the ID token against a remote JWKS with no dependencies.Pre-Mortem
limits.memory: 256Miunchanged, while adapter-node plusopenid-client,jose,undiciandbits-uican push RSS past the limit under sustained load. Because "raise the limit in this same change" is stated as an acceptance criterion but no manifest edit implements it, the memory pivot that motivated this rewrite can simply repeat.nix build ./oz#node, thenix-store -qRclosure copy into/nix/store/, and the symlinkednodebinary are fragile, andnpm cineeds network access undersandbox = false. The plan itself leaves "decide and document" the runtime Node closure unresolved, so a build-stage failure or a brokenENTRYPOINTis likely.Agent({ connect: { ca } })path is flagged as unsettled (undici #2200 is open), and a missing or mis-setORIGINmakes every legitimate form POST fail SvelteKit's CSRF check. Either failure yields a console that builds and passes tests but cannot list previews or submit actions in the cluster.Ah that is a dealbreaker, rewrite the plan to utilize Astro instead of Next.js.
Alright another change of plans then, the final change:
what shadcn-svelte uses to write those styles.
Utilize Next.js for Ozto Utilize SvelteKit for Oz