home-hosted 0.3.0 β†’ 0.4.1

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
@@ -16,7 +16,7 @@ them, watches them, restarts what dies, and shows you one page of what is going
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) Β· [🎨 BYOU](#-bring-your-own-ui-byou)
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
 
@@ -34,44 +34,12 @@ shortcuts wizards), and UI directions you could build yourself β€”
34
34
 
35
35
  ---
36
36
 
37
- ## πŸ€” Why?
38
-
39
- Running services on a home machine usually means one of two extremes: `tmux` sessions you
40
- forget about, or a hand-written systemd unit per service (times six).
41
-
42
- | | |
43
- | --- | --- |
44
- | ❌ **"Is it still running?"** | You check with `ps`, then `curl`, then hope. |
45
- | ❌ **Silent deaths** | Something crashes at 3am and you notice days later. |
46
- | ❌ **One terminal per service** | Logs scroll away in tabs you closed. |
47
- | ❌ **Fragile restarts** | The box reboots and half the stack is gone. |
48
- | βœ… **home-hosted** | Declare it once, watch it forever, one command to stop it all. |
49
-
50
- ```text
51
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
52
- your browser ──▢│ home-hosted Β· 127.0.0.1:3999 β”‚
53
- β”‚ your UI + JSON API + SSE logs β”‚
54
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
55
- β”‚ supervises
56
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
57
- β–Ό β–Ό β–Ό
58
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
59
- β”‚ gateway β”‚ β”‚ files β”‚ β”‚ bot β”‚
60
- β”‚ :4000 β”‚ β”‚ :4010 β”‚ β”‚ ... β”‚
61
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
62
- health βœ“ health βœ“ restarts ↻
63
- ```
64
-
65
- It ships with **nothing**. No blessed paths, no "data directory" setting, no opinion about
66
- what you run β€” a server is a command, some arguments, and the environment you give it.
67
-
68
- ---
69
-
70
37
  ## ⚑ Quick start
71
38
 
72
39
  ```bash
73
40
  npx home-hosted # start it β€” detached, it stays running
74
41
  npx home-hosted status # where is it, is it healthy
42
+ # pnpm instead of npx: `pnpm dlx home-hosted …`
75
43
  ```
76
44
 
77
45
  That is it. The panel is on **<http://127.0.0.1:3999>** and keeps running after the terminal
@@ -89,7 +57,7 @@ npx home-hosted down # stops the panel *and* everything it started
89
57
  <summary><b>πŸ“¦ Install it instead of npx-ing it</b></summary>
90
58
 
91
59
  ```bash
92
- npm install -g home-hosted
60
+ npm install -g home-hosted # or: pnpm add -g home-hosted
93
61
  home-hosted up
94
62
  ```
95
63
 
@@ -99,18 +67,18 @@ Everything it owns β€” config, secrets, logs, TLS, backups β€” lives in `$HHOSTE
99
67
  </details>
100
68
 
101
69
  <details>
102
- <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>
103
71
 
104
- Install the servers next to it and commit the whole project β€” it *is* the setup:
72
+ Commit the project and it *is* the setup. `init` scaffolds it:
105
73
 
106
74
  ```bash
107
- mkdir my-servers && cd my-servers
108
- npm init -y
109
- npm install home-hosted 9router serve # the lockfile pins them, so `npm ci` reproduces
110
-
111
- 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
112
78
  ```
113
79
 
80
+ It writes a manifest whose scripts all pass `--home ./state`:
81
+
114
82
  ```jsonc
115
83
  // package.json
116
84
  {
@@ -118,19 +86,23 @@ npx home-hosted up --home ./state # state lives inside the proje
118
86
  "up": "home-hosted up --home ./state",
119
87
  "down": "home-hosted down --home ./state",
120
88
  "status": "home-hosted status --home ./state"
89
+ // …and restart, set-password, set-token, migrate
121
90
  }
122
91
  }
123
92
  ```
124
93
 
125
94
  ```gitignore
126
- state/* # secrets, logs, TLS keys and archives stay local…
127
- !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
128
99
  ```
129
100
 
130
- <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)**.
131
104
 
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**.
105
+ <sub>Call them as `pnpm run up` β€” `pnpm up` is pnpm's own update, not your script.</sub>
134
106
 
135
107
  </details>
136
108
 
@@ -156,83 +128,101 @@ WantedBy=multi-user.target
156
128
 
157
129
  ---
158
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
+
159
187
  ## ✨ Features
160
188
 
161
189
  | | |
162
190
  | --- | --- |
163
191
  | 🚦 **Lifecycle** | Start, stop, restart from the panel or the API; `autostart` entries come up with it. |
164
192
  | ♻️ **Auto-restart** | Exponential backoff on crash, with the counter reset once a process stays up. |
165
- | 🩺 **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. |
166
194
  | πŸ”— **Ordered startup** | `dependsOn` waits for a dependency to be *healthy* β€” not merely spawned β€” and stops in reverse. |
167
195
  | πŸ“œ **Logs** | Live per-server stream, buffer plus rotated files on disk, search, download, one click to clear. |
168
196
  | πŸ“ˆ **Resources** | CPU and RSS of the whole process tree, with an optional memory ceiling that triggers a restart. |
169
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). |
170
200
  | πŸ’Ύ **Backups** | One click for config, secrets, TLS and your declared data directories β€” plain `.zip`, or AES-256 with a password, restored per path. |
171
- | 🚚 **Portable setup** | Restore a shared backup onto a blank instance and the whole server setup is back: definitions, data, secrets and all. |
172
201
  | 🎨 **BYOU β€” Bring Your Own UI** | Upload a static build, `home-hosted ui-revert` to go back. [UI_CREATION.md](./UI_CREATION.md) |
173
- | πŸ”” **Notifications** | Telegram (grammY) on crash, unhealthy, forced restart, recovery and host thresholds. |
174
- | πŸ” **Security** | httpOnly cookie sessions, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
175
- | 🧩 **Server-agnostic** | `command` + `args` + `env` + `cwd`. Nothing in the code knows what you run. |
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. |
176
204
  | πŸ–₯ **Cross-platform** | Linux, macOS and Windows: `/proc`, `ps` or Win32_Process, process groups or `taskkill /T`, no shell dependencies. |
177
205
 
178
206
  ---
179
207
 
180
- ## 🧩 Adding a server
208
+ ## 🧩 Servers
181
209
 
182
- 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`:
183
211
 
184
212
  ```json
185
213
  {
186
214
  "id": "myapp",
187
215
  "command": "node",
188
216
  "args": ["server.js"],
189
- "cwd": "./apps/myapp",
190
217
  "port": 8080,
191
218
  "autostart": true,
192
- "env": { "NODE_ENV": "production" },
193
- "health": { "mode": "http", "http": { "path": "/healthz" } }
219
+ "dataEnvs": { "DATA_DIR": "{projectDir}/data/myapp" }
194
220
  }
195
221
  ```
196
222
 
197
- Relative paths resolve against the directory you ran `home-hosted` from, so a project can keep its
198
- servers and its state together.
199
-
200
- <details>
201
- <summary><b>🧾 Field reference</b></summary>
202
-
203
- | field | what it does |
204
- | --- | --- |
205
- | `command`, `args`, `cwd` | what to run, with `{placeholders}` resolved per entry |
206
- | `env`, `dataEnvs`, `envFile` | environment; `dataEnvs` also marks data directories for backups, `envFile` keeps secrets out of the config |
207
- | `port`, `bind` | enables the readiness wait, health checks and the conflict preflight; `local` keeps it on `127.0.0.1` |
208
- | `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body) |
209
- | `restart.*` | backoff: `maxRetries`, `baseDelayMs`, `factor`, `maxDelayMs`, `resetAfterMs` |
210
- | `stop.*` | signal, `killGroup`, grace period, and whether to sweep leftover port holders |
211
- | `dependsOn` | ids that must be healthy first; stopped in reverse order |
212
- | `resources.maxRssBytes` | restart when the process tree grows past a limit |
213
- | `bootstrap` | one command to run once before the first start (migrations, warmups) |
214
- | `backupPaths` | extra paths this entry owns, included in backups |
215
-
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`).
231
-
232
- Every supervised process also gets `HHOSTED_SERVER_ID` and `HHOSTED_CONTROL_PORT`, so a
233
- service can tell which entry it is and how to reach the panel.
234
-
235
- </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).**
236
226
 
237
227
  ---
238
228
 
@@ -245,6 +235,9 @@ service can tell which entry it is and how to reach the panel.
245
235
  | `home-hosted restart` | `down`, then `up` |
246
236
  | `home-hosted status` | pid, URL, health, uptime, state and log paths (`--json` for scripts) |
247
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 |
248
241
  | `home-hosted ui-revert` | go back to the stock panel UI after uploading your own |
249
242
 
250
243
  <details>
@@ -264,55 +257,81 @@ service can tell which entry it is and how to reach the panel.
264
257
 
265
258
  </details>
266
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
+
267
286
  ---
268
287
 
269
288
  ## πŸ” Security
270
289
 
271
290
  Everything binds `127.0.0.1` until you say otherwise.
272
291
 
273
- - **A password is required to expose the panel.** Replace the default, then bind to `lan` β€”
274
- in the UI, in the config, or with `--host lan`. The same guard applies in all three places.
275
- - **Sessions** live in memory only; the cookie is `HttpOnly` and `SameSite=Strict`, and the
276
- login route locks out repeated failures per IP.
277
- - **Secrets never enter the config**: the password hash, the Telegram token and the TLS key
278
- live in `$HHOSTED_HOME/.control-secrets.json` with mode `0600`.
279
- - **Behind a proxy** turn on `trustProxy` and let `cookieSecure: auto` add `Secure` on https,
280
- 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.
281
308
 
282
309
  ---
283
310
 
284
- ## πŸ’Ύ Backups
311
+ ## πŸ”” Notifications
285
312
 
286
- **Settings β†’ Backups** archives the config, the secrets file, the TLS pair and the data
287
- directories your entries declare β€” as an ordinary `.zip`:
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).**
288
317
 
289
- - no password β†’ **plain zip**, openable by every operating system out of the box
290
- - with a password β†’ **WinZip AES-256**, openable in 7-Zip, WinRAR, Keka, PeaZip, Ark…
291
- - restore is selective: check config, secrets, TLS or individual data paths, and nothing
292
- else is touched
318
+ ---
293
319
 
294
- ### 🚚 One archive is a whole setup
320
+ ## πŸ’Ύ Backups
295
321
 
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.
299
-
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.
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.
303
324
 
304
325
  <details>
305
- <summary><b>πŸ—‚ Declaring data directories</b></summary>
326
+ <summary><b>🚚 One archive is a whole setup</b></summary>
306
327
 
307
- ```json
308
- {
309
- "dataEnvs": { "DATA_DIR": "{home}/.myapp" },
310
- "backupPaths": ["{home}/.myapp/uploads"]
311
- }
312
- ```
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.
313
331
 
314
- `dataEnvs` is exported to the process *and* backed up, so a data directory is declared once.
315
- 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.
316
335
 
317
336
  </details>
318
337
 
@@ -326,44 +345,22 @@ panel if yours breaks.
326
345
 
327
346
  Two ship in this repo: `uis/stock`, and `uis/noc-console` for TUI and shortcuts wizards; a release
328
347
  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.
348
+ the server never cares what built it.
331
349
 
332
350
  <details>
333
351
  <summary><b>πŸ€– Or have an agent build the UI you actually want</b></summary>
334
352
 
335
353
  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:
354
+ specific:
337
355
 
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`.
356
+ > Help me build a UI for `home-hosted`: nostalgic game theme, including … features.
341
357
 
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.
358
+ [UI_CREATION.md](./UI_CREATION.md) has the endpoints, the SSE frames, the auth rules and a checklist.
345
359
 
346
360
  </details>
347
361
 
348
362
  ---
349
363
 
350
- ## 🌐 HTTP API
351
-
352
- The panel is a client of its own API, so everything is scriptable:
353
-
354
- | | |
355
- | --- | --- |
356
- | `GET /api/state` | the full snapshot: config, live status, host vitals |
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 |
359
- | `POST /api/servers/:id/{start,stop,restart}` | lifecycle |
360
- | `PATCH /api/servers/:id`, `PATCH /api/settings` | edit configuration |
361
- | `GET /api/logs`, `/api/backups`, `/api/notifications` | logs, archives, Telegram |
362
- | `GET /healthz` | no session needed β€” the one an external monitor wants |
363
- | `GET /api/metrics` | Prometheus text |
364
-
365
- ---
366
-
367
364
  ## ❓ FAQ
368
365
 
369
366
  <details>
@@ -383,9 +380,12 @@ the same way: every supervised process tree is stopped before the panel exits.
383
380
  </details>
384
381
 
385
382
  <details>
386
- <summary><b>Which ports does it use?</b></summary>
383
+ <summary><b>Nothing starts and the port is busy</b></summary>
387
384
 
388
- 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.
389
389
 
390
390
  </details>
391
391
 
@@ -395,9 +395,9 @@ Just the control panel, `3999` by default. Supervised servers use the ports you
395
395
  `$HHOSTED_HOME`, default `~/.home-hosted`:
396
396
 
397
397
  ```text
398
- 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
399
399
  servers.config.schema.json regenerated on every start, for editor autocomplete
400
- .control-secrets.json password hash + Telegram token (mode 0600)
400
+ .control-secrets.json password hash + API token hash + Telegram token (mode 0600)
401
401
  .logs/ rotated per-server logs + history
402
402
  .tls/ an uploaded PEM pair
403
403
  .backups/ zip archives
@@ -409,19 +409,18 @@ run.json the running panel (pid, url, token, mode 0600)
409
409
  </details>
410
410
 
411
411
  <details>
412
- <summary><b>Windows support, really?</b></summary>
412
+ <summary><b>Which ports does it use?</b></summary>
413
413
 
414
- Yes. Process trees are sampled from Win32_Process, termination uses `taskkill /T`, and the
415
- shipped examples avoid POSIX-only commands. CPU temperature and swap are best-effort where the
416
- 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.
417
415
 
418
416
  </details>
419
417
 
420
418
  <details>
421
- <summary><b>Nothing starts and the port is busy</b></summary>
419
+ <summary><b>Windows support, really?</b></summary>
422
420
 
423
- A supervised server whose port is taken is reported rather than started over β€” the panel names
424
- 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.
425
424
 
426
425
  </details>
427
426
 
@@ -429,12 +428,9 @@ the holder. The control port itself is checked before the listener is opened.
429
428
 
430
429
  ## πŸ—‚ Working on it
431
430
 
432
- <details>
433
- <summary><b>Layout, scripts and conventions</b></summary>
434
-
435
431
  ```text
436
432
  src/ control plane: config, supervisor, API, providers, services
437
- 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)
438
434
  src/index.ts the control plane itself, used by `up --foreground`
439
435
  uis/ UIs: `stock` (shipped) and alternatives β€” any framework, static output
440
436
  bin/ the published entry point
@@ -442,8 +438,18 @@ bin/ the published entry point
442
438
 
443
439
  `pnpm dev` runs the panel with `tsx watch` plus the stock UI's dev server (state goes to
444
440
  `.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.
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 |
447
453
 
448
454
  </details>
449
455
 
package/SERVERS.md ADDED
@@ -0,0 +1,101 @@
1
+ # Servers
2
+
3
+ Every supervised process is one entry in `$HHOSTED_HOME/servers.config.json`. Add it with **βž• Add
4
+ server** in the panel, or write it by hand β€” the panel writes the same file.
5
+
6
+ ```json
7
+ {
8
+ "id": "myapp",
9
+ "command": "node",
10
+ "args": ["server.js"],
11
+ "cwd": "./apps/myapp",
12
+ "port": 8080,
13
+ "autostart": true,
14
+ "env": { "NODE_ENV": "production" },
15
+ "health": { "mode": "http", "http": { "path": "/healthz" } }
16
+ }
17
+ ```
18
+
19
+ Relative paths resolve against the directory you ran `home-hosted` from, so a project can keep its
20
+ servers and its state together.
21
+
22
+ ## Field reference
23
+
24
+ | field | what it does |
25
+ | --- | --- |
26
+ | `command`, `args`, `cwd` | what to run, with `{placeholders}` resolved per entry |
27
+ | `env`, `dataEnvs`, `envFile` | environment; `dataEnvs` also marks data directories for backups, `envFile` keeps secrets out of the config |
28
+ | `port`, `bind` | enables the readiness wait, health checks and the conflict preflight; `local` keeps it on `127.0.0.1` |
29
+ | `onPortConflict` | `block` (default), `warn`, or `adopt` β€” see below |
30
+ | `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body) |
31
+ | `health.unhealthyThreshold`, `forceRestartAfterMs` | how many failed probes before the card warns, and when to restart anyway |
32
+ | `restart.*` | backoff: `maxRetries`, `baseDelayMs`, `factor`, `maxDelayMs`, `resetAfterMs` |
33
+ | `stop.*` | `signal`, `killGroup`, `graceMs`, and `killPortHolders` to sweep a leftover listener |
34
+ | `dependsOn` | ids that must be healthy first; stopped in reverse order |
35
+ | `resources.maxRssBytes` | restart when the process tree grows past a limit |
36
+ | `bootstrap` | one command to run once before the first start (migrations, warmups) |
37
+ | `backupPaths` | extra paths this entry owns, included in backups |
38
+
39
+ ### Placeholders
40
+
41
+ | placeholder | resolves to |
42
+ | --- | --- |
43
+ | `{id}` `{label}` | the entry's id, and its label (falling back to the id) |
44
+ | `{port}` | its configured port, empty when it has none |
45
+ | `{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) |
46
+ | `{lanIp}` | this machine's LAN address, or `127.0.0.1` |
47
+ | `{cwd}` | the entry's working directory (its `cwd`, else the project directory) |
48
+ | `{projectDir}` | where you ran `home-hosted` (or `--project`) β€” `$HHOSTED_PROJECT` |
49
+ | `{dataRoot}` | the state directory β€” `$HHOSTED_HOME`, default `~/.home-hosted` |
50
+ | `{home}` | the current user's home directory |
51
+
52
+ Unknown placeholders stay visible instead of silently becoming empty, and `${VAR}` reads from the
53
+ environment (including `envFile`). Every supervised process also gets `HHOSTED_SERVER_ID` and
54
+ `HHOSTED_CONTROL_PORT`, so a service can tell which entry it is and how to reach the panel.
55
+
56
+ `dataEnvs` is the one that pays for itself twice: the value is exported to the process *and* the path
57
+ is picked up by Backups, so a data directory is declared once.
58
+
59
+ ## A busy port
60
+
61
+ Preflight runs before every start, so two servers cannot silently fight over one port.
62
+
63
+ | `onPortConflict` | what happens when something already listens |
64
+ | --- | --- |
65
+ | `block` *(default)* | the entry goes to **conflict** with `port 4000 is already in use (pid 4242)`, and does not start |
66
+ | `warn` | it starts anyway β€” useful when the listener is a leftover you are replacing |
67
+ | `follow` | if the listener is a **detached restart of this same entry**, the panel adopts it as-is; anything else blocks, exactly like `block` |
68
+ | `reclaim` | same detection, but it stops that successor and starts a fully supervised process of its own |
69
+
70
+ ### When a program restarts itself
71
+
72
+ Some programs restart themselves by launching a new detached process and exiting β€” a plugin doing an
73
+ update, a `re-exec` on config change. The panel used to see the successor's port as a conflict and sit
74
+ there blocked while the service was actually up.
75
+
76
+ Every process gets `HHOSTED_SERVER_ID` in its environment and a successor inherits it, so the preflight
77
+ can tell a successor from a stranger (Linux `/proc`, macOS `ps -E`; Windows has no per-process
78
+ environment, so there you get the conflict banner and the free-port button). Two policies act on that:
79
+
80
+ | policy | what it does | trade-off |
81
+ | --- | --- | --- |
82
+ | `follow` | adopts the successor: pid, health probe, CPU/RSS, stop, and it starts its own process again when the successor exits | **keeps exactly what the program set up**, but its output is not captured β€” the pipe belongs to whoever spawned it, so that entry's log in the panel goes quiet |
83
+ | `reclaim` | stops the successor, then starts a fully supervised process of its own | **full features** β€” live logs, resources, stop semantics all behave like any other entry β€” at the cost of one restart |
84
+
85
+ Both are shown as **detached** in the panel while adopted, both can be stopped and restarted like any
86
+ other entry, and neither will ever start a second copy on top of a **stranger**: that still blocks,
87
+ exactly like `block`.
88
+
89
+ If the holder is ours under a different policy, the conflict message says so, which is how the setting
90
+ is discovered:
91
+
92
+ ```text
93
+ port 4374 is already in use (pid 912) β€” pid 912 is a detached restart of this entry:
94
+ set onPortConflict to "follow" to adopt it, or "reclaim" to replace it with a supervised process
95
+ ```
96
+
97
+ ## Editing fields
98
+
99
+ Changes from the panel are atomic and validated before they are written. Editing an entry in the file
100
+ by hand is picked up without a restart; a value the schema rejects is refused with the exact path, and
101
+ the panel keeps running on the config it already had.