home-hosted 0.6.8 → 0.7.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/AGENTS.md +69 -38
- package/README.md +131 -64
- package/dist/cli.js +3239 -1592
- package/dist/cli.js.map +1 -1
- package/docs/DDNS.md +12 -9
- package/docs/NOTIFICATIONS.md +11 -9
- package/docs/SERVERS.md +14 -8
- package/docs/UI_CREATION.md +40 -26
- 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-B_4sGjNj.css +0 -2
- package/uis/stock/dist/assets/index-BuB3VX_S.js +0 -46
package/AGENTS.md
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
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
|
-
|
|
14
|
-
|
|
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.
|
|
15
18
|
|
|
16
19
|
## Commands
|
|
17
20
|
|
|
18
21
|
```sh
|
|
19
22
|
pnpm run up|down|restart|status # detached; `down` asks /_hh/shutdown, signals are the fallback
|
|
20
23
|
pnpm run start # up --foreground (systemd, docker, a foreground shell)
|
|
21
|
-
pnpm dev # tsx-watch panel :
|
|
24
|
+
pnpm dev # tsx-watch panel :6000 + the stock UI's Vite :6001 (proxies /api), state in .dev-state/
|
|
25
|
+
pnpm dev --ui noc-console # the same, for another UI; --port/--ui-port override
|
|
22
26
|
pnpm run build # dist/cli.js + the stock UI (uis/stock/dist)
|
|
23
27
|
pnpm run build:uis # every UI under uis/, zipped into uis/dist/ (release assets)
|
|
24
28
|
pnpm run quickcheck # eslint + tsc + vue-tsc for every UI under uis/
|
|
@@ -33,6 +37,12 @@ pnpm run media # regenerate docs/media (mockups, both served
|
|
|
33
37
|
|
|
34
38
|
The published bin is `home-hosted`, with an `hh` alias: both names run the same CLI.
|
|
35
39
|
|
|
40
|
+
**Dev servers and test instances stay in the 6xxx range** — panel 6000, UI 6001, test-spawned panels
|
|
41
|
+
6100+ (`pnpm dev` takes `--ui`/`--port`/`--ui-port`, or `HHOSTED_DEV_PANEL_PORT` /
|
|
42
|
+
`HHOSTED_DEV_UI_PORT`). Never assume 3999 is free: an installed panel or another dev instance may hold
|
|
43
|
+
it, and the CLI's port preflight runs *before* its config guard, so a busy default port turns config
|
|
44
|
+
tests red for the wrong reason.
|
|
45
|
+
|
|
36
46
|
Releases are dispatched from `.github/workflows/release.yml` with a version (and a `dry-run` switch
|
|
37
47
|
that stops before pushing). It verifies the version, lints/types/tests, builds the CLI plus the stock
|
|
38
48
|
UI and every UI zip, lets changelogen write the changelog, bump `package.json`, commit and tag
|
|
@@ -84,10 +94,15 @@ a minor without one.
|
|
|
84
94
|
- `src/api/**` — one file per URL group (`$.routes.ts` = several routes), mirroring the path.
|
|
85
95
|
- `src/shared/contracts.ts` — every ArkType schema (config, API and SSE DTOs), shared with the UIs;
|
|
86
96
|
the OpenAPI spec is generated from it, never hand-written.
|
|
87
|
-
- `src/config/` — `
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`
|
|
97
|
+
- `src/config/` — split by scope. `settings.ts` (`GlobalSettingsStore`: `.hh/settings.json` — listener,
|
|
98
|
+
auth, TLS policy, host vitals, backups) and `store.ts` (`WorkspaceStore`: a workspace's
|
|
99
|
+
`settings.json` + `servers.config.json`, validate/merge/atomic commit, reporting
|
|
100
|
+
`configError`/`configWarnings` instead of throwing on a bad file). `workspaces.ts` is the registry
|
|
101
|
+
(`workspaces.json`), `layout.ts` the one-time relocation of a pre-workspace `$HHOSTED_HOME`.
|
|
102
|
+
`schema.ts` holds the three on-disk shapes plus the `meta` stamp, `parse.ts` the tolerant readers
|
|
103
|
+
(`parseGlobalSettings`/`parseWorkspaceSettings`/`parseServersFile`: unknown keys are reported and
|
|
104
|
+
kept, everything else blocking), `patch.ts` the merge helpers, `migrations.ts` the schema constant
|
|
105
|
+
and ordered step registry, and `secrets.ts`/`seed.ts` the two secret scopes and the seeds.
|
|
91
106
|
- `src/providers/` — stateless leaves: `process` (spawn, `terminate`, `terminatePid` for a process we
|
|
92
107
|
adopted), `port` (probe, holder lookup, `terminatePids`), `proc` (the sampler, plus
|
|
93
108
|
`processCarriesServerId` for the environment marker and `processTreePids` for tree ownership),
|
|
@@ -96,12 +111,13 @@ a minor without one.
|
|
|
96
111
|
`log-tail` (an offset reader that survives rotation and truncation), `ddns/` (one `DdnsProvider` per
|
|
97
112
|
registrar over one HTTP call each, plus `ip` and `zone` helpers), health-check, host, telegram,
|
|
98
113
|
archive.
|
|
99
|
-
- `src/services/` — stateful orchestration:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`
|
|
104
|
-
|
|
114
|
+
- `src/services/` — stateful orchestration: `panel` (one `WorkspaceRuntime` per workspace, workspace
|
|
115
|
+
CRUD, and the single aggregate state frame — every per-workspace service hangs off it), supervisor,
|
|
116
|
+
control-server, config-watch, state, auth + exposure, dependencies, history, log-buffer/log-files,
|
|
117
|
+
notifications, host-monitor, ddns, backups, tls, ui, plus `init` (the scaffold behind
|
|
118
|
+
`home-hosted init`: a manifest, a `.gitignore`, and the prompts stay in the CLI), `nanny` (the
|
|
119
|
+
process a persistent entry runs under) and `log-relay` (the tailer that feeds its lines to the
|
|
120
|
+
supervisor). It names no server — the scaffold must stay as neutral as the supervisor.
|
|
105
121
|
- `src/middleware/auth.ts` — the `/api/*` guard, and `requestIdentity()`, the one place a request's
|
|
106
122
|
credentials are read: the `hh_session` cookie or `Authorization: Bearer <api token>`. A token is
|
|
107
123
|
a first-class credential (same authority as a signed-in browser) and is verified from the secrets
|
|
@@ -112,7 +128,7 @@ a minor without one.
|
|
|
112
128
|
- `uis/<name>/` — each UI is a Vite app (Vue 3 + Tailwind v4) built through `uis/vite.shared.ts`;
|
|
113
129
|
`stock` is the one shipped inside the package. Aliases: `@` → that UI's `src`, `@shared` →
|
|
114
130
|
`src/shared`, `@server` → `src` (**types only** — never import runtime server code into a UI).
|
|
115
|
-
Its `public/ui.json` is the UI's identity: `name` and `version` (what Settings shows),
|
|
131
|
+
Its `public/ui.json` is the UI's identity: `name` and `version` (what Global Settings → Interface shows),
|
|
116
132
|
`repo`/`tag`/`asset` (which release carries it, for `ui-update`) and `unix` (when it was built).
|
|
117
133
|
`UiService` carries those fields into the installed `$HHOSTED_HOME/.ui/ui.json` unchanged and adds
|
|
118
134
|
`uploadedAt`/`files` of its own.
|
|
@@ -141,7 +157,7 @@ a minor without one.
|
|
|
141
157
|
- A single on/off setting is a `ToggleSwitch`; `CheckField` is only for picking items out of a set
|
|
142
158
|
(the restore plan). A checkbox in a `FieldGroup` grid reads as misaligned next to the inputs.
|
|
143
159
|
- A create body carries only what differs from what the entry would inherit — the schema's defaults
|
|
144
|
-
with
|
|
160
|
+
with `Workspace Settings → Server defaults` on top (`inheritBaseline` + `diffFields` in both
|
|
145
161
|
UIs). A value written into `servers.config.json` stops following those defaults, so anything the
|
|
146
162
|
person did not decide stays out; the editor applies the same rule to an edit.
|
|
147
163
|
- ArkType at every runtime boundary: routes use `validate('json'|'query'|'param', schema)` then
|
|
@@ -207,9 +223,10 @@ either is a last resort, and never an accidental one.
|
|
|
207
223
|
release does not know never blocks a save — removing a key from a schema must not brick the page
|
|
208
224
|
that writes it.
|
|
209
225
|
- **Migrations ship inside the package** (`src/config/migrations.ts`), are ordered, idempotent and
|
|
210
|
-
described in one line each. `home-hosted migrate` prints the plan, keeps `
|
|
211
|
-
refuses to write a config it cannot read, and needs consent: `--yes`, `HHOSTED_MIGRATE=allow`,
|
|
212
|
-
person at a terminal.
|
|
226
|
+
described in one line each. `home-hosted migrate` prints the plan, keeps a `.bak` beside each file it
|
|
227
|
+
rewrites, refuses to write a config it cannot read, and needs consent: `--yes`, `HHOSTED_MIGRATE=allow`,
|
|
228
|
+
or a person at a terminal. The layout relocation is not a schema change: it runs automatically from
|
|
229
|
+
the CLI pre-pass (`ensureLayout()`), so an upgrade never starts against a half-moved directory. Starting a fetch of migration
|
|
213
230
|
code from GitHub was considered and rejected: the panel supervises processes, so remote code is an
|
|
214
231
|
RCE surface, and a migration would age against a newer store API anyway.
|
|
215
232
|
|
|
@@ -217,11 +234,13 @@ either is a last resort, and never an accidental one.
|
|
|
217
234
|
|
|
218
235
|
- **Server-agnostic.** No blessed ids, no `dataDir`-style globals: a server gets only its own
|
|
219
236
|
`command`/`args`/`env`/`dataEnvs`/`envFile`/`bootstrap`. A test fails if a core file learns one.
|
|
220
|
-
- **Paths.** `dataRoot` is state
|
|
237
|
+
- **Paths.** `dataRoot` is state, and the state itself lives under `dataRoot/.hh` (global files plus
|
|
238
|
+
one directory per workspace); `projectDir` is the base for relative entry paths. `{id}{port}`
|
|
221
239
|
`{host}{bind}{cwd}{projectDir}{dataRoot}{home}` and `${ENV}` expand in config; there is no
|
|
222
240
|
package-relative state.
|
|
223
|
-
- **Secrets never enter the config.**
|
|
224
|
-
|
|
241
|
+
- **Secrets never enter the config.** Global `.hh/.control-secrets.json` holds the password hash and
|
|
242
|
+
API token hash, each workspace's `.secrets.json` holds its Telegram token and DDNS credentials, and
|
|
243
|
+
the TLS pair sits in `.hh/.tls/`; every one is 0600 and the config holds only policy.
|
|
225
244
|
- **A port holder that is this entry's own successor is not a stranger.** A program that restarts
|
|
226
245
|
itself leaves a detached process behind; with `follow` the panel adopts it as-is (pid, liveness,
|
|
227
246
|
health, resources, stop — but not its output); with `reclaim` it stops that successor and starts a
|
|
@@ -264,8 +283,14 @@ either is a last resort, and never an accidental one.
|
|
|
264
283
|
- **The CLI drives a running panel through `/_hh`, never `/api`.** `down` and `start`/`stop` read
|
|
265
284
|
run.json's token and speak over loopback through one guard (`assertLocalCall`), so they need no
|
|
266
285
|
session, password or API token — and a new local command must not open an unauthenticated `/api` route.
|
|
286
|
+
- **A server id is only unique inside its workspace.** Anything keyed by id alone — SSE
|
|
287
|
+
(`EventHub` scopes on `serverKey(workspaceId, serverId)`), log buffers, series, route params — must key
|
|
288
|
+
on the pair, or two workspaces' same-named servers mix frames.
|
|
289
|
+
- **A workspace is the ownership boundary.** Servers, secrets, logs and nanny state belong to exactly
|
|
290
|
+
one workspace; only listener, auth, TLS, host vitals, backups, UI and `run.json` are panel-wide.
|
|
291
|
+
A per-workspace service is reached through `PanelService.requireWorkspace(id)`, never a module global.
|
|
267
292
|
- **UIs are external clients.** Nothing in `src/**` may know a UI's markup or files;
|
|
268
|
-
`$HHOSTED_HOME/.ui` overrides the packaged UI at runtime.
|
|
293
|
+
`$HHOSTED_HOME/.hh/.ui` overrides the packaged UI at runtime.
|
|
269
294
|
- **A restore writes only where a config says** — archive allowlist, `origin` matching first,
|
|
270
295
|
symlinks never recreated.
|
|
271
296
|
- **Cross-platform.** Linux/macOS/Windows: `/proc` vs `ps` vs Win32_Process, process groups vs
|
|
@@ -275,7 +300,11 @@ either is a last resort, and never an accidental one.
|
|
|
275
300
|
## Gotchas
|
|
276
301
|
|
|
277
302
|
- `run.json` is the daemon's identity and `down`'s credential; keep `runtimeSchema` in sync with the
|
|
278
|
-
`Runtime` written in `src/index.ts`.
|
|
303
|
+
`Runtime` written in `src/index.ts`. It now lives at `.hh/run.json`.
|
|
304
|
+
- **`ensureLayout()` runs before any command resolves a state path** (the CLI pre-pass) and is
|
|
305
|
+
idempotent. It moves rather than copies, never overwrites an existing `.hh` file, and removes a
|
|
306
|
+
legacy directory only when every entry in it actually moved — a refused move must not become a
|
|
307
|
+
delete.
|
|
279
308
|
- Moving the listener (host/port/TLS) kills the connection answering that request, so
|
|
280
309
|
`PATCH /api/settings`, the TLS routes and `/_hh/shutdown` defer with `afterResponse()`.
|
|
281
310
|
- `Supervisor.start()` sets `starting` synchronously before its first await, and `stop()` sets
|
|
@@ -285,15 +314,15 @@ either is a last resort, and never an accidental one.
|
|
|
285
314
|
is true, one Stop on an already-stopped server made that entry unstartable until a daemon restart.
|
|
286
315
|
- Timers and `void`-ed promises in the supervisor carry a `.catch`: a throw in the tick or in a retry
|
|
287
316
|
is an unhandled rejection, and Node 24 ends the process on one.
|
|
288
|
-
- **`Settings → Server defaults` merge into the nested groups key by key** (`mergeDefaults`
|
|
289
|
-
`src/config/schema.ts`, used by
|
|
317
|
+
- **`Workspace Settings → Server defaults` merge into the nested groups key by key** (`mergeDefaults`
|
|
318
|
+
in `src/config/schema.ts`, used by the servers parser and `validateServer`). A shallow
|
|
290
319
|
`{ ...defaults, ...entry }` lets an entry that decides `restart.maxRetries` silently drop the
|
|
291
|
-
|
|
320
|
+
workspace's `restart.baseDelayMs` — and the create/edit diff flow depends on inheriting exactly that.
|
|
292
321
|
- The curated CLI dispatch reads only the **flag** forms (`--help`/`-h`, `--version`/`-v`) after a
|
|
293
322
|
command name, never a bare `help`/`version`: that is an option's value (`init --name help`).
|
|
294
323
|
- Port preflight re-probes after 300 ms — a just-closed listener can still complete a handshake.
|
|
295
|
-
- **
|
|
296
|
-
`
|
|
324
|
+
- **A workspace's config is re-read whenever its files change on disk** (`src/services/config-watch.ts`
|
|
325
|
+
→ `WorkspaceStore.reloadFromDisk()`, wired per workspace in `src/services/panel.ts`). The watch is on the *directory*, because
|
|
297
326
|
an editor's save is a temporary file renamed over the target — the inode changes, the name does not
|
|
298
327
|
— and a two-second poll backs it up where `fs.watch` is undependable (network mounts). The store
|
|
299
328
|
compares the bytes it last read and the bytes it wrote, so the panel's own saves never reload
|
|
@@ -356,8 +385,10 @@ either is a last resort, and never an accidental one.
|
|
|
356
385
|
## Where to extend
|
|
357
386
|
|
|
358
387
|
- **Route**: `src/api/<x>.ts` (chained factory) → mount in `src/app.ts` → schemas in
|
|
359
|
-
`src/shared/contracts.ts` → `describeRoute` + `jsonBody`.
|
|
360
|
-
|
|
388
|
+
`src/shared/contracts.ts` → `describeRoute` + `jsonBody`. A workspace-scoped route takes
|
|
389
|
+
`?workspace=<id>` (`workspaceQuerySchema` + `requireWorkspace` in `src/helpers/workspace.ts`); an
|
|
390
|
+
omitted id means the default workspace, never another one.
|
|
391
|
+
- **Server field**: `serverSchema` + its patch in contracts, merge keys in `config/patch.ts` when
|
|
361
392
|
nested, the form in `uis/stock/src/components/settings/`, and the contract tests.
|
|
362
393
|
- **UI**: a new `uis/<name>/` with a `vite.config.ts` from the shared factory;
|
|
363
394
|
`node scripts/build-uis.mjs <name> --zip`. The contract is `docs/UI_CREATION.md`. A change to one
|
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,10 +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
|
-
token and
|
|
380
|
-
TLS pair in `.tls/`. DDNS credentials
|
|
381
|
-
(default `hh` — set your own).
|
|
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).
|
|
382
438
|
- **Behind a proxy** turn on `trustProxy` and let `cookieSecure: auto` add `Secure` on https, or
|
|
383
439
|
upload a PEM pair and let home-hosted terminate TLS itself.
|
|
384
440
|
|
|
@@ -395,21 +451,23 @@ stays in the secrets file. **Two minutes of setup: [NOTIFICATIONS.md](./docs/NOT
|
|
|
395
451
|
|
|
396
452
|
## 🌐 Dynamic DNS
|
|
397
453
|
|
|
398
|
-
**Settings → Dynamic DNS** keeps a list of hostnames pointed at this machine's public IP —
|
|
399
|
-
account, paste its credentials, add hostnames. The panel checks the address on an interval and
|
|
400
|
-
provider only when it actually changed, and the last confirmed address survives a restart.
|
|
401
|
-
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).**
|
|
402
459
|
|
|
403
460
|
---
|
|
404
461
|
|
|
405
462
|
## 💾 Backups
|
|
406
463
|
|
|
407
|
-
**
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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.
|
|
413
471
|
|
|
414
472
|
<details>
|
|
415
473
|
<summary><b>🚚 One archive is a whole setup</b></summary>
|
|
@@ -418,11 +476,11 @@ Start a **blank** home-hosted anywhere — another machine, another user, a fres
|
|
|
418
476
|
archive and restore. Definitions come back, data lands where *this* machine's config says, and
|
|
419
477
|
`autostart` entries come up immediately.
|
|
420
478
|
|
|
421
|
-
It works because an archive carries
|
|
422
|
-
**declaration** (`omniroute:DATA_DIR`), not by an absolute path from the
|
|
423
|
-
writes where no config declares. A declaration using `{projectDir}`,
|
|
424
|
-
the restoring panel (`{projectDir}` is `HHOSTED_PROJECT` or the
|
|
425
|
-
literal absolute path is restored to that same path.
|
|
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.
|
|
426
484
|
|
|
427
485
|
</details>
|
|
428
486
|
|
|
@@ -430,9 +488,9 @@ literal absolute path is restored to that same path.
|
|
|
430
488
|
|
|
431
489
|
## 🎨 Bring your own UI (BYOU)
|
|
432
490
|
|
|
433
|
-
The panel is a static site: `$HHOSTED_HOME/.ui` overrides the packaged one, and **
|
|
434
|
-
Interface** takes a zip. No restart, no fork — and `home-hosted ui-revert` brings back the
|
|
435
|
-
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.
|
|
436
494
|
|
|
437
495
|
Two ship in this repo: `uis/stock`, and `uis/noc-console` for TUI and shortcuts wizards; a release
|
|
438
496
|
attaches both as `home-hosted-ui-<name>.zip`. Yours can be anything that compiles to static files —
|
|
@@ -492,16 +550,24 @@ opened.
|
|
|
492
550
|
<details>
|
|
493
551
|
<summary><b>Where is my state?</b></summary>
|
|
494
552
|
|
|
495
|
-
`$HHOSTED_HOME`, default `~/.home-hosted
|
|
553
|
+
`$HHOSTED_HOME/.hh`, default `~/.home-hosted/.hh`. Global files at the top level, one directory per
|
|
554
|
+
workspace:
|
|
496
555
|
|
|
497
556
|
```text
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
.control-secrets.json password hash + API token hash
|
|
501
|
-
.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)
|
|
502
560
|
.tls/ an uploaded PEM pair
|
|
561
|
+
.ui/ an installed custom UI
|
|
503
562
|
.backups/ zip archives
|
|
563
|
+
.logs/ the panel's own console log
|
|
504
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
|
|
505
571
|
```
|
|
506
572
|
|
|
507
573
|
`home-hosted status` prints the paths.
|
|
@@ -539,9 +605,10 @@ scripts/ builds, typechecks, media capture, release helpers
|
|
|
539
605
|
test/ the vitest suite
|
|
540
606
|
```
|
|
541
607
|
|
|
542
|
-
`pnpm dev` runs the panel with `tsx watch` plus
|
|
543
|
-
|
|
544
|
-
|
|
608
|
+
`pnpm dev` runs the panel with `tsx watch` plus one UI's dev server on the 6xxx range — panel `6000`,
|
|
609
|
+
UI `6001` (`pnpm dev --ui noc-console`), state in `.dev-state/` — so a dev instance never fights an
|
|
610
|
+
installed panel's 3999. `pnpm build` produces `dist/` and `uis/stock/dist/`; `pnpm quickcheck` is lint
|
|
611
|
+
plus types; `pnpm test` is vitest; `pnpm run media` regenerates the GIF above.
|
|
545
612
|
|
|
546
613
|
<details>
|
|
547
614
|
<summary><b>📚 Which doc do I need?</b></summary>
|