From 20ac1445caabb26d765a80acedca9cee3e04e75c Mon Sep 17 00:00:00 2001 From: urania Date: Thu, 25 Jun 2026 14:46:48 +0200 Subject: [PATCH] stabilized architecture --- bun.lock | 20 + db.sqlite | Bin 94208 -> 94208 bytes messages/en.json | 1401 ++++++++--------- package.json | 2 + src/hooks.server.ts | 3 +- .../blocks/sidebar/app-sidebar.svelte | 2 +- .../blocks/sidebar/machines-nav.svelte | 97 +- .../components/blocks/terminal-dialog.svelte | 480 +++++- .../components/dashboard/data-table.svelte | 209 +++ src/lib/remotes/machines.remote.ts | 27 +- src/lib/remotes/networking.remote.ts | 26 +- src/lib/remotes/packages.remote.ts | 49 +- src/lib/remotes/pam-users.remote.ts | 18 +- src/lib/remotes/server.remote.ts | 53 +- src/lib/remotes/services.remote.ts | 51 +- src/lib/remotes/storage.remote.ts | 8 +- src/lib/remotes/system.remote.ts | 55 +- src/lib/remotes/terminal.remote.ts | 125 ++ src/lib/remotes/users.remote.ts | 7 +- src/lib/remotes/utils.ts | 48 +- src/lib/server/db/custom-types.ts | 2 +- src/lib/server/terminal/session.ts | 181 +++ src/lib/utils.ts | 9 + src/routes/+layout.svelte | 3 +- src/routes/admin/users/+page.svelte | 3 +- .../machines/[machineId]/terminal/+server.ts | 34 - src/routes/auth/forgot-password/+page.svelte | 5 +- src/routes/auth/reset-password/+page.svelte | 3 +- src/routes/auth/sign-in/+page.svelte | 3 +- src/routes/auth/sign-up/+page.svelte | 5 +- src/routes/dashboard/[machineId]/+page.svelte | 7 +- .../[machineId]/networking/hosts/+page.svelte | 261 +-- .../networking/interfaces/+page.svelte | 419 ++--- .../interfaces/[name]/configure/+page.svelte | 3 +- .../networking/routes/+page.svelte | 225 +-- .../packages/installed/+page.svelte | 277 ++-- .../[machineId]/packages/updates/+page.svelte | 235 +-- .../[machineId]/services/+page.svelte | 495 ++---- .../[machineId]/services/[name]/+page.svelte | 5 +- .../[machineId]/storage/fstab/+page.svelte | 243 +-- .../[machineId]/storage/mounts/+page.svelte | 312 ++-- .../[machineId]/system/date-time/+page.svelte | 3 +- .../[machineId]/system/hostname/+page.svelte | 3 +- .../system/localization/+page.svelte | 3 +- .../[machineId]/system/power/+page.svelte | 3 +- .../dashboard/[machineId]/users/+page.svelte | 360 ++--- .../[machineId]/users/[username]/+page.svelte | 3 +- .../[machineId]/users/groups/+page.svelte | 328 ++-- .../users/groups/[group]/+page.svelte | 3 +- 49 files changed, 2925 insertions(+), 3192 deletions(-) create mode 100644 src/lib/components/dashboard/data-table.svelte create mode 100644 src/lib/remotes/terminal.remote.ts create mode 100644 src/lib/server/terminal/session.ts delete mode 100644 src/routes/api/machines/[machineId]/terminal/+server.ts diff --git a/bun.lock b/bun.lock index 44ab653..4d4a619 100644 --- a/bun.lock +++ b/bun.lock @@ -16,6 +16,7 @@ "ogl": "^1.0.11", "openapi-fetch": "^0.17.0", "runed": "^0.37.1", + "ssh2": "^1.17.0", "swapy": "^1.0.5", "undici": "^8.5.0", "uqr": "^0.1.3", @@ -37,6 +38,7 @@ "@types/bun": "^1.3.14", "@types/node": "^24", "@types/nodemailer": "^8.0.1", + "@types/ssh2": "^1.15.5", "@types/ws": "^8.18.1", "bits-ui": "^2.16.3", "clsx": "^2.1.1", @@ -499,6 +501,8 @@ "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -577,6 +581,8 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + "better-auth": ["better-auth@1.6.20", "", { "dependencies": { "@better-auth/core": "1.6.20", "@better-auth/drizzle-adapter": "1.6.20", "@better-auth/kysely-adapter": "1.6.20", "@better-auth/memory-adapter": "1.6.20", "@better-auth/mongo-adapter": "1.6.20", "@better-auth/prisma-adapter": "1.6.20", "@better-auth/telemetry": "1.6.20", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.6", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-fSpGHGRKiGRiYVd3QTQtuVZ8oxpiSe/7ip0Rpvt/Sy8zQbEbVKUPMOhE0gLXg+FjqTUsIo7582hxUYxtEcqUpA=="], "better-call": ["better-call@1.3.6", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-no1jI+h6Bkxs1NVBo4rONbVIzsPjZ8IUu7IHaJBiFwVX1XEQGN8KpHots5fSWmXe9nNyLuLIcgx6WEUcE6EDaA=="], @@ -585,6 +591,8 @@ "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], @@ -617,6 +625,8 @@ "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], @@ -949,6 +959,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nan": ["nan@2.27.0", "", {}, "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ=="], + "nanoid": ["nanoid@3.3.14", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ=="], "nanostores": ["nanostores@1.3.0", "", {}, "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA=="], @@ -1097,6 +1109,8 @@ "sqlite-wasm-kysely": ["sqlite-wasm-kysely@0.3.0", "", { "dependencies": { "@sqlite.org/sqlite-wasm": "^3.48.0-build2" }, "peerDependencies": { "kysely": "*" } }, "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg=="], + "ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="], + "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], @@ -1165,6 +1179,8 @@ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -1277,6 +1293,8 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "bits-ui/runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="], @@ -1329,6 +1347,8 @@ "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], diff --git a/db.sqlite b/db.sqlite index 632e45e23318047f926f70876fdebf6d1362fc40..c35d9d396463b1c3c85a9d65c523512f9843e45c 100644 GIT binary patch delta 1220 zcmc(eUr19?9LLYkdw1`h%X_f$!5m~#RE%wR+nw9&4@?bLce{7kHqD3Zx-H9TGhMVS z(np#7nnJ{Y|gL}5_*7#Z|XAVE|_nb1SpLq1p!LUqqdNrdPj=sx_ozjJ@T-}jvF z=bpYvL*Jxfa7_U$Q_Rfu-Mt+8$`L5@=?IMaB8= z+mPysyWB!9$mk8wU$hNrSa0C`wbkwEc*>v+hF}moobD>0{_u0`_UKyB1Q{GaYpM)a z3WL;Q=lK9wpg_AHfU72cj_Hn;8S@jpWDBJdT<_(~(K3##c|2~P+vgOl2~kK|MOV^i zRpTw9wIyEZjaOEB#Dw7VcW1iPq-qJPb*;4ry<%%PoC>?8h!V+mCOpUD^>#6`zoWJn_Ug{M8nIqVWCBr#C}h2%h`&)uWztSpyS+h()f^Gr zDNB3(A)zy13B@e#P}MdTv5QLqqZxc3rGfC zeJSc|y*Y`~SN|LArilL^!$uR!uhjb+(?b4AX^$1y&awOy!Y}!$dxc?m!EfF~Md5FL zbWSrh!`IbAh{f5h_%a^AXYna~5@+$P(PCkq@8u{O-o7DmYW~%G29Ke(S(3e$A0Fqh zg7~S6AA4A=uoP|lGFx#B%ZTPEl1)!Pd4`iDqk(Le#zV;bl$ohu@lCYh!|3T6PM>Bm zkeN0E1z=!Eq$v_9mZrd1K(D8aV4x`uNNve;*O--*uY7cN!b#~bq?DnxyJ-mi0PCJh A>;M1& delta 1789 zcmb7EO^D-U7;dKR{AAqis)*t$yNVuUn3*(9JI(4UUw)dV&6hN7(oU+frfHfsO`GZT zCz+zw883pcKkgx`hlRz1;6-Hx#hW539zBQ$R~G~?df1CUh@k1Hi=ZIBhnJ7%dB2DE zdtScxx$E)guE)>cy%2FO+(U)=t*3qyWB2^}QvKp}_v9_egz`Nq-u)pOGB zjoVk>jVvx7oI3jB^1(_8{SLkN=tEa;J$5WMd*Shg74^jO!I8(#K4>;t&9u=>uN$r8 z);f{F((8K4)Yp?nD}!S=p54Nmxm~?ydhTvl9<%I-E8xwB$yaoXLn;cf^Yv zT}rtu$s|1?=X4Z7-jRxBb__Dun9?ihsj8QzHJ#||Gr=9_2DnYt&Ap*89@IL3n3N{{ zfgrj54yUj6siFeYDI=dERHK~RwDgM00IE0`wMV0!;i=8dB!+Edhz)EbnI%t=VXV)C zVmV$sNwZ~z&+#-+*h3VBJWm@6O@p>+0gtCUaqTe^CPX+bQ*nm)a3jrq=;=s;c>`ldvMC8 zpv_`1>4q3Q#8AlG1U+sRO8%!}uIsU$mjAOA8U^yrz8D}IjBEiGc9Ihw@=nFe8zYre zh+Jw~Qi>I-F-;3KU+mY~Y%AZBI!Q%Gn~c}#C}i0oXDqoT4YA!$wq0*jo0GNP$+C1vUVo|5VsMgI3E}nd0-aH01NRWjVtJF18z7dt#(2G&5z>utWHwZH=K~ z5rWP7BEc7-V(f+IlwgA~3N;==1$ct3VRIlXOk16Dug*b2vYR&Kv<&QNgvhv_)GN28 znwrt@oFgeNUhlyaD)p+)Otfj5eKfU7`)qyVG=W&y~mG%qvxq%$Q`KGk=Nnl&_fGY9nP>@8tObB9ya&O9l_m2KH-;i*A}m4!h`^EGpqgLI*h z)stDdKc<~+>;R(`qb&5sk8BIq|Kt{p@Z@hF1CqVpV$&GW)yEV_ck!z*_#%+q{W abAj7`NPl*JJQj^0OR*@j5)a;X5#e`}9VB)D diff --git a/messages/en.json b/messages/en.json index dadca3a..0e4b6e1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,704 +1,701 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", - "account": "Account", - "agent_update_failed": "Agent update did not complete - check `nadir logs` on the host", - "agent_update_started": "Updating agent... ({from} → {to})", - "agent_update_success": "Agent updated to {version}", - "agent_updates": "Agent outdated", - "already_have_account": "Already have an account?", - "appname": "NadiЯ", - "back_to_login": "Back to login", - "backup_code": "Backup code", - "backup_codes_notice": "Store these somewhere safe. Each code works once if you lose your device.", - "backup_codes_title": "Save your backup codes", - "cancel": "Cancel", - "check_your_email": "Check your email", - "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", - "dashboard_ascending": "Ascending", - "dashboard_clock": "Clock", - "dashboard_col_free": "Free", - "dashboard_col_ip": "IP", - "dashboard_col_method": "Method", - "dashboard_col_mount": "Mount", - "dashboard_col_name": "Name", - "dashboard_col_path": "Path", - "dashboard_col_sensor": "Sensor", - "dashboard_col_size": "Size", - "dashboard_col_status": "Status", - "dashboard_col_temp": "Temp", - "dashboard_col_time": "Time", - "dashboard_col_usage": "Usage", - "dashboard_col_user": "User", - "dashboard_cpu": "CPU", - "dashboard_cpu_detail": "{cores} cores · {current} ({min}–{max})", - "dashboard_descending": "Descending", - "dashboard_dns": "DNS", - "dashboard_free_of": "{free} free of {total}", - "dashboard_hardware_clock": "Hardware clock", - "dashboard_heatmap_less": "Less", - "dashboard_heatmap_more": "More", - "dashboard_heatmap_samples": "{count} samples", - "dashboard_interval_10s": "Every 10s", - "dashboard_interval_30s": "Every 30s", - "dashboard_interval_5s": "Every 5s", - "dashboard_interval_custom": "Custom", - "dashboard_interval_second": "Every second", - "dashboard_kernel": "Kernel", - "dashboard_keymap": "Keymap", - "dashboard_language": "Language", - "dashboard_load_average": "Load Average", - "dashboard_load_detail": "{loadPct}% of {cores}c · 5m {load5} · 15m {load15}", - "dashboard_local_time": "Local time", - "dashboard_locale": "Locale", - "dashboard_logical_cores": "{cores} logical cores", - "dashboard_memory": "Memory", - "dashboard_nameserver": "Nameserver", - "dashboard_network": "Network", - "dashboard_next": "Next", - "dashboard_none": "none", - "dashboard_not_synced": "Not synced", - "dashboard_nothing_to_show": "Nothing to show.", - "dashboard_os": "OS", - "dashboard_packages": "Packages", - "dashboard_pagination_info": "{start}–{end} of {total}", - "dashboard_pause": "Pause auto-refresh", - "dashboard_prev": "Prev", - "dashboard_recent_activity": "Recent activity", - "dashboard_refresh": "Refresh", - "dashboard_resume": "Resume auto-refresh", - "dashboard_search_placeholder": "Search", - "dashboard_seconds_abbreviation": "s", - "dashboard_since": "since {bootTime}", - "dashboard_status_down": "down", - "dashboard_status_up": "up", - "dashboard_storage": "Storage", - "dashboard_swap": "Swap {swapPct}%", - "dashboard_synchronized": "Synchronized", - "dashboard_syncing": "Syncing…", - "dashboard_system": "System", - "dashboard_temperatures": "Temperatures", - "dashboard_time": "Time", - "dashboard_timezone": "Timezone", - "dashboard_up_to_date": "up to date", - "dashboard_updates": "{count} updates", - "dashboard_uptime": "Uptime", - "dashboard_used_percent": "{used}% used", - "dashboard_utc": "UTC", - "delete": "Delete", - "download": "Download", - "edit": "Edit", - "email": "Email", - "email_complete_body": "Your account has been created. Choose a password to finish setting it up.", - "email_complete_button": "Set password", - "email_complete_heading": "Complete your registration", - "email_complete_subject": "Complete your registration", - "email_greeting": "Hi {name},", - "email_link_fallback": "If the button doesn't work, copy and paste this link into your browser:", - "email_otp_body": "Use this code to finish signing in:", - "email_otp_heading": "Your verification code", - "email_otp_ignore": "If you didn't try to sign in, you can safely ignore this email.", - "email_otp_subject": "Your verification code", - "email_placeholder": "admin@example.com", - "email_reset_body": "We received a request to reset the password for your account. Click the button below to choose a new password.", - "email_reset_button": "Reset password", - "email_reset_heading": "Reset your password", - "email_reset_ignore": "If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.", - "email_reset_subject": "Reset your password", - "email_verify_body": "Click the button below to verify your email address and activate your account.", - "email_verify_button": "Verify email", - "email_verify_heading": "Verify your email", - "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.", - "errors_invalid_code": "Invalid or expired code, try again.", - "errors_non_empty": "This field is required", - "errors_not_found": "This item has not been found", - "errors_password_too_short": "Password must be at least {min} characters", - "errors_password_weak": "Use upper- and lower-case letters and at least one number.", - "errors_passwords_no_match": "Passwords do not match", - "errors_unauthenticated": "Unauthenticated", - "errors_username_too_short": "Username must be at least {min} characters", - "errors_wrong_credentials": "Wrong credentials, try again.", - "finish": "Finish", - "forbidden": "You are not allowed to this operation.", - "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", - "groups_add_member_description": "Adds this group to the user's supplementary set.", - "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", - "groups_created": "Group created", - "groups_delete_description": "Runs groupdel. Fails if it is the primary group of any existing user.", - "groups_delete_title": "Delete {name}?", - "groups_deleted": "Group deleted", - "groups_filter_show_system": "Show system groups", - "groups_filter_system_gid_hint": "(gid < 1000)", - "groups_member_added": "Added {username}", - "groups_member_removed": "Removed {username}", - "groups_members_description": "Supplementary members are managed via usermod -G on each user. Primary-group members (users whose primary gid is {gid}) appear below but cannot be removed from here.", - "groups_members_title": "Members", - "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.", - "groups_primary_label": "Primary ({count})", - "groups_search_placeholder": "Search name or gid…", - "groups_supplementary_label": "Supplementary ({count})", - "home": "Home", - "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", - "machine_actions": "Server actions", - "machine_add": "Add server", - "machine_add_description": "Connect a new server to manage from this dashboard.", - "machine_address": "Address", - "machine_address_placeholder": "http://127.0.0.1:9999", - "machine_delete_confirm": "This permanently removes \"{name}\" from the database. The connected server is not touched.", - "machine_delete_title": "Delete server?", - "machine_edit": "Edit server", - "machine_edit_description": "Update the connection details for this server.", - "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", - "machine_offline_node_dest": "Agent", - "machine_offline_node_proxy": "Web UI", - "machine_offline_node_you": "You", - "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…", - "machine_token": "Token", - "machine_token_keep": "Leave blank to keep the current token.", - "machine_token_placeholder": "Agent bearer token", - "manual_entry_key": "Can't scan? Enter this key in your authenticator app manually:", - "name": "Name", - "name_placeholder": "Jane Doe", - "nav_admin": "Admin", - "nav_admin_config": "Config", - "nav_admin_config_desc": "Application-wide configuration.", - "nav_admin_users": "Users", - "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.", - "nav_system_hostname": "Hostname", - "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.", - "reset_password_action": "Update password", - "reset_password_description": "Choose a strong password you don't use anywhere else.", - "reset_password_title": "Set a new password", - "save": "Save", - "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", - "system_locale_generate_button": "Generate", - "system_locale_generate_desc": "Install a new locale on the host. On Debian/Ubuntu/Arch this uncomments the entry in /etc/locale.gen and runs locale-gen; on RHEL/Fedora it uses localedef.", - "system_locale_generate_invalid": "Use the form xx_XX.UTF-8 (e.g. fr_FR.UTF-8)", - "system_locale_generate_placeholder": "e.g. ja_JP.UTF-8", - "system_locale_keymap": "Console keymap", - "system_locale_lang": "System locale (LANG)", - "system_locale_language": "Fallback language (LANGUAGE)", - "system_locale_language_add": "Add language", - "system_locale_language_button": "Save", - "system_locale_language_desc": "Set the fallback language priority list for system messages and translations (optional).", - "system_locale_language_empty": "No fallback language set.", - "system_locale_language_placeholder": "e.g. en_US:en", - "system_locale_no_keymap_found": "No keymap found.", - "system_locale_no_locale_found": "No locale found.", - "system_locale_search_keymap_placeholder": "Search keymap…", - "system_locale_search_locale_placeholder": "Search locale…", - "system_locale_x11": "X11 layout", - "system_power_confirm_description": "The machine will be unreachable while it shuts down. This cannot be undone from here.", - "system_power_confirm_poweroff_title": "Power off this machine?", - "system_power_confirm_reboot_title": "Reboot this machine?", - "system_power_poweroff": "Power off", - "system_power_reboot": "Reboot", - "system_time_current": "Current time", - "system_time_manual": "Manual time", - "system_time_manual_hint": "Set the system clock to a specific RFC3339 time. Available only when NTP is off.", - "system_time_no_timezone_found": "No timezone found.", - "system_time_ntp": "Network time (NTP)", - "system_time_ntp_hint": "Automatically synchronize the clock with NTP servers.", - "system_time_ntp_not_synced": "Not synchronized", - "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", - "theme_light": "Light", - "theme_system": "System", - "trust_device": "Trust this device for 30 days", - "two_factor_description": "Enter the 6-digit code from your authenticator app.", - "two_factor_title": "Two-factor authentication", - "use_authenticator": "Use authenticator app", - "use_backup_code": "Use a backup code", - "username": "Username", - "username_placeholder": "admin", - "users_action_set_password": "Set password", - "users_actions": "Actions", - "users_active": "Active", - "users_add": "Add User", - "users_ban": "Ban", - "users_ban_action_title": "Ban user?", - "users_ban_reason": "Reason (optional)", - "users_banned": "Banned", - "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)", - "users_create_field_create_home": "Create home directory", - "users_create_field_shell": "Shell", - "users_create_field_system": "System account", - "users_create_title": "Create user", - "users_created": "User created", - "users_created_at": "Joined", - "users_delete": "Delete", - "users_delete_ban_email": "Also ban this email", - "users_delete_confirm_description": "This permanently removes the user. Optionally ban the email to prevent re-registration.", - "users_delete_confirm_title": "Delete user?", - "users_delete_description": "Runs userdel on the host. This cannot be undone.", - "users_delete_field_remove_home": "Also remove home directory and mail spool", - "users_delete_title": "Delete {username}?", - "users_deleted": "User deleted", - "users_description": "Manage application users.", - "users_details": "Details", - "users_edit": "Edit", - "users_edit_description": "Update user details.", - "users_edit_title": "Edit user", - "users_filter": "Filter", - "users_filter_24h": "Last 24h", - "users_filter_30d": "Last 30 days", - "users_filter_7d": "Last 7 days", - "users_filter_active": "Active", - "users_filter_active_hint": "Users with a recent session.", - "users_filter_any_time": "Any Time", - "users_filter_count": "{n} filters active", - "users_filter_date_from": "From", - "users_filter_date_range": "Date Range", - "users_filter_date_to": "To", - "users_filter_display": "Display", - "users_filter_email_verified": "Email verified only", - "users_filter_joined": "Joined", - "users_filter_online_hint": "Users with an active session.", - "users_filter_online_only": "Online users only", - "users_filter_reset": "Reset All", - "users_filter_shell_only": "Login-capable shell only", - "users_filter_show_banned": "Show banned users", - "users_filter_show_system": "Show system users", - "users_filter_system_uid_hint": "(uid < 1000)", - "users_filter_title": "Filter Users", - "users_group_primary_badge": "(primary)", - "users_group_sys_badge": "sys", - "users_groups_title": "Groups", - "users_groups_updated": "Groups updated", - "users_invite": "Invite", - "users_invite_description": "Send an email invitation. The user sets their own password.", - "users_invite_title": "Invite user", - "users_invited": "Invitation sent", - "users_nav_description": "PAM/Unix accounts from /etc/passwd on this machine.", - "users_nav_title": "System users", - "users_next": "Next", - "users_no_gecos": "No GECOS comment", - "users_no_groups": "No groups.", - "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", - "users_role_user": "User", - "users_rows_per_page": "Rows per page", - "users_saved": "User saved", - "users_search_placeholder": "Search by email…", - "users_set_password_description": "Piped to chpasswd over stdin; never appears in the process list.", - "users_set_password_title": "Set password — {username}", - "users_status": "Status", - "users_title": "Users", - "users_type_system": "system", - "users_type_user": "user", - "users_unban": "Unban", - "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", - "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" + "$schema": "https://inlang.com/schema/inlang-message-format", + "account": "Account", + "agent_update_failed": "Agent update did not complete - check `nadir logs` on the host", + "agent_update_started": "Updating agent... ({from} → {to})", + "agent_update_success": "Agent updated to {version}", + "agent_updates": "Agent outdated", + "already_have_account": "Already have an account?", + "appname": "NadiЯ", + "back_to_login": "Back to login", + "backup_code": "Backup code", + "backup_codes_notice": "Store these somewhere safe. Each code works once if you lose your device.", + "backup_codes_title": "Save your backup codes", + "cancel": "Cancel", + "check_your_email": "Check your email", + "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", + "dashboard_ascending": "Ascending", + "dashboard_clock": "Clock", + "dashboard_col_free": "Free", + "dashboard_col_ip": "IP", + "dashboard_col_method": "Method", + "dashboard_col_mount": "Mount", + "dashboard_col_name": "Name", + "dashboard_col_path": "Path", + "dashboard_col_sensor": "Sensor", + "dashboard_col_size": "Size", + "dashboard_col_status": "Status", + "dashboard_col_temp": "Temp", + "dashboard_col_time": "Time", + "dashboard_col_usage": "Usage", + "dashboard_col_user": "User", + "dashboard_cpu": "CPU", + "dashboard_cpu_detail": "{cores} cores · {current} ({min}–{max})", + "dashboard_descending": "Descending", + "dashboard_dns": "DNS", + "dashboard_free_of": "{free} free of {total}", + "dashboard_hardware_clock": "Hardware clock", + "dashboard_interval_10s": "Every 10s", + "dashboard_interval_30s": "Every 30s", + "dashboard_interval_5s": "Every 5s", + "dashboard_interval_custom": "Custom", + "dashboard_interval_second": "Every second", + "dashboard_kernel": "Kernel", + "dashboard_keymap": "Keymap", + "dashboard_language": "Language", + "dashboard_load_average": "Load Average", + "dashboard_load_detail": "{loadPct}% of {cores}c · 5m {load5} · 15m {load15}", + "dashboard_local_time": "Local time", + "dashboard_locale": "Locale", + "dashboard_logical_cores": "{cores} logical cores", + "dashboard_memory": "Memory", + "dashboard_nameserver": "Nameserver", + "dashboard_network": "Network", + "dashboard_next": "Next", + "dashboard_none": "none", + "dashboard_not_synced": "Not synced", + "dashboard_nothing_to_show": "Nothing to show.", + "dashboard_os": "OS", + "dashboard_packages": "Packages", + "dashboard_pagination_info": "{start}–{end} of {total}", + "dashboard_pause": "Pause auto-refresh", + "dashboard_prev": "Prev", + "dashboard_recent_activity": "Recent activity", + "dashboard_refresh": "Refresh", + "dashboard_resume": "Resume auto-refresh", + "dashboard_search_placeholder": "Search", + "dashboard_since": "since {bootTime}", + "dashboard_status_down": "down", + "dashboard_status_up": "up", + "dashboard_storage": "Storage", + "dashboard_swap": "Swap {swapPct}%", + "dashboard_synchronized": "Synchronized", + "dashboard_syncing": "Syncing…", + "dashboard_system": "System", + "dashboard_temperatures": "Temperatures", + "dashboard_time": "Time", + "dashboard_timezone": "Timezone", + "dashboard_up_to_date": "up to date", + "dashboard_updates": "{count} updates", + "dashboard_uptime": "Uptime", + "dashboard_used_percent": "{used}% used", + "dashboard_utc": "UTC", + "delete": "Delete", + "download": "Download", + "edit": "Edit", + "email": "Email", + "email_complete_body": "Your account has been created. Choose a password to finish setting it up.", + "email_complete_button": "Set password", + "email_complete_heading": "Complete your registration", + "email_complete_subject": "Complete your registration", + "email_greeting": "Hi {name},", + "email_link_fallback": "If the button doesn't work, copy and paste this link into your browser:", + "email_otp_body": "Use this code to finish signing in:", + "email_otp_heading": "Your verification code", + "email_otp_ignore": "If you didn't try to sign in, you can safely ignore this email.", + "email_otp_subject": "Your verification code", + "email_placeholder": "admin@example.com", + "email_reset_body": "We received a request to reset the password for your account. Click the button below to choose a new password.", + "email_reset_button": "Reset password", + "email_reset_heading": "Reset your password", + "email_reset_ignore": "If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.", + "email_reset_subject": "Reset your password", + "email_verify_body": "Click the button below to verify your email address and activate your account.", + "email_verify_button": "Verify email", + "email_verify_heading": "Verify your email", + "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.", + "errors_invalid_code": "Invalid or expired code, try again.", + "errors_non_empty": "This field is required", + "errors_not_found": "This item has not been found", + "errors_password_too_short": "Password must be at least {min} characters", + "errors_password_weak": "Use upper- and lower-case letters and at least one number.", + "errors_passwords_no_match": "Passwords do not match", + "errors_unauthenticated": "Unauthenticated", + "errors_username_too_short": "Username must be at least {min} characters", + "errors_wrong_credentials": "Wrong credentials, try again.", + "finish": "Finish", + "forbidden": "You are not allowed to this operation.", + "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", + "groups_add_member_description": "Adds this group to the user's supplementary set.", + "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", + "groups_created": "Group created", + "groups_delete_description": "Runs groupdel. Fails if it is the primary group of any existing user.", + "groups_delete_title": "Delete {name}?", + "groups_deleted": "Group deleted", + "groups_filter_show_system": "Show system groups", + "groups_filter_system_gid_hint": "(gid < 1000)", + "groups_member_added": "Added {username}", + "groups_member_removed": "Removed {username}", + "groups_members_description": "Supplementary members are managed via usermod -G on each user. Primary-group members (users whose primary gid is {gid}) appear below but cannot be removed from here.", + "groups_members_title": "Members", + "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.", + "groups_primary_label": "Primary ({count})", + "groups_search_placeholder": "Search name or gid…", + "groups_supplementary_label": "Supplementary ({count})", + "home": "Home", + "invalid_reset_link": "This link is invalid or has expired.", + "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", + "machine_actions": "Server actions", + "machine_add": "Add server", + "machine_add_description": "Connect a new server to manage from this dashboard.", + "machine_address": "Address", + "machine_address_placeholder": "http://127.0.0.1:9999", + "machine_delete_confirm": "This permanently removes \"{name}\" from the database. The connected server is not touched.", + "machine_delete_title": "Delete server?", + "machine_edit": "Edit server", + "machine_edit_description": "Update the connection details for this server.", + "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", + "machine_offline_node_dest": "Agent", + "machine_offline_node_proxy": "Web UI", + "machine_offline_node_you": "You", + "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_reorder": "Reorder servers", + "machine_reorder_done": "Done reordering", + "machine_save": "Add server", + "machine_save_edit": "Save changes", + "machine_search_placeholder": "Search servers…", + "machine_token": "Token", + "machine_token_keep": "Leave blank to keep the current token.", + "machine_token_placeholder": "Agent bearer token", + "manual_entry_key": "Can't scan? Enter this key in your authenticator app manually:", + "name": "Name", + "nav_admin": "Admin", + "nav_admin_config": "Config", + "nav_admin_config_desc": "Application-wide configuration.", + "nav_admin_users": "Users", + "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_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_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.", + "nav_system_hostname": "Hostname", + "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_started": "Installing package {name}...", + "packages_install_success": "Package {name} installed successfully.", + "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_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_update_available": "Update available {from} > {to}", + "packages_update_single": "Update", + "packages_update_started": "Upgrading package {name}...", + "packages_update_success": "Package {name} upgraded successfully.", + "packages_upgrade_all": "Upgrade All", + "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", + "remember_me": "Remember me", + "reset_link_sent": "If an account exists for that email, a reset link is on its way.", + "reset_password_action": "Update password", + "reset_password_description": "Choose a strong password you don't use anywhere else.", + "reset_password_title": "Set a new password", + "save": "Save", + "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_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_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_groups": "Groups", + "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_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", + "system_locale_generate_button": "Generate", + "system_locale_generate_desc": "Install a new locale on the host. On Debian/Ubuntu/Arch this uncomments the entry in /etc/locale.gen and runs locale-gen; on RHEL/Fedora it uses localedef.", + "system_locale_generate_invalid": "Use the form xx_XX.UTF-8 (e.g. fr_FR.UTF-8)", + "system_locale_generate_placeholder": "e.g. ja_JP.UTF-8", + "system_locale_keymap": "Console keymap", + "system_locale_lang": "System locale (LANG)", + "system_locale_language": "Fallback language (LANGUAGE)", + "system_locale_language_add": "Add language", + "system_locale_language_button": "Save", + "system_locale_language_desc": "Set the fallback language priority list for system messages and translations (optional).", + "system_locale_language_empty": "No fallback language set.", + "system_locale_no_keymap_found": "No keymap found.", + "system_locale_no_locale_found": "No locale found.", + "system_locale_search_keymap_placeholder": "Search keymap…", + "system_locale_search_locale_placeholder": "Search locale…", + "system_locale_x11": "X11 layout", + "system_power_confirm_description": "The machine will be unreachable while it shuts down. This cannot be undone from here.", + "system_power_confirm_poweroff_title": "Power off this machine?", + "system_power_confirm_reboot_title": "Reboot this machine?", + "system_power_poweroff": "Power off", + "system_power_reboot": "Reboot", + "system_time_current": "Current time", + "system_time_manual": "Manual time", + "system_time_manual_hint": "Set the system clock to a specific RFC3339 time. Available only when NTP is off.", + "system_time_no_timezone_found": "No timezone found.", + "system_time_ntp": "Network time (NTP)", + "system_time_ntp_hint": "Automatically synchronize the clock with NTP servers.", + "system_time_ntp_not_synced": "Not synchronized", + "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.", + "terminal_auth_private_key": "Private Key", + "terminal_credential_cleared": "Saved credentials cleared", + "terminal_credential_forgotten": "Forgot saved credentials for {username}", + "terminal_credential_load_failed": "Could not load saved credentials", + "terminal_credential_saved_toast": "Credentials saved for {username}", + "terminal_connect": "Connect", + "terminal_connect_desc": "Connect to this host via SSH.", + "terminal_discard": "Discard", + "terminal_download_history": "Download history", + "terminal_forget": "Forget", + "terminal_history_truncated": "Output truncated, last 2 MB shown", + "terminal_connecting": "Connecting…", + "terminal_connecting_desc": "Opening SSH session to {credential} on port {port}", + "terminal_loaded_from_saved": "Loaded saved credentials", + "terminal_password_placeholder": "Enter password", + "terminal_port": "Port", + "terminal_private_key_placeholder": "Paste your SSH private key (including headers)", + "terminal_remember_credential": "Remember credentials", + "terminal_saved_credential_for": "Saved credentials for {username}", + "terminal_section_auth": "Authentication", + "terminal_section_connection": "Connection", + "terminal_session_ended": "Session ended", + "terminal_ssh_failed": "SSH connection failed", + "terminal_use_saved": "Use saved", + "theme": "Theme", + "theme_dark": "Dark", + "theme_light": "Light", + "theme_system": "System", + "trust_device": "Trust this device for 30 days", + "two_factor_description": "Enter the 6-digit code from your authenticator app.", + "two_factor_title": "Two-factor authentication", + "use_authenticator": "Use authenticator app", + "use_backup_code": "Use a backup code", + "username": "Username", + "username_placeholder": "admin", + "users_action_set_password": "Set password", + "users_actions": "Actions", + "users_active": "Active", + "users_add": "Add User", + "users_ban": "Ban", + "users_ban_reason": "Reason (optional)", + "users_banned": "Banned", + "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)", + "users_create_field_create_home": "Create home directory", + "users_create_field_shell": "Shell", + "users_create_field_system": "System account", + "users_create_title": "Create user", + "users_created": "User created", + "users_created_at": "Joined", + "users_delete": "Delete", + "users_delete_ban_email": "Also ban this email", + "users_delete_confirm_description": "This permanently removes the user. Optionally ban the email to prevent re-registration.", + "users_delete_confirm_title": "Delete user?", + "users_delete_description": "Runs userdel on the host. This cannot be undone.", + "users_delete_field_remove_home": "Also remove home directory and mail spool", + "users_delete_title": "Delete {username}?", + "users_deleted": "User deleted", + "users_description": "Manage application users.", + "users_details": "Details", + "users_edit": "Edit", + "users_edit_description": "Update user details.", + "users_edit_title": "Edit user", + "users_filter": "Filter", + "users_filter_24h": "Last 24h", + "users_filter_30d": "Last 30 days", + "users_filter_7d": "Last 7 days", + "users_filter_active": "Active", + "users_filter_any_time": "Any Time", + "users_filter_count": "{n} filters active", + "users_filter_date_from": "From", + "users_filter_date_range": "Date Range", + "users_filter_date_to": "To", + "users_filter_display": "Display", + "users_filter_email_verified": "Email verified only", + "users_filter_joined": "Joined", + "users_filter_online_only": "Online users only", + "users_filter_reset": "Reset All", + "users_filter_shell_only": "Login-capable shell only", + "users_filter_show_banned": "Show banned users", + "users_filter_show_system": "Show system users", + "users_filter_system_uid_hint": "(uid < 1000)", + "users_filter_title": "Filter Users", + "users_group_primary_badge": "(primary)", + "users_group_sys_badge": "sys", + "users_groups_title": "Groups", + "users_groups_updated": "Groups updated", + "users_invite": "Invite", + "users_invite_description": "Send an email invitation. The user sets their own password.", + "users_invite_title": "Invite user", + "users_invited": "Invitation sent", + "users_nav_description": "PAM/Unix accounts from /etc/passwd on this machine.", + "users_nav_title": "System users", + "users_next": "Next", + "users_no_gecos": "No GECOS comment", + "users_no_groups": "No groups.", + "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", + "users_role_user": "User", + "users_rows_per_page": "Rows per page", + "users_saved": "User saved", + "users_search_placeholder": "Search by email…", + "users_set_password_description": "Piped to chpasswd over stdin; never appears in the process list.", + "users_set_password_title": "Set password — {username}", + "users_status": "Status", + "users_title": "Users", + "users_type_system": "system", + "users_type_user": "user", + "users_unban": "Unban", + "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", + "system_nadir_username": "Authenticated User", + "system_nadir_permissions": "Resolved Permissions", + "system_nadir_modules": "Registered Modules", + "system_nadir_no_permissions": "No permissions defined" } diff --git a/package.json b/package.json index 560f6d4..2c8d225 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/bun": "^1.3.14", "@types/node": "^24", "@types/nodemailer": "^8.0.1", + "@types/ssh2": "^1.15.5", "@types/ws": "^8.18.1", "bits-ui": "^2.16.3", "clsx": "^2.1.1", @@ -82,6 +83,7 @@ "ogl": "^1.0.11", "openapi-fetch": "^0.17.0", "runed": "^0.37.1", + "ssh2": "^1.17.0", "swapy": "^1.0.5", "undici": "^8.5.0", "uqr": "^0.1.3", diff --git a/src/hooks.server.ts b/src/hooks.server.ts index cf2f2b1..880c295 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,7 +1,6 @@ import { type Handle, redirect } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; -import { dev } from '$app/env'; -import { building } from '$app/environment'; +import { building, dev } from '$app/environment'; import { auth } from '$lib/auth/server'; import { env } from '$lib/const/schema'; import { getTextDirection } from '$lib/paraglide/runtime'; diff --git a/src/lib/components/blocks/sidebar/app-sidebar.svelte b/src/lib/components/blocks/sidebar/app-sidebar.svelte index cd85db4..ea79d56 100644 --- a/src/lib/components/blocks/sidebar/app-sidebar.svelte +++ b/src/lib/components/blocks/sidebar/app-sidebar.svelte @@ -253,7 +253,7 @@ const terminalState = getContext<{ open: boolean }>('terminalState'); const canShowTerminal = $derived( - user.role === 'admin' && whoamiResource?.current?.permissions?.['terminal']?.includes('root') + user.role === 'admin' && whoamiResource?.current?.permissions?.['system']?.includes('root') ); const activeItem = $derived( diff --git a/src/lib/components/blocks/sidebar/machines-nav.svelte b/src/lib/components/blocks/sidebar/machines-nav.svelte index ea5b5c7..7329527 100644 --- a/src/lib/components/blocks/sidebar/machines-nav.svelte +++ b/src/lib/components/blocks/sidebar/machines-nav.svelte @@ -22,9 +22,10 @@ reorderMachines } from '$lib/remotes/machines.remote'; import { auditLog, serverInfo, systemDetails } from '$lib/remotes/server.remote'; - import { untrack } from 'svelte'; + import { extractErrorMessage } from '$lib/utils'; + import { onDestroy, tick, untrack } from 'svelte'; import { toast } from 'svelte-sonner'; - import { createSwapy } from 'swapy'; + import { createSwapy, type Swapy } from 'swapy'; const id = $props.id(); const isMobile = new IsMobile(); @@ -38,33 +39,39 @@ let listEl: HTMLDivElement | undefined = $state(); let items: { address: string; id: string; name: null | string }[] = $state([]); - const idSet = $derived([...items.map((i) => i.id)].sort().join('|')); + let reordering = $state(false); + let inst = $state(null); - $effect(() => { - if (!listEl || !idSet) return; - const inst = createSwapy(listEl, { - animation: 'dynamic', - autoScrollOnDrag: true, - dragAxis: 'y' - }); - inst.onSwapEnd(async ({ hasChanged }) => { - if (!hasChanged) return; - const map = inst.slotItemMap().asObject; - const ids = untrack(() => items) - .map((mc) => map[mc.id]) - .filter((x): x is string => !!x); - try { - inst.enable(false); - await reorderMachines({ ids, startIndex: (pageNum - 1) * PAGE_SIZE }); - } catch (error) { - console.error(error); - toast.error(m.errors_generic()); - } finally { - inst.enable(true); - } - }); - return () => inst.destroy(); - }); + onDestroy(() => inst?.destroy()); + + async function toggleReorder() { + reordering = !reordering; + if (reordering) { + await tick(); + inst = createSwapy(listEl!, { + animation: 'dynamic', + autoScrollOnDrag: true, + dragAxis: 'y' + }); + inst.onSwapEnd(async ({ hasChanged }) => { + if (!hasChanged) return; + const map = inst!.slotItemMap().asObject; + const ids = items.map((mc) => map[mc.id]).filter((x): x is string => !!x); + try { + inst!.enable(false); + await reorderMachines({ ids, startIndex: (pageNum - 1) * PAGE_SIZE }); + } catch (error) { + console.error(error); + toast.error(m.errors_generic()); + } finally { + inst?.enable(true); + } + }); + } else { + inst?.destroy(); + inst = null; + } + } const onSearch = () => { pageNum = 1; @@ -79,7 +86,12 @@ untrack(() => { const cur = [...items.map((i) => i.id)].sort().join('|'); const next = [...data.items.map((i) => i.id)].sort().join('|'); - if (cur !== next) items = data.items; + if (cur !== next) { + items = data.items; + reordering = false; + inst?.destroy(); + inst = null; + } }); }); }); @@ -87,9 +99,6 @@ {#snippet addForm()} - - {#snippet icon()} @@ -107,7 +116,7 @@ } catch (error) { console.error(error); toast.error( - (error as { body?: { message?: string } })?.body?.message || m.errors_generic() + extractErrorMessage(error) ?? m.errors_generic() ); } })} @@ -172,6 +181,14 @@ > + {#if isMobile.current} @@ -244,12 +261,14 @@ >{machine.address} -
- -
+ {#if reordering} +
+ +
+ {/if} {:else} diff --git a/src/lib/components/blocks/terminal-dialog.svelte b/src/lib/components/blocks/terminal-dialog.svelte index 530be00..5488429 100644 --- a/src/lib/components/blocks/terminal-dialog.svelte +++ b/src/lib/components/blocks/terminal-dialog.svelte @@ -2,13 +2,50 @@ import type { FitAddon as TFitAddon } from '@xterm/addon-fit'; import type { Terminal as TTerminal } from '@xterm/xterm'; + import KeyRoundIcon from '@lucide/svelte/icons/key-round'; + import LogInIcon from '@lucide/svelte/icons/log-in'; import TerminalIcon from '@lucide/svelte/icons/terminal'; + import UserIcon from '@lucide/svelte/icons/user'; import { page } from '$app/state'; + import { Button } from '$lib/components/ui/button'; + import { Checkbox } from '$lib/components/ui/checkbox'; import * as Dialog from '$lib/components/ui/dialog'; + import { Input } from '$lib/components/ui/input'; + import { Label } from '$lib/components/ui/label'; + import { Separator } from '$lib/components/ui/separator'; + import { Spinner } from '$lib/components/ui/spinner'; + import * as Tabs from '$lib/components/ui/tabs'; + import { Textarea } from '$lib/components/ui/textarea'; import '@xterm/xterm/css/xterm.css'; import { m } from '$lib/paraglide/messages'; - import { getMachine } from '$lib/remotes/machines.remote'; + import { + closeSshTerminal, + decryptCredential, + encryptCredential, + openSshTerminal, + resizeSshTerminal, + streamSshTerminal, + writeSshTerminal + } from '$lib/remotes/terminal.remote'; + import { extractErrorMessage } from '$lib/utils'; import { getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + type View = 'connecting' | 'ended' | 'form' | 'terminal'; + + function stripAnsi(text: string): string { + const E = String.fromCharCode(27); + const B = String.fromCharCode(7); + const control = new RegExp( + `[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]`, + 'g' + ); + return text + .replace(new RegExp(`${E}\\[[0-9;<=>?]*[a-zA-Z]`, 'g'), '') + .replace(new RegExp(`${E}\\][0-9;]*[^${E}]*(?:${E}\\\\|${B})`, 'g'), '') + .replace(new RegExp(`${E}[PX^_].*?${E}\\\\`, 'g'), '') + .replace(control, ''); + } interface TerminalState { open: boolean; @@ -17,59 +54,182 @@ const terminalState = getContext('terminalState'); let containerElement = $state(null); - let socket = $state(null); let term = $state(null); let fitAddon = $state(null); + let view = $state('form'); + let sessionId = $state(null); - const machineId = $derived(page.params.machineId); - const machineResource = $derived(machineId ? getMachine(machineId) : null); - const machineAddress = $derived(machineResource?.current?.address ?? null); + let username = $state('root'); + let port = $state('22'); + let authMethod = $state<'key' | 'password'>('password'); + let password = $state(''); + let privateKey = $state(''); - 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 ''; + let saveCredential = $state(false); + let hasSaved = $state(false); + let encryptedBlob = $state(null); + let loadingSaved = $state(false); + + let capturedHistory = $state(''); + let sessionEnded = $state(false); + + const machineId = $derived(page.params.machineId ?? ''); + const storageKey = $derived(`terminal:cred:${machineId}:${username}`); + const sessionActive = $derived(sessionId !== null && !sessionEnded); + + function resetForm() { + username = 'root'; + port = '22'; + authMethod = 'password'; + password = ''; + privateKey = ''; + saveCredential = false; + hasSaved = false; + encryptedBlob = null; + loadingSaved = false; + capturedHistory = ''; + sessionEnded = false; + } + $effect(() => { + if (!terminalState.open || view !== 'form') return; + const blob = localStorage.getItem(storageKey); + if (blob) { + hasSaved = true; + encryptedBlob = blob; + } else { + hasSaved = false; + encryptedBlob = null; } }); - function sendResize(cols: number, rows: number) { - if (socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ cols, rows })); + async function useSaved() { + if (!encryptedBlob) return; + loadingSaved = true; + try { + const { data } = await decryptCredential({ encrypted: encryptedBlob }); + const parsed = JSON.parse(data) as { authMethod: 'key' | 'password'; credential: string }; + hasSaved = false; + encryptedBlob = null; + view = 'connecting'; + const result = await openSshTerminal({ + machineId, + [parsed.authMethod === 'password' ? 'password' : 'privateKey']: parsed.credential, + port: parseInt(port) || 22, + username + }); + sessionId = result.sessionId; + view = 'terminal'; + } catch (err) { + view = 'form'; + toast.error( + extractErrorMessage(err) ?? m.terminal_ssh_failed() + ); + localStorage.removeItem(storageKey); + } finally { + loadingSaved = false; } } - function cleanup() { - if (socket) { - socket.close(); - socket = null; + function forgetSaved() { + localStorage.removeItem(storageKey); + hasSaved = false; + encryptedBlob = null; + saveCredential = false; + toast.success(m.terminal_credential_forgotten({ username })); + } + + async function connect() { + view = 'connecting'; + const auth: { + machineId: string; + password?: string; + port: number; + privateKey?: string; + username: string; + } = { + machineId, + port: parseInt(port) || 22, + username + }; + if (authMethod === 'password' && password) { + auth.password = password; + } else if (authMethod === 'key' && privateKey) { + auth.privateKey = privateKey; + } + + try { + const result = await openSshTerminal(auth); + sessionId = result.sessionId; + + if (saveCredential) { + const payload = JSON.stringify({ authMethod, credential: password || privateKey }); + const { encrypted } = await encryptCredential({ data: payload }); + localStorage.setItem(storageKey, encrypted); + toast.success(m.terminal_credential_saved_toast({ username })); + } + + view = 'terminal'; + } catch (err) { + view = 'form'; + toast.error( + extractErrorMessage(err) ?? m.terminal_ssh_failed() + ); + } + } + + function downloadHistory() { + const blob = new Blob([capturedHistory], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `terminal-${machineId}-${username}-${Date.now()}.txt`; + a.click(); + URL.revokeObjectURL(url); + handleClose(); + } + + function handleClose() { + if (sessionId) { + closeSshTerminal(sessionId); + sessionId = null; } if (term) { term.dispose(); term = null; } fitAddon = null; + view = 'form'; + resetForm(); + terminalState.open = false; } $effect(() => { - if (!terminalState.open || !socketUrl) { - cleanup(); + if (!terminalState.open) { + if (sessionId) { + closeSshTerminal(sessionId); + sessionId = null; + } + if (term) { + term.dispose(); + term = null; + } + fitAddon = null; + view = 'form'; + resetForm(); return; } - let active = true; + if (!sessionId || view !== 'terminal') return; - const initTerminal = async () => { + let active = true; + let localSid = sessionId; + let iter: AsyncIterator<{ data: unknown; event: string }> | null = null; + + const init = 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'); - + const { Terminal } = await import('@xterm/xterm'); if (!active) return; term = new Terminal({ @@ -77,40 +237,62 @@ fontFamily: 'Geist Mono, JetBrains Mono, Fira Code, monospace', fontSize: 14, theme: { - background: '#09090b', // zinc-950 + background: '#09090b', cursor: '#fafafa', - foreground: '#fafafa', // zinc-50 - selectionBackground: '#27272a' // zinc-800 + foreground: '#fafafa', + selectionBackground: '#27272a' } }); - socket = new WebSocket(socketUrl); - socket.binaryType = 'arraybuffer'; - - socket.onopen = () => { - if (!active) { - cleanup(); - return; + term.onData((data) => { + if (localSid) { + writeSshTerminal({ data, sessionId: localSid }); } + }); - const attachAddon = new AttachAddon(socket!); - term?.loadAddon(attachAddon); + fitAddon = new FitAddon(); + term.loadAddon(fitAddon); + term.open(containerElement); + fitAddon.fit(); - fitAddon = new FitAddon(); - term?.loadAddon(fitAddon); - term?.open(containerElement!); - fitAddon?.fit(); - if (term) sendResize(term.cols, term.rows); - else console.error('terminal not initialized'); - }; + iter = streamSshTerminal(localSid)[Symbol.asyncIterator](); + (async () => { + try { + while (active) { + const { done, value } = await iter!.next(); + if (done) { + sessionEnded = true; + break; + } + if (value.event === 'output') { + term?.write(value.data as string); + const plain = stripAnsi(value.data as string); + capturedHistory += plain; + if (capturedHistory.length > 2_000_000) { + capturedHistory = capturedHistory.slice(-1_000_000); + } + } else if (value.event === 'error') { + sessionEnded = true; + toast.error(value.data as string); + } + } + } catch (err) { + if (active) { + sessionEnded = true; + toast.error( + extractErrorMessage(err) ?? m.errors_generic() + ); + } + } + })(); }; - initTerminal(); + init(); const handleResize = () => { - if (fitAddon && term) { + if (fitAddon && term && localSid) { fitAddon.fit(); - sendResize(term.cols, term.rows); + resizeSshTerminal({ cols: term.cols, rows: term.rows, sessionId: localSid }); } }; @@ -119,32 +301,202 @@ return () => { active = false; window.removeEventListener('resize', handleResize); - cleanup(); + iter?.return?.(); + if (term) { + term.dispose(); + term = null; + } + fitAddon = null; }; }); - + { if (!o && sessionActive) return; if (!o) handleClose(); }}> - - - {m.nav_system_terminal()} + {#if view === 'form'} + +
+
+ +
+
+ {m.nav_system_terminal()} + + {m.terminal_connect_desc()} + +
+
+
+ +
+
+

{m.terminal_section_connection()}

+
+
+ +
+ + +
+
+
+ + +
+
+
+ +
+
+

{m.terminal_section_auth()}

+
+ + {#if hasSaved && !loadingSaved} +
+ + {m.terminal_saved_credential_for({ username })} + +
+ + +
+
+ {/if} + + {#if loadingSaved} +
+ +
+ {/if} + + { + if (v === 'password' || v === 'key') authMethod = v; + }} + > + + {m.password()} + {m.terminal_auth_private_key()} + +
+ + +
+ + +
+
+ + +