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

77 lines
5.6 KiB
Markdown

# 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 `<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.