Files
2026-06-26 00:31:29 +02:00

5.6 KiB

Working on this repo

A SvelteKit web UI that drives nadir-agent, a per-host REST agent. Default to lazy.

Stack

  • SvelteKit 2.27+ with experimental.remoteFunctions and Svelte 5 compilerOptions.experimental.async. Use await directly in templates, query/query.batch/command/form from $app/server.
  • Svelte 5 runes only: $state, $derived, $derived.by, $effect, $props, $bindable. No stores, no $:. New .svelte files are runes mode (legacy is auto-disabled outside node_modules).
  • shadcn-svelte for UI ($lib/components/ui/*). Bits UI underneath. Lucide icons (@lucide/svelte/icons/*).
  • Paraglide for i18n. Every user-visible string goes through m.xxx() and lives in messages/en.json (alphabetical). No raw English in components.
  • Drizzle ORM (sqlite, src/lib/server/db). Better-auth (src/lib/auth/server.ts).
  • openapi-fetch over the generated src/lib/server/nadir-agent/schema.d.ts. Always import type from it.
  • Toasts via svelte-sonner (toast.success / toast.error).
  • Persisted UI state via PersistedState from runed.
  • Drag-and-drop via swapy (existing usage in machines-nav.svelte is the reference).

Routing

  • Typed routes are mandatory. Always resolve('/dashboard/[machineId]/foo', { machineId }). Never resolve(\/dashboard/${id}/foo` as Pathname)— the cast bypasses the newRouteId` overload.
  • Pre-resolve URLs at the data-definition site when you build a list; store the resolved string. Consumer just uses <a href={item.url}>.
  • For internal components that receive a runtime-built href (e.g. Button, Breadcrumb), accept the string as-is. Callers resolve before passing.

Remote functions (src/lib/remotes/*.remote.ts)

  • Group per-domain (machines, system, storage, pam-users, …). One file per agent area.
  • Talk to the agent via nadirForMachine(machineId) from ./utils — it handles auth, 401/404/500 routing, token decryption. Don't reinvent.
  • Single-instance per-page reads stay as query. Use query.batch only for the genuine n+1 (e.g. one row per machine). Don't batch by default — it's machinery, not a free win.
  • After a command that mutates remote state, call .refresh() on every query whose data it just invalidated. Refresh inside the command, not at the call site.
  • Validate inputs with v.object({...}) from $lib (valibot). Optional fields are v.optional(v.string()), no ? shortcuts.

Pages

  • New section pattern (storage, system, users) for an agent area:
    • dashboard/[machineId]/<area>/+page.svelte is the landing card grid.
    • Sub-pages do the work. List pages get search, filters in a Popover, pagination via PersistedState, a refresh icon-button. Mirror users/groups/+page.svelte for shape.
  • Add a section to app-sidebar.svelte (typed resolve() calls) and the breadcrumb labels in breadcrumbs.svelte. New i18n keys in en.json.
  • Long strings (mount options, device paths, overlay merged dirs) MUST truncate. Pattern: class="max-w-[Nrem]" on the cell, <span class="block truncate" title={value}>{value}</span> inside. Native title tooltip — no tooltip component for plain truncation.
  • Keys for {#each} over kernel/system data must allow duplicates (e.g. binfmt_misc mounts can repeat). Compose: (mt.mountpoint + '\0' + mt.device + i).
  • For queries displayed inside an {#each} that may unmount, read .current instead of {#await}derived_inert fires when the each block dies while the promise is still in-flight.

Sidebar machine health

  • Status dot reads machineHealth(id).current (a query.batch). Refresh paths:
    • Dashboard polling tick refreshes it for the selected machine.
    • Sidebar refresh button refreshes all visible rows AND the selected machine's dashboard queries — both directions stay aligned when the agent flaps.

Style and conventions

  • The as cast is a code smell. Reach for typed APIs (resolve(routeId, params), valibot schemas, generic helpers) before reaching for as.
  • No defensive code at internal boundaries — trust your own functions. Validate only at trust boundaries (HTTP input, user input).
  • Comments are rare. Mark deliberate simplifications with // ponytail: <ceiling>, <upgrade path>. Never explain what the code does.
  • No emojis anywhere unless I ask.
  • Prefer editing existing files; resist creating new ones. No *.md summaries, no walkthrough docs, no planning files.
  • Run npm run check after non-trivial edits — 0 errors is the bar before reporting done. Don't ship if it doesn't pass.

Iconography

  • Any visual marker (cursor, check, cross, arrow, status dot, "loading…" decoration) goes through a Lucide icon from @lucide/svelte/icons/*. No ASCII or Unicode glyphs as UI (e.g. , , , , , ). Plain text inside copy/messages is fine.
  • Spinners → Loader2Icon class="size-N animate-spin". Cursors → square or terminal icon. Status arrows → arrow-up/arrow-down etc.

Browser-side gotchas

  • <input pattern> is now /v flag — - inside a char class is a range operator. Escape it: [A-Za-z0-9.\-]+, not [A-Za-z0-9.-]+.
  • Modern Svelte/Vite already strips import type. If a chunk seems bloated, suspect a heavy runtime dep (e.g. better-auth) before suspecting the schema.

Things I have already decided

  • No mock databases in tests — integration tests hit a real DB.
  • Bundled refactors over many small PRs in this area.
  • Terse responses, no trailing recap. The diff is the report.

Things to ask before doing

  • Anything destructive across the agent (mass unmount, mass delete, bulk power-off).
  • New external dependencies.
  • Changes that touch auth, encryption, or token handling.