From 522ab09bd867d6a7f8ad19a48de4f22f0e31bebe Mon Sep 17 00:00:00 2001 From: urania Date: Wed, 24 Jun 2026 12:34:14 +0200 Subject: [PATCH] feat: expand dashboard with storage, network, and package management features while enhancing UI components and remote services --- CLAUDE.md | 76 ++ README.md | 4 +- bun.lock | 17 + db.sqlite | Bin 94208 -> 94208 bytes .../migration.sql | 1 + .../snapshot.json | 770 ++++++++++++++++++ messages/en.json | 320 +++++++- package.json | 10 +- scripts/replace-lucide-icons.ts | 112 +++ src/lib/components/blocks/app-controls.svelte | 10 +- .../blocks/breadcrumbs/breadcrumbs.svelte | 17 +- .../blocks/sidebar/app-sidebar.svelte | 311 +++++-- .../blocks/sidebar/machines-nav.svelte | 67 +- .../components/blocks/terminal-dialog.svelte | 150 ++++ src/lib/components/blocks/trunk/trunk.svelte | 4 +- .../dashboard/activity-panel.svelte | 2 +- .../components/dashboard/cpu-heatmap.svelte | 2 +- .../components/dashboard/data-panel.svelte | 4 +- .../components/dashboard/network-panel.svelte | 2 +- .../components/dashboard/storage-panel.svelte | 4 +- .../components/dashboard/system-panel.svelte | 2 +- .../dashboard/temperature-panel.svelte | 2 +- src/lib/components/seo/page-meta.svelte | 31 + src/lib/components/ui/badge/badge.svelte | 3 +- src/lib/components/ui/button/button.svelte | 3 +- .../ui/copy-button/copy-button.svelte | 96 +++ src/lib/components/ui/copy-button/index.ts | 3 + .../ui/dialog/dialog-content.svelte | 2 +- src/lib/components/ui/sidebar/sidebar.svelte | 6 +- src/lib/hooks/use-clipboard.svelte.ts | 99 +++ src/lib/remotes/machines.remote.ts | 24 +- src/lib/remotes/networking.remote.ts | 166 ++++ src/lib/remotes/packages.remote.ts | 120 +++ src/lib/remotes/pam-users.remote.ts | 70 +- src/lib/remotes/server.remote.ts | 67 +- src/lib/remotes/services.remote.ts | 158 ++++ src/lib/remotes/storage.remote.ts | 50 ++ src/lib/remotes/system.remote.ts | 224 ++--- src/lib/remotes/utils.ts | 91 ++- src/lib/server/db/index.ts | 8 +- src/lib/server/nadir-agent/schema.d.ts | 167 +++- src/routes/+error.svelte | 81 ++ src/routes/+layout.svelte | 22 +- src/routes/+page.svelte | 6 + src/routes/admin/+page.svelte | 6 + src/routes/admin/config/+page.svelte | 6 + src/routes/admin/users/+page.svelte | 6 +- .../machines/[machineId]/terminal/+server.ts | 34 + src/routes/auth/+layout.svelte | 4 +- src/routes/auth/2fa/+page.svelte | 2 + src/routes/auth/forgot-password/+page.svelte | 6 + src/routes/auth/reset-password/+page.svelte | 6 + src/routes/auth/setup-2fa/+page.svelte | 2 + src/routes/auth/sign-in/+page.svelte | 2 + src/routes/auth/sign-up/+page.svelte | 2 + src/routes/dashboard/+page.svelte | 6 + src/routes/dashboard/[machineId]/+page.svelte | 62 +- .../[machineId]/networking/+page.svelte | 64 ++ .../[machineId]/networking/dns/+page.svelte | 50 ++ .../[machineId]/networking/hosts/+page.svelte | 345 ++++++++ .../networking/interfaces/+page.svelte | 391 +++++++++ .../networking/interfaces/[name]/+page.svelte | 159 ++++ .../interfaces/[name]/configure/+page.svelte | 355 ++++++++ .../networking/routes/+page.svelte | 216 +++++ .../[machineId]/packages/+page.svelte | 40 + .../packages/installed/+page.svelte | 436 ++++++++++ .../[machineId]/packages/updates/+page.svelte | 319 ++++++++ .../[machineId]/services/+page.svelte | 428 ++++++++++ .../[machineId]/services/[name]/+page.svelte | 576 +++++++++++++ .../[machineId]/storage/+page.svelte | 40 + .../[machineId]/storage/fstab/+page.svelte | 255 ++++++ .../[machineId]/storage/mounts/+page.svelte | 378 +++++++++ .../dashboard/[machineId]/system/+page.svelte | 59 +- .../[machineId]/system/date-time/+page.svelte | 12 +- .../[machineId]/system/hostname/+page.svelte | 14 +- .../system/localization/+page.svelte | 17 +- .../[machineId]/system/nadir/+page.svelte | 208 +++++ .../[machineId]/system/power/+page.svelte | 14 +- .../dashboard/[machineId]/users/+page.svelte | 76 +- .../[machineId]/users/[username]/+page.svelte | 41 +- .../[machineId]/users/groups/+page.svelte | 59 +- .../users/groups/[group]/+page.svelte | 53 +- src/routes/install.sh/+server.ts | 70 ++ vite.config.ts | 2 +- 84 files changed, 7718 insertions(+), 487 deletions(-) create mode 100644 CLAUDE.md create mode 100644 drizzle/20260623171651_glossy_star_brand/migration.sql create mode 100644 drizzle/20260623171651_glossy_star_brand/snapshot.json create mode 100644 scripts/replace-lucide-icons.ts create mode 100644 src/lib/components/blocks/terminal-dialog.svelte create mode 100644 src/lib/components/seo/page-meta.svelte create mode 100644 src/lib/components/ui/copy-button/copy-button.svelte create mode 100644 src/lib/components/ui/copy-button/index.ts create mode 100644 src/lib/hooks/use-clipboard.svelte.ts create mode 100644 src/lib/remotes/networking.remote.ts create mode 100644 src/lib/remotes/packages.remote.ts create mode 100644 src/lib/remotes/services.remote.ts create mode 100644 src/lib/remotes/storage.remote.ts create mode 100644 src/routes/+error.svelte create mode 100644 src/routes/api/machines/[machineId]/terminal/+server.ts create mode 100644 src/routes/dashboard/[machineId]/networking/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/networking/dns/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/networking/hosts/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/networking/interfaces/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/networking/interfaces/[name]/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/networking/interfaces/[name]/configure/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/networking/routes/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/packages/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/packages/installed/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/packages/updates/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/services/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/services/[name]/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/storage/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/storage/fstab/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/storage/mounts/+page.svelte create mode 100644 src/routes/dashboard/[machineId]/system/nadir/+page.svelte create mode 100644 src/routes/install.sh/+server.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..61451ec --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# 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. diff --git a/README.md b/README.md index 1ada0c0..ef4e955 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ on the dashboard, and drive everyday tasks from the browser. ## Getting started Prerequisites: [Bun](https://bun.com) and a reachable nadir-agent instance with -a machine token (see the agent README's *Connecting a dashboard* section). +a machine token (see the agent README's _Connecting a dashboard_ section). ```sh bun install @@ -111,7 +111,7 @@ PORT=3000 ORIGIN=https://nadir.example.com bun run build/index.js Put it behind the same reverse proxy you use for nadir-agent, or co-host them. The agent's CSRF rules apply when the UI calls it cross-origin - see the agent -README's *Connecting a dashboard* section. +README's _Connecting a dashboard_ section. --- diff --git a/bun.lock b/bun.lock index 9c807d3..44ab653 100644 --- a/bun.lock +++ b/bun.lock @@ -8,12 +8,16 @@ "@better-auth/infra": "^0.2.14", "@better-svelte-email/components": "^2.1.1", "@better-svelte-email/server": "^2.1.1", + "@xterm/addon-attach": "^0.12.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "better-auth": "^1.6.20", "nodemailer": "^9.0.1", "ogl": "^1.0.11", "openapi-fetch": "^0.17.0", "runed": "^0.37.1", "swapy": "^1.0.5", + "undici": "^8.5.0", "uqr": "^0.1.3", "valibot": "^1.4.1", }, @@ -33,6 +37,7 @@ "@types/bun": "^1.3.14", "@types/node": "^24", "@types/nodemailer": "^8.0.1", + "@types/ws": "^8.18.1", "bits-ui": "^2.16.3", "clsx": "^2.1.1", "drizzle-kit": "^1.0.0-beta.22", @@ -53,6 +58,7 @@ "prettier-plugin-tailwindcss": "^0.8.0", "shadcn-svelte": "^1.3.0", "svelte": "^5.56.1", + "svelte-adapter-bun": "^1.0.1", "svelte-check": "^4.6.0", "svelte-sonner": "^1.1.0", "sveltekit-superforms": "^2.30.0", @@ -64,6 +70,7 @@ "typescript-eslint": "^8.60.1", "vaul-svelte": "^1.0.0-next.7", "vite": "^8.0.16", + "ws": "^8.21.0", }, }, }, @@ -536,6 +543,12 @@ "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], + "@xterm/addon-attach": ["@xterm/addon-attach@0.12.0", "", {}, "sha512-1lxvXM4JYSm60lbFmE8WMOy2oF2ip3Ye8jWorSAmwy7x8FiC53netEJ5RguL8+FSRj79MUQsNCb2hprY2QA2ig=="], + + "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="], + + "@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="], + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -1100,6 +1113,8 @@ "svelte": ["svelte@5.56.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.11", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA=="], + "svelte-adapter-bun": ["svelte-adapter-bun@1.0.1", "", { "dependencies": { "rolldown": "^1.0.0-beta.38" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0", "typescript": "^5" } }, "sha512-tNOvfm8BGgG+rmEA7hkmqtq07v7zoo4skLQc+hIoQ79J+1fkEMpJEA2RzCIe3aPc8JdrsMJkv3mpiZPMsgahjA=="], + "svelte-check": ["svelte-check@4.6.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "0.1.1", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ=="], "svelte-eslint-parser": ["svelte-eslint-parser@1.8.0", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0", "semver": "^7.7.2" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-mikR1qwIVy3t5WthUoAXkMwxkXvabZP9FJgdx35Ei7EbGWmctva1Pih16Koeor/bdNNq8NXHlwKGS6NkYTawLg=="], @@ -1160,6 +1175,8 @@ "typescript-eslint": ["typescript-eslint@8.61.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.61.1", "@typescript-eslint/parser": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/utils": "8.61.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw=="], + "undici": ["undici@8.5.0", "", {}, "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], diff --git a/db.sqlite b/db.sqlite index e363219782b639f4327ccd65e9d3f5db72fe9b5a..632e45e23318047f926f70876fdebf6d1362fc40 100644 GIT binary patch delta 4003 zcmb_eU5w+_brwgO*&VGt+C_|YQrq!(8zZ}&)rh~6xT))qB1KXXNs%HYQGq)qDT*RR ziIPZ(6p>b$4N@czj*|@9N%D{%^r1i-AU_j$gQ92yv}lVWPemX4;G~NL=u6W!1^N>h zc_?S&1O@!CE|4qefH?QugYP@vIhXv{!`+WP-2KcmH@r+H^K|=J>U@ zo-7nIH+O#c*~#tQA7=kG`)3z7u3z0b`^pZPKkYqr<>}|113nI%{cPti-gEcxRZ~AP zt!4P?VQ9tSozl&-f6u;n@uTefpS`x<$m{@a25_&w@XfFOHt^JIC~$+XypR6`p7|WG zn>p1oySrx(Up_qh-8q`b&FTuVuVFx(TM@$o~LS92_Ra*4C z;w*4?+yTp2O_EWTCs_#;aJ}LrUEb-3u!qO`qzkDLQry&MqS$R&H7`-PT8F7_Fw#x@ zvX@MfaCSm!__9gOhg)Glj{^&vINl75`cPGyRycBk4x+l)Z?MEhw@|~c`(Q=25m1Bu z6^2o$??r8o~!icOqH=@ln|Fpgau9Q_!0|6~}-@G$7Y!+|vG`f9J?>r)l!^cy3wZ=Suk z_}137G2Z3lR;}S#Ea8YQjffPt5fu*QlRAEE)WddTQeHW-#U*yNX9w!ah7x6j@?skq z50@v5>nh>gU1$>m??z~+JRMJ~-GH6bOHu8|h!S@<>Pa$~;Okhdo*1btfR%;kVmJP2;B~mw-PO4~g zJHyEhD zgQq<{1DiS`sF3DHPNS`MD-a@s7b%mLD@N(%!VRF-YO zyW&&^nNWyLavdc_@nC-Q=N3nf5$qT$qsI`4@qXfXo?gO^!Nc2q2>Q_B+1JD;m^+8W zeC}C2%V;`b2}3H!Ncr$~lU4ZE-9yirSci;dPW(HEweixQS|td@kHO2|VMib7OXuw) zXAi~0vyuqrp8eV924@fb!zX|*{rL4SUP*!c-9VeUhx+(U!V93C* zf^HPdHaSI4+J211MMNM1#Zf8R?vK-J084f;Cp(44Xcru_tniIzbpoHT=z3h+qOi0=iNSh*Y;V(-mS;+x%E4#7~lQnnTL_x_I{xl2JV1qXH>yjcK>hZp+~zgKwqH?kkuPhtLgmfwFIlqau!C$o3;{=I|$ zzIuPZo&T5oa|d_Qk+`0F;*%fQ*;k*xdjH9nKmFqQt2|KLN-LTUnFc&GEUptX?3!9v z>lP(c{g4xwS{w0coPwfO-E)+dG#8sPy&}rsiqtz;qU!CWr=v5y<*(`s*d+(nCTOh| zJ%>Qo?e%QY68(72=tncMsqh$LvIwf0a=m2h9iJk|=3*}#hv7UpzaaomZHt$Sq!~^) ztkJT<)wvp>(2u-%p5tn!QmYYT%O)a@By3J5nj;<*dn40BE1^*l9da1@k{zWtKiBjJ zT;hUs8XGY#t>t-AV+cs4Yh%7`OzRW9&xKlV=ovF995QjjlUuY;DICmG+J;YPgm-C> zhy2vXKl0^y>X*((DIc4$K8ff^W8a2S%M;QsaZ!Y1Qj$&3yaTN{p7S~pyEQ`7vNCX_P0>OXBHKn#`9X>9QpCwE7BZ3thIO(iBBBN!wm-Nt>>dDK0`G_(z*E zPZFalO}GS+)}KuAQ~9n8$|T2PJjcSe?F9xbj16YIHjz$^N@$FC&{%No6|dN6%cWxh z<(j-=ZqjF@xD5K+t3z)MFOh}n)j9c&pM72ToBGg_ISYN$Z@kvNsdVoxn= zaNQFXA0AA3Bri_8p5WGM3^VXViYSXN9}SdAS)@!zNPLi2q_WtfG0o?!!BmQP%8yf3 zLD8M3UjS;97u~v(I?AF#BxzQV?=>dlXe*Zpf*{9nYTvoYaikE7Rt$}!PLQXiG%%7U z5>dm)ar&xbqjn3G2trs`o0YeYk1#v+AKAZ1L{alGWYYD=3+HbM!1uPN&ArQw`%k{4S4;#0A+w0!uvtW*GFZg1 z^kTPk9K%e+z;O&BDAM!I$qv5qVV9>Fc~Fv^x*;r+UU$-4ji?DuEUQLR<+`Y^8f7|I zg4LNI*Q53kgbgckgfX78?&xqJZ5B}iZl`#^3C~So4F&6VyW0*n)GRo{y@86Vab@hx z#}eGwV66?@tdnAiwPls*Ho(i<=3c1;f#7la-Ug2$7`un3>$I_((%DbxSVPb-EDI@` z7*s8yhF&ciC{`|3z!3@}Rm(&Wu)W;+@zjr-RkDecT}MakPGjQLd-dLE$eT_r)*R5s z#2L!c;{k`k4Nc#;OJou$vTRnRI%xJPYb?1=gwc@0WkMhfAsNxI)9A^zPJTGuRfFc{|Wgk(=nBC`>W9Lv~%UE`h3db zwDI;sC1aU7Hqxz8Q6G(rB8s6_+L$m}tRhtmM?nLEhERQ}Z>Lt|FDK5pJD)q9rrS~- zk%1a!wYfNwaT%3YF*aB({Gm>BQODD0(WumhdRd^b(Ld;ZD^(7XLEig@LNS#|&XKN`RXp2`=_GJA!O{$}Bi3!g50vT(of(ZbhL ze6_+WSF<_b@BaDTALa^wn!&$+eBawGT*>D4-+J#i{-*HxO#VmDfB$~Au$u*dT=vpt tbC-5!FPGg1cGDwI?_8S8UcOB5p8vxB{?=|Tn*pxmvcUfC`L}(b^}l=N=End4 delta 867 zcmZp8z}oPDb%L~DAOiz~I1s}C|3n>QoSU)kpW!|${P=JGJ z@&YEO$v>F{xEkF>*u`aK8JiL(zhTlVFz^}hq&_I=cvcA6oi@+Q?_Q?R#!tx~3G9w>%QxnIupyXt)4AVjn^NgYh=i(4^Bhys>oWMe_&|*)Q3fE+#q@p}m zOI@$Dq)Oi`OK(qIH;+i&O3%bZPq%C*%kW48ql}=uBEPI`-Lev=kb*`AMt0w5UshID zi=4d7Lf5nc$I|j7U&oBRD)WrMuwwtbBnw@4(@JwQ!=lvU)Dqo-#Ny)e{Gt>q^GsK- zU`O+m{E{G_DBlRr^h95BL>u+X1m+(*hO& zdrLzb6Y4?0iR_h aU#u2oF#{QV_X7_%fkarDr*Fw;^Z@`2o(lv3 diff --git a/drizzle/20260623171651_glossy_star_brand/migration.sql b/drizzle/20260623171651_glossy_star_brand/migration.sql new file mode 100644 index 0000000..1a42f04 --- /dev/null +++ b/drizzle/20260623171651_glossy_star_brand/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `machines` ADD `ca_cert` text; \ No newline at end of file diff --git a/drizzle/20260623171651_glossy_star_brand/snapshot.json b/drizzle/20260623171651_glossy_star_brand/snapshot.json new file mode 100644 index 0000000..7fd3247 --- /dev/null +++ b/drizzle/20260623171651_glossy_star_brand/snapshot.json @@ -0,0 +1,770 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "1a550330-2944-441c-aea2-18b98455929c", + "prevIds": [ + "b3403d40-d4c3-4f1a-8d1d-763c548f5bc7" + ], + "ddl": [ + { + "name": "machines", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "two_factor", + "entityType": "tables" + }, + { + "name": "user", + "entityType": "tables" + }, + { + "name": "verification", + "entityType": "tables" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'http://127.0.0.1:9999'", + "generated": null, + "name": "address", + "entityType": "columns", + "table": "machines" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ca_cert", + "entityType": "columns", + "table": "machines" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "machines" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "machines" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "order", + "entityType": "columns", + "table": "machines" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token", + "entityType": "columns", + "table": "machines" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token_expires_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "password", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "provider_id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "scope", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "impersonated_by", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ip_address", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "backup_codes", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "verified", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ban_expires", + "entityType": "columns", + "table": "user" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "banned", + "entityType": "columns", + "table": "user" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ban_reason", + "entityType": "columns", + "table": "user" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "user" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "display_username", + "entityType": "columns", + "table": "user" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "user" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "email_verified", + "entityType": "columns", + "table": "user" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "image", + "entityType": "columns", + "table": "user" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "user" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "user" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "two_factor_enabled", + "entityType": "columns", + "table": "user" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "user" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "username", + "entityType": "columns", + "table": "user" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "verification" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "verification" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "verification" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "identifier", + "entityType": "columns", + "table": "verification" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "verification" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "value", + "entityType": "columns", + "table": "verification" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_account_user_id_user_id_fk", + "entityType": "fks", + "table": "account" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_user_id_user_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_two_factor_user_id_user_id_fk", + "entityType": "fks", + "table": "two_factor" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "machines_pk", + "table": "machines", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "two_factor_pk", + "table": "two_factor", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_pk", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "verification_pk", + "table": "verification", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "account_userId_idx", + "entityType": "indexes", + "table": "account" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_userId_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "secret", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "twoFactor_secret_idx", + "entityType": "indexes", + "table": "two_factor" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "twoFactor_userId_idx", + "entityType": "indexes", + "table": "two_factor" + }, + { + "columns": [ + { + "value": "identifier", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "verification_identifier_idx", + "entityType": "indexes", + "table": "verification" + }, + { + "columns": [ + "order" + ], + "nameExplicit": false, + "name": "machines_order_unique", + "entityType": "uniques", + "table": "machines" + }, + { + "columns": [ + "token" + ], + "nameExplicit": false, + "name": "session_token_unique", + "entityType": "uniques", + "table": "session" + }, + { + "columns": [ + "email" + ], + "nameExplicit": false, + "name": "user_email_unique", + "entityType": "uniques", + "table": "user" + }, + { + "columns": [ + "username" + ], + "nameExplicit": false, + "name": "user_username_unique", + "entityType": "uniques", + "table": "user" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/messages/en.json b/messages/en.json index 8d9a72d..dadca3a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -16,6 +16,9 @@ "code": "Code", "confirm_password": "Confirm password", "continue_action": "Continue", + "copied": "Copied", + "copy": "Copy", + "copy_failed": "Failed to copy", "create_account": "Create account", "dashboard": "Dashboard", "dashboard_architecture": "Architecture", @@ -115,6 +118,17 @@ "email_verify_ignore": "If you didn't create an account, you can safely ignore this email.", "email_verify_subject": "Verify your email address", "enter_password_to_continue": "Confirm your password to continue", + "error_action_back": "Go back", + "error_action_home": "Go home", + "error_action_retry": "Try again", + "error_code_label": "Error {status}", + "error_details": "Details", + "error_generic_description": "Something went wrong while loading this page.", + "error_generic_title": "Unexpected error", + "error_not_found_description": "The page or resource you are looking for could not be found. It may have been removed, renamed, or never existed.", + "error_not_found_title": "Not found", + "error_unauthorized_description": "You do not have access to this resource. Try signing in again or contact your administrator.", + "error_unauthorized_title": "Access denied", "errors_address_invalid": "Enter a valid URL, e.g. http://127.0.0.1:9999", "errors_email_invalid": "Enter a valid email address", "errors_generic": "An error occurred during this operation, please review Nadir Logs for more information.", @@ -132,6 +146,7 @@ "forgot_password": "Forgot your password?", "forgot_password_description": "Enter your email and we'll send you a reset link.", "forgot_password_title": "Forgot your password?", + "groups": "Groups", "groups_add": "Add group", "groups_add_member": "Add member", "groups_add_member_action": "add", @@ -139,6 +154,9 @@ "groups_add_member_no_results": "No matching users.", "groups_add_member_search_placeholder": "Search user…", "groups_add_member_title": "Add member to {name}", + "groups_col_gid": "GID", + "groups_col_members": "Members", + "groups_gid_optional": "GID (optional)", "groups_create_description": "Adds a Unix group via groupadd.", "groups_create_field_system": "System group", "groups_create_title": "Create group", @@ -155,6 +173,7 @@ "groups_nav_description": "Unix groups from /etc/group on this machine.", "groups_nav_title": "Groups", "groups_no_results": "No groups found.", + "groups_remove_member_aria": "Remove {name}", "groups_no_supplementary_members": "No supplementary members.", "groups_not_found": "Group not found: {name}", "groups_primary_empty": "None.", @@ -165,6 +184,7 @@ "invalid_reset_link": "This link is invalid or has expired.", "language": "Language", "login": "Login", + "more": "More", "login_social_description": "You have to login to use this platform. Use your favorite social or your credentials", "login_with": "Login with {social}", "logout": "Logout", @@ -180,6 +200,7 @@ "machine_name": "Name", "machine_name_placeholder": "Production server", "machine_none": "No servers yet.", + "machine_offline": "Offline", "machine_offline_code": "Error 502", "machine_offline_description": "Could not reach the nadir-agent at {address}. The host may be down, the agent stopped, or the address is wrong.", "machine_offline_details": "Show error details", @@ -189,6 +210,8 @@ "machine_offline_status_connected": "CONNECTED", "machine_offline_status_unreachable": "UNREACHABLE", "machine_offline_title": "{name} is offline", + "machine_online": "Online", + "machine_refresh_health": "Refresh status", "machine_save": "Add server", "machine_save_edit": "Save changes", "machine_search_placeholder": "Search servers…", @@ -205,6 +228,29 @@ "nav_admin_users_desc": "Manage user accounts, roles and access.", "nav_dashboard_overview": "Overview", "nav_dashboard_overview_desc": "System status at a glance.", + "nav_networking": "Networking", + "nav_networking_desc": "Interfaces, routes, hosts, DNS.", + "nav_networking_dns": "DNS", + "nav_networking_dns_desc": "Nameservers from /etc/resolv.conf.", + "nav_networking_hosts": "Hosts", + "nav_networking_hosts_desc": "Static host entries in /etc/hosts.", + "nav_networking_interfaces": "Interfaces", + "nav_networking_interfaces_desc": "Network interfaces and their addresses.", + "nav_networking_routes": "Routes", + "nav_networking_routes_desc": "Kernel routing table.", + "nav_packages": "Packages", + "nav_packages_desc": "Manage installed packages and system updates.", + "nav_packages_installed": "Installed packages", + "nav_packages_installed_desc": "List, search, and remove installed software packages.", + "nav_packages_updates": "Available updates", + "nav_packages_updates_desc": "Check and install updates for existing packages.", + "nav_services": "Services", + "nav_services_desc": "Manage systemd service units.", + "nav_storage": "Storage", + "nav_storage_fstab": "Fstab", + "nav_storage_fstab_desc": "Persistent mount definitions in /etc/fstab.", + "nav_storage_mounts": "Mounts", + "nav_storage_mounts_desc": "Active filesystem mounts on this machine.", "nav_system": "System", "nav_system_datetime": "Date & Time", "nav_system_datetime_desc": "Clock, timezone and time synchronisation.", @@ -212,20 +258,129 @@ "nav_system_hostname_desc": "Identify this machine on the network.", "nav_system_localization": "Localization", "nav_system_localization_desc": "Language, locale and region settings.", + "nav_system_nadir": "Nadir Agent", + "nav_system_nadir_desc": "Active modules and user permissions.", "nav_system_power": "Power", "nav_system_power_desc": "Reboot or power off the machine.", + "nav_system_terminal": "Terminal", + "nav_system_terminal_desc": "Open an interactive terminal session on this host.", "nav_users_groups": "Groups", "nav_users_groups_desc": "Unix groups from /etc/group.", "nav_users_system_users": "System users", "nav_users_system_users_desc": "PAM/Unix accounts on this machine.", + "networking_apply": "Apply", + "networking_apply_confirm_body": "The agent will activate this configuration and auto-revert after {seconds}s unless you confirm. Make sure you can still reach the host after applying.", + "networking_apply_confirm_title": "Apply with rollback?", + "networking_apply_done": "Configuration applied — waiting for confirmation.", + "networking_col_destination": "Destination", + "networking_col_gateway": "Gateway", + "networking_col_hostnames": "Hostnames", + "networking_col_interface": "Interface", + "networking_col_ip": "IP", + "networking_col_ipv4": "IPv4", + "networking_col_ipv6": "IPv6", + "networking_col_mac": "MAC", + "networking_col_metric": "Metric", + "networking_col_mtu": "MTU", + "networking_col_name": "Name", + "networking_col_source": "Source", + "networking_col_state": "State", + "networking_configure": "Configure", + "networking_configure_description": "Apply addressing, gateway, DNS and static routes. Changes auto-rollback after the configured timeout unless confirmed.", + "networking_confirm": "Confirm", + "networking_confirmed": "Change confirmed.", + "networking_details": "Details", + "networking_dns_add": "Add nameserver", + "networking_dns_description": "Read-only view of the system resolver. Configure per interface.", + "networking_dns_per_interface_hint": "DNS is set per interface; there is no standalone write endpoint.", + "networking_dns_placeholder": "1.1.1.1", + "networking_dns_section": "DNS", + "networking_dns_section_hint": "Resolvers for this interface (IPv4 or IPv6).", + "networking_dns_servers": "Nameservers", + "networking_host_add": "Add host", + "networking_host_add_description": "Map an IP to one or more hostnames. Multiple names separated by spaces.", + "networking_host_edit": "Edit host", + "networking_host_remove": "Remove host", + "networking_host_remove_description": "This will delete the entry from /etc/hosts.", + "networking_host_remove_title": "Remove {ip}?", + "networking_host_removed": "Host entry removed.", + "networking_host_saved": "Host entry saved.", + "networking_hosts_description": "Static name-to-address mappings in /etc/hosts.", + "networking_hosts_search_placeholder": "Search by IP or hostname...", + "networking_interfaces_description": "Network interfaces and their current addresses.", + "networking_interfaces_search_placeholder": "Search by name, MAC, address...", + "networking_ipv4_section": "IPv4", + "networking_ipv4_section_hint": "Static address or DHCP.", + "networking_ipv6_section": "IPv6", + "networking_ipv6_section_hint": "Auto (SLAAC), static, or ignore to disable.", + "networking_link_down": "Bring down", + "networking_link_down_done": "Interface {name} brought down.", + "networking_link_up": "Bring up", + "networking_link_up_done": "Interface {name} brought up.", + "networking_method_dhcp": "DHCP", + "networking_method_ignore": "Ignore (disable)", + "networking_method_slaac": "Auto (SLAAC)", + "networking_method_static": "Static", + "networking_no_dns": "No nameservers configured.", + "networking_no_hosts": "No host entries.", + "networking_no_interfaces": "No interfaces.", + "networking_no_routes": "No routes.", + "networking_pending_banner": "Pending change on {iface} — auto-rollback in {seconds}s.", + "networking_rollback": "Rollback", + "networking_rollback_seconds": "Rollback timeout (seconds)", + "networking_rollback_seconds_hint": "Auto-revert after this many seconds unless confirmed. Leave 0 for the agent default (60s).", + "networking_rolled_back": "Change rolled back.", + "networking_route_add": "Add route", + "networking_route_destination": "Destination (CIDR or 'default')", + "networking_route_gateway": "Next-hop gateway", + "networking_routes_description": "Kernel routing table.", + "networking_routes_search_placeholder": "Search by destination, gateway, interface...", + "networking_routes_section": "Static routes", + "networking_routes_section_hint": "Optional routes installed alongside this interface.", "new_password": "New password", "no_account": "No account yet?", + "optional": "Optional", "or": "Or", + "packages_col_name": "Package", + "packages_col_version": "Version", + "packages_install": "Install Package", + "packages_install_button": "Install", + "packages_install_desc": "Enter the name of the package you want to install from the repositories.", + "packages_install_failed": "Installation of package {name} failed.", + "packages_install_started": "Installing package {name}...", + "packages_install_success": "Package {name} installed successfully.", + "packages_installed": "Installed", + "packages_manager_label": "Package Manager", + "packages_no_packages": "No packages found.", + "packages_no_updates": "No updates available.", + "packages_remove_confirm_desc": "This will uninstall {name} from the host. Any dependent packages might also be affected.", + "packages_remove_confirm_title": "Remove {name}?", + "packages_remove_failed": "Removal of package {name} failed.", + "packages_remove_started": "Removing package {name}...", + "packages_remove_success": "Package {name} removed successfully.", + "packages_search_placeholder": "Search packages by name...", + "packages_stream_connection_error": "Connection Error: {message}", + "packages_stream_error": "ERROR: {message}", + "packages_stream_failed": "Operation failed.", + "packages_terminal_desc": "Streaming output from the package manager.", + "packages_terminal_output": "Terminal Output", + "packages_title": "Packages", + "packages_update_available": "Update available {from} > {to}", + "packages_update_single": "Update", + "packages_update_started": "Upgrading package {name}...", + "packages_update_success": "Package {name} upgraded successfully.", + "packages_updates": "Updates", + "packages_upgrade_all": "Upgrade All", + "packages_upgrade_all_desc": "Upgrade all packages to their latest versions.", + "packages_upgrade_failed": "Upgrade failed.", + "packages_upgrade_started": "Starting upgrade of all packages...", + "packages_upgrade_success": "Upgrade completed successfully.", "pagination_next": "Next", "pagination_page_of": "Page {page} of {pages}", "pagination_previous": "Previous", "password": "Password", "password_hint": "At least 8 characters, mixing upper- and lower-case letters and a number.", + "prefix": "Prefix", "privacy_policy": "Privacy Policy", "remember_me": "Remember me", "reset_link_sent": "If an account exists for that email, a reset link is on its way.", @@ -236,12 +391,157 @@ "saved": "Saved", "scan_qr": "Add this key to your authenticator app, then enter the generated code below.", "send_reset_link": "Send reset link", + "seo_desc_admin_config": "Application-wide configuration settings.", + "seo_desc_admin_users": "Manage user accounts, roles and access.", + "seo_desc_auth_2fa": "Verify your identity with a two-factor authentication code.", + "seo_desc_auth_forgot_password": "Reset your password via email.", + "seo_desc_auth_reset_password": "Choose a new password for your account.", + "seo_desc_auth_setup_2fa": "Add an extra layer of security with two-factor authentication.", + "seo_desc_auth_sign_in": "Sign in to manage your servers.", + "seo_desc_auth_sign_up": "Create an account to get started.", + "seo_desc_dashboard": "Server overview and health at a glance.", + "seo_desc_machine_detail": "System status, performance metrics and key information for this machine.", + "seo_desc_networking": "Network interfaces, routes, hosts and DNS configuration.", + "seo_desc_networking_dns": "Nameserver configuration from /etc/resolv.conf.", + "seo_desc_networking_hosts": "Static host entries in /etc/hosts.", + "seo_desc_networking_interfaces": "Network interfaces and their addresses.", + "seo_desc_networking_routes": "Kernel routing table.", + "seo_desc_packages": "Manage installed packages and system updates.", + "seo_desc_packages_installed": "List, search, and remove installed software packages.", + "seo_desc_packages_updates": "Check and install available package updates.", + "seo_desc_root": "Web-based server management dashboard.", + "seo_desc_services": "List, filter, and manage systemd service units.", + "seo_desc_services_detail": "Manage and monitor a systemd service unit.", + "seo_desc_storage": "Filesystem mounts and fstab configuration.", + "seo_desc_storage_fstab": "Persistent mount definitions in /etc/fstab.", + "seo_desc_storage_mounts": "Active filesystem mounts on this machine.", + "seo_desc_system": "Date and time, hostname, localization and power controls.", + "seo_desc_system_datetime": "Clock, timezone and time synchronisation settings.", + "seo_desc_system_hostname": "View and change the machine hostname.", + "seo_desc_system_localization": "Language, locale and region settings.", + "seo_desc_system_nadir": "View active modules and permission matrix for the connected Nadir agent.", + "seo_desc_system_power": "Reboot or power off the machine.", + "seo_desc_users": "PAM/Unix user accounts on this machine.", + "seo_desc_users_detail": "View and manage a PAM/Unix user account.", + "seo_desc_users_groups": "Unix groups from /etc/group on this machine.", + "seo_desc_users_groups_detail": "View and manage a Unix group.", + "seo_title_admin_config": "Config", + "seo_title_admin_users": "Users", + "seo_title_auth_2fa": "Two-factor authentication", + "seo_title_auth_forgot_password": "Forgot password", + "seo_title_auth_reset_password": "Reset password", + "seo_title_auth_setup_2fa": "Set up two-factor authentication", + "seo_title_auth_sign_in": "Sign in", + "seo_title_auth_sign_up": "Sign up", + "seo_title_dashboard": "Dashboard", + "seo_title_machine_detail": "Machine overview", + "seo_title_networking": "Networking", + "seo_title_networking_dns": "DNS", + "seo_title_networking_hosts": "Hosts", + "seo_title_networking_interfaces": "Interfaces", + "seo_title_networking_routes": "Routes", + "seo_title_packages": "Packages", + "seo_title_packages_installed": "Installed packages", + "seo_title_packages_updates": "Available updates", + "seo_title_root": "Home", + "seo_title_services": "Services", + "seo_title_services_detail": "Service detail", + "seo_title_storage": "Storage", + "seo_title_storage_fstab": "Fstab", + "seo_title_storage_mounts": "Mounts", + "seo_title_system": "System", + "seo_title_system_datetime": "Date and time", + "seo_title_system_hostname": "Hostname", + "seo_title_system_localization": "Localization", + "seo_title_system_nadir": "Nadir Agent", + "seo_title_system_power": "Power", + "seo_title_users": "System users", + "seo_title_users_detail": "User detail", + "seo_title_users_groups": "Groups", + "seo_title_users_groups_detail": "Group detail", + "services_action_disable": "Disable", + "services_action_disabled": "Service {name} disabled successfully.", + "services_action_enable": "Enable", + "services_action_enabled": "Service {name} enabled successfully.", + "services_action_restart": "Restart", + "services_action_restarted": "Service {name} restarted successfully.", + "services_action_start": "Start", + "services_action_started": "Service {name} started successfully.", + "services_action_stop": "Stop", + "services_action_stopped": "Service {name} stopped successfully.", + "services_active_filter": "Active State", + "services_col_active": "Active State", + "services_col_description": "Description", + "services_col_load": "Load State", + "services_col_sub": "Sub State", + "services_col_unit": "Unit", + "services_description": "List, filter, and manage systemd service units.", + "services_details_active_state": "Active State", + "services_details_load_state": "Load State", + "services_details_sub_state": "Sub State", + "services_details_title": "Service Details", + "services_details_unit_file_state": "Startup Type", + "services_filter_active": "Active", + "services_filter_dead": "Dead", + "services_filter_error": "Error", + "services_filter_exited": "Exited", + "services_filter_failed": "Failed", + "services_filter_inactive": "Inactive", + "services_filter_loaded": "Loaded", + "services_filter_masked": "Masked", + "services_filter_not_found": "Not Found", + "services_filter_other": "Other", + "services_filter_running": "Running", + "services_filter_title": "Filter Services", + "services_load_filter": "Load State", + "services_logs_autoscroll": "Autoscroll", + "services_logs_clear": "Clear", + "services_logs_empty": "No log entries.", + "services_logs_lines": "Lines", + "services_logs_live": "Live", + "services_logs_live_paused": "Paused", + "services_logs_live_streaming": "Streaming", + "services_logs_priority": "Max priority", + "services_logs_search_placeholder": "Filter log lines...", + "services_logs_since": "Since", + "services_logs_since_15m": "Last 15 minutes", + "services_logs_since_1h": "Last hour", + "services_logs_since_6h": "Last 6 hours", + "services_logs_since_all": "All", + "services_logs_since_today": "Today", + "services_logs_since_yesterday": "Since yesterday", + "services_logs_title": "Logs", + "services_no_services": "No services found.", + "services_search_placeholder": "Search service units...", + "services_sub_filter": "Sub State", + "services_title": "Services", "settings": "Settings", "setup_2fa_description": "Add an extra layer of security to your account.", "setup_2fa_title": "Set up two-factor authentication", "sign_up": "Sign up", "sign_up_description": "Sign up with your email and a username.", "sign_up_title": "Create your account", + "storage_badge_fstab": "fstab", + "storage_col_device": "Device", + "storage_col_dump": "Dump", + "storage_col_fstype": "Type", + "storage_col_mountpoint": "Mount point", + "storage_col_options": "Options", + "storage_col_pass": "Pass", + "storage_filter_show_pseudo": "Show pseudo filesystems", + "storage_filter_show_pseudo_hint": "proc, sysfs, tmpfs, cgroup, …", + "storage_fstab_description": "Persistent mount definitions from /etc/fstab.", + "storage_mount_add": "Add mount", + "storage_mount_add_description": "Appends an /etc/fstab entry and mounts it. If the mount fails the entry is rolled back.", + "storage_mount_added": "Filesystem mounted", + "storage_mount_remove": "Unmount", + "storage_mount_remove_description": "Unmounts the filesystem and removes its /etc/fstab entry. This cannot be undone.", + "storage_mount_remove_title": "Unmount {mountpoint}?", + "storage_mount_removed": "Filesystem unmounted", + "storage_mounts_description": "Active mounts from the kernel mount table.", + "storage_mounts_search_placeholder": "Search by device, mount point or type…", + "storage_no_fstab": "No fstab entries.", + "storage_no_mounts": "No mounts found.", "system_hostname_current": "Current hostname", "system_hostname_invalid": "Hostname is invalid", "system_locale_generate": "Generate new locale", @@ -277,6 +577,14 @@ "system_time_ntp_synced": "Synchronized", "system_time_search_timezone_placeholder": "Search timezone…", "system_time_timezone": "Timezone", + "syslog_alert": "1 alert", + "syslog_crit": "2 crit", + "syslog_debug": "7 debug", + "syslog_emerg": "0 emerg", + "syslog_err": "3 err", + "syslog_info": "6 info", + "syslog_notice": "5 notice", + "syslog_warning": "4 warning", "terms_notice": "By clicking continue, you agree to our Terms of Service and Privacy Policy.", "theme": "Theme", "theme_dark": "Dark", @@ -300,6 +608,7 @@ "users_col_comment": "Comment", "users_col_home": "Home", "users_col_type": "Type", + "users_col_uid": "UID", "users_create": "Create", "users_create_description": "Add a new user to the system.", "users_create_field_comment": "Comment (GECOS)", @@ -347,7 +656,6 @@ "users_group_primary_badge": "(primary)", "users_group_sys_badge": "sys", "users_groups_title": "Groups", - "users_pam_groups_description": "Supplementary groups. Replaces the full set via usermod -G. Primary group is set at user creation and not editable here.", "users_groups_updated": "Groups updated", "users_invite": "Invite", "users_invite_description": "Send an email invitation. The user sets their own password.", @@ -361,12 +669,14 @@ "users_no_results": "No users found.", "users_page_of": "Page {page} of {total}", "users_pam_create_description": "Adds a PAM account via useradd. Password stays locked until you set one.", + "users_pam_groups_description": "Supplementary groups. Replaces the full set via usermod -G. Primary group is set at user creation and not editable here.", "users_pam_search_placeholder": "Search username, GECOS, uid…", "users_pending": "Pending", "users_pending_expires": "Invite expires {date}", "users_pending_no_invite": "Email not verified", "users_prev": "Previous", "users_primary_gid": "Primary GID", + "users_primary_group": "Primary group", "users_resend_invite": "Resend invite", "users_role": "Role", "users_role_admin": "Admin", @@ -384,5 +694,11 @@ "verification_sent": "We sent a verification link to {email}. Click it to activate your account.", "verify": "Verify", "verify_your_email": "You need to first verify your email address", - "welcome_back": "Welcome back" + "welcome_back": "Welcome back", + "system_nadir_username": "Authenticated User", + "system_nadir_permissions": "Resolved Permissions", + "system_nadir_modules": "Registered Modules", + "system_nadir_module_id": "Module ID", + "system_nadir_module_name": "Display Name", + "system_nadir_no_permissions": "No permissions defined" } diff --git a/package.json b/package.json index 2cfcbca..560f6d4 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "version": "0.0.1", "type": "module", "scripts": { + "dev:types": "bun run type:generate && bun --bun vite dev --host", "dev": "bun --bun vite dev --host", "build": "vite build", "preview": "vite preview", @@ -34,6 +35,7 @@ "@types/bun": "^1.3.14", "@types/node": "^24", "@types/nodemailer": "^8.0.1", + "@types/ws": "^8.18.1", "bits-ui": "^2.16.3", "clsx": "^2.1.1", "drizzle-kit": "^1.0.0-beta.22", @@ -54,6 +56,7 @@ "prettier-plugin-tailwindcss": "^0.8.0", "shadcn-svelte": "^1.3.0", "svelte": "^5.56.1", + "svelte-adapter-bun": "^1.0.1", "svelte-check": "^4.6.0", "svelte-sonner": "^1.1.0", "sveltekit-superforms": "^2.30.0", @@ -64,18 +67,23 @@ "typescript": "^6.0.3", "typescript-eslint": "^8.60.1", "vaul-svelte": "^1.0.0-next.7", - "vite": "^8.0.16" + "vite": "^8.0.16", + "ws": "^8.21.0" }, "dependencies": { "@better-auth/infra": "^0.2.14", "@better-svelte-email/components": "^2.1.1", "@better-svelte-email/server": "^2.1.1", + "@xterm/addon-attach": "^0.12.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "better-auth": "^1.6.20", "nodemailer": "^9.0.1", "ogl": "^1.0.11", "openapi-fetch": "^0.17.0", "runed": "^0.37.1", "swapy": "^1.0.5", + "undici": "^8.5.0", "uqr": "^0.1.3", "valibot": "^1.4.1" } diff --git a/scripts/replace-lucide-icons.ts b/scripts/replace-lucide-icons.ts new file mode 100644 index 0000000..68bc44f --- /dev/null +++ b/scripts/replace-lucide-icons.ts @@ -0,0 +1,112 @@ +// scripts/replace-lucide-imports.ts + +const lucidePath = '@lucide/svelte'; + +function parseSpec(spec: string) { + const parts = spec.split(/\s+as\s+/).map((s) => s.trim()); + const name = parts[0]; + const alias = parts[1] ?? name; + return { alias, name }; +} + +async function processFile(path: string) { + const file = Bun.file(path); + const text = await file.text(); + + const regex = /import\s*\{[^}]+\}\s*from\s*["']@lucide\/svelte["'];?/g; + + const matches = [...text.matchAll(regex)]; + if (matches.length === 0) return; + + let updatedText = text; + + for (const match of matches) { + const block = match[0]; + + const inside = block.match(/\{([^}]+)\}/); + if (!inside) continue; + + const items = inside[1] + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + + const resolved: string[] = []; + + for (const item of items) { + const { name } = parseSpec(item); + const path = await resolveIconPath(name); + resolved.push(path); + } + + const replacement = transformImportBlock(block, resolved); + + updatedText = updatedText.replace(block, replacement); + } + + if (updatedText !== text) { + console.log(`Updated ${path}`); + await Bun.write(path, updatedText); + } +} + +/** + * Safe import builder with fallback prompt + */ +async function resolveIconPath(name: string): Promise { + let cleanName = name; + if (cleanName.endsWith('Icon')) { + cleanName = cleanName.slice(0, -4); + } + const kebab = toKebab(cleanName); + const candidate = `${lucidePath}/icons/${kebab}`; + + // Bun runtime check: verify physical file exists in dist/icons + const file = Bun.file(`node_modules/${lucidePath}/dist/icons/${kebab}.js`); + + if (await file.exists()) return candidate; + + // fallback interactive prompt + const input = await prompt( + `Icon not found: "${name}" → "${kebab}". Enter correct kebab name (or press enter to skip): ` + ); + + if (!input) return candidate; // fallback anyway + + return `${lucidePath}/icons/${input}`; +} + +function toKebab(input: string): string { + return input + .replace(/([a-z0-9])([A-Z])/g, '$1-$2') + .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') + .replace(/([a-zA-Z])([0-9])/g, '$1-$2') // handle digit transitions like Trash2 -> trash-2 + .toLowerCase(); +} + +function transformImportBlock(block: string, resolved: string[]): string { + const inside = block.match(/\{([^}]+)\}/); + if (!inside) return block; + + const items = inside[1] + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + + const imports: string[] = []; + + for (const item of items) { + const { alias } = parseSpec(item); + const iconPath = resolved.shift(); + + imports.push(`import ${alias} from "${iconPath}";`); + } + + return imports.join('\n'); +} + +const glob = new Bun.Glob('src/**/*.{ts,tsx,js,jsx,svelte}'); + +for await (const file of glob.scan('.')) { + await processFile(file); +} diff --git a/src/lib/components/blocks/app-controls.svelte b/src/lib/components/blocks/app-controls.svelte index 304f277..1508d49 100644 --- a/src/lib/components/blocks/app-controls.svelte +++ b/src/lib/components/blocks/app-controls.svelte @@ -17,7 +17,6 @@ const labelOf = (l: string) => names.of(l) ?? l; const pickLocale = (l: ReturnType) => setLocale(l); - {#snippet localePicker()} @@ -55,13 +54,16 @@ setMode('light')}> - {m.theme_light()} + + {m.theme_light()} setMode('dark')}> - {m.theme_dark()} + + {m.theme_dark()} setMode('system')}> - {m.theme_system()} + + {m.theme_system()} diff --git a/src/lib/components/blocks/breadcrumbs/breadcrumbs.svelte b/src/lib/components/blocks/breadcrumbs/breadcrumbs.svelte index 4763e6d..a0293a0 100644 --- a/src/lib/components/blocks/breadcrumbs/breadcrumbs.svelte +++ b/src/lib/components/blocks/breadcrumbs/breadcrumbs.svelte @@ -1,7 +1,4 @@ {#snippet sectionContent(section: NavSection)} - - {#each section.items as item (item.url)} - + {item.title()} + - {item.title()} - - {item.description()} - - - {/each} - {#if section.url === '/dashboard'|| section.url === `/dashboard/${page.params.machineId}`} + {item.description()} + + + {/each} + {#if section.showsMachines} {/if} {/snippet} @@ -192,7 +320,28 @@ {#each navMain as item (item.url)} - + {#if item.url === resolve('/admin') && canShowTerminal} + + (terminalState.open = true)} + > + {#snippet tooltipContent()} + {m.nav_system_terminal()} + {/snippet} + {#snippet child({ props })} + + {/snippet} + + + {/if} + { // On mobile, open the second drawer instead of navigating — diff --git a/src/lib/components/blocks/sidebar/machines-nav.svelte b/src/lib/components/blocks/sidebar/machines-nav.svelte index 0f42c26..ea5b5c7 100644 --- a/src/lib/components/blocks/sidebar/machines-nav.svelte +++ b/src/lib/components/blocks/sidebar/machines-nav.svelte @@ -1,9 +1,13 @@ {#snippet addForm()} + + + + {#snippet icon()} + + {/snippet} + {installSH} + +
addMachine.validate()} {...addMachine.preflight(machineSchema).enhance(async ({ submit }) => { @@ -139,7 +154,25 @@ {/snippet}
- +
+ + +
{#if isMobile.current} @@ -197,7 +230,15 @@ data-swapy-no-drag class="flex-1 flex flex-col items-start gap-1 p-4 text-sm leading-tight transition-all" > - {machine.name} + + + {machine.name} + {machine.address} + import type { FitAddon as TFitAddon } from '@xterm/addon-fit'; + import type { Terminal as TTerminal } from '@xterm/xterm'; + + import TerminalIcon from '@lucide/svelte/icons/terminal'; + import { page } from '$app/state'; + import * as Dialog from '$lib/components/ui/dialog'; + import '@xterm/xterm/css/xterm.css'; + import { m } from '$lib/paraglide/messages'; + import { getMachine } from '$lib/remotes/machines.remote'; + import { getContext } from 'svelte'; + + interface TerminalState { + open: boolean; + } + + const terminalState = getContext('terminalState'); + + let containerElement = $state(null); + let socket = $state(null); + let term = $state(null); + let fitAddon = $state(null); + + const machineId = $derived(page.params.machineId); + const machineResource = $derived(machineId ? getMachine(machineId) : null); + const machineAddress = $derived(machineResource?.current?.address ?? null); + + const socketUrl = $derived.by(() => { + if (!machineAddress) return ''; + try { + const url = new URL(machineAddress); + const wsProtocol = url.protocol === 'https:' ? 'wss' : 'ws'; + return `${wsProtocol}://${url.host}/api/terminal`; + } catch { + return ''; + } + }); + + function sendResize(cols: number, rows: number) { + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ cols, rows })); + } + } + + function cleanup() { + if (socket) { + socket.close(); + socket = null; + } + if (term) { + term.dispose(); + term = null; + } + fitAddon = null; + } + + $effect(() => { + if (!terminalState.open || !socketUrl) { + cleanup(); + return; + } + + let active = true; + + const initTerminal = async () => { + if (!containerElement) return; + + // Dynamically import xterm and addons to prevent SSR issues + const { Terminal } = await import('@xterm/xterm'); + const { AttachAddon } = await import('@xterm/addon-attach'); + const { FitAddon } = await import('@xterm/addon-fit'); + + if (!active) return; + + term = new Terminal({ + cursorBlink: true, + fontFamily: 'Geist Mono, JetBrains Mono, Fira Code, monospace', + fontSize: 14, + theme: { + background: '#09090b', // zinc-950 + cursor: '#fafafa', + foreground: '#fafafa', // zinc-50 + selectionBackground: '#27272a' // zinc-800 + } + }); + + socket = new WebSocket(socketUrl); + socket.binaryType = 'arraybuffer'; + + socket.onopen = () => { + if (!active) { + cleanup(); + return; + } + + const attachAddon = new AttachAddon(socket!); + term?.loadAddon(attachAddon); + + fitAddon = new FitAddon(); + term?.loadAddon(fitAddon); + term?.open(containerElement!); + fitAddon?.fit(); + if (term) sendResize(term.cols, term.rows); + else console.error('terminal not initialized'); + }; + }; + + initTerminal(); + + const handleResize = () => { + if (fitAddon && term) { + fitAddon.fit(); + sendResize(term.cols, term.rows); + } + }; + + window.addEventListener('resize', handleResize); + + return () => { + active = false; + window.removeEventListener('resize', handleResize); + cleanup(); + }; + }); + + + + + + + {m.nav_system_terminal()} + +
+
+
+
+
+ + diff --git a/src/lib/components/blocks/trunk/trunk.svelte b/src/lib/components/blocks/trunk/trunk.svelte index 77de090..2fa6af5 100644 --- a/src/lib/components/blocks/trunk/trunk.svelte +++ b/src/lib/components/blocks/trunk/trunk.svelte @@ -190,8 +190,8 @@ width: 100%; height: 100%; overflow: hidden; - display:grid; - place-content:center + display: grid; + place-content: center; } .trunk :global(canvas) { display: block; diff --git a/src/lib/components/dashboard/activity-panel.svelte b/src/lib/components/dashboard/activity-panel.svelte index 3a24e42..c92862b 100644 --- a/src/lib/components/dashboard/activity-panel.svelte +++ b/src/lib/components/dashboard/activity-panel.svelte @@ -1,5 +1,5 @@ import type { Snippet } from 'svelte'; - import { Server } from '@lucide/svelte'; + import Server from '@lucide/svelte/icons/server'; import * as Card from '$lib/components/ui/card'; import { m } from '$lib/paraglide/messages'; diff --git a/src/lib/components/dashboard/temperature-panel.svelte b/src/lib/components/dashboard/temperature-panel.svelte index 938aa80..42ebc69 100644 --- a/src/lib/components/dashboard/temperature-panel.svelte +++ b/src/lib/components/dashboard/temperature-panel.svelte @@ -1,5 +1,5 @@ + + + {fullTitle} + + {#if noIndex} + + {/if} + + + + + + + + diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte index 0ddf945..989bf56 100644 --- a/src/lib/components/ui/badge/badge.svelte +++ b/src/lib/components/ui/badge/badge.svelte @@ -13,7 +13,8 @@ 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20', ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50', link: 'text-primary underline-offset-4 hover:underline', - outline: 'border-foreground /50 text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground', + outline: + 'border-foreground /50 text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground', secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80' } } diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte index 6285713..e134dc9 100644 --- a/src/lib/components/ui/button/button.svelte +++ b/src/lib/components/ui/button/button.svelte @@ -1,5 +1,4 @@ + + + + diff --git a/src/lib/components/ui/copy-button/index.ts b/src/lib/components/ui/copy-button/index.ts new file mode 100644 index 0000000..37158fa --- /dev/null +++ b/src/lib/components/ui/copy-button/index.ts @@ -0,0 +1,3 @@ +import CopyButton from './copy-button.svelte'; + +export { CopyButton }; diff --git a/src/lib/components/ui/dialog/dialog-content.svelte b/src/lib/components/ui/dialog/dialog-content.svelte index fbd78ae..e7afedb 100644 --- a/src/lib/components/ui/dialog/dialog-content.svelte +++ b/src/lib/components/ui/dialog/dialog-content.svelte @@ -30,7 +30,7 @@ bind:ref data-slot="dialog-content" class={cn( - 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none', + 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-lg sm:min-w-max fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none', className )} {...restProps} diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte index e675ac5..961d0ca 100644 --- a/src/lib/components/ui/sidebar/sidebar.svelte +++ b/src/lib/components/ui/sidebar/sidebar.svelte @@ -83,10 +83,10 @@