home-hosted 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -21,11 +21,15 @@ pnpm run quickcheck # eslint + tsc + vue-tsc for every UI under u
21
21
  pnpm exec vitest run # `pnpm test` is vitest in watch mode
22
22
  pnpm run check # quickcheck + vitest run --coverage
23
23
  pnpm run set-password # non-interactive through HHOSTED_PASSWORD
24
+ pnpm run media # regenerate docs/media (mockups, both served UIs, tour.gif)
24
25
  ```
25
26
 
26
- Releases are dispatched from `.github/workflows/release.yml` with a version: changelogen writes the
27
- changelog, bumps, commits and tags, the GitHub release carries the UI zips, and npm publishes through
28
- trusted publishing.
27
+ Releases are dispatched from `.github/workflows/release.yml` with a version (and a `dry-run` switch
28
+ that stops before pushing). It verifies the version against `package.json`, lints/types/tests,
29
+ builds the CLI plus the stock UI and every UI zip, lets changelogen write the changelog and tag
30
+ `v<version>`, creates the GitHub release with the UI bundles attached, and publishes to npm through
31
+ trusted publishing (OIDC, no token). npm only offers a trusted publisher for a package that already
32
+ exists, so the first release has to be published by hand.
29
33
 
30
34
  ## Architecture (and why)
31
35
 
@@ -53,6 +57,11 @@ trusted publishing.
53
57
  `src/shared`, `@server` → `src` (**types only** — never import runtime server code into a UI).
54
58
  - `bin/home-hosted.mjs` — the published bin: `dist/cli.js`, or `src/cli.ts` through tsx when the
55
59
  build is missing (a linked checkout).
60
+ - `scripts/` — `build-uis.mjs` (build one UI, optionally zip it), `typecheck-uis.mjs`,
61
+ `capture-media.mjs` (the README's media), `check-release-version.mjs` and `release-notes.mjs`
62
+ (used by the release workflow), `dev.mjs` (`pnpm dev`).
63
+ - `docs/` — `mockups/*.html`, hand-written UI examples in four directions, and `media/*`: their
64
+ screenshots plus the README's `tour.gif`, all regenerated by `pnpm run media`.
56
65
 
57
66
  ## Conventions
58
67
 
@@ -110,6 +119,10 @@ trusted publishing.
110
119
  encryption flags are cached and refreshed in the background.
111
120
  - `vite.server.config.ts` targets `node22` while `engines` requires >= 24 — deliberate margin, leave it.
112
121
  - `pnpm test` watches; CI runs `vitest run`.
122
+ - `pnpm run media` drives Chromium through Playwright, which needs fonts *and* the X/NSS/Mesa
123
+ libraries. In a bare container point `FONTCONFIG_PATH` at a `fonts.conf` covering any TTF and
124
+ `LD_LIBRARY_PATH` at a directory holding those libraries — without fonts, Skia panics instead of
125
+ rendering, and the failure looks like a broken page rather than a missing package.
113
126
 
114
127
  ## Where to extend
115
128
 
package/README.md CHANGED
@@ -6,17 +6,29 @@
6
6
 
7
7
  <sub>The harness for your servers.</sub>
8
8
 
9
- Point it at the things you run — a gateway, a media server, a bot, a database — and it
10
- starts them, watches them, restarts them when they die, and gives you one page to see
11
- what is actually going on. Or BYOU — [bring your own UI](#-bring-your-own-ui-byou), for a
12
- specialized panel that fits you exactly.
9
+ Point it at the things you run — a gateway, a media server, a bot, a database — and it starts
10
+ them, watches them, restarts what dies, and shows you one page of what is going on. Or
11
+ [BYOU](#-bring-your-own-ui-byou), for a specialized UI that fits you exactly.
13
12
 
14
13
  [![npm](https://img.shields.io/npm/v/home-hosted.svg)](https://www.npmjs.com/package/home-hosted)
15
14
  [![Downloads](https://img.shields.io/npm/dm/home-hosted.svg)](https://www.npmjs.com/package/home-hosted)
15
+ [![CI](https://github.com/NamesMT/home-hosted/actions/workflows/quickcheck.yml/badge.svg)](https://github.com/NamesMT/home-hosted/actions/workflows/quickcheck.yml)
16
16
  [![License](https://img.shields.io/npm/l/home-hosted.svg)](./LICENSE)
17
17
  [![Node](https://img.shields.io/node/v/home-hosted.svg)](https://nodejs.org)
18
18
 
19
- [🚀 Quick start](#-quick-start) · [✨ Features](#-features) · [🧩 Adding a server](#-adding-a-server) · [🛠 CLI](#-cli) · [🔐 Security](#-security) · [💾 Backups](#-backups)
19
+ [🚀 Quick start](#-quick-start) · [✨ Features](#-features) · [🧩 Adding a server](#-adding-a-server) · [🛠 CLI](#-cli) · [🔐 Security](#-security) · [💾 Backups](#-backups) · [🎨 BYOU](#-bring-your-own-ui-byou)
20
+
21
+ </div>
22
+
23
+ ---
24
+
25
+ <div align="center">
26
+
27
+ ![Six views of the panel: the stock UI, the NOC-console, and UI examples](docs/media/tour.gif)
28
+
29
+ <sub>The stock panel, the [NOC-console](./uis/noc-console) that ships alongside it (for TUI and
30
+ shortcuts wizards), and UI directions you could build yourself —
31
+ <a href="#-bring-your-own-ui-byou">BYOU</a>.</sub>
20
32
 
21
33
  </div>
22
34
 
@@ -38,7 +50,7 @@ forget about, or a hand-written systemd unit per service (times six).
38
50
  ```text
39
51
  ┌──────────────────────────────────────┐
40
52
  your browser ──▶│ home-hosted · 127.0.0.1:3999 │
41
- │ Vue panel + JSON API + SSE logs │
53
+ │ your UI + JSON API + SSE logs │
42
54
  └───────────────┬──────────────────────┘
43
55
  │ supervises
44
56
  ┌──────────────────────────┼──────────────────────────┐
@@ -62,8 +74,8 @@ npx home-hosted # start it — detached, it stays running
62
74
  npx home-hosted status # where is it, is it healthy
63
75
  ```
64
76
 
65
- That is it. The panel is on **<http://127.0.0.1:3999>**, and it keeps running after the
66
- terminal closes. Stop it whenever you like:
77
+ That is it. The panel is on **<http://127.0.0.1:3999>** and keeps running after the terminal
78
+ closes. <sub>Needs Node 24 or newer.</sub> Stop it whenever you like:
67
79
 
68
80
  ```bash
69
81
  npx home-hosted down # stops the panel *and* everything it started
@@ -71,7 +83,7 @@ npx home-hosted down # stops the panel *and* everything it started
71
83
 
72
84
  > [!NOTE]
73
85
  > The first boot writes a default password (`hh`) so the panel is never unprotected. Change
74
- > it under **Settings → Password** — binding beyond `127.0.0.1` stays refused until you do.
86
+ > it under **Settings → Authentication** — binding beyond `127.0.0.1` stays refused until you do.
75
87
 
76
88
  <details>
77
89
  <summary><b>📦 Install it instead of npx-ing it</b></summary>
@@ -79,30 +91,22 @@ npx home-hosted down # stops the panel *and* everything it started
79
91
  ```bash
80
92
  npm install -g home-hosted
81
93
  home-hosted up
82
-
83
- # or keep a project with its own servers *and* its own state directory
84
- cd ~/my-servers
85
- HHOSTED_HOME=./state npx home-hosted up
86
94
  ```
87
95
 
88
- Before the first npm release, the same commands work from a checkout: `pnpm install && pnpm run up`
89
- in this repo, or `pnpm link` into a project that should depend on it.
90
-
91
- Everything home-hosted owns — config, secrets, logs, TLS, backups — lives in one directory:
92
- `$HHOSTED_HOME`, defaulting to `~/.home-hosted`. Delete it and nothing of yours is left behind.
96
+ Everything it owns — config, secrets, logs, TLS, backups — lives in `$HHOSTED_HOME`, default
97
+ `~/.home-hosted`. Delete that and nothing of yours is left behind.
93
98
 
94
99
  </details>
95
100
 
96
101
  <details>
97
102
  <summary><b>📁 Keep it in a project you can take anywhere (and share)</b></summary>
98
103
 
99
- Pin the version, install the servers next to it, and commit the whole project — it *is* the
100
- setup:
104
+ Install the servers next to it and commit the whole project — it *is* the setup:
101
105
 
102
106
  ```bash
103
107
  mkdir my-servers && cd my-servers
104
108
  npm init -y
105
- npm install home-hosted@0.2.0 9router serve # pinned, so `git clone && npm ci` is enough
109
+ npm install home-hosted 9router serve # the lockfile pins them, so `npm ci` reproduces
106
110
 
107
111
  npx home-hosted up --home ./state # state lives inside the project
108
112
  ```
@@ -125,8 +129,8 @@ state/* # secrets, logs, TLS keys and archives stay local…
125
129
 
126
130
  <sub>Call them as `npm run up` — `npm up` is npm's own update, not your script.</sub>
127
131
 
128
- One clone, `npm ci`, `npm run up`, and the whole setup is up on any machine with Node.
129
- **Settings → Backups** exports the same thing as one archive for someone else to restore.
132
+ One clone, `npm ci`, `npm run up`, and the whole setup is up on any machine with Node — or export
133
+ it as one archive under **Settings → Backups**.
130
134
 
131
135
  </details>
132
136
 
@@ -165,7 +169,7 @@ WantedBy=multi-user.target
165
169
  | 🌡️ **Host vitals** | Load, memory, swap, disk and CPU temperature, with thresholds that notify once and again on recovery. |
166
170
  | 💾 **Backups** | One click for config, secrets, TLS and your declared data directories — plain `.zip`, or AES-256 with a password, restored per path. |
167
171
  | 🚚 **Portable setup** | Restore a shared backup onto a blank instance and the whole server setup is back: definitions, data, secrets and all. |
168
- | 🎨 **BYOU — Bring Your Own UI** | The panel is a static site you can replace: upload your own build (or ship a whole alternative), and `home-hosted ui-revert` puts the stock one back. See [UI_CREATION.md](./UI_CREATION.md). |
172
+ | 🎨 **BYOU — Bring Your Own UI** | Upload a static build, `home-hosted ui-revert` to go back. [UI_CREATION.md](./UI_CREATION.md) |
169
173
  | 🔔 **Notifications** | Telegram (grammY) on crash, unhealthy, forced restart, recovery and host thresholds. |
170
174
  | 🔐 **Security** | httpOnly cookie sessions, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
171
175
  | 🧩 **Server-agnostic** | `command` + `args` + `env` + `cwd`. Nothing in the code knows what you run. |
@@ -190,13 +194,8 @@ Use **➕ Add server** in the panel, or write it into `servers.config.json`:
190
194
  }
191
195
  ```
192
196
 
193
- Relative paths resolve against the directory you ran `home-hosted` from, so a project can keep
194
- its servers and its state together:
195
-
196
- ```bash
197
- cd ~/my-servers
198
- HHOSTED_HOME=./state npx home-hosted up
199
- ```
197
+ Relative paths resolve against the directory you ran `home-hosted` from, so a project can keep its
198
+ servers and its state together.
200
199
 
201
200
  <details>
202
201
  <summary><b>🧾 Field reference</b></summary>
@@ -214,9 +213,21 @@ HHOSTED_HOME=./state npx home-hosted up
214
213
  | `bootstrap` | one command to run once before the first start (migrations, warmups) |
215
214
  | `backupPaths` | extra paths this entry owns, included in backups |
216
215
 
217
- Placeholders: `{id}` `{label}` `{port}` `{host}` `{displayHost}` `{bind}` `{lanIp}` `{cwd}`
218
- `{projectDir}` `{dataRoot}` `{home}`, plus `${VAR}` from the environment. Unknown placeholders
219
- stay visible instead of silently becoming empty.
216
+ Placeholders, and what each one resolves to:
217
+
218
+ | placeholder | resolves to |
219
+ | --- | --- |
220
+ | `{id}` `{label}` | the entry's id, and its label (falling back to the id) |
221
+ | `{port}` | its configured port, empty when it has none |
222
+ | `{bind}` `{host}` `{displayHost}` | `local`/`lan`/an address; the address it binds (`127.0.0.1`, `0.0.0.0`); the address to *show* (`127.0.0.1`, the LAN IP) |
223
+ | `{lanIp}` | this machine's LAN address, or `127.0.0.1` |
224
+ | `{cwd}` | the entry's working directory (its `cwd`, else the project directory) |
225
+ | `{projectDir}` | where you ran `home-hosted` (or `--project`) — `$HHOSTED_PROJECT` |
226
+ | `{dataRoot}` | the state directory — `$HHOSTED_HOME`, default `~/.home-hosted` |
227
+ | `{home}` | the current user's home directory |
228
+
229
+ Unknown placeholders stay visible instead of silently becoming empty, and `${VAR}` reads from the
230
+ environment (including `envFile`).
220
231
 
221
232
  Every supervised process also gets `HHOSTED_SERVER_ID` and `HHOSTED_CONTROL_PORT`, so a
222
233
  service can tell which entry it is and how to reach the panel.
@@ -282,15 +293,13 @@ directories your entries declare — as an ordinary `.zip`:
282
293
 
283
294
  ### 🚚 One archive is a whole setup
284
295
 
285
- Start a **blank** home-hosted anywhere — another machine, another user, a fresh container —
286
- open **Settings → Backups**, upload the archive and restore: the server definitions come
287
- back, their data lands where *this* machine's config says it should, and the entries marked
288
- `autostart` are brought up right away. Nothing to re-declare by hand.
296
+ Start a **blank** home-hosted anywhere — another machine, another user, a fresh container — upload
297
+ the archive under **Settings → Backups** and restore. Definitions come back, data lands where *this*
298
+ machine's config says, and `autostart` entries come up immediately.
289
299
 
290
- That works because a backup carries its own `servers.config.json`, and paths are matched by
291
- the **declaration** (`9router:DATA_DIR`), not by the absolute path they had on the source
292
- machine — so `{home}` stays this user's home. A restore never writes anywhere a config does
293
- not declare: yours, or the one inside the archive you chose to restore.
300
+ It works because an archive carries its own `servers.config.json` and paths are matched by the
301
+ **declaration** (`9router:DATA_DIR`), not by an absolute path from the source machine. A restore
302
+ never writes where no config declares.
294
303
 
295
304
  <details>
296
305
  <summary><b>🗂 Declaring data directories</b></summary>
@@ -309,24 +318,32 @@ A path already covered by a declared parent is skipped.
309
318
 
310
319
  ---
311
320
 
312
- Two UIs live in this repo: **stock** (the panel you get by default) and **noc-console**, an
313
- operations console over the same API. `pnpm run build:uis` builds and zips every one of them, and
314
- each release attaches them as `home-hosted-ui-<name>.zip` — install one from Settings → Interface.
315
-
316
321
  ## 🎨 Bring your own UI (BYOU)
317
322
 
318
- The panel is a static site served from `$HHOSTED_HOME/.ui` when one is installed, and from
319
- the package otherwise. **Settings → Interface** takes a zip; that is the whole install step,
320
- no restart and no fork.
323
+ The panel is a static site: `$HHOSTED_HOME/.ui` overrides the packaged one, and **Settings →
324
+ Interface** takes a zip. No restart, no fork — and `home-hosted ui-revert` brings back the stock
325
+ panel if yours breaks.
321
326
 
322
- ```bash
323
- home-hosted ui-revert # if it breaks, the stock panel is one command away
324
- ```
327
+ Two ship in this repo: `uis/stock`, and `uis/noc-console` for TUI and shortcuts wizards; a release
328
+ attaches both as `home-hosted-ui-<name>.zip`. Yours can be anything that compiles to static files —
329
+ the server never cares what built it. `/openapi/ui` documents the API; [UI_CREATION.md](./UI_CREATION.md)
330
+ has the rules and a worked example.
331
+
332
+ <details>
333
+ <summary><b>🤖 Or have an agent build the UI you actually want</b></summary>
334
+
335
+ The whole contract fits in one file, so a coding agent can do this. Point it at this repo and be
336
+ specific about the result:
325
337
 
326
- The API is documented at `/openapi/ui` (and `/openapi/spec.json`), generated from the same
327
- schemas the server validates with, so a UI can be built in any framework — the stock one is
328
- just the first client. [UI_CREATION.md](./UI_CREATION.md) has the contract, the rules and a
329
- worked example.
338
+ > Help me build an UI for `home-hosted`: a nostalgic game theme — servers as a party menu, health as
339
+ > HP bars, logs in a text-box pane, keyboard navigation, and a save-state corner for backups.
340
+ > Follow `UI_CREATION.md`.
341
+
342
+ Zip the build, install it under **Settings → Interface**, and `home-hosted ui-revert` undoes it if
343
+ you change your mind. [`docs/mockups/`](./docs/mockups) holds example directions to borrow from, or
344
+ ignore all of them.
345
+
346
+ </details>
330
347
 
331
348
  ---
332
349
 
@@ -337,7 +354,8 @@ The panel is a client of its own API, so everything is scriptable:
337
354
  | | |
338
355
  | --- | --- |
339
356
  | `GET /api/state` | the full snapshot: config, live status, host vitals |
340
- | `GET /api/servers/:id/stream` | SSE stream of state and log lines |
357
+ | `GET /api/events` | SSE: the live state, plus logs (`?logs=0`, `?serverId=…`) |
358
+ | `GET /api/servers/:id/stream` | SSE: one server's state and logs |
341
359
  | `POST /api/servers/:id/{start,stop,restart}` | lifecycle |
342
360
  | `PATCH /api/servers/:id`, `PATCH /api/settings` | edit configuration |
343
361
  | `GET /api/logs`, `/api/backups`, `/api/notifications` | logs, archives, Telegram |
@@ -409,20 +427,25 @@ the holder. The control port itself is checked before the listener is opened.
409
427
 
410
428
  ---
411
429
 
412
- ## 🗂 Layout
430
+ ## 🗂 Working on it
431
+
432
+ <details>
433
+ <summary><b>Layout, scripts and conventions</b></summary>
413
434
 
414
435
  ```text
415
436
  src/ control plane: config, supervisor, API, providers, services
416
437
  src/cli.ts the command line (up/down/status/restart/set-password)
417
438
  src/index.ts the control plane itself, used by `up --foreground`
418
- uis/ UIs: `stock` (shipped) and alternatives, each a Vite app
439
+ uis/ UIs: `stock` (shipped) and alternatives — any framework, static output
419
440
  bin/ the published entry point
420
- vite.server.config.ts bundles src/cli.ts into dist/cli.js
421
441
  ```
422
442
 
423
- Development: `pnpm dev` runs the panel with `tsx watch` plus the Vite dev server (state goes to
424
- `.dev-state/`), `pnpm build` produces `dist/` and `uis/stock/dist/`, `pnpm quickcheck` is lint plus
425
- types, and `pnpm test` is vitest.
443
+ `pnpm dev` runs the panel with `tsx watch` plus the stock UI's dev server (state goes to
444
+ `.dev-state/`); `pnpm build` produces `dist/` and `uis/stock/dist/`; `pnpm quickcheck` is lint plus
445
+ types; `pnpm test` is vitest; `pnpm run media` regenerates the GIF above. [AGENTS.md](./AGENTS.md)
446
+ has the architecture and the rules worth knowing before changing anything.
447
+
448
+ </details>
426
449
 
427
450
  ---
428
451
 
package/UI_CREATION.md CHANGED
@@ -1,77 +1,100 @@
1
1
  # Building a UI for home-hosted
2
2
 
3
- The panel's UI is **a static site, and it is replaceable**. The stock one ships in the
4
- package; yours is a folder of files that the control plane serves instead — same API, same
5
- authentication, no server changes and no fork.
3
+ The panel's UI is **a static site, and it is replaceable**. The stock one ships in the package;
4
+ yours is a folder of files the control plane serves instead — same API, same authentication, no
5
+ server changes and no fork.
6
+
7
+ ## Ask an AI to build it
8
+
9
+ The whole contract is this file, so a coding agent can do the work. Point it at this repo and be
10
+ specific about what you want:
11
+
12
+ > Help me build a UI for `home-hosted`: a nostalgic game theme. Servers as a party menu, health as
13
+ > HP bars, logs in a text-box pane, keyboard navigation, and a save-state corner for backups.
14
+ > Follow `UI_CREATION.md`.
15
+
16
+ Then zip the build and install it (below). Useful constraints to include in the prompt: the API is
17
+ same-origin (relative `/api/...`), `401 { code: 'AUTH_REQUIRED' }` means "show the login screen",
18
+ live data should come from SSE, and assets must be self-hosted. Existing directions to borrow from
19
+ live in [`docs/mockups/`](./docs/mockups) — or ask for something else entirely; the server does not
20
+ care what your UI looks like.
21
+
22
+ ## Install it
6
23
 
7
24
  ```text
8
- $HHOSTED_HOME/.ui/ ← your build lives here
25
+ $HHOSTED_HOME/.ui/ ← where your build lives
9
26
  index.html ← required, at the root
10
27
  assets/…
11
28
  ui.json ← optional: { "name": "my-panel", "version": "2.1.0" }
12
29
  ```
13
30
 
14
- ## Install it
15
-
16
- **Settings → Interface** → pick a `.zip` and *Install UI* (refresh to see it). Or drop the
17
- files in `$HHOSTED_HOME/.ui` yourself. The zip may contain the files at its root, or inside
18
- one wrapper directory (`zip -r ui.zip dist` also works).
31
+ **Settings → Interface** → pick a `.zip` → *Install UI* (refresh to see it). Or drop the files into
32
+ `$HHOSTED_HOME/.ui` yourself. The zip may hold the files at its root or inside one wrapper directory
33
+ (`zip -r ui.zip dist` works too). `ui.json` is optional; it is what the settings page shows as
34
+ installed.
19
35
 
20
- There is no build step on the server side: whatever you upload is served as-is. So ship
21
- plain HTML/JS/CSS, or the output of your own Vite/Next/Astro build with relative asset paths.
36
+ Nothing is built on the server side: whatever you upload is served as-is, so ship plain
37
+ HTML/JS/CSS or the output of your own Vite/Next/Astro build with relative asset paths.
22
38
 
23
39
  **If it breaks:** `home-hosted ui-revert` puts the stock panel back (or *Revert to stock* in
24
- the settings page). The CLI also prints a reminder on startup while a custom UI is active,
25
- because a broken UI must never lock you out of your own server.
40
+ Settings). The CLI also prints a reminder at startup while a custom UI is active — a broken UI must
41
+ never lock you out of your own machine.
26
42
 
27
43
  ## Rules of the road
28
44
 
29
- 1. **Static only.** No server code, no environment variables, no filesystem. Anything you
30
- need comes from the API.
31
- 2. **Same origin, relative paths.** Call `/api/...` (never an absolute host): the panel may
32
- be reached over loopback, a LAN address, TLS or a proxy.
33
- 3. **`index.html` at the root** (or inside a single wrapper directory). Deep links work:
34
- unknown paths fall back to `index.html`, so client-side routing is fine.
35
- 4. **Keep the login flow.** The API answers `401` with `{"code":"AUTH_REQUIRED"}` when a
36
- session is missing — route to your login screen and `POST /api/auth/login`.
37
- 5. **Self-host your assets.** An offline home server should not need a CDN.
38
- 6. **Limits:** a zip of at most 20 000 entries / 512 MB uncompressed, no absolute paths, no
39
- `..`, no symlinks, no drive letters.
45
+ 1. **Static only.** No server code, no filesystem, no environment variables. Everything comes from
46
+ the API.
47
+ 2. **Same origin, relative paths.** Call `/api/...`, never an absolute host: the panel may be
48
+ reached over loopback, a LAN address, TLS or a proxy.
49
+ 3. **`index.html` at the root** (or in a single wrapper directory). Unknown paths fall back to it,
50
+ so client-side routing and deep links just work.
51
+ 4. **Keep the login flow.** Without a session the API answers `401` with `{"code":"AUTH_REQUIRED"}` —
52
+ show your login screen and `POST /api/auth/login`.
53
+ 5. **Self-host your assets.** An offline home server should not need a CDN, and neither should its
54
+ panel.
55
+ 6. **Limits:** ≤ 20 000 entries, ≤ 512 MB uncompressed, no absolute paths, no `..`, no symlinks, no
56
+ drive letters.
57
+ 7. **Offline and plain-http friendly.** Assume a LAN over `http://`: no `Secure`-only cookies, no
58
+ hard-coded port, no https-only APIs.
40
59
 
41
60
  ## The API
42
61
 
43
- Two ways to consume it, both generated from the same ArkType schemas the server validates
44
- with:
62
+ Both routes below are generated from the same ArkType schemas the server validates with, so they
63
+ cannot drift:
45
64
 
46
- | | |
47
- | --- | --- |
48
65
  | approach | how |
49
66
  | --- | --- |
50
- | **OpenAPI** | `GET /openapi/spec.json` (no session needed); browse it at `GET /openapi/ui` |
51
- | **Typed RPC** | building inside this repo: `import type { AppType } from '@server/app'` + `hc<AppType>()`, as `uis/stock/src/lib/rpc.ts` does |
67
+ | **OpenAPI** | `GET /openapi/spec.json` (no session needed); browse it at `/openapi/ui` |
68
+ | **Typed RPC** | inside this repo: `import type { AppType } from '@server/app'` + `hc<AppType>()`, as `uis/stock/src/lib/rpc.ts` does |
52
69
  | **Generated types** | `npx openapi-typescript http://127.0.0.1:3999/openapi/spec.json -o src/api.d.ts` |
53
70
 
54
- The stock UI (`uis/stock/src/lib/api.ts`) is the reference client: `fetch` for everything, with
55
- ArkType validating the responses at runtime. Either style is fine.
71
+ `uis/stock/src/lib/api.ts` is the reference client: plain `fetch`, with ArkType validating the
72
+ responses at runtime. Either style is fine.
56
73
 
57
- ### The endpoints you will actually use
74
+ ### Endpoints you will actually use
58
75
 
59
76
  | endpoint | what it gives you |
60
77
  | --- | --- |
61
- | `GET /api/state` | everything: panel settings, servers with live status, host vitals. Live clients should use SSE instead |
62
- | `GET /api/events` | **the live feed.** `event: hello` carries the full state, then `state`, `server` and `log` frames |
78
+ | `GET /api/state` | everything: panel settings, servers with live status, host vitals |
79
+ | `GET /api/events` | **the live feed**: `hello` carries the full state, then `state`, `server`, `log` |
63
80
  | `GET /api/servers/:id/stream` | one server's `server` + `log` frames |
64
81
  | `POST /api/servers/:id/{start,stop,restart}`, `/api/servers/{start-all,stop-all}` | lifecycle |
65
- | `GET /api/servers`, `POST /api/servers`, `PATCH` / `DELETE /api/servers/:id` | the entries themselves |
66
- | `GET /api/logs`, `GET /api/logs/:id?tail=&search=&stream=`, `GET /api/logs/:id/download?file=` | persisted logs |
67
- | `GET` / `PATCH /api/settings`, `POST` / `DELETE /api/settings/tls` and `/api/settings/ui` | the panel's own configuration |
68
- | `POST /api/backups`, `GET /api/backups`, `POST /api/backups/restore`, `GET /api/backups/:name/download`, `DELETE /api/backups/:name` | archives |
69
- | `POST` / `DELETE /api/notifications/token`, `POST /api/notifications/test`, `POST /api/notifications/detect-chats` | Telegram |
70
- | `GET /healthz` | liveness, **no session** — 503 when an autostart server has crashed |
71
- | `GET /api/metrics` | Prometheus text |
72
-
73
- `GET /healthz` and `GET /openapi/*` are the only unauthenticated reads; the SPA shell itself
74
- is public too, so your app can load before a session exists.
82
+ | `GET` / `POST /api/servers`, `PATCH` / `DELETE /api/servers/:id` | the entries themselves |
83
+ | `GET /api/logs`, `/api/logs/:id?tail=&search=`, `/api/logs/:id/download?file=` | persisted logs |
84
+ | `GET` / `PATCH /api/settings` | the panel's own config (`control.label`, host thresholds, backups, …) |
85
+ | `POST` / `DELETE /api/settings/ui` | install or revert a UI — what the settings page calls |
86
+ | `POST` / `DELETE /api/settings/tls` | upload or clear a PEM pair |
87
+ | `GET` / `POST /api/backups`, `/api/backups/restore`, `/api/backups/:name/download` | archives |
88
+ | `POST` / `DELETE /api/notifications/token`, `/api/notifications/test`, `/detect-chats` | Telegram |
89
+ | `POST /api/auth/login`, `GET /api/auth/session`, `POST /api/auth/logout` | the session |
90
+ | `GET /healthz` | liveness — **no session**, and `503` when an autostart server has crashed |
91
+ | `GET /api/metrics` | Prometheus text (needs a session) |
92
+
93
+ `GET /healthz` and `GET /openapi/*` are the only unauthenticated reads; the SPA shell itself is
94
+ public, so your app can load before a session exists.
95
+
96
+ Two settings worth reflecting: `control.label` is the panel's own name (the stock shell shows it),
97
+ and `GET /api/settings` includes `ui` — which UI is being served, and its metadata.
75
98
 
76
99
  ### Failures
77
100
 
@@ -81,40 +104,39 @@ Every failing request answers with one envelope:
81
104
  { "message": "unknown server \"web\"", "code": "UNKNOWN_SERVER", "detail": { "…": "…" } }
82
105
  ```
83
106
 
84
- `code` is stable and machine-readable (`AUTH_REQUIRED` drives the login redirect); `detail`
85
- carries validation issues or context when there is any. Status codes are the usual ones
86
- (400 bad input, 401 no session, 403 bad token/origin, 404 unknown id, 409 conflict, 413 too
87
- large).
107
+ `code` is stable and machine-readable (`AUTH_REQUIRED` drives the login redirect); `detail` carries
108
+ validation issues when there are any. Status codes are the usual ones: 400 bad input, 401 no
109
+ session, 403 bad token or origin, 404 unknown id, 409 conflict, 413 too large.
88
110
 
89
111
  ### SSE frames
90
112
 
91
113
  | `event:` | `data:` |
92
114
  | --- | --- |
93
- | `hello` | `{ ts, state }` — the first frame, with the complete snapshot |
94
- | `state` | `{ ts, state }` — anything changed: a status, a resource sample, the host vitals |
115
+ | `hello` | `{ ts, state }` — the first frame, the complete snapshot |
116
+ | `state` | `{ ts, state }` — anything changed: a status, a resource sample, host vitals |
95
117
  | `server` | `{ ts, serverId, server }` — one entry, after an action or a probe |
96
118
  | `log` | `{ ts, serverId, lines }` — new output (dropped under backpressure, never state) |
97
119
  | `ping` | the current time, every 15 s |
98
120
 
99
- The exact frames are in `src/shared/contracts.ts` (`sseMessageSchema`) — the server validates
100
- against them before writing, so that schema is also your best type source.
101
-
102
- Send `?logs=0` to skip log frames, or `?serverId=<id>` for one server. The server pings every
103
- 15 s.
121
+ Send `?logs=0` to skip log frames, or `?serverId=<id>` to follow one server. The exact shapes are
122
+ `sseMessageSchema` in `src/shared/contracts.ts` — the server validates against it before writing, so
123
+ that schema is also your best type source.
104
124
 
105
- ## Adding a UI to this repo
125
+ ## Building one in this repo
106
126
 
107
- `uis/<name>/` is a Vite app: `index.html`, `src/`, a `tsconfig.json` (copy a sibling's) and a 2-line
108
- `vite.config.ts` calling `createUiConfig` from `uis/vite.shared.ts`. `public/ui.json` names it.
127
+ The repo's own UIs are Vite apps: `uis/<name>/` holds `index.html`, `src/`, a `tsconfig.json`
128
+ (copied from a sibling) and a two-line `vite.config.ts` calling `createUiConfig` from
129
+ `uis/vite.shared.ts`, with `public/ui.json` naming it. Yours does not have to be Vite — only the
130
+ output matters.
109
131
 
110
132
  ```sh
111
- node scripts/build-uis.mjs <name> --zip # builds it and writes uis/dist/home-hosted-ui-<name>.zip
112
- pnpm run build:uis # every UI, zipped; the release attaches them as assets
133
+ node scripts/build-uis.mjs <name> --zip # builds it, writes uis/dist/home-hosted-ui-<name>.zip
134
+ pnpm run build:uis # every UI, zipped; releases attach them as assets
113
135
  ```
114
136
 
115
- Only `stock` ships inside the npm package — the others are release assets you upload from
116
- Settings → Interface. `pnpm run quickcheck` type-checks every UI, and `pnpm test` (vitest) picks up
117
- any `test/*.test.ts` you add (use relative imports; the `@` alias points at `stock`).
137
+ Only `stock` ships inside the npm package; the rest are release assets you install from Settings.
138
+ `pnpm run quickcheck` type-checks every UI, and `pnpm test` picks up any `test/*.test.ts` you add
139
+ (use relative imports — the `@` alias points at `stock`).
118
140
 
119
141
  ## A worked example
120
142
 
@@ -124,25 +146,23 @@ npm create vite@latest my-panel -- --template vue-ts
124
146
  cd my-panel && npm install
125
147
  npm run build # → dist/
126
148
 
127
- # 2. make sure the API base is relative, then zip the build
149
+ # 2. keep the API base relative, then zip the build
128
150
  cd dist && zip -r ../my-panel.zip . && cd ..
129
151
 
130
- # 3. Settings → Interface → Install UI, and refresh
152
+ # 3. Settings → Interface → Install UI, then refresh
131
153
  ```
132
154
 
133
- Your client needs the session cookie, which the browser sends automatically once you log in
134
- on that origin. For local development, `pnpm dev` in this repo runs the panel on 3999 and a
135
- Vite dev server on 3998 with `/api` proxied, so you can point your own dev server at
136
- `http://127.0.0.1:3999` the same way.
155
+ Your client needs the session cookie, which the browser sends automatically once you log in on that
156
+ origin. For local development `pnpm dev` runs the panel on 3999 and a Vite dev server on 3998 with
157
+ `/api` proxied — point your own dev server at `http://127.0.0.1:3999` the same way.
137
158
 
138
159
  ## Checklist
139
160
 
140
161
  - [ ] `index.html` at the root of the zip, assets referenced relatively
141
162
  - [ ] only `/api/...` calls, no absolute origins, no hard-coded port
142
163
  - [ ] `401 { code: 'AUTH_REQUIRED' }` handled with a login screen
143
- - [ ] live data from SSE (a panel that only polls feels broken)
164
+ - [ ] live data from SSE — a panel that only polls feels broken
144
165
  - [ ] deep links render (the server falls back to `index.html`)
145
- - [ ] assets self-hosted; no CDN dependencies
146
- - [ ] works offline over plain http on a LAN (no `Secure`-only cookies, no https assumptions)
147
- - [ ] `ui.json` with a name and version, so *Settings → Interface* can tell you what is
148
- installed
166
+ - [ ] assets self-hosted, no CDN
167
+ - [ ] works over plain http on a LAN
168
+ - [ ] `ui.json` with a name and version, so Settings can tell you what is installed
package/dist/cli.js CHANGED
@@ -1180,7 +1180,7 @@ function checkExposure(control, passwordSet, usingDefaultPassword = false) {
1180
1180
  };
1181
1181
  if (!passwordSet) return {
1182
1182
  exposed,
1183
- blockedReason: `the control panel is bound to ${control.host} but no password is set (run \`pnpm run auth:set-password\`)`
1183
+ blockedReason: `the control panel is bound to ${control.host} but no password is set (run \`pnpm run set-password\`)`
1184
1184
  };
1185
1185
  if (usingDefaultPassword) return {
1186
1186
  exposed,