home-hosted 0.7.16 → 0.7.18

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/docs/DDNS.md CHANGED
@@ -30,7 +30,9 @@ credentials button saves on its own, being a secret. Provider credentials never
30
30
 
31
31
  Credentials live in the workspace's `.secrets.json` (0600), keyed by the account `id` and sealed with
32
32
  AES-256-GCM under `HHOSTED_DDNS_SECRET` — set your own, or the default `hh` only stops a casual
33
- read. The block above is the workspace's `.hh/<workspace>/settings.json`; the credentials are cached
33
+ read. The panel logs a warning when it seals credentials under that default, so a copied state
34
+ directory is not the first time you hear about it. The block above is the workspace's
35
+ `.hh/<workspace>/settings.json`; the credentials are cached
34
36
  separately in `.secrets.json`, both beside that workspace's `servers.config.json`. The account does
35
37
  not have to be saved first, and the provider is stored with the entry, so an id that later changes
36
38
  provider does not silently keep the old secret. Removing an account drops its credentials on the next
@@ -49,7 +51,9 @@ Header fields:
49
51
  | `domains[]` | — | `host`, `account`, `types` (`A`/`AAAA`, default `A`), `proxied` (default off; Cloudflare only), optional `ttl`, `zone`, `enabled`. |
50
52
 
51
53
  Per-hostname `zone` pins the registered domain — only needed where a provider has to be told the apex
52
- and the guess is wrong (`home.example.co.uk` is handled; an unusual suffix may not be).
54
+ and the guess is wrong (`home.example.co.uk` is handled; an unusual suffix may not be). It must be a
55
+ domain the host is **inside**: a typo is refused on save, because the record name would otherwise become
56
+ the whole hostname and the write would go to a domain that is not yours.
53
57
 
54
58
  ## Providers
55
59
 
@@ -67,13 +67,18 @@ panel did not start is ever killed.
67
67
  | `url` | For `target: "external"`, e.g. `http://10.0.0.5:8080`. The port is optional: without one the scheme's own default is used (`:80` for `http:`, `:443` for `https:`). An `https://` upstream is dialled with TLS, and **its certificate is not verified** — a homelab upstream behind a self-signed certificate works, and so does an impostor. |
68
68
  | `path` | Optional prefix, so one hostname can serve several apps. |
69
69
  | `tls` | `auto` (default), `off`, `manual`. |
70
+ | `dnsAccount` | Optional `workspace/account` for a DNS-01 challenge, referencing a DNS account from Settings. Empty picks the workspace's only account that can write TXT records, so a single-account setup needs nothing here. |
70
71
 
71
72
  The list's hostname is a link to where the route answers — `https://host:<https port>`, or
72
73
  the plain port for `tls: "off"`, path included. A route that is switched off is plain text.
73
74
 
74
75
  A route pointing at the panel is exposure, and answers to the same rule as binding the
75
76
  listener beyond loopback: authentication on and a non-default password, or the save is
76
- refused.
77
+ refused. `tls` does not change that bar — it is about who can reach the panel, not whether
78
+ the hop is encrypted, and `tls: "off"` is if anything the worse case. The engine upstreams a
79
+ panel route to `127.0.0.1`, which the panel's own guard reads as a local request, so a
80
+ plain-HTTP route with no password set would serve the panel to anyone who reached that
81
+ hostname.
77
82
 
78
83
  ## HTTPS
79
84
 
@@ -143,9 +148,11 @@ route still serves it — so the two can never disagree.
143
148
 
144
149
  Certificates are read, never edited: the panel reports each pair's subject, issuer,
145
150
  expiry and the hostnames it covers (from its SANs, wildcards included), and marks a pair
146
- no route would be served from. An expired pair is never served — the route that asks for
147
- it is refused with the reason. An uploaded pair is re-read as soon as its file changes,
148
- so editing or replacing one outside the panel is picked up on the next look; the engine's
151
+ no route would be served from. A pair outside its validity window — expired, or not valid
152
+ yet — is never served, and the route that asks for it is refused with the reason; a pair
153
+ whose key does not match its certificate is refused the same way. An uploaded pair is
154
+ re-read as soon as its file changes, so editing or replacing one outside the panel is
155
+ picked up on the next look; the engine's
149
156
  own certificate store (the issued certificates) is read at most every 15 seconds.
150
157
 
151
158
  Certificates are managed by the engine, inside `.hh/.proxy/engine/`, and nothing else on
package/docs/SERVERS.md CHANGED
@@ -31,14 +31,16 @@ servers and its state together.
31
31
  | --- | --- |
32
32
  | `command`, `args`, `cwd` | what to run, with `{placeholders}` resolved per entry |
33
33
  | `env`, `dataEnvs`, `envFile` | environment; `dataEnvs` also marks data directories for backups, `envFile` keeps secrets out of the config |
34
+ | `enabled` | `true` (default) starts it; `false` keeps the entry in the config but refuses to start it, stops it if it is running, and leaves a disabled `persistent` entry unattached at boot |
34
35
  | `port`, `bind` | enables the readiness wait, health checks and the conflict preflight; `local` keeps it on `127.0.0.1` |
35
36
  | `onPortConflict` | `block` (default), `warn`, `follow`, `reclaim`, or `kill` — see below |
36
37
  | `persistent` | run it under its own nanny so it survives the panel — see below |
37
- | `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body) |
38
+ | `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body — a `HEAD` carries no body, so it is warned about and the body check is skipped) |
38
39
  | `health.unhealthyThreshold`, `forceRestartAfterMs` | how many failed probes before the card warns, and when to restart anyway |
39
40
  | `restart.*` | backoff: `maxRetries`, `baseDelayMs`, `factor`, `maxDelayMs`, `resetAfterMs` |
40
41
  | `stop.*` | `signal`, `killGroup`, `graceMs`, and `killPortHolders` to sweep a leftover listener |
41
- | `dependsOn` | ids that must be healthy first; stopped in reverse order |
42
+ | `dependsOn` | ids that must be healthy first. `stop-all`/`down` stop dependents before their dependencies; stopping **one** server does not cascade — the log names any dependent it left running |
43
+ | `logBufferLines` | how many lines of live output the panel keeps in memory per entry (50–100000, default 500). The file on disk is separate: this is the buffer the Logs page streams from |
42
44
  | `resources.maxRssBytes` | restart when the process tree grows past a limit |
43
45
  | `bootstrap` | one command to run once before the first start (migrations, warmups) |
44
46
  | `backupPaths`, `backupIgnoreGenerated` | extra paths this entry owns, included in backups; the flag (on by default) skips the known build and dependency directories inside them — `node_modules`, `dist`, `.next`, framework caches |
@@ -167,7 +169,10 @@ What that changes:
167
169
  workspace's `.logs/`, because there is no pipe to carry the output.
168
170
  - **A crash while the panel is away is reported, not restarted.** Retries and backoff remain the
169
171
  panel's job, so an entry that dies with nobody watching shows up as **crashed** on the next boot
170
- with the exit code in its log.
172
+ with the reason in its log — the exit code, explained when it has a conventional meaning
173
+ (`code 137 (SIGKILL — which the kernel sends when memory runs out)`), because that is the one an
174
+ operator would otherwise have to look up. `home-hosted status` prints the directory those files
175
+ live in, and names the `<id>.log` convention once there is one to read.
171
176
  - **Port policies still apply, as a fallback.** A persistent entry reattaches from its state file
172
177
  before any preflight runs, so `onPortConflict` matters only when that state is gone — and `follow`
173
178
  is then the policy that adopts instead of blocking.
@@ -133,6 +133,7 @@ naming one that does not exist is a `404 UNKNOWN_WORKSPACE`, never a silent fall
133
133
  | `GET /api/servers/:id/stream` | one server's `server` + `log` frames |
134
134
  | `POST /api/servers/:id/{start,stop,restart}`, `/api/servers/{start-all,stop-all}` | lifecycle (workspace-scoped) |
135
135
  | `POST /api/servers/:id/free-port` | ask whatever holds that server's port to stop (`409` when nothing can be freed — a supervised listener is refused, never killed) |
136
+ | `POST /api/servers/:id/clear-logs` | forget the buffered lines; the file on disk is cleared separately through `DELETE /api/logs/:id` |
136
137
  | `GET` / `POST /api/servers`, `PATCH` / `DELETE /api/servers/:id` | the entries themselves, in one workspace |
137
138
  | `GET /api/logs`, `/api/logs/:id?tail=&search=`, `/api/logs/:id/download?file=` | persisted logs, for one workspace |
138
139
  | `GET` / `PATCH /api/settings` | the panel-wide settings (`control.label`, host thresholds, backups, UI) |
@@ -141,7 +142,9 @@ naming one that does not exist is a `404 UNKNOWN_WORKSPACE`, never a silent fall
141
142
  | `POST` / `DELETE /api/settings/tls` | upload or clear a PEM pair |
142
143
  | `GET` / `PATCH /api/proxy`, `POST /api/proxy/{engine,start,stop,apply,revert}`, `PUT` / `DELETE /api/proxy/certificates/:id`, `POST /api/proxy/routes/:id/retry-certificate` | the panel-wide reverse proxy: engine, route table, its uploaded PEM pairs. Its live view also rides in the state frame as an optional `proxy` — read it defensively |
143
144
  | `GET` / `POST /api/backups`, `/api/backups/restore`, `/api/backups/:name/download` | archives; entries are global (`global:settings`, `global:secrets`, `global:tls`) or per workspace, whose leaves are `workspace:<id>:settings`, `:servers`, `:secrets` plus `data:<path>`; a create/restore body `include` selects them |
145
+ | `POST` / `DELETE /api/auth/password` | set the panel password, or clear it and disable auth. `POST` needs the current one unless none is set; an empty password is refused |
144
146
  | `POST` / `DELETE /api/notifications/token`, `/api/notifications/test`, `/detect-chats` | Telegram, per workspace |
147
+ | `GET` / `PUT /api/ddns`, `PUT` / `DELETE /api/ddns/credentials/:id`, `POST /api/ddns/check` | dynamic DNS, per workspace: the policy plus live per-record state, one account's sealed credentials (write-only — the view exposes ids and whether each is set, never the values), and a pass run now |
145
148
  | `POST /api/auth/login`, `GET /api/auth/session`, `POST /api/auth/logout` | the session |
146
149
  | `GET /healthz` | liveness — **no session**, and `503` when an autostart server has crashed |
147
150
  | `GET /api/metrics` | Prometheus text (needs a session) |
@@ -185,10 +188,14 @@ session, 403 bad token or origin, 404 unknown id, 409 conflict, 413 too large.
185
188
  | --- | --- |
186
189
  | `hello` | `{ ts, state }` — the first frame, the complete snapshot |
187
190
  | `state` | `{ ts, state }` — anything changed: a status, a resource sample, host vitals, a workspace |
188
- | `server` | `{ ts, serverId, server }` — one entry, `server.workspaceId` says which workspace |
189
- | `log` | `{ ts, serverId, lines }` — new output (dropped under backpressure, never state) |
191
+ | `server` | `{ ts, workspaceId, serverId, server }` — one entry |
192
+ | `log` | `{ ts, workspaceId, serverId, lines }` — new output (dropped under backpressure, never state) |
190
193
  | `ping` | the current time, every 15 s |
191
194
 
195
+ `workspaceId` is on every `server` and `log` frame the panel sends, and the schema marks it optional only
196
+ so an **older** panel still parses: treat a frame without it as ambiguous and look the id up rather than
197
+ guessing, because a server id is unique only inside its workspace.
198
+
192
199
  `GET /api/events` takes `?logs=0` to skip log frames and `?serverId=<id>` to follow one server — but
193
200
  it matches on the id alone, so an id used in two workspaces mixes their frames. Use
194
201
  `GET /api/servers/:id/stream?workspace=<id>` for one server: it validates the pair and replays the
@@ -233,8 +240,10 @@ pnpm run build:uis # every UI, zipped; releases attach th
233
240
 
234
241
  Only `stock` ships inside the npm package; the rest are release assets you install from Global
235
242
  settings.
236
- `pnpm run quickcheck` type-checks every UI, and `pnpm test` picks up any `test/*.test.ts` you add
237
- (use relative imports — the `@` alias points at `stock`).
243
+ `pnpm run quickcheck` type-checks every UI, and `pnpm test` picks up any `test/*.test.ts` you add.
244
+ `@/…` resolves from the UI that asks — each UI's own `src`, so a test can import its own components
245
+ and logic directly; use `@shared/…` for what the panel and the UIs genuinely share
246
+ (`src/shared/`), and never import runtime server code (`@server` is types only).
238
247
 
239
248
  ## A worked example
240
249
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "home-hosted",
3
3
  "type": "module",
4
- "version": "0.7.16",
4
+ "version": "0.7.18",
5
5
  "packageManager": "pnpm@12.5.1",
6
6
  "description": "Self-hosted control panel that keeps your home-server processes alive: supervision, restarts, port conflicts, logs — and a UI you can replace.",
7
7
  "author": "NamesMT <dangquoctrung123@gmail.com>",
@@ -80,9 +80,10 @@
80
80
  "set-token": "tsx src/cli.ts set-token",
81
81
  "migrate": "tsx src/cli.ts migrate",
82
82
  "lint": "eslint .",
83
+ "test:docs": "node scripts/check-doc-links.mjs",
83
84
  "test": "vitest",
84
85
  "test:types": "tsc --noEmit && node scripts/typecheck-uis.mjs",
85
- "quickcheck": "pnpm run lint && pnpm run test:types",
86
+ "quickcheck": "pnpm run lint && pnpm run test:types && pnpm run test:docs",
86
87
  "check": "pnpm run quickcheck && vitest run --coverage",
87
88
  "prepare": "simple-git-hooks",
88
89
  "prepack": "pnpm run build",