# 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 new`RouteId` overload. - Pre-resolve URLs at the data-definition site when you build a list; store the resolved string. Consumer just uses ``. - 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]//+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, `{value}` 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: , `. 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 - `` 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.