home-hosted 0.6.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +75 -38
- package/README.md +127 -55
- package/dist/cli.js +3374 -1490
- package/dist/cli.js.map +1 -1
- package/docs/DDNS.md +19 -12
- package/docs/NOTIFICATIONS.md +11 -9
- package/docs/SERVERS.md +14 -8
- package/docs/UI_CREATION.md +36 -24
- package/package.json +1 -1
- package/uis/stock/dist/assets/index-Ocn66d5H.js +46 -0
- package/uis/stock/dist/assets/index-S1nhkKEu.css +2 -0
- package/uis/stock/dist/index.html +2 -2
- package/uis/stock/dist/ui.json +8 -0
- package/uis/stock/dist/assets/index-C6tbSbCK.js +0 -46
- package/uis/stock/dist/assets/index-Do5DvzS4.css +0 -2
package/AGENTS.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
3
|
`home-hosted` is a Node 24 / TypeScript harness for self-hosted servers: `up` starts a Hono/srvx
|
|
4
|
-
panel (default `127.0.0.1:3999`) that supervises the entries in
|
|
5
|
-
and serves a UI. User docs: `README.md`, `docs/SERVERS.md` (entries and port
|
|
6
|
-
`docs/NOTIFICATIONS.md`, `docs/DDNS.md`; UI authors: `docs/UI_CREATION.md`.
|
|
4
|
+
panel (default `127.0.0.1:3999`) that supervises the entries in each workspace's
|
|
5
|
+
`servers.config.json` and serves a UI. User docs: `README.md`, `docs/SERVERS.md` (entries and port
|
|
6
|
+
conflicts), `docs/NOTIFICATIONS.md`, `docs/DDNS.md`; UI authors: `docs/UI_CREATION.md`.
|
|
7
7
|
|
|
8
|
-
State lives only in `$HHOSTED_HOME` (default `~/.home-hosted`)
|
|
9
|
-
|
|
10
|
-
`.
|
|
11
|
-
|
|
12
|
-
`
|
|
13
|
-
|
|
8
|
+
State lives only in `$HHOSTED_HOME/.hh` (default `~/.home-hosted/.hh`). Global files sit at its top
|
|
9
|
+
level: `settings.json` (listener, auth, TLS policy, host vitals, backups), `workspaces.json`,
|
|
10
|
+
`.control-secrets.json` (0600: password hash, API token hash), `.tls/`, `.backups/`, `.ui/`, `.logs/`
|
|
11
|
+
(the panel console), and `run.json` — the live daemon's pid/url/token, 0600. Every workspace owns
|
|
12
|
+
`.hh/<id>/`: `settings.json` (server defaults, log retention, notifications, DDNS), `servers.config.json`,
|
|
13
|
+
`.secrets.json` (0600: Telegram bot token and DDNS credentials sealed with AES-256-GCM under
|
|
14
|
+
`HHOSTED_DDNS_SECRET`), `.logs/`, and `.state/` (a persistent entry's nanny state plus its 0600 spawn
|
|
15
|
+
spec until the nanny reads it, plus `ddns.json`). A pre-`.hh` instance is relocated automatically by
|
|
16
|
+
`ensureLayout()` (`src/config/layout.ts`). The package ships **no servers**: never commit a config, a
|
|
17
|
+
seed entry, or a path that names one.
|
|
14
18
|
|
|
15
19
|
## Commands
|
|
16
20
|
|
|
@@ -83,10 +87,15 @@ a minor without one.
|
|
|
83
87
|
- `src/api/**` — one file per URL group (`$.routes.ts` = several routes), mirroring the path.
|
|
84
88
|
- `src/shared/contracts.ts` — every ArkType schema (config, API and SSE DTOs), shared with the UIs;
|
|
85
89
|
the OpenAPI spec is generated from it, never hand-written.
|
|
86
|
-
- `src/config/` — `
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`
|
|
90
|
+
- `src/config/` — split by scope. `settings.ts` (`GlobalSettingsStore`: `.hh/settings.json` — listener,
|
|
91
|
+
auth, TLS policy, host vitals, backups) and `store.ts` (`WorkspaceStore`: a workspace's
|
|
92
|
+
`settings.json` + `servers.config.json`, validate/merge/atomic commit, reporting
|
|
93
|
+
`configError`/`configWarnings` instead of throwing on a bad file). `workspaces.ts` is the registry
|
|
94
|
+
(`workspaces.json`), `layout.ts` the one-time relocation of a pre-workspace `$HHOSTED_HOME`.
|
|
95
|
+
`schema.ts` holds the three on-disk shapes plus the `meta` stamp, `parse.ts` the tolerant readers
|
|
96
|
+
(`parseGlobalSettings`/`parseWorkspaceSettings`/`parseServersFile`: unknown keys are reported and
|
|
97
|
+
kept, everything else blocking), `patch.ts` the merge helpers, `migrations.ts` the schema constant
|
|
98
|
+
and ordered step registry, and `secrets.ts`/`seed.ts` the two secret scopes and the seeds.
|
|
90
99
|
- `src/providers/` — stateless leaves: `process` (spawn, `terminate`, `terminatePid` for a process we
|
|
91
100
|
adopted), `port` (probe, holder lookup, `terminatePids`), `proc` (the sampler, plus
|
|
92
101
|
`processCarriesServerId` for the environment marker and `processTreePids` for tree ownership),
|
|
@@ -95,12 +104,13 @@ a minor without one.
|
|
|
95
104
|
`log-tail` (an offset reader that survives rotation and truncation), `ddns/` (one `DdnsProvider` per
|
|
96
105
|
registrar over one HTTP call each, plus `ip` and `zone` helpers), health-check, host, telegram,
|
|
97
106
|
archive.
|
|
98
|
-
- `src/services/` — stateful orchestration:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`
|
|
103
|
-
|
|
107
|
+
- `src/services/` — stateful orchestration: `panel` (one `WorkspaceRuntime` per workspace, workspace
|
|
108
|
+
CRUD, and the single aggregate state frame — every per-workspace service hangs off it), supervisor,
|
|
109
|
+
control-server, config-watch, state, auth + exposure, dependencies, history, log-buffer/log-files,
|
|
110
|
+
notifications, host-monitor, ddns, backups, tls, ui, plus `init` (the scaffold behind
|
|
111
|
+
`home-hosted init`: a manifest, a `.gitignore`, and the prompts stay in the CLI), `nanny` (the
|
|
112
|
+
process a persistent entry runs under) and `log-relay` (the tailer that feeds its lines to the
|
|
113
|
+
supervisor). It names no server — the scaffold must stay as neutral as the supervisor.
|
|
104
114
|
- `src/middleware/auth.ts` — the `/api/*` guard, and `requestIdentity()`, the one place a request's
|
|
105
115
|
credentials are read: the `hh_session` cookie or `Authorization: Bearer <api token>`. A token is
|
|
106
116
|
a first-class credential (same authority as a signed-in browser) and is verified from the secrets
|
|
@@ -111,7 +121,7 @@ a minor without one.
|
|
|
111
121
|
- `uis/<name>/` — each UI is a Vite app (Vue 3 + Tailwind v4) built through `uis/vite.shared.ts`;
|
|
112
122
|
`stock` is the one shipped inside the package. Aliases: `@` → that UI's `src`, `@shared` →
|
|
113
123
|
`src/shared`, `@server` → `src` (**types only** — never import runtime server code into a UI).
|
|
114
|
-
Its `public/ui.json` is the UI's identity: `name` and `version` (what Settings shows),
|
|
124
|
+
Its `public/ui.json` is the UI's identity: `name` and `version` (what Global Settings → Interface shows),
|
|
115
125
|
`repo`/`tag`/`asset` (which release carries it, for `ui-update`) and `unix` (when it was built).
|
|
116
126
|
`UiService` carries those fields into the installed `$HHOSTED_HOME/.ui/ui.json` unchanged and adds
|
|
117
127
|
`uploadedAt`/`files` of its own.
|
|
@@ -140,7 +150,7 @@ a minor without one.
|
|
|
140
150
|
- A single on/off setting is a `ToggleSwitch`; `CheckField` is only for picking items out of a set
|
|
141
151
|
(the restore plan). A checkbox in a `FieldGroup` grid reads as misaligned next to the inputs.
|
|
142
152
|
- A create body carries only what differs from what the entry would inherit — the schema's defaults
|
|
143
|
-
with
|
|
153
|
+
with `Workspace Settings → Server defaults` on top (`inheritBaseline` + `diffFields` in both
|
|
144
154
|
UIs). A value written into `servers.config.json` stops following those defaults, so anything the
|
|
145
155
|
person did not decide stays out; the editor applies the same rule to an edit.
|
|
146
156
|
- ArkType at every runtime boundary: routes use `validate('json'|'query'|'param', schema)` then
|
|
@@ -149,7 +159,9 @@ a minor without one.
|
|
|
149
159
|
envelope `{ message, code, detail }`. Never hand-roll `c.json({ error })`.
|
|
150
160
|
- Document routes with `describeRoute` + `jsonBody(schema)`; `jsonBody` needs a real schema.
|
|
151
161
|
- Patch schemas carry no defaults; nested groups (`restart`/`health`/`stop`/`auth`/`tls`/`telegram`/
|
|
152
|
-
`http`) merge key-by-key, and an explicit `null` clears a key.
|
|
162
|
+
`http`) merge key-by-key, and an explicit `null` clears a key. `ddns` is the exception: its
|
|
163
|
+
`accounts`/`domains` lists are **replaced** (a merge cannot express removing a hostname), and only
|
|
164
|
+
`ipv4`/`ipv6` merge.
|
|
153
165
|
- Two-sided bounds read inclusively (`'1 <= number.integer <= 512'`). `test/shared/contracts.test.ts`
|
|
154
166
|
pins every boundary and the patch/schema parity — update it with any schema change.
|
|
155
167
|
- A new field in a **response** DTO is optional (`'x?'`) and its clients read it defensively. An
|
|
@@ -165,6 +177,12 @@ a minor without one.
|
|
|
165
177
|
- A destructive action that is one click away confirms in a **popover** (`KillPortButton.vue`),
|
|
166
178
|
never by arming the same button for a second press: an impatient double click on an arming
|
|
167
179
|
button fires it. Keep the safe choice first in the popover's tab order.
|
|
180
|
+
- **Docs ship with the change.** A user-visible change updates the docs that describe it
|
|
181
|
+
(`README.md`, `docs/*.md`) in the same commit — do not forget them; a stale doc is a bug like
|
|
182
|
+
stale code.
|
|
183
|
+
- **UIs move together.** `uis/stock` is not the only client: a change to it — or to a shared
|
|
184
|
+
contract it reads — lands in every other UI under `uis/`, and each altered UI bumps its
|
|
185
|
+
`ui.json` per the rule above.
|
|
168
186
|
|
|
169
187
|
## Compatibility
|
|
170
188
|
|
|
@@ -194,10 +212,14 @@ either is a last resort, and never an accidental one.
|
|
|
194
212
|
- **Unknown keys are dropped from the resolved config, kept on disk, and listed in a startup
|
|
195
213
|
warning.** Dropping one is the normal way a newer config looks here, so it must never fail the
|
|
196
214
|
group it sits in (that used to reset `control` — port, bind, auth policy — to schema defaults).
|
|
215
|
+
A *write* to a group validates the same tolerant way (`parseTolerant` via the store), so a key this
|
|
216
|
+
release does not know never blocks a save — removing a key from a schema must not brick the page
|
|
217
|
+
that writes it.
|
|
197
218
|
- **Migrations ship inside the package** (`src/config/migrations.ts`), are ordered, idempotent and
|
|
198
|
-
described in one line each. `home-hosted migrate` prints the plan, keeps `
|
|
199
|
-
refuses to write a config it cannot read, and needs consent: `--yes`, `HHOSTED_MIGRATE=allow`,
|
|
200
|
-
person at a terminal.
|
|
219
|
+
described in one line each. `home-hosted migrate` prints the plan, keeps a `.bak` beside each file it
|
|
220
|
+
rewrites, refuses to write a config it cannot read, and needs consent: `--yes`, `HHOSTED_MIGRATE=allow`,
|
|
221
|
+
or a person at a terminal. The layout relocation is not a schema change: it runs automatically from
|
|
222
|
+
the CLI pre-pass (`ensureLayout()`), so an upgrade never starts against a half-moved directory. Starting a fetch of migration
|
|
201
223
|
code from GitHub was considered and rejected: the panel supervises processes, so remote code is an
|
|
202
224
|
RCE surface, and a migration would age against a newer store API anyway.
|
|
203
225
|
|
|
@@ -205,11 +227,13 @@ either is a last resort, and never an accidental one.
|
|
|
205
227
|
|
|
206
228
|
- **Server-agnostic.** No blessed ids, no `dataDir`-style globals: a server gets only its own
|
|
207
229
|
`command`/`args`/`env`/`dataEnvs`/`envFile`/`bootstrap`. A test fails if a core file learns one.
|
|
208
|
-
- **Paths.** `dataRoot` is state
|
|
230
|
+
- **Paths.** `dataRoot` is state, and the state itself lives under `dataRoot/.hh` (global files plus
|
|
231
|
+
one directory per workspace); `projectDir` is the base for relative entry paths. `{id}{port}`
|
|
209
232
|
`{host}{bind}{cwd}{projectDir}{dataRoot}{home}` and `${ENV}` expand in config; there is no
|
|
210
233
|
package-relative state.
|
|
211
|
-
- **Secrets never enter the config.**
|
|
212
|
-
|
|
234
|
+
- **Secrets never enter the config.** Global `.hh/.control-secrets.json` holds the password hash and
|
|
235
|
+
API token hash, each workspace's `.secrets.json` holds its Telegram token and DDNS credentials, and
|
|
236
|
+
the TLS pair sits in `.hh/.tls/`; every one is 0600 and the config holds only policy.
|
|
213
237
|
- **A port holder that is this entry's own successor is not a stranger.** A program that restarts
|
|
214
238
|
itself leaves a detached process behind; with `follow` the panel adopts it as-is (pid, liveness,
|
|
215
239
|
health, resources, stop — but not its output); with `reclaim` it stops that successor and starts a
|
|
@@ -252,8 +276,14 @@ either is a last resort, and never an accidental one.
|
|
|
252
276
|
- **The CLI drives a running panel through `/_hh`, never `/api`.** `down` and `start`/`stop` read
|
|
253
277
|
run.json's token and speak over loopback through one guard (`assertLocalCall`), so they need no
|
|
254
278
|
session, password or API token — and a new local command must not open an unauthenticated `/api` route.
|
|
279
|
+
- **A server id is only unique inside its workspace.** Anything keyed by id alone — SSE
|
|
280
|
+
(`EventHub` scopes on `serverKey(workspaceId, serverId)`), log buffers, series, route params — must key
|
|
281
|
+
on the pair, or two workspaces' same-named servers mix frames.
|
|
282
|
+
- **A workspace is the ownership boundary.** Servers, secrets, logs and nanny state belong to exactly
|
|
283
|
+
one workspace; only listener, auth, TLS, host vitals, backups, UI and `run.json` are panel-wide.
|
|
284
|
+
A per-workspace service is reached through `PanelService.requireWorkspace(id)`, never a module global.
|
|
255
285
|
- **UIs are external clients.** Nothing in `src/**` may know a UI's markup or files;
|
|
256
|
-
`$HHOSTED_HOME/.ui` overrides the packaged UI at runtime.
|
|
286
|
+
`$HHOSTED_HOME/.hh/.ui` overrides the packaged UI at runtime.
|
|
257
287
|
- **A restore writes only where a config says** — archive allowlist, `origin` matching first,
|
|
258
288
|
symlinks never recreated.
|
|
259
289
|
- **Cross-platform.** Linux/macOS/Windows: `/proc` vs `ps` vs Win32_Process, process groups vs
|
|
@@ -263,7 +293,11 @@ either is a last resort, and never an accidental one.
|
|
|
263
293
|
## Gotchas
|
|
264
294
|
|
|
265
295
|
- `run.json` is the daemon's identity and `down`'s credential; keep `runtimeSchema` in sync with the
|
|
266
|
-
`Runtime` written in `src/index.ts`.
|
|
296
|
+
`Runtime` written in `src/index.ts`. It now lives at `.hh/run.json`.
|
|
297
|
+
- **`ensureLayout()` runs before any command resolves a state path** (the CLI pre-pass) and is
|
|
298
|
+
idempotent. It moves rather than copies, never overwrites an existing `.hh` file, and removes a
|
|
299
|
+
legacy directory only when every entry in it actually moved — a refused move must not become a
|
|
300
|
+
delete.
|
|
267
301
|
- Moving the listener (host/port/TLS) kills the connection answering that request, so
|
|
268
302
|
`PATCH /api/settings`, the TLS routes and `/_hh/shutdown` defer with `afterResponse()`.
|
|
269
303
|
- `Supervisor.start()` sets `starting` synchronously before its first await, and `stop()` sets
|
|
@@ -273,15 +307,15 @@ either is a last resort, and never an accidental one.
|
|
|
273
307
|
is true, one Stop on an already-stopped server made that entry unstartable until a daemon restart.
|
|
274
308
|
- Timers and `void`-ed promises in the supervisor carry a `.catch`: a throw in the tick or in a retry
|
|
275
309
|
is an unhandled rejection, and Node 24 ends the process on one.
|
|
276
|
-
- **`Settings → Server defaults` merge into the nested groups key by key** (`mergeDefaults`
|
|
277
|
-
`src/config/schema.ts`, used by
|
|
310
|
+
- **`Workspace Settings → Server defaults` merge into the nested groups key by key** (`mergeDefaults`
|
|
311
|
+
in `src/config/schema.ts`, used by the servers parser and `validateServer`). A shallow
|
|
278
312
|
`{ ...defaults, ...entry }` lets an entry that decides `restart.maxRetries` silently drop the
|
|
279
|
-
|
|
313
|
+
workspace's `restart.baseDelayMs` — and the create/edit diff flow depends on inheriting exactly that.
|
|
280
314
|
- The curated CLI dispatch reads only the **flag** forms (`--help`/`-h`, `--version`/`-v`) after a
|
|
281
315
|
command name, never a bare `help`/`version`: that is an option's value (`init --name help`).
|
|
282
316
|
- Port preflight re-probes after 300 ms — a just-closed listener can still complete a handshake.
|
|
283
|
-
- **
|
|
284
|
-
`
|
|
317
|
+
- **A workspace's config is re-read whenever its files change on disk** (`src/services/config-watch.ts`
|
|
318
|
+
→ `WorkspaceStore.reloadFromDisk()`, wired per workspace in `src/services/panel.ts`). The watch is on the *directory*, because
|
|
285
319
|
an editor's save is a temporary file renamed over the target — the inode changes, the name does not
|
|
286
320
|
— and a two-second poll backs it up where `fs.watch` is undependable (network mounts). The store
|
|
287
321
|
compares the bytes it last read and the bytes it wrote, so the panel's own saves never reload
|
|
@@ -344,11 +378,14 @@ either is a last resort, and never an accidental one.
|
|
|
344
378
|
## Where to extend
|
|
345
379
|
|
|
346
380
|
- **Route**: `src/api/<x>.ts` (chained factory) → mount in `src/app.ts` → schemas in
|
|
347
|
-
`src/shared/contracts.ts` → `describeRoute` + `jsonBody`.
|
|
348
|
-
|
|
381
|
+
`src/shared/contracts.ts` → `describeRoute` + `jsonBody`. A workspace-scoped route takes
|
|
382
|
+
`?workspace=<id>` (`workspaceQuerySchema` + `requireWorkspace` in `src/helpers/workspace.ts`); an
|
|
383
|
+
omitted id means the default workspace, never another one.
|
|
384
|
+
- **Server field**: `serverSchema` + its patch in contracts, merge keys in `config/patch.ts` when
|
|
349
385
|
nested, the form in `uis/stock/src/components/settings/`, and the contract tests.
|
|
350
386
|
- **UI**: a new `uis/<name>/` with a `vite.config.ts` from the shared factory;
|
|
351
|
-
`node scripts/build-uis.mjs <name> --zip`. The contract is `docs/UI_CREATION.md`.
|
|
387
|
+
`node scripts/build-uis.mjs <name> --zip`. The contract is `docs/UI_CREATION.md`. A change to one
|
|
388
|
+
UI (`stock`, `noc-console`) is carried to the others in the same commit.
|
|
352
389
|
- **Capability**: a stateless `src/providers/*` returning plain data; a DDNS provider is one file plus
|
|
353
390
|
a registry line (`src/providers/ddns/index.ts`), and the settings form, credentials and validation
|
|
354
391
|
follow from its metadata — see `docs/DDNS.md`.
|
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
<sub>The harness for your servers.</sub>
|
|
8
8
|
|
|
9
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.
|
|
10
|
+
them, watches them, restarts what dies, and shows you one page of what is going on. Split them into
|
|
11
|
+
[workspaces](#-workspaces) when one panel holds more than one setup. Or
|
|
11
12
|
[BYOU](#-bring-your-own-ui-byou), for a specialized UI that fits you exactly.
|
|
12
13
|
|
|
13
14
|
[](https://www.npmjs.com/package/home-hosted)
|
|
@@ -16,7 +17,7 @@ them, watches them, restarts what dies, and shows you one page of what is going
|
|
|
16
17
|
[](./LICENSE)
|
|
17
18
|
[](https://nodejs.org)
|
|
18
19
|
|
|
19
|
-
[🚀 Quick start](#-quick-start) · [🤖 Agents & API](#-agents-scripts-and-tools) · [✨ Features](#-features) · [
|
|
20
|
+
[🚀 Quick start](#-quick-start) · [🗂 Workspaces](#-workspaces) · [🧩 Servers](./docs/SERVERS.md) · [🤖 Agents & API](#-agents-scripts-and-tools) · [✨ Features](#-features) · [🛠 CLI](#-cli) · [🔔 Notifications](./docs/NOTIFICATIONS.md) · [🎨 BYOU](#-bring-your-own-ui-byou)
|
|
20
21
|
|
|
21
22
|
</div>
|
|
22
23
|
|
|
@@ -101,7 +102,7 @@ npx home-hosted down # stops the panel *and* everything it started
|
|
|
101
102
|
|
|
102
103
|
> [!NOTE]
|
|
103
104
|
> The first boot writes a default password (`hh`) so the panel is never unprotected. Change
|
|
104
|
-
> it under **
|
|
105
|
+
> it under **Global settings → Authentication** — binding beyond `127.0.0.1` stays refused until you do.
|
|
105
106
|
|
|
106
107
|
<details>
|
|
107
108
|
<summary><b>📦 Install it instead of npx-ing it</b></summary>
|
|
@@ -111,8 +112,8 @@ npm install -g home-hosted # or: pnpm add -g home-hosted
|
|
|
111
112
|
home-hosted up # `hh up` does the same
|
|
112
113
|
```
|
|
113
114
|
|
|
114
|
-
Everything it owns —
|
|
115
|
-
`~/.home-hosted`. Delete that and nothing of yours is left behind.
|
|
115
|
+
Everything it owns — workspaces, settings, secrets, logs, TLS, backups — lives under
|
|
116
|
+
`$HHOSTED_HOME/.hh`, default `~/.home-hosted/.hh`. Delete that and nothing of yours is left behind.
|
|
116
117
|
|
|
117
118
|
<sub>An installed home-hosted answers to **`hh`** too — `hh up`, `hh status`, `hh down`. Only the
|
|
118
119
|
installed form gets it; npx stays `npx home-hosted …`.</sub>
|
|
@@ -146,9 +147,13 @@ It writes a manifest whose scripts all pass `--home ./state`:
|
|
|
146
147
|
|
|
147
148
|
```gitignore
|
|
148
149
|
node_modules/
|
|
149
|
-
state/*
|
|
150
|
-
!state/
|
|
151
|
-
|
|
150
|
+
state/.hh/* # secrets, logs, TLS keys and archives stay local…
|
|
151
|
+
!state/.hh/settings.json # …but the workspace definitions are committed
|
|
152
|
+
!state/.hh/workspaces.json
|
|
153
|
+
state/.hh/*/*
|
|
154
|
+
!state/.hh/*/settings.json
|
|
155
|
+
!state/.hh/*/servers.config.json
|
|
156
|
+
data/ # per-server data directories, declared through dataEnvs
|
|
152
157
|
```
|
|
153
158
|
|
|
154
159
|
One clone, `pnpm install --frozen-lockfile`, `pnpm run up` — the setup is up on any machine with Node.
|
|
@@ -196,25 +201,27 @@ home-hosted set-token --generate
|
|
|
196
201
|
# hh_9uA2… (printed once; only its hash is kept, mode 0600)
|
|
197
202
|
|
|
198
203
|
curl -H "Authorization: Bearer hh_9uA2…" http://127.0.0.1:3999/api/state
|
|
199
|
-
curl -H "Authorization: Bearer hh_9uA2…" -X POST http://127.0.0.1:3999/api/servers/omniroute/restart
|
|
200
|
-
curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/
|
|
204
|
+
curl -H "Authorization: Bearer hh_9uA2…" -X POST 'http://127.0.0.1:3999/api/servers/omniroute/restart?workspace=default'
|
|
205
|
+
curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/servers/omniroute/stream?workspace=default' # SSE
|
|
201
206
|
home-hosted status --json # machine-readable: pid, url, health, paths
|
|
202
207
|
```
|
|
203
208
|
|
|
204
209
|
A token has the same authority as a signed-in browser and outlives restarts; `set-token --clear`
|
|
205
|
-
revokes it instantly.
|
|
210
|
+
revokes it instantly. Workspace-scoped routes take `?workspace=<id>`; omitting it means the panel's
|
|
211
|
+
default workspace.
|
|
206
212
|
|
|
207
213
|
<details>
|
|
208
214
|
<summary><b>🌐 The endpoints worth knowing</b></summary>
|
|
209
215
|
|
|
210
216
|
| | |
|
|
211
217
|
| --- | --- |
|
|
212
|
-
| `GET /api/state` | the full snapshot:
|
|
213
|
-
| `GET /api/
|
|
214
|
-
| `GET /api/
|
|
215
|
-
| `
|
|
218
|
+
| `GET /api/state` | the full snapshot: global settings, every workspace with its servers, host vitals |
|
|
219
|
+
| `GET`/`POST /api/workspaces`, `PATCH`/`DELETE /api/workspaces/:id` | the registry: list, create, rename, remove |
|
|
220
|
+
| `GET /api/events` | SSE: the live state, plus logs (`?logs=0`, `?serverId=…`; a `server` payload carries its `workspaceId`) |
|
|
221
|
+
| `GET /api/servers/:id/stream` | SSE: one server's state and logs (`?workspace=…`) |
|
|
222
|
+
| `POST /api/servers/:id/{start,stop,restart}` | lifecycle (workspace-scoped) |
|
|
216
223
|
| `POST /api/servers/:id/free-port` | ask whatever holds that server's port to stop |
|
|
217
|
-
| `PATCH /api/servers/:id`, `PATCH /api/settings` | edit configuration |
|
|
224
|
+
| `PATCH /api/servers/:id`, `PATCH /api/settings`, `PATCH /api/settings/workspace` | edit configuration |
|
|
218
225
|
| `GET /api/logs`, `/api/backups` | history and archives |
|
|
219
226
|
| `PUT`/`DELETE /api/notifications/token`, `POST /api/notifications/{test,detect-chats}` | the bot credential, a test send |
|
|
220
227
|
| `GET /healthz` | no session needed — the one an external monitor wants (its per-server detail needs a credential) |
|
|
@@ -246,8 +253,9 @@ can be told what to be: *"Help me build a UI for home-hosted: nostalgic game the
|
|
|
246
253
|
|
|
247
254
|
| | |
|
|
248
255
|
| --- | --- |
|
|
256
|
+
| 🗂 **Workspaces** | One panel, many scopes: pick a workspace in the header and it owns its servers, secrets, logs and settings. Create, rename and delete them there. Panel-wide things — listener, auth, host vitals, backups, TLS, UI — stay in **Global settings**. |
|
|
249
257
|
| 🚦 **Lifecycle** | Start, stop, restart from the panel or the API; `autostart` entries come up with it. |
|
|
250
|
-
| 📝 **Hand edits welcome** | Change `servers.config.json` in an editor, a `git checkout` or a config tool: the panel notices within seconds, no restart. A file it cannot read is reported in the panel, and the running servers are left alone. |
|
|
258
|
+
| 📝 **Hand edits welcome** | Change a workspace's `servers.config.json` in an editor, a `git checkout` or a config tool: the panel notices within seconds, no restart. A file it cannot read is reported in the panel, and the running servers are left alone. |
|
|
251
259
|
| ♻️ **Auto-restart** | Exponential backoff on crash, with the counter reset once a process stays up. |
|
|
252
260
|
| 🩺 **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](./docs/SERVERS.md#when-a-program-restarts-itself) a program that restarts itself. |
|
|
253
261
|
| 🔗 **Ordered startup** | `dependsOn` waits for a dependency to be *healthy* — not merely spawned — and stops in reverse. |
|
|
@@ -257,7 +265,7 @@ can be told what to be: *"Help me build a UI for home-hosted: nostalgic game the
|
|
|
257
265
|
| 🤖 **Token API** | Scripts and agents drive it with `Authorization: Bearer` — no browser, no session. [↑](#-agents-scripts-and-tools) |
|
|
258
266
|
| 🔔 **Notifications** | Telegram on crash, unhealthy, forced restart, recovery, host thresholds and DNS changes — [setup here](./docs/NOTIFICATIONS.md). |
|
|
259
267
|
| 🌐 **Dynamic DNS** | Keep hostnames pointed at your public IP — Cloudflare, Namecheap, Spaceship, Porkbun, GoDaddy, Gandi and more. [DDNS.md](./docs/DDNS.md) |
|
|
260
|
-
| 💾 **Backups** |
|
|
268
|
+
| 💾 **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. |
|
|
261
269
|
| 🎨 **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) |
|
|
262
270
|
| 🔐 **Security** | Cookie sessions, API tokens, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
|
|
263
271
|
| 🧩 **No special treatment** | A server is `command` + `args` + `env` + `cwd`; nothing is built in for any particular app. |
|
|
@@ -265,9 +273,46 @@ can be told what to be: *"Help me build a UI for home-hosted: nostalgic game the
|
|
|
265
273
|
|
|
266
274
|
---
|
|
267
275
|
|
|
276
|
+
## 🗂 Workspaces
|
|
277
|
+
|
|
278
|
+
A workspace is the ownership boundary: its own servers, secrets, logs, nanny state and settings. The
|
|
279
|
+
header picker is the whole control — choose one, create one, rename it, or delete it (which stops
|
|
280
|
+
what it supervises first). The first boot creates a `default` workspace; a pre-workspaces instance is
|
|
281
|
+
relocated into one automatically.
|
|
282
|
+
|
|
283
|
+
> [!IMPORTANT]
|
|
284
|
+
> **Upgrading from before workspaces (0.6 → 0.7)?** That release is breaking: state moves under
|
|
285
|
+
> `$HHOSTED_HOME/.hh`, settings split global vs. per-workspace, and backups become two-level. The
|
|
286
|
+
> relocation runs on the next command — `home-hosted migrate` reports it and stamps every file. See
|
|
287
|
+
> [Upgrading](#-cli).
|
|
288
|
+
|
|
289
|
+
| page | url | scope |
|
|
290
|
+
| --- | --- | --- |
|
|
291
|
+
| **Overview** | `/w/<workspace>` | the selected workspace: its servers, counts and live status |
|
|
292
|
+
| **Servers** · **Logs** · **Workspace settings** | `/w/<workspace>/servers` · `/w/<workspace>/logs` · `/w/<workspace>/settings` | the selected workspace |
|
|
293
|
+
| **Global Overview** | `/global/overview` | host vitals plus every server from every workspace |
|
|
294
|
+
| **Global settings** | `/global/settings` | Listener, Authentication, Host vitals, Backups, TLS, Interface, Paths |
|
|
295
|
+
|
|
296
|
+
Every page is bookmarked by its own URL: the workspace id is part of it, so a link opens the same
|
|
297
|
+
workspace even in a browser that never selected it. `/` opens **Global Overview**. The sidebar lists
|
|
298
|
+
the panel-wide pages first, then the selected workspace's.
|
|
299
|
+
|
|
300
|
+
<details>
|
|
301
|
+
<summary><b>🗂 What lives where</b></summary>
|
|
302
|
+
|
|
303
|
+
| | |
|
|
304
|
+
| --- | --- |
|
|
305
|
+
| global — `$HHOSTED_HOME/.hh/` | `settings.json` (listener, auth, TLS, host vitals, backups), `workspaces.json`, `.control-secrets.json` (password + API token, 0600), `.tls/`, `.ui/`, `.backups/`, `.logs/`, `run.json` |
|
|
306
|
+
| workspace — `.hh/<id>/` | `settings.json` (server defaults, logs, notifications, DDNS), `servers.config.json`, `.secrets.json` (Telegram + DDNS credentials, 0600), `.logs/`, `.state/` |
|
|
307
|
+
|
|
308
|
+
</details>
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
268
312
|
## 🧩 Servers
|
|
269
313
|
|
|
270
|
-
An entry is a few lines. Add one with **➕ Add server**, or write it into
|
|
314
|
+
An entry is a few lines. Add one with **➕ Add server**, or write it into the selected workspace's
|
|
315
|
+
`servers.config.json` (`$HHOSTED_HOME/.hh/<workspace>/servers.config.json`):
|
|
271
316
|
|
|
272
317
|
```json
|
|
273
318
|
{
|
|
@@ -294,11 +339,11 @@ restarts itself), and how hand-edits are validated: [SERVERS.md](./docs/SERVERS.
|
|
|
294
339
|
| `home-hosted down` | stop it cleanly — supervised processes included, persistent entries left running |
|
|
295
340
|
| `home-hosted restart` | `down`, then `up` |
|
|
296
341
|
| `home-hosted status` | pid, URL, health, uptime, state and log paths (`--json` for scripts) |
|
|
297
|
-
| `home-hosted start <id>` | start one server — and anything it `dependsOn` |
|
|
298
|
-
| `home-hosted stop <id>` | stop one server, nothing else |
|
|
342
|
+
| `home-hosted start <id>` | start one server in the default workspace — and anything it `dependsOn` (`--workspace <id>`) |
|
|
343
|
+
| `home-hosted stop <id>` | stop one server, nothing else (`--workspace <id>`) |
|
|
299
344
|
| `home-hosted set-password` | set the panel password without opening a browser |
|
|
300
345
|
| `home-hosted set-token` | set the API token scripts and agents use (`--generate`, `--clear`) |
|
|
301
|
-
| `home-hosted migrate` |
|
|
346
|
+
| `home-hosted migrate` | relocate a pre-workspaces state directory and stamp every config for this release (`--dry-run`, `--yes`) |
|
|
302
347
|
| `home-hosted init` | scaffold a project that keeps `state/` and its data in the repo |
|
|
303
348
|
| `home-hosted ui-switch` | install a UI from a release asset, a zip file or a URL (interactive) |
|
|
304
349
|
| `home-hosted ui-update` | bring an installed UI up to date, or pick a release (`--old`, `--check`) |
|
|
@@ -314,7 +359,7 @@ up, restart -c/--config -p/--port --host --open --no-autostart --foregroun
|
|
|
314
359
|
|
|
315
360
|
down (no flags)
|
|
316
361
|
status --json
|
|
317
|
-
start, stop <id>
|
|
362
|
+
start, stop <id> [-w/--workspace <id>] (both need the panel up)
|
|
318
363
|
init --dir --name --pm --no-install -y/--yes
|
|
319
364
|
set-password --clear
|
|
320
365
|
set-token --generate --clear
|
|
@@ -328,14 +373,34 @@ env vars HHOSTED_PASSWORD, HHOSTED_MIGRATE=allow, HHOSTED_TOKEN, GITHUB
|
|
|
328
373
|
```
|
|
329
374
|
|
|
330
375
|
`up` and `restart` share the same flags: `restart` is `down`, then `up` with exactly what it was given.
|
|
376
|
+
`-c/--config` is the **default workspace's** servers file (`<state>/.hh/default/servers.config.json`),
|
|
377
|
+
for a launcher that pins one; a workspace picked in the UI keeps its own. `start`/`stop` omit
|
|
378
|
+
`--workspace` to act in the panel's default workspace.
|
|
331
379
|
|
|
332
380
|
</details>
|
|
333
381
|
|
|
334
382
|
<details>
|
|
335
383
|
<summary><b>🧭 Upgrading, and why the panel sometimes refuses to start</b></summary>
|
|
336
384
|
|
|
337
|
-
|
|
338
|
-
|
|
385
|
+
> **Breaking in 0.7 — workspaces.** State moved under `$HHOSTED_HOME/.hh`: global files at its top
|
|
386
|
+
> level, one directory per workspace. Settings split into panel-wide **Global settings** and
|
|
387
|
+
> per-workspace **Workspace settings**, backups became two-level, and `--config` now points at the
|
|
388
|
+
> default workspace's servers file. The old `servers.config.json` becomes
|
|
389
|
+
> `.hh/default/servers.config.json`.
|
|
390
|
+
|
|
391
|
+
Relocation is automatic: the CLI relocates a pre-`.hh` `$HHOSTED_HOME` before any command runs, so an
|
|
392
|
+
existing instance keeps working after the upgrade. `home-hosted migrate` reports what it moved and
|
|
393
|
+
stamps every file for this release:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
home-hosted migrate --dry-run # print the steps, write nothing
|
|
397
|
+
home-hosted migrate # ask, then write — keeps a .bak beside each rewritten file
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Unattended, consent comes from `--yes` or `HHOSTED_MIGRATE=allow`; without it the command stops.
|
|
401
|
+
|
|
402
|
+
Each config records what wrote it: `meta.writtenBy` (the release) and `meta.schema` (the shape). That
|
|
403
|
+
buys two guarantees:
|
|
339
404
|
|
|
340
405
|
- **A newer home-hosted always reads an older config** — every existing key keeps its meaning.
|
|
341
406
|
- **Keys a newer release added are ignored, not fatal.** The panel names them in its log, leaves them
|
|
@@ -346,15 +411,6 @@ or a config whose schema is newer than the running release stops `up` with the e
|
|
|
346
411
|
than starting with defaults that quietly differ from your file. Fix the file, or install the release
|
|
347
412
|
that wrote it.
|
|
348
413
|
|
|
349
|
-
When a release changes the shape itself, `home-hosted migrate` applies the steps it ships:
|
|
350
|
-
|
|
351
|
-
```bash
|
|
352
|
-
home-hosted migrate --dry-run # print the steps, write nothing
|
|
353
|
-
home-hosted migrate # ask, then write — keeps servers.config.json.bak
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
Unattended, consent comes from `--yes` or `HHOSTED_MIGRATE=allow`; without it the command stops.
|
|
357
|
-
|
|
358
414
|
</details>
|
|
359
415
|
|
|
360
416
|
---
|
|
@@ -375,8 +431,10 @@ Everything binds `127.0.0.1` until you say otherwise.
|
|
|
375
431
|
never taken from the message, and anything the panel supervises is refused, not killed. A server
|
|
376
432
|
that [restarts itself](./docs/SERVERS.md#when-a-program-restarts-itself) can be followed, or replaced
|
|
377
433
|
with a supervised copy.
|
|
378
|
-
- **Secrets never enter the config**: the password hash
|
|
379
|
-
|
|
434
|
+
- **Secrets never enter the config**: the password hash and API token hash live in the global
|
|
435
|
+
`.hh/.control-secrets.json`, each workspace's Telegram bot token and DDNS credentials in its own
|
|
436
|
+
`.hh/<workspace>/.secrets.json`, all mode `0600`; the TLS pair sits in `.hh/.tls/`. DDNS credentials
|
|
437
|
+
are sealed with AES-256-GCM under `HHOSTED_DDNS_SECRET` (default `hh` — set your own).
|
|
380
438
|
- **Behind a proxy** turn on `trustProxy` and let `cookieSecure: auto` add `Secure` on https, or
|
|
381
439
|
upload a PEM pair and let home-hosted terminate TLS itself.
|
|
382
440
|
|
|
@@ -393,19 +451,23 @@ stays in the secrets file. **Two minutes of setup: [NOTIFICATIONS.md](./docs/NOT
|
|
|
393
451
|
|
|
394
452
|
## 🌐 Dynamic DNS
|
|
395
453
|
|
|
396
|
-
**Settings → Dynamic DNS** keeps a list of hostnames pointed at this machine's public IP —
|
|
397
|
-
account, paste its credentials, add hostnames. The panel checks the address on an interval and
|
|
398
|
-
provider only when it actually changed, and the last confirmed address survives a restart.
|
|
399
|
-
tokens stay in the secrets file. **Providers and the config shape:
|
|
454
|
+
**Workspace Settings → Dynamic DNS** keeps a list of hostnames pointed at this machine's public IP —
|
|
455
|
+
add an account, paste its credentials, add hostnames. The panel checks the address on an interval and
|
|
456
|
+
calls a provider only when it actually changed, and the last confirmed address survives a restart.
|
|
457
|
+
Provider tokens stay in the workspace's secrets file. **Providers and the config shape:
|
|
458
|
+
[DDNS.md](./docs/DDNS.md).**
|
|
400
459
|
|
|
401
460
|
---
|
|
402
461
|
|
|
403
462
|
## 💾 Backups
|
|
404
463
|
|
|
405
|
-
**
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
464
|
+
**Global settings → Backups** archives the panel's own settings, its secrets and the TLS pair — plus
|
|
465
|
+
every workspace you pick. It is two-level: the top of *Create backup…* lists the global items and
|
|
466
|
+
each workspace; opening a workspace lists its **settings**, **servers**, **secrets** and every data
|
|
467
|
+
directory its entries declare, and you can drop any single piece. *Restore…* shows the same list from
|
|
468
|
+
an archive before anything is written. Either way it is an ordinary `.zip`, or WinZip AES-256 with a
|
|
469
|
+
password, restored per path. Known build output and dependency directories (`node_modules`, `dist`,
|
|
470
|
+
`.next`, framework caches) are skipped per entry; `backupIgnoreGenerated: false` captures them anyway.
|
|
409
471
|
|
|
410
472
|
<details>
|
|
411
473
|
<summary><b>🚚 One archive is a whole setup</b></summary>
|
|
@@ -414,9 +476,11 @@ Start a **blank** home-hosted anywhere — another machine, another user, a fres
|
|
|
414
476
|
archive and restore. Definitions come back, data lands where *this* machine's config says, and
|
|
415
477
|
`autostart` entries come up immediately.
|
|
416
478
|
|
|
417
|
-
It works because an archive carries
|
|
418
|
-
**declaration** (`omniroute:DATA_DIR`), not by an absolute path from the
|
|
419
|
-
writes where no config declares.
|
|
479
|
+
It works because an archive carries each workspace's `settings.json` and `servers.config.json`, and
|
|
480
|
+
paths are matched by the **declaration** (`omniroute:DATA_DIR`), not by an absolute path from the
|
|
481
|
+
source machine. A restore never writes where no config declares. A declaration using `{projectDir}`,
|
|
482
|
+
`{dataRoot}` or `{home}` follows the restoring panel (`{projectDir}` is `HHOSTED_PROJECT` or the
|
|
483
|
+
panel's cwd, not `HHOSTED_HOME`); a literal absolute path is restored to that same path.
|
|
420
484
|
|
|
421
485
|
</details>
|
|
422
486
|
|
|
@@ -424,9 +488,9 @@ writes where no config declares.
|
|
|
424
488
|
|
|
425
489
|
## 🎨 Bring your own UI (BYOU)
|
|
426
490
|
|
|
427
|
-
The panel is a static site: `$HHOSTED_HOME/.ui` overrides the packaged one, and **
|
|
428
|
-
Interface** takes a zip. No restart, no fork — and `home-hosted ui-revert` brings back the
|
|
429
|
-
panel if yours breaks.
|
|
491
|
+
The panel is a static site: `$HHOSTED_HOME/.hh/.ui` overrides the packaged one, and **Global
|
|
492
|
+
settings → Interface** takes a zip. No restart, no fork — and `home-hosted ui-revert` brings back the
|
|
493
|
+
stock panel if yours breaks.
|
|
430
494
|
|
|
431
495
|
Two ship in this repo: `uis/stock`, and `uis/noc-console` for TUI and shortcuts wizards; a release
|
|
432
496
|
attaches both as `home-hosted-ui-<name>.zip`. Yours can be anything that compiles to static files —
|
|
@@ -486,16 +550,24 @@ opened.
|
|
|
486
550
|
<details>
|
|
487
551
|
<summary><b>Where is my state?</b></summary>
|
|
488
552
|
|
|
489
|
-
`$HHOSTED_HOME`, default `~/.home-hosted
|
|
553
|
+
`$HHOSTED_HOME/.hh`, default `~/.home-hosted/.hh`. Global files at the top level, one directory per
|
|
554
|
+
workspace:
|
|
490
555
|
|
|
491
556
|
```text
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
.control-secrets.json password hash + API token hash
|
|
495
|
-
.logs/ rotated per-server logs + history
|
|
557
|
+
settings.json panel-wide: listener, auth, TLS policy, host vitals, backups
|
|
558
|
+
workspaces.json the registry: ids and labels, in selector order
|
|
559
|
+
.control-secrets.json password hash + API token hash (mode 0600)
|
|
496
560
|
.tls/ an uploaded PEM pair
|
|
561
|
+
.ui/ an installed custom UI
|
|
497
562
|
.backups/ zip archives
|
|
563
|
+
.logs/ the panel's own console log
|
|
498
564
|
run.json the running panel (pid, url, token, mode 0600)
|
|
565
|
+
|
|
566
|
+
<workspace>/settings.json server defaults, log retention, notifications, DDNS
|
|
567
|
+
<workspace>/servers.config.json your servers, plus meta: which release and schema wrote it
|
|
568
|
+
<workspace>/.secrets.json Telegram token + DDNS credentials (mode 0600)
|
|
569
|
+
<workspace>/.logs/ rotated per-server logs + history
|
|
570
|
+
<workspace>/.state/ persistent entries' nanny state
|
|
499
571
|
```
|
|
500
572
|
|
|
501
573
|
`home-hosted status` prints the paths.
|