home-hosted 0.2.0 → 0.4.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/README.md CHANGED
@@ -6,52 +6,31 @@
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) · [🤖 Agents & API](#-agents-scripts-and-tools) · [✨ Features](#-features) · [🧩 Servers](./SERVERS.md) · [🛠 CLI](#-cli) · [🔔 Notifications](./NOTIFICATIONS.md) · [🎨 BYOU](#-bring-your-own-ui-byou)
20
20
 
21
21
  </div>
22
22
 
23
23
  ---
24
24
 
25
- ## 🤔 Why?
26
-
27
- Running services on a home machine usually means one of two extremes: `tmux` sessions you
28
- forget about, or a hand-written systemd unit per service (times six).
25
+ <div align="center">
29
26
 
30
- | | |
31
- | --- | --- |
32
- | ❌ **"Is it still running?"** | You check with `ps`, then `curl`, then hope. |
33
- | ❌ **Silent deaths** | Something crashes at 3am and you notice days later. |
34
- | ❌ **One terminal per service** | Logs scroll away in tabs you closed. |
35
- | ❌ **Fragile restarts** | The box reboots and half the stack is gone. |
36
- | ✅ **home-hosted** | Declare it once, watch it forever, one command to stop it all. |
27
+ ![Six views of the panel: the stock UI, the NOC-console, and UI examples](docs/media/tour.gif)
37
28
 
38
- ```text
39
- ┌──────────────────────────────────────┐
40
- your browser ──▶│ home-hosted · 127.0.0.1:3999 │
41
- │ Vue panel + JSON API + SSE logs │
42
- └───────────────┬──────────────────────┘
43
- │ supervises
44
- ┌──────────────────────────┼──────────────────────────┐
45
- ▼ ▼ ▼
46
- ┌─────────┐ ┌─────────┐ ┌─────────┐
47
- │ gateway │ │ files │ │ bot │
48
- │ :4000 │ │ :4010 │ │ ... │
49
- └─────────┘ └─────────┘ └─────────┘
50
- health ✓ health ✓ restarts ↻
51
- ```
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>
52
32
 
53
- It ships with **nothing**. No blessed paths, no "data directory" setting, no opinion about
54
- what you run — a server is a command, some arguments, and the environment you give it.
33
+ </div>
55
34
 
56
35
  ---
57
36
 
@@ -60,10 +39,11 @@ what you run — a server is a command, some arguments, and the environment you
60
39
  ```bash
61
40
  npx home-hosted # start it — detached, it stays running
62
41
  npx home-hosted status # where is it, is it healthy
42
+ # pnpm instead of npx: `pnpm dlx home-hosted …`
63
43
  ```
64
44
 
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:
45
+ That is it. The panel is on **<http://127.0.0.1:3999>** and keeps running after the terminal
46
+ closes. <sub>Needs Node 24 or newer.</sub> Stop it whenever you like:
67
47
 
68
48
  ```bash
69
49
  npx home-hosted down # stops the panel *and* everything it started
@@ -71,42 +51,34 @@ npx home-hosted down # stops the panel *and* everything it started
71
51
 
72
52
  > [!NOTE]
73
53
  > 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.
54
+ > it under **Settings → Authentication** — binding beyond `127.0.0.1` stays refused until you do.
75
55
 
76
56
  <details>
77
57
  <summary><b>📦 Install it instead of npx-ing it</b></summary>
78
58
 
79
59
  ```bash
80
- npm install -g home-hosted
60
+ npm install -g home-hosted # or: pnpm add -g home-hosted
81
61
  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
62
  ```
87
63
 
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.
64
+ Everything it owns — config, secrets, logs, TLS, backups — lives in `$HHOSTED_HOME`, default
65
+ `~/.home-hosted`. Delete that and nothing of yours is left behind.
93
66
 
94
67
  </details>
95
68
 
96
69
  <details>
97
- <summary><b>📁 Keep it in a project you can take anywhere (and share)</b></summary>
70
+ <summary><b>📁 Keep a whole setup in a project you can take anywhere</b></summary>
98
71
 
99
- Pin the version, install the servers next to it, and commit the whole project — it *is* the
100
- setup:
72
+ Commit the project and it *is* the setup. `init` scaffolds it:
101
73
 
102
74
  ```bash
103
- mkdir my-servers && cd my-servers
104
- npm init -y
105
- npm install home-hosted@0.2.0 9router serve # pinned, so `git clone && npm ci` is enough
106
-
107
- npx home-hosted up --home ./state # state lives inside the project
75
+ npx home-hosted init # or: pnpm dlx home-hosted init
76
+ # project directory, package name, package manager, install, git — every step has a default
77
+ # `--yes` takes them all, for an agent or a CI job
108
78
  ```
109
79
 
80
+ It writes a manifest whose scripts all pass `--home ./state`:
81
+
110
82
  ```jsonc
111
83
  // package.json
112
84
  {
@@ -114,19 +86,23 @@ npx home-hosted up --home ./state # state lives inside the proje
114
86
  "up": "home-hosted up --home ./state",
115
87
  "down": "home-hosted down --home ./state",
116
88
  "status": "home-hosted status --home ./state"
89
+ // …and restart, set-password, set-token, migrate
117
90
  }
118
91
  }
119
92
  ```
120
93
 
121
94
  ```gitignore
122
- state/* # secrets, logs, TLS keys and archives stay local…
123
- !state/servers.config.json # …but the server definitions are committed
95
+ node_modules/
96
+ state/* # secrets, logs, TLS keys and archives stay local…
97
+ !state/servers.config.json # …but the server definitions are committed
98
+ data/ # per-server data directories, declared through dataEnvs
124
99
  ```
125
100
 
126
- <sub>Call them as `npm run up` — `npm up` is npm's own update, not your script.</sub>
101
+ One clone, `pnpm install --frozen-lockfile`, `pnpm run up` — the setup is up on any machine with Node.
102
+ Worked example, with per-server data inside the project:
103
+ **[hhosted-9router-dsh](https://github.com/NamesMT/hhosted-9router-dsh)**.
127
104
 
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.
105
+ <sub>Call them as `pnpm run up` — `pnpm up` is pnpm's own update, not your script.</sub>
130
106
 
131
107
  </details>
132
108
 
@@ -152,76 +128,101 @@ WantedBy=multi-user.target
152
128
 
153
129
  ---
154
130
 
131
+ ## 🤖 Agents, scripts and tools
132
+
133
+ Everything the panel does, a script or an agent can do: the API takes a **long-lived token** in place
134
+ of the browser cookie. One command for a credential, then plain HTTP to start, stop, inspect, restart
135
+ and read logs.
136
+
137
+ ```bash
138
+ home-hosted set-token --generate
139
+ # hh_9uA2… (printed once; only its hash is kept, mode 0600)
140
+
141
+ curl -H "Authorization: Bearer hh_9uA2…" http://127.0.0.1:3999/api/state
142
+ curl -H "Authorization: Bearer hh_9uA2…" -X POST http://127.0.0.1:3999/api/servers/9router/restart
143
+ curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/events?logs=1' # SSE
144
+ home-hosted status --json # machine-readable: pid, url, health, paths
145
+ ```
146
+
147
+ A token has the same authority as a signed-in browser and outlives restarts; `set-token --clear`
148
+ revokes it instantly.
149
+
150
+ <details>
151
+ <summary><b>🌐 The endpoints worth knowing</b></summary>
152
+
153
+ | | |
154
+ | --- | --- |
155
+ | `GET /api/state` | the full snapshot: config, live status, host vitals |
156
+ | `GET /api/events` | SSE: the live state, plus logs (`?logs=0`, `?serverId=…`) |
157
+ | `GET /api/servers/:id/stream` | SSE: one server's state and logs |
158
+ | `POST /api/servers/:id/{start,stop,restart}` | lifecycle |
159
+ | `POST /api/servers/:id/free-port` | ask whatever holds that server's port to stop |
160
+ | `PATCH /api/servers/:id`, `PATCH /api/settings` | edit configuration |
161
+ | `GET /api/logs`, `/api/backups`, `/api/notifications` | logs, archives, Telegram |
162
+ | `GET /healthz` | no session needed — the one an external monitor wants (its per-server detail needs a credential) |
163
+ | `GET /api/metrics` | Prometheus text |
164
+
165
+ `GET /openapi/spec.json` describes all of it, `/openapi/ui` is the browsable version, and every error
166
+ comes back as one envelope (`{ message, code, detail }`) with a stable `code` a tool can branch on.
167
+
168
+ </details>
169
+
170
+ <details>
171
+ <summary><b>🧑‍💻 Pointing an agent at it</b></summary>
172
+
173
+ Give the agent four things and it can run your home server without guessing:
174
+
175
+ 1. the token (`home-hosted set-token --generate`),
176
+ 2. `http://127.0.0.1:3999/openapi/spec.json` — the API it may call,
177
+ 3. `home-hosted status --json` — where things are,
178
+ 4. [SERVERS.md](./SERVERS.md) — how an entry is declared when it needs a new server.
179
+
180
+ For a UI rather than the API, [UI_CREATION.md](./UI_CREATION.md) is the whole contract, and the panel
181
+ can be told what to be: *"Help me build a UI for home-hosted: nostalgic game theme, including …"*.
182
+
183
+ </details>
184
+
185
+ ---
186
+
155
187
  ## ✨ Features
156
188
 
157
189
  | | |
158
190
  | --- | --- |
159
191
  | 🚦 **Lifecycle** | Start, stop, restart from the panel or the API; `autostart` entries come up with it. |
160
192
  | ♻️ **Auto-restart** | Exponential backoff on crash, with the counter reset once a process stays up. |
161
- | 🩺 **Health that acts** | TCP or HTTP probes per server: warn on the card, force a restart after a timeout, and check ports before starting. |
193
+ | 🩺 **Health that acts** | TCP or HTTP probes per server: warn on the card, force a restart after a timeout, check ports before starting — and [follow or replace](./SERVERS.md#when-a-program-restarts-itself) a program that restarts itself. |
162
194
  | 🔗 **Ordered startup** | `dependsOn` waits for a dependency to be *healthy* — not merely spawned — and stops in reverse. |
163
195
  | 📜 **Logs** | Live per-server stream, buffer plus rotated files on disk, search, download, one click to clear. |
164
196
  | 📈 **Resources** | CPU and RSS of the whole process tree, with an optional memory ceiling that triggers a restart. |
165
197
  | 🌡️ **Host vitals** | Load, memory, swap, disk and CPU temperature, with thresholds that notify once and again on recovery. |
198
+ | 🤖 **Token API** | Scripts and agents drive it with `Authorization: Bearer` — no browser, no session. [↑](#-agents-scripts-and-tools) |
199
+ | 🔔 **Notifications** | Telegram on crash, unhealthy, forced restart, recovery and host thresholds — [setup here](./NOTIFICATIONS.md). |
166
200
  | 💾 **Backups** | One click for config, secrets, TLS and your declared data directories — plain `.zip`, or AES-256 with a password, restored per path. |
167
- | 🚚 **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). |
169
- | 🔔 **Notifications** | Telegram (grammY) on crash, unhealthy, forced restart, recovery and host thresholds. |
170
- | 🔐 **Security** | httpOnly cookie sessions, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
171
- | 🧩 **Server-agnostic** | `command` + `args` + `env` + `cwd`. Nothing in the code knows what you run. |
201
+ | 🎨 **BYOU — Bring Your Own UI** | Upload a static build, `home-hosted ui-revert` to go back. [UI_CREATION.md](./UI_CREATION.md) |
202
+ | 🔐 **Security** | Cookie sessions, API tokens, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
203
+ | 🧩 **No special treatment** | A server is `command` + `args` + `env` + `cwd`; nothing is built in for any particular app. |
172
204
  | 🖥 **Cross-platform** | Linux, macOS and Windows: `/proc`, `ps` or Win32_Process, process groups or `taskkill /T`, no shell dependencies. |
173
205
 
174
206
  ---
175
207
 
176
- ## 🧩 Adding a server
208
+ ## 🧩 Servers
177
209
 
178
- Use **➕ Add server** in the panel, or write it into `servers.config.json`:
210
+ An entry is a few lines. Add one with **➕ Add server**, or write it into `servers.config.json`:
179
211
 
180
212
  ```json
181
213
  {
182
214
  "id": "myapp",
183
215
  "command": "node",
184
216
  "args": ["server.js"],
185
- "cwd": "./apps/myapp",
186
217
  "port": 8080,
187
218
  "autostart": true,
188
- "env": { "NODE_ENV": "production" },
189
- "health": { "mode": "http", "http": { "path": "/healthz" } }
219
+ "dataEnvs": { "DATA_DIR": "{projectDir}/data/myapp" }
190
220
  }
191
221
  ```
192
222
 
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
- ```
200
-
201
- <details>
202
- <summary><b>🧾 Field reference</b></summary>
203
-
204
- | field | what it does |
205
- | --- | --- |
206
- | `command`, `args`, `cwd` | what to run, with `{placeholders}` resolved per entry |
207
- | `env`, `dataEnvs`, `envFile` | environment; `dataEnvs` also marks data directories for backups, `envFile` keeps secrets out of the config |
208
- | `port`, `bind` | enables the readiness wait, health checks and the conflict preflight; `local` keeps it on `127.0.0.1` |
209
- | `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body) |
210
- | `restart.*` | backoff: `maxRetries`, `baseDelayMs`, `factor`, `maxDelayMs`, `resetAfterMs` |
211
- | `stop.*` | signal, `killGroup`, grace period, and whether to sweep leftover port holders |
212
- | `dependsOn` | ids that must be healthy first; stopped in reverse order |
213
- | `resources.maxRssBytes` | restart when the process tree grows past a limit |
214
- | `bootstrap` | one command to run once before the first start (migrations, warmups) |
215
- | `backupPaths` | extra paths this entry owns, included in backups |
216
-
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.
220
-
221
- Every supervised process also gets `HHOSTED_SERVER_ID` and `HHOSTED_CONTROL_PORT`, so a
222
- service can tell which entry it is and how to reach the panel.
223
-
224
- </details>
223
+ `dataEnvs` declares a data directory once: it is exported to the process *and* picked up by Backups.
224
+ **Every field, every placeholder, the port-conflict policies (including adopting a server that
225
+ restarts itself), and how hand-edits are validated: [SERVERS.md](./SERVERS.md).**
225
226
 
226
227
  ---
227
228
 
@@ -234,6 +235,9 @@ service can tell which entry it is and how to reach the panel.
234
235
  | `home-hosted restart` | `down`, then `up` |
235
236
  | `home-hosted status` | pid, URL, health, uptime, state and log paths (`--json` for scripts) |
236
237
  | `home-hosted set-password` | set the panel password without opening a browser |
238
+ | `home-hosted set-token` | set the API token scripts and agents use (`--generate`, `--clear`) |
239
+ | `home-hosted migrate` | bring `servers.config.json` up to this release's schema (`--dry-run`, `--yes`) |
240
+ | `home-hosted init` | scaffold a project that keeps `state/` and its data in the repo |
237
241
  | `home-hosted ui-revert` | go back to the stock panel UI after uploading your own |
238
242
 
239
243
  <details>
@@ -253,96 +257,107 @@ service can tell which entry it is and how to reach the panel.
253
257
 
254
258
  </details>
255
259
 
260
+ <details>
261
+ <summary><b>🧭 Upgrading, and why the panel sometimes refuses to start</b></summary>
262
+
263
+ `servers.config.json` records what wrote it: `meta.writtenBy` (the release) and `meta.schema` (the
264
+ config shape). That buys two guarantees:
265
+
266
+ - **A newer home-hosted always reads an older config** — every existing key keeps its meaning.
267
+ - **Keys a newer release added are ignored, not fatal.** The panel names them in its log, leaves them
268
+ in the file, and never resets the settings around them.
269
+
270
+ What it will not do is run a config it cannot read. A wrong value, a duplicate id, an unreadable file
271
+ or a config whose schema is newer than the running release stops `up` with the exact problem, rather
272
+ than starting with defaults that quietly differ from your file. Fix the file, or install the release
273
+ that wrote it.
274
+
275
+ When a release changes the shape itself, `home-hosted migrate` applies the steps it ships:
276
+
277
+ ```bash
278
+ home-hosted migrate --dry-run # print the steps, write nothing
279
+ home-hosted migrate # ask, then write — keeps servers.config.json.bak
280
+ ```
281
+
282
+ Unattended, consent comes from `--yes` or `HHOSTED_MIGRATE=allow`; without it the command stops.
283
+
284
+ </details>
285
+
256
286
  ---
257
287
 
258
288
  ## 🔐 Security
259
289
 
260
290
  Everything binds `127.0.0.1` until you say otherwise.
261
291
 
262
- - **A password is required to expose the panel.** Replace the default, then bind to `lan` —
263
- in the UI, in the config, or with `--host lan`. The same guard applies in all three places.
264
- - **Sessions** live in memory only; the cookie is `HttpOnly` and `SameSite=Strict`, and the
265
- login route locks out repeated failures per IP.
266
- - **Secrets never enter the config**: the password hash, the Telegram token and the TLS key
267
- live in `$HHOSTED_HOME/.control-secrets.json` with mode `0600`.
268
- - **Behind a proxy** turn on `trustProxy` and let `cookieSecure: auto` add `Secure` on https,
269
- or upload a PEM pair and let home-hosted terminate TLS itself.
292
+ - **A password is required to expose the panel.** Replace the default, then bind to `lan` — in the
293
+ UI, in the config, or with `--host lan`. The same guard applies in all three places.
294
+ - **Sessions** live in memory only; the cookie is `HttpOnly` and `SameSite=Strict`, and the login
295
+ route locks out repeated failures per IP.
296
+ - **API tokens** for scripts and agents: `home-hosted set-token --generate` prints one once, and a
297
+ request proves itself with `Authorization: Bearer …` — the same access as a signed-in browser,
298
+ stored as a SHA-256 hash, revoked with `set-token --clear`.
299
+ - **Port conflicts** are named — `port 4010 is already in use (pid 4242)` — and can be resolved from
300
+ a confirmation popover on that banner or card. The process is looked up again at that moment,
301
+ never taken from the message, and anything the panel supervises is refused, not killed. A server
302
+ that [restarts itself](./SERVERS.md#when-a-program-restarts-itself) can be followed, or replaced
303
+ with a supervised copy.
304
+ - **Secrets never enter the config**: the password hash, the API token hash, the Telegram bot token
305
+ and the TLS key live in `$HHOSTED_HOME/.control-secrets.json` with mode `0600`.
306
+ - **Behind a proxy** turn on `trustProxy` and let `cookieSecure: auto` add `Secure` on https, or
307
+ upload a PEM pair and let home-hosted terminate TLS itself.
270
308
 
271
309
  ---
272
310
 
273
- ## 💾 Backups
274
-
275
- **Settings → Backups** archives the config, the secrets file, the TLS pair and the data
276
- directories your entries declare — as an ordinary `.zip`:
311
+ ## 🔔 Notifications
277
312
 
278
- - no password → **plain zip**, openable by every operating system out of the box
279
- - with a password → **WinZip AES-256**, openable in 7-Zip, WinRAR, Keka, PeaZip, Ark…
280
- - restore is selective: check config, secrets, TLS or individual data paths, and nothing
281
- else is touched
313
+ Telegram, when something happens while you are not looking: a server that gave up restarting, a
314
+ failing health check, a forced restart, a recovery, or a host threshold (disk, memory, swap, load,
315
+ temperature). Opt-in, rate-limited per server *and* reason, and the bot token stays in the secrets
316
+ file. **Two minutes of setup: [NOTIFICATIONS.md](./NOTIFICATIONS.md).**
282
317
 
283
- ### 🚚 One archive is a whole setup
318
+ ---
284
319
 
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.
320
+ ## 💾 Backups
289
321
 
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.
322
+ **Settings → Backups** archives the config, secrets, TLS pair and every data directory your entries
323
+ declare — an ordinary `.zip`, or WinZip AES-256 with a password, restored per path.
294
324
 
295
325
  <details>
296
- <summary><b>🗂 Declaring data directories</b></summary>
326
+ <summary><b>🚚 One archive is a whole setup</b></summary>
297
327
 
298
- ```json
299
- {
300
- "dataEnvs": { "DATA_DIR": "{home}/.myapp" },
301
- "backupPaths": ["{home}/.myapp/uploads"]
302
- }
303
- ```
328
+ Start a **blank** home-hosted anywhere — another machine, another user, a fresh container — upload the
329
+ archive and restore. Definitions come back, data lands where *this* machine's config says, and
330
+ `autostart` entries come up immediately.
304
331
 
305
- `dataEnvs` is exported to the process *and* backed up, so a data directory is declared once.
306
- A path already covered by a declared parent is skipped.
332
+ It works because an archive carries its own `servers.config.json` and paths are matched by the
333
+ **declaration** (`9router:DATA_DIR`), not by an absolute path from the source machine. A restore never
334
+ writes where no config declares.
307
335
 
308
336
  </details>
309
337
 
310
338
  ---
311
339
 
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
340
  ## 🎨 Bring your own UI (BYOU)
317
341
 
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.
342
+ The panel is a static site: `$HHOSTED_HOME/.ui` overrides the packaged one, and **Settings →
343
+ Interface** takes a zip. No restart, no fork — and `home-hosted ui-revert` brings back the stock
344
+ panel if yours breaks.
321
345
 
322
- ```bash
323
- home-hosted ui-revert # if it breaks, the stock panel is one command away
324
- ```
346
+ Two ship in this repo: `uis/stock`, and `uis/noc-console` for TUI and shortcuts wizards; a release
347
+ attaches both as `home-hosted-ui-<name>.zip`. Yours can be anything that compiles to static files —
348
+ the server never cares what built it.
325
349
 
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.
350
+ <details>
351
+ <summary><b>🤖 Or have an agent build the UI you actually want</b></summary>
330
352
 
331
- ---
353
+ The whole contract fits in one file, so a coding agent can do this. Point it at this repo and be
354
+ specific:
332
355
 
333
- ## 🌐 HTTP API
356
+ > Help me build a UI for `home-hosted`: nostalgic game theme, including … features.
334
357
 
335
- The panel is a client of its own API, so everything is scriptable:
358
+ [UI_CREATION.md](./UI_CREATION.md) has the endpoints, the SSE frames, the auth rules and a checklist.
336
359
 
337
- | | |
338
- | --- | --- |
339
- | `GET /api/state` | the full snapshot: config, live status, host vitals |
340
- | `GET /api/servers/:id/stream` | SSE stream of state and log lines |
341
- | `POST /api/servers/:id/{start,stop,restart}` | lifecycle |
342
- | `PATCH /api/servers/:id`, `PATCH /api/settings` | edit configuration |
343
- | `GET /api/logs`, `/api/backups`, `/api/notifications` | logs, archives, Telegram |
344
- | `GET /healthz` | no session needed — the one an external monitor wants |
345
- | `GET /api/metrics` | Prometheus text |
360
+ </details>
346
361
 
347
362
  ---
348
363
 
@@ -365,9 +380,12 @@ the same way: every supervised process tree is stopped before the panel exits.
365
380
  </details>
366
381
 
367
382
  <details>
368
- <summary><b>Which ports does it use?</b></summary>
383
+ <summary><b>Nothing starts and the port is busy</b></summary>
369
384
 
370
- Just the control panel, `3999` by default. Supervised servers use the ports you give them.
385
+ A supervised server whose port is taken is reported rather than started over — the panel names the
386
+ holder and offers to free it, and a program that restarts itself can be followed or reclaimed instead
387
+ ([SERVERS.md](./SERVERS.md#a-busy-port)). The control port itself is checked before the listener is
388
+ opened.
371
389
 
372
390
  </details>
373
391
 
@@ -377,9 +395,9 @@ Just the control panel, `3999` by default. Supervised servers use the ports you
377
395
  `$HHOSTED_HOME`, default `~/.home-hosted`:
378
396
 
379
397
  ```text
380
- servers.config.json your servers (the UI writes it back atomically)
398
+ servers.config.json your servers, plus meta: which release and schema wrote it
381
399
  servers.config.schema.json regenerated on every start, for editor autocomplete
382
- .control-secrets.json password hash + Telegram token (mode 0600)
400
+ .control-secrets.json password hash + API token hash + Telegram token (mode 0600)
383
401
  .logs/ rotated per-server logs + history
384
402
  .tls/ an uploaded PEM pair
385
403
  .backups/ zip archives
@@ -391,38 +409,49 @@ run.json the running panel (pid, url, token, mode 0600)
391
409
  </details>
392
410
 
393
411
  <details>
394
- <summary><b>Windows support, really?</b></summary>
412
+ <summary><b>Which ports does it use?</b></summary>
395
413
 
396
- Yes. Process trees are sampled from Win32_Process, termination uses `taskkill /T`, and the
397
- shipped examples avoid POSIX-only commands. CPU temperature and swap are best-effort where the
398
- OS does not expose them to an unprivileged process.
414
+ Just the control panel, `3999` by default. Supervised servers use the ports you give them.
399
415
 
400
416
  </details>
401
417
 
402
418
  <details>
403
- <summary><b>Nothing starts and the port is busy</b></summary>
419
+ <summary><b>Windows support, really?</b></summary>
404
420
 
405
- A supervised server whose port is taken is reported rather than started over — the panel names
406
- the holder. The control port itself is checked before the listener is opened.
421
+ Yes. Process trees are sampled from Win32_Process, termination uses `taskkill /T`, and the shipped
422
+ examples avoid POSIX-only commands. CPU temperature and swap are best-effort where the OS does not
423
+ expose them to an unprivileged process, and adopting a self-restarted process is Linux/macOS only.
407
424
 
408
425
  </details>
409
426
 
410
427
  ---
411
428
 
412
- ## 🗂 Layout
429
+ ## 🗂 Working on it
413
430
 
414
431
  ```text
415
432
  src/ control plane: config, supervisor, API, providers, services
416
- src/cli.ts the command line (up/down/status/restart/set-password)
433
+ src/cli.ts the command line (up/down/status/restart/set-password/set-token/migrate)
417
434
  src/index.ts the control plane itself, used by `up --foreground`
418
- uis/ UIs: `stock` (shipped) and alternatives, each a Vite app
435
+ uis/ UIs: `stock` (shipped) and alternatives — any framework, static output
419
436
  bin/ the published entry point
420
- vite.server.config.ts bundles src/cli.ts into dist/cli.js
421
437
  ```
422
438
 
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.
439
+ `pnpm dev` runs the panel with `tsx watch` plus the stock UI's dev server (state goes to
440
+ `.dev-state/`); `pnpm build` produces `dist/` and `uis/stock/dist/`; `pnpm quickcheck` is lint plus
441
+ types; `pnpm test` is vitest; `pnpm run media` regenerates the GIF above.
442
+
443
+ <details>
444
+ <summary><b>📚 The docs, and which one you want</b></summary>
445
+
446
+ | file | for |
447
+ | --- | --- |
448
+ | [SERVERS.md](./SERVERS.md) | declaring a server: every field, placeholders, port conflicts |
449
+ | [NOTIFICATIONS.md](./NOTIFICATIONS.md) | Telegram alerts, end to end |
450
+ | [UI_CREATION.md](./UI_CREATION.md) | building a UI against the API |
451
+ | [AGENTS.md](./AGENTS.md) | the architecture and the rules worth knowing before changing anything |
452
+ | [/openapi/ui](http://127.0.0.1:3999/openapi/ui) | the live API, on your own panel |
453
+
454
+ </details>
426
455
 
427
456
  ---
428
457