home-hosted 0.7.3 → 0.7.4

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
@@ -277,10 +277,12 @@ either is a last resort, and never an accidental one.
277
277
  boot through `.state/<id>.json` before any port preflight (`nannyIsAlive` = live pid **and** a fresh
278
278
  heartbeat, the `HHOSTED_SERVER_ID` marker or the argv), tails that file for live logs, and reads
279
279
  `lastExit` once so a crash nobody watched is reported. The spawn spec is consumed by the read and
280
- swept at boot — it carries expanded env. Two lifetimes are pinned: the nanny **exits with its child**
280
+ swept at boot — it carries expanded env. Three lifetimes are pinned: the nanny **exits with its child**
281
281
  (an inherited pipe would keep it alive, and the panel would report a healthy entry whose server is a
282
- detached stranger), and a stop reaches the child by pid from the state file, because `SIGKILL` and
283
- `killGroup: false` cannot be forwarded — which is why a self-restarting program belongs on `reclaim`.
282
+ detached stranger), a stop reaches the child by pid from the state file, because `SIGKILL` and
283
+ `killGroup: false` cannot be forwarded — which is why a self-restarting program belongs on `reclaim` —
284
+ and its `SIGTERM`/`SIGINT` traps are armed *before* the child exists and before that state file
285
+ appears, so a stop that beats the spawn can never orphan a child nobody has a record of.
284
286
  The nanny never restarts anything: retries and health stay the supervisor's. `logs.persist: false`
285
287
  keeps that file out of the Logs page but never stops it being written — it is the transport.
286
288
  - **A port is only ever freed by re-listing its listeners.** `POST /api/servers/:id/free-port` never
package/README.md CHANGED
@@ -25,7 +25,7 @@ them, watches them, restarts what dies, and shows you one page of what is going
25
25
 
26
26
  <div align="center">
27
27
 
28
- ![Six views of the panel: the stock UI, the NOC-console, and UI examples](docs/media/tour.gif)
28
+ ![Eight views of the panel: the stock UI and its reverse proxy, the NOC-console, and UI examples](docs/media/tour.gif)
29
29
 
30
30
  <sub>The stock panel, the [NOC-console](./uis/noc-console) that ships alongside it (for TUI and
31
31
  shortcuts wizards), and UI directions you could build yourself —
@@ -163,13 +163,13 @@ Worked example, with per-server data inside the project:
163
163
  <sub>Call them as `pnpm run up` — `pnpm up` is pnpm's own update, not your script.</sub>
164
164
 
165
165
  > [!TIP]
166
- > Give a project its own panel port (`control.port`, e.g. `4399`) — the default `3999` is what the
167
- > global instance and every other project also want.
166
+ > Recommended to give a project its own panel port (`control.port`, e.g. `4399`), to avoid port conflict
167
+ > with other projects.
168
168
 
169
169
  </details>
170
170
 
171
171
  <details>
172
- <summary><b>🧰 Run it under systemd or Docker (no daemon needed)</b></summary>
172
+ <summary><b>🧰 Run it under systemd or Docker</b></summary>
173
173
 
174
174
  ```bash
175
175
  home-hosted up --foreground # stays in the foreground, logs to stderr
@@ -179,12 +179,16 @@ home-hosted up --foreground # stays in the foreground, logs to stderr
179
179
  [Unit]
180
180
  Description=home-hosted
181
181
  [Service]
182
+ User=$YOUR_USER
182
183
  ExecStart=/usr/local/bin/home-hosted up --foreground
183
184
  Environment=HHOSTED_HOME=/srv/home-hosted
184
185
  Restart=always
185
186
  [Install]
186
187
  WantedBy=multi-user.target
187
188
  ```
189
+ > [!CAUTION]
190
+ > Note: change `$YOUR_USER` to your actual user account, or remove for `root`.
191
+ > You could also change `/srv/home-hosted` to change where the data stores.
188
192
 
189
193
  </details>
190
194
 
@@ -231,8 +235,7 @@ default workspace.
231
235
  | `GET /healthz` | no session needed — the one an external monitor wants (its per-server detail needs a credential) | ❌ |
232
236
  | `GET /api/metrics` | Prometheus text (needs a token or session, like every `/api` route) | ❌ |
233
237
 
234
- ✅ takes `?workspace=<id>`; ❌ is panel-wide. Omitting it means the panel's default workspace — never
235
- another one.
238
+ ✅ takes `?workspace=<id>`; ❌ is panel-wide. Omitting `?workspace` means `default` workspace is used.
236
239
 
237
240
  `GET /openapi/spec.json` describes all of it, `/openapi/ui` is the browsable version, and every error
238
241
  comes back as one envelope (`{ message, code, detail }`) with a stable `code` a tool can branch on.
@@ -273,7 +276,7 @@ can be told what to be: *"Help me build a UI for home-hosted: nostalgic game the
273
276
  | 🔔 **Notifications** | Telegram on crash, unhealthy, forced restart, recovery, host thresholds and DNS changes — [setup here](./docs/NOTIFICATIONS.md). |
274
277
  | 🌐 **Dynamic DNS** | Keep hostnames pointed at your public IP — Cloudflare, Namecheap, Spaceship, Porkbun, GoDaddy, Gandi and more. [DDNS.md](./docs/DDNS.md) |
275
278
  | 🔀 **Reverse proxy** | One engine in front of everything, with automatic HTTPS: `git.example.com` → a Gitea entry, `media.example.com` → a service on another box, `panel.example.com` → the panel. Caddy, installed and supervised by the panel. [REVERSE_PROXY.md](./docs/REVERSE_PROXY.md) |
276
- | 💾 **Backups** | Two levels: pick global settings, global secrets, TLS or a whole workspace at the top, then the pieces inside it (settings, servers, secrets, data directories) — plain `.zip`, or AES-256 with a password, restored per path. |
279
+ | 💾 **Backups** | Deep and customizable: pick global settings, global secrets, TLS, Workspaces, then the pieces inside it (workspace settings, servers, secrets, data directories) — plain `.zip`, or AES-256 with a password, customized restore is supported too! |
277
280
  | 🎨 **BYOU — Bring Your Own UI** | Upload a static build, `ui-update` to follow its releases, `ui-revert` to go back. [UI_CREATION.md](./docs/UI_CREATION.md) |
278
281
  | 🔐 **Security** | Cookie sessions, API tokens, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
279
282
  | 🧩 **No special treatment** | A server is `command` + `args` + `env` + `cwd`; nothing is built in for any particular app. |
@@ -283,27 +286,17 @@ can be told what to be: *"Help me build a UI for home-hosted: nostalgic game the
283
286
 
284
287
  ## 🗂 Workspaces
285
288
 
286
- A workspace is the ownership boundary: its own servers, secrets, logs, nanny state and settings. The
287
- header picker is the whole control — choose one, create one, rename it, or delete it (which stops
288
- what it supervises first). The first boot creates a `default` workspace; a pre-workspaces instance is
289
- relocated into one automatically.
290
-
291
- > [!IMPORTANT]
292
- > **Upgrading from before workspaces (0.6 → 0.7)?** That release is breaking: state moves under
293
- > `$HHOSTED_HOME/.hh`, settings split global vs. per-workspace, and backups become two-level. The
294
- > relocation runs on the next command — `home-hosted migrate` reports it and stamps every file. See
295
- > [Upgrading](#-cli).
289
+ A workspace is the ownership boundary: its own servers, secrets, logs, states and settings.
290
+ The first boot creates a `default` workspace.
296
291
 
297
292
  | page | url | scope |
298
293
  | --- | --- | --- |
299
294
  | **Overview** | `/w/<workspace>` | the selected workspace: its servers, counts and live status |
300
295
  | **Servers** · **Logs** · **Workspace settings** | `/w/<workspace>/servers` · `/w/<workspace>/logs` · `/w/<workspace>/settings` | the selected workspace |
296
+ | **One server** | `/w/<workspace>/servers/<id>` | that entry: config, live logs, resources |
301
297
  | **Global Overview** | `/global/overview` | host vitals plus every server from every workspace |
302
298
  | **Global settings** | `/global/settings` | Listener, Authentication, Host vitals, Backups, TLS, Interface, Paths |
303
-
304
- Every page is bookmarked by its own URL: the workspace id is part of it, so a link opens the same
305
- workspace even in a browser that never selected it. `/` opens **Global Overview**. The sidebar lists
306
- the panel-wide pages first, then the selected workspace's.
299
+ | **Reverse Proxy** | `/proxy` | panel-wide: one engine, every workspace's servers ([REVERSE_PROXY.md](./docs/REVERSE_PROXY.md)) |
307
300
 
308
301
  <details>
309
302
  <summary><b>🗂 What lives where</b></summary>
@@ -334,6 +327,7 @@ An entry is a few lines. Add one with **➕ Add server**, or write it into the s
334
327
  ```
335
328
 
336
329
  `dataEnvs` declares a data directory once: it is exported to the process *and* picked up by Backups.
330
+
337
331
  **Every field, every placeholder, the port-conflict policies (including adopting a server that
338
332
  restarts itself), and how hand-edits are validated: [SERVERS.md](./docs/SERVERS.md).**
339
333
 
@@ -352,9 +346,9 @@ restarts itself), and how hand-edits are validated: [SERVERS.md](./docs/SERVERS.
352
346
  | `home-hosted set-password` | set the panel password without opening a browser |
353
347
  | `home-hosted set-token` | set the API token scripts and agents use (`--generate`, `--clear`) |
354
348
  | `home-hosted migrate` | relocate a pre-workspaces state directory and stamp every config for this release (`--dry-run`, `--yes`) |
355
- | `home-hosted init` | scaffold a project that keeps `state/` and its data in the repo |
349
+ | `home-hosted init` | scaffold a self-contained project (`HHOSTED_HOME` sets to the repo) |
356
350
  | `home-hosted ui-switch` | install a UI from a release asset, a zip file or a URL (interactive) |
357
- | `home-hosted ui-update` | bring an installed UI up to date, or pick a release (`--old`, `--check`) |
351
+ | `home-hosted ui-update` | bring an installed UI up to date automatically (official UI) or pick a release (`--old`, `--check`) |
358
352
  | `home-hosted ui-revert` | go back to the stock panel UI after uploading your own |
359
353
 
360
354
  <details>
@@ -390,16 +384,6 @@ for a launcher that pins one; a workspace picked in the UI keeps its own. `start
390
384
  <details>
391
385
  <summary><b>🧭 Upgrading, and why the panel sometimes refuses to start</b></summary>
392
386
 
393
- > **Breaking in 0.7 — workspaces.** State moved under `$HHOSTED_HOME/.hh`: global files at its top
394
- > level, one directory per workspace. Settings split into panel-wide **Global settings** and
395
- > per-workspace **Workspace settings**, backups became two-level, and `--config` now points at the
396
- > default workspace's servers file. The old `servers.config.json` becomes
397
- > `.hh/default/servers.config.json`.
398
-
399
- Relocation is automatic: the CLI relocates a pre-`.hh` `$HHOSTED_HOME` before any command runs, so an
400
- existing instance keeps working after the upgrade. `home-hosted migrate` reports what it moved and
401
- stamps every file for this release:
402
-
403
387
  ```bash
404
388
  home-hosted migrate --dry-run # print the steps, write nothing
405
389
  home-hosted migrate # ask, then write — keeps a .bak beside each rewritten file
@@ -582,7 +566,10 @@ run.json the running panel (pid, url, token, mode 0600)
582
566
  <details>
583
567
  <summary><b>Which ports does it use?</b></summary>
584
568
 
585
- Just the control panel, `3999` by default. Supervised servers use the ports you give them.
569
+ The control panel, `3999` by default. Supervised servers use the ports you give them. The reverse
570
+ proxy is the one panel-wide listener, and only when you switch it on: `80`/`443` for automatic HTTPS,
571
+ or `4480`/`4443` unprivileged with your router forwarding to them
572
+ ([REVERSE_PROXY.md](./docs/REVERSE_PROXY.md#ports)).
586
573
 
587
574
  </details>
588
575
 
@@ -634,7 +621,7 @@ plus types; `pnpm test` is vitest; `pnpm run media` regenerates the GIF above.
634
621
  <details>
635
622
  <summary><b>🔗 Interesting resources</b></summary>
636
623
 
637
- - [dsh-home-hosted](https://github.com/NamesMT/dsh-home-hosted) — home-hosted servers management with boot autostart from inside [DeepSeek Harness](https://github.com/deepseek-ai/dsh)
624
+ - [dsh-home-hosted](https://github.com/NamesMT/dsh-home-hosted) — home-hosted servers management with boot autostart from inside [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
638
625
 
639
626
  <sub><i>+ PR to add yours</i></sub>
640
627
 
package/dist/cli.js CHANGED
@@ -17657,34 +17657,42 @@ async function runNanny(spec, statePath) {
17657
17657
  text
17658
17658
  });
17659
17659
  };
17660
+ let stopping = false;
17661
+ let pending = null;
17662
+ let spawned = null;
17663
+ const stopChild = (child, signal) => {
17664
+ emit("system", `${signal} — stopping this entry (grace ${spec.stop.graceMs}ms)`);
17665
+ terminate(child, {
17666
+ signal: spec.stop.signal,
17667
+ killGroup: false,
17668
+ graceMs: spec.stop.graceMs
17669
+ });
17670
+ };
17671
+ const onSignal = (signal) => {
17672
+ if (stopping) return;
17673
+ stopping = true;
17674
+ if (spawned === null) pending = signal;
17675
+ else stopChild(spawned, signal);
17676
+ };
17677
+ process.on("SIGTERM", () => onSignal("SIGTERM"));
17678
+ process.on("SIGINT", () => onSignal("SIGINT"));
17660
17679
  const child = spawnManaged({
17661
17680
  command: spec.command,
17662
17681
  args: spec.args,
17663
17682
  cwd: spec.cwd,
17664
17683
  env: spec.env
17665
17684
  }, { detached: false });
17685
+ spawned = child;
17666
17686
  childPid = child.pid ?? null;
17667
17687
  persistState();
17668
17688
  emit("system", `persistent: nanny pid ${process.pid} runs this entry`);
17689
+ if (pending !== null) stopChild(child, pending);
17669
17690
  const stdout = new LineSplitter((stream, text) => emit(stream, text));
17670
17691
  const stderr = new LineSplitter((stream, text) => emit(stream, text));
17671
17692
  child.stdout?.on("data", (chunk) => stdout.push("stdout", chunk));
17672
17693
  child.stderr?.on("data", (chunk) => stderr.push("stderr", chunk));
17673
17694
  const heartbeat = setInterval(persistState, NANNY_HEARTBEAT_MS);
17674
17695
  heartbeat.unref();
17675
- let stopping = false;
17676
- const onSignal = (signal) => {
17677
- if (stopping) return;
17678
- stopping = true;
17679
- emit("system", `${signal} — stopping this entry (grace ${spec.stop.graceMs}ms)`);
17680
- terminate(child, {
17681
- signal: spec.stop.signal,
17682
- killGroup: false,
17683
- graceMs: spec.stop.graceMs
17684
- });
17685
- };
17686
- process.on("SIGTERM", () => onSignal("SIGTERM"));
17687
- process.on("SIGINT", () => onSignal("SIGINT"));
17688
17696
  await new Promise((resolve) => {
17689
17697
  let settled = false;
17690
17698
  const finish = (code, signal, runtimeMs, note) => {