5.6 KiB
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.remoteFunctionsand Svelte 5compilerOptions.experimental.async. Useawaitdirectly in templates,query/query.batch/command/formfrom$app/server. - Svelte 5 runes only:
$state,$derived,$derived.by,$effect,$props,$bindable. No stores, no$:. New.sveltefiles are runes mode (legacy is auto-disabled outsidenode_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 inmessages/en.json(alphabetical). No raw English in components. - Drizzle ORM (sqlite,
src/lib/server/db). Better-auth (src/lib/auth/server.ts). openapi-fetchover the generatedsrc/lib/server/nadir-agent/schema.d.ts. Alwaysimport typefrom it.- Toasts via
svelte-sonner(toast.success/toast.error). - Persisted UI state via
PersistedStatefromruned. - Drag-and-drop via
swapy(existing usage inmachines-nav.svelteis the reference).
Routing
- Typed routes are mandatory. Always
resolve('/dashboard/[machineId]/foo', { machineId }). Neverresolve(\/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. Usequery.batchonly 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
commandthat 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 arev.optional(v.string()), no?shortcuts.
Pages
- New section pattern (
storage,system,users) for an agent area:dashboard/[machineId]/<area>/+page.svelteis the landing card grid.- Sub-pages do the work. List pages get search, filters in a
Popover, pagination viaPersistedState, a refresh icon-button. Mirrorusers/groups/+page.sveltefor shape.
- Add a section to
app-sidebar.svelte(typedresolve()calls) and the breadcrumb labels inbreadcrumbs.svelte. New i18n keys inen.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. Nativetitletooltip — no tooltip component for plain truncation. - Keys for
{#each}over kernel/system data must allow duplicates (e.g.binfmt_miscmounts can repeat). Compose:(mt.mountpoint + '\0' + mt.device + i). - For queries displayed inside an
{#each}that may unmount, read.currentinstead of{#await}—derived_inertfires when the each block dies while the promise is still in-flight.
Sidebar machine health
- Status dot reads
machineHealth(id).current(aquery.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
ascast is a code smell. Reach for typed APIs (resolve(routeId, params), valibot schemas, generic helpers) before reaching foras. - 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
*.mdsummaries, no walkthrough docs, no planning files. - Run
npm run checkafter 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 →squareorterminalicon. Status arrows →arrow-up/arrow-downetc.
Browser-side gotchas
<input pattern>is now/vflag —-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.