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/docs/DDNS.md CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  Keep a list of hostnames pointed at this machine's public IP. The panel checks the address on an
4
4
  interval and calls a provider only when it actually changed — restart-safe, because the last
5
- confirmed address per hostname is kept in `$HHOSTED_HOME/.state/ddns.json`.
5
+ confirmed address per hostname is kept in the workspace's `.hh/<workspace>/.state/ddns.json`.
6
6
 
7
- **Settings → Dynamic DNS** is the whole setup: add an account, paste its credentials, add hostnames.
8
- The block is saved by the page's own **Save settings**, like every other section on it; the credentials
9
- button saves on its own, being a secret. Provider credentials never enter `servers.config.json`.
7
+ **Workspace Settings → Dynamic DNS** is the whole setup: add an account, paste its credentials, add
8
+ hostnames. The block is saved by the page's own **Save settings**, like every other section on it; the
9
+ credentials button saves on its own, being a secret. Provider credentials never enter
10
+ `servers.config.json`.
10
11
 
11
12
  ```json
12
13
  {
@@ -27,11 +28,13 @@ button saves on its own, being a secret. Provider credentials never enter `serve
27
28
  }
28
29
  ```
29
30
 
30
- Credentials live in `.control-secrets.json` (0600), keyed by the account `id` and sealed with
31
+ Credentials live in the workspace's `.secrets.json` (0600), keyed by the account `id` and sealed with
31
32
  AES-256-GCM under `HHOSTED_DDNS_SECRET` — set your own, or the default `hh` only stops a casual
32
- read. The account does not have to be saved first, and the provider is stored with the entry, so an
33
- id that later changes provider does not silently keep the old secret. Removing an account drops its
34
- credentials on the next save.
33
+ read. The block above is the workspace's `.hh/<workspace>/settings.json`; the credentials are cached
34
+ separately in `.secrets.json`, both beside that workspace's `servers.config.json`. The account does
35
+ not have to be saved first, and the provider is stored with the entry, so an id that later changes
36
+ provider does not silently keep the old secret. Removing an account drops its credentials on the next
37
+ save.
35
38
 
36
39
  Header fields:
37
40
 
@@ -72,7 +75,7 @@ DNS API), and the Namecheap XML API (a full zone rewrite per update).
72
75
  ## Notifications
73
76
 
74
77
  Telegram carries one message per pass that changed a record, and one for a pass that failed — the
75
- same switch as every other event, under Settings → Notifications → *Dynamic DNS changes and
78
+ same switch as every other event, under Workspace Settings → Notifications → *Dynamic DNS changes and
76
79
  failures* (`telegram.onDdns`). There is no second switch in this section.
77
80
 
78
81
  ## Adding a provider
@@ -6,7 +6,7 @@ opt-in, and the bot token never leaves the secrets file.
6
6
  ## Setup
7
7
 
8
8
  1. Talk to [@BotFather](https://t.me/BotFather), run `/newbot`, and copy the token it prints.
9
- 2. In the panel: **Settings → Notifications**, paste the token, press **Detect chats**.
9
+ 2. In the panel: **Workspace Settings → Notifications**, paste the token, press **Detect chats**.
10
10
  3. Send your bot a message from the chat you want the alerts in — a bot cannot open a conversation —
11
11
  then pick that chat from the list (or paste its id).
12
12
  4. Press **Test**: the chat gets a message, and the panel shows the result and its timestamp.
@@ -49,9 +49,9 @@ curl -X POST http://127.0.0.1:3999/api/notifications/test \
49
49
  | host thresholds breached, and recovered | `onHost` | ✅ |
50
50
  | a dynamic DNS record changed, or a pass failed | `onDdns` | ✅ |
51
51
 
52
- Host thresholds themselves — disk, memory, swap, load, temperature — are **Settings → Host**.
53
- Dynamic DNS carries one message per pass that changed a record, and one per failed pass — see
54
- [DDNS.md](./DDNS.md).
52
+ Host thresholds themselves — disk, memory, swap, load, temperature — are **Global settings → Host
53
+ vitals**. Dynamic DNS carries one message per pass that changed a record, and one per failed pass —
54
+ see [DDNS.md](./DDNS.md).
55
55
 
56
56
  ## Quiet periods
57
57
 
@@ -61,12 +61,14 @@ disables the throttle.
61
61
 
62
62
  ## Where the token lives
63
63
 
64
- `$HHOSTED_HOME/.control-secrets.json`, mode `0600`, next to the password and API-token hashes. It is
65
- never written into `servers.config.json`, so committing or sharing a config cannot leak it — and
66
- `GET /api/settings` reports `tokenSet: true|false`, never the token itself.
64
+ The workspace's `.hh/<workspace>/.secrets.json`, mode `0600`, next to that workspace's DDNS
65
+ credentials. The password and API-token hashes are panel-wide and live in
66
+ `.hh/.control-secrets.json`. The token is never written into `servers.config.json`, so committing or
67
+ sharing a config cannot leak it — and `GET /api/settings/workspace` reports `tokenSet: true|false`,
68
+ never the token itself.
67
69
 
68
70
  ## Delivery failures
69
71
 
70
72
  Sends are fire-and-forget: supervision never waits on a chat API. A failure is logged and the last
71
- result is shown in **Settings → Notifications** (`lastResult`, `lastResultAt`), so a wrong chat id is
72
- visible there instead of silently swallowing alerts.
73
+ result is shown in **Workspace Settings → Notifications** (`lastResult`, `lastResultAt`), so a wrong
74
+ chat id is visible there instead of silently swallowing alerts.
package/docs/SERVERS.md CHANGED
@@ -1,7 +1,13 @@
1
1
  # Servers
2
2
 
3
- Every supervised process is one entry in `$HHOSTED_HOME/servers.config.json`. Add it with **➕ Add
4
- server** in the panel, or write it by hand — the panel writes the same file.
3
+ Every supervised process is one entry in a **workspace**'s `servers.config.json` —
4
+ `$HHOSTED_HOME/.hh/<workspace-id>/servers.config.json`, default `~/.home-hosted/.hh/default/…`.
5
+ Add it with **➕ Add server** in the panel, or write it by hand — the panel writes the same file.
6
+
7
+ <sub>A workspace is the ownership boundary: its servers, secrets, logs and nanny state are its own.
8
+ Pick one in the header; the workspace's server defaults and log retention sit in
9
+ **Workspace Settings**. Panel-wide things (listener, auth, TLS, host vitals, backups, UI) stay in
10
+ **Global settings**.</sub>
5
11
 
6
12
  ```json
7
13
  {
@@ -140,14 +146,14 @@ holder it cannot prove is yours.
140
146
  ## Persistent entries
141
147
 
142
148
  `"persistent": true` means "keep this running whatever happens to the panel". It is off by default,
143
- it is per entry (not a `Settings → Server defaults` field), and it covers the three ways the panel can
144
- go away: `down`, a restart, or being killed outright.
149
+ it is per entry (not a **Workspace Settings → Server defaults** field), and it covers the three ways
150
+ the panel can go away: `down`, a restart, or being killed outright.
145
151
 
146
152
  How it works: the panel does not run the entry directly. It spawns a **nanny** — the same CLI, hidden
147
153
  `__nanny` mode — which starts the entry, owns its pipes and writes its output to the entry's own log
148
154
  file. The nanny is what survives; the panel reattaches to it on the next boot through
149
- `$HHOSTED_HOME/.state/<id>.json`, and a stale file is how it learns how a child ended while nobody was
150
- watching.
155
+ `.hh/<workspace>/.state/<id>.json`, and a stale file is how it learns how a child ended while nobody
156
+ was watching.
151
157
 
152
158
  What that changes:
153
159
 
@@ -157,8 +163,8 @@ What that changes:
157
163
  that is already running is adopted, because leaving it unmanaged would make the panel treat its own
158
164
  server as a stranger on the port.
159
165
  - **Logs keep flowing.** The panel tails the nanny's file, so history survives the panel being down;
160
- `logs.persist: false` still keeps it out of the Logs page — but a file has to exist in
161
- `.logs/`, because there is no pipe to carry the output.
166
+ `logs.persist: false` still keeps it out of the Logs page — but a file has to exist in the
167
+ workspace's `.logs/`, because there is no pipe to carry the output.
162
168
  - **A crash while the panel is away is reported, not restarted.** Retries and backoff remain the
163
169
  panel's job, so an entry that dies with nobody watching shows up as **crashed** on the next boot
164
170
  with the exit code in its log.
@@ -22,15 +22,15 @@ care what your UI looks like.
22
22
  ## Install it
23
23
 
24
24
  ```text
25
- $HHOSTED_HOME/.ui/ ← where your build lives
25
+ $HHOSTED_HOME/.hh/.ui/ ← where your build lives
26
26
  index.html ← required, at the root
27
27
  assets/…
28
28
  ui.json ← optional, but see below
29
29
  ```
30
30
 
31
- **Settings → Interface** → pick a `.zip` → *Install UI* (refresh to see it). Or drop the files into
32
- `$HHOSTED_HOME/.ui` yourself. The zip may hold the files at its root or inside one wrapper directory
33
- (`zip -r ui.zip dist` works too).
31
+ **Global settings → Interface** → pick a `.zip` → *Install UI* (refresh to see it). Or drop the files
32
+ into `$HHOSTED_HOME/.hh/.ui` yourself. The zip may hold the files at its root or inside one wrapper
33
+ directory (`zip -r ui.zip dist` works too).
34
34
 
35
35
  Without the settings page, `home-hosted ui-switch` installs one from a GitHub release asset (its
36
36
  default), from a local `.zip` (`--file ./ui.zip`), or from a URL (`--file https://…/ui.zip`).
@@ -53,7 +53,7 @@ installed, and what lets it offer you the next version:
53
53
 
54
54
  | field | |
55
55
  | --- | --- |
56
- | `name`, `version` | what Settings shows as installed |
56
+ | `name`, `version` | what Global settings → Interface shows as installed |
57
57
  | `repo` | `owner/name` the releases come from — the one field `ui-update` needs |
58
58
  | `tag` | the release this build came from, so an update can tell newer from older |
59
59
  | `asset` | the release asset name, when a release carries more than one UI |
@@ -71,8 +71,8 @@ Nothing is built on the server side: whatever you upload is served as-is, so shi
71
71
  HTML/JS/CSS or the output of your own Vite/Next/Astro build with relative asset paths.
72
72
 
73
73
  **If it breaks:** `home-hosted ui-revert` puts the stock panel back (or *Revert to stock* in
74
- Settings). The CLI also prints a reminder at startup while a custom UI is active — a broken UI must
75
- never lock you out of your own machine.
74
+ Global settings). The CLI also prints a reminder at startup while a custom UI is active — a broken UI
75
+ must never lock you out of your own machine.
76
76
 
77
77
  ## Rules of the road
78
78
 
@@ -107,20 +107,25 @@ responses at runtime. Either style is fine.
107
107
 
108
108
  ### Endpoints you will actually use
109
109
 
110
+ Workspace-scoped routes take `?workspace=<id>`. Omitting it means the panel's default workspace;
111
+ naming one that does not exist is a `404 UNKNOWN_WORKSPACE`, never a silent fallback.
112
+
110
113
  | endpoint | what it gives you |
111
114
  | --- | --- |
112
- | `GET /api/state` | everything: panel settings, servers with live status, host vitals |
115
+ | `GET /api/state` | everything: global settings, every workspace with its servers and live status, host vitals |
116
+ | `GET` / `POST /api/workspaces`, `PATCH` / `DELETE /api/workspaces/:id` | the registry: list, create, rename, remove (removal stops and disposes that workspace's servers) |
113
117
  | `GET /api/events` | **the live feed**: `hello` carries the full state, then `state`, `server`, `log` |
114
118
  | `GET /api/servers/:id/stream` | one server's `server` + `log` frames |
115
- | `POST /api/servers/:id/{start,stop,restart}`, `/api/servers/{start-all,stop-all}` | lifecycle |
119
+ | `POST /api/servers/:id/{start,stop,restart}`, `/api/servers/{start-all,stop-all}` | lifecycle (workspace-scoped) |
116
120
  | `POST /api/servers/:id/free-port` | ask whatever holds that server's port to stop (`403`-safe: supervised listeners are refused) |
117
- | `GET` / `POST /api/servers`, `PATCH` / `DELETE /api/servers/:id` | the entries themselves |
118
- | `GET /api/logs`, `/api/logs/:id?tail=&search=`, `/api/logs/:id/download?file=` | persisted logs |
119
- | `GET` / `PATCH /api/settings` | the panel's own config (`control.label`, host thresholds, backups, …) |
120
- | `POST` / `DELETE /api/settings/ui` | install or revert a UI — what the settings page calls |
121
+ | `GET` / `POST /api/servers`, `PATCH` / `DELETE /api/servers/:id` | the entries themselves, in one workspace |
122
+ | `GET /api/logs`, `/api/logs/:id?tail=&search=`, `/api/logs/:id/download?file=` | persisted logs, for one workspace |
123
+ | `GET` / `PATCH /api/settings` | the panel-wide settings (`control.label`, host thresholds, backups, UI) |
124
+ | `GET` / `PATCH /api/settings/workspace` | one workspace's settings: server defaults, log retention, notifications |
125
+ | `POST` / `DELETE /api/settings/ui` | install or revert a UI — what Global settings calls |
121
126
  | `POST` / `DELETE /api/settings/tls` | upload or clear a PEM pair |
122
- | `GET` / `POST /api/backups`, `/api/backups/restore`, `/api/backups/:name/download` | archives; a create body may carry `include` (item ids `config`, `secrets`, `tls`, `data:<path>`, from `GET /api/backups`), and a restore body `include` selects what to apply |
123
- | `POST` / `DELETE /api/notifications/token`, `/api/notifications/test`, `/detect-chats` | Telegram |
127
+ | `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 |
128
+ | `POST` / `DELETE /api/notifications/token`, `/api/notifications/test`, `/detect-chats` | Telegram, per workspace |
124
129
  | `POST /api/auth/login`, `GET /api/auth/session`, `POST /api/auth/logout` | the session |
125
130
  | `GET /healthz` | liveness — **no session**, and `503` when an autostart server has crashed |
126
131
  | `GET /api/metrics` | Prometheus text (needs a session) |
@@ -128,6 +133,10 @@ responses at runtime. Either style is fine.
128
133
  `GET /healthz` and `GET /openapi/*` are the only unauthenticated reads; the SPA shell itself is
129
134
  public, so your app can load before a session exists.
130
135
 
136
+ A `ServerView` carries `workspaceId` — the same server id can exist in two workspaces, so key a list
137
+ by the pair, not by `serverId` alone. A `server` frame embeds that view; a `log` frame carries only
138
+ `serverId`, so match it against the workspace's servers.
139
+
131
140
  Anything calling the API from outside a browser — a script, a test, an agent, a native shell — can
132
141
  skip the login dance with an API token: `home-hosted set-token --generate` prints one once, and
133
142
  `Authorization: Bearer <token>` authenticates every `/api` request with the same authority as a
@@ -137,7 +146,7 @@ Two settings worth reflecting: `control.label` is the panel's own name (the stoc
137
146
  and `GET /api/settings` includes `ui` — which UI is being served, and its metadata.
138
147
 
139
148
  An entry body is partial by design: `POST /api/servers` and `PATCH /api/servers/:id` take only the
140
- fields the person decided, and the panel's **Server defaults** fill the rest. Sending a value the
149
+ fields the person decided, and the workspace's **Server defaults** fill the rest. Sending a value the
141
150
  person never chose freezes it against those defaults, so build the body as a diff
142
151
  (`inheritBaseline` and `diffFields` in `src/shared/patch-diff.ts`).
143
152
 
@@ -158,14 +167,16 @@ session, 403 bad token or origin, 404 unknown id, 409 conflict, 413 too large.
158
167
  | `event:` | `data:` |
159
168
  | --- | --- |
160
169
  | `hello` | `{ ts, state }` — the first frame, the complete snapshot |
161
- | `state` | `{ ts, state }` — anything changed: a status, a resource sample, host vitals |
162
- | `server` | `{ ts, serverId, server }` — one entry, after an action or a probe |
170
+ | `state` | `{ ts, state }` — anything changed: a status, a resource sample, host vitals, a workspace |
171
+ | `server` | `{ ts, serverId, server }` — one entry, `server.workspaceId` says which workspace |
163
172
  | `log` | `{ ts, serverId, lines }` — new output (dropped under backpressure, never state) |
164
173
  | `ping` | the current time, every 15 s |
165
174
 
166
- Send `?logs=0` to skip log frames, or `?serverId=<id>` to follow one server. The exact shapes are
167
- `sseMessageSchema` in `src/shared/contracts.ts` — the server validates against it before writing, so
168
- that schema is also your best type source.
175
+ `GET /api/events` takes `?logs=0` to skip log frames and `?serverId=<id>` to follow one server — but
176
+ it matches on the id alone, so an id used in two workspaces mixes their frames. Use
177
+ `GET /api/servers/:id/stream?workspace=<id>` for one server: it validates the pair and replays the
178
+ current state first. The exact shapes are `sseMessageSchema` in `src/shared/contracts.ts` — the
179
+ server validates against it before writing, so that schema is also your best type source.
169
180
 
170
181
  ## Editing settings without fighting the stream
171
182
 
@@ -203,7 +214,8 @@ node scripts/build-uis.mjs <name> --zip # builds it, writes uis/dist/home-host
203
214
  pnpm run build:uis # every UI, zipped; releases attach them as assets
204
215
  ```
205
216
 
206
- Only `stock` ships inside the npm package; the rest are release assets you install from Settings.
217
+ Only `stock` ships inside the npm package; the rest are release assets you install from Global
218
+ settings.
207
219
  `pnpm run quickcheck` type-checks every UI, and `pnpm test` picks up any `test/*.test.ts` you add
208
220
  (use relative imports — the `@` alias points at `stock`).
209
221
 
@@ -218,13 +230,15 @@ pnpm run build # → dist/
218
230
  # 2. keep the API base relative, then zip the build
219
231
  cd dist && zip -r ../my-panel.zip . && cd ..
220
232
 
221
- # 3. Settings → Interface → Install UI, then refresh
233
+ # 3. Global settings → Interface → Install UI, then refresh
222
234
  ```
223
235
 
224
236
  Your client needs the session cookie, which the browser sends automatically once you log in on that
225
237
  origin (a non-browser client uses an API token instead, above). For local development `pnpm dev` runs
226
- the panel on 3999 and a Vite dev server on 3998 with `/api` proxied — point your own dev server at
227
- `http://127.0.0.1:3999` the same way.
238
+ the panel on `6000` and a Vite dev server on `6001` with `/api` proxied — point your own dev server at
239
+ `http://127.0.0.1:6000` the same way. Both stay in the 6xxx range so a dev instance never collides
240
+ with an installed panel on the default 3999; override with `--port`/`--ui-port` or
241
+ `HHOSTED_DEV_PANEL_PORT`/`HHOSTED_DEV_UI_PORT`.
228
242
 
229
243
  ## Checklist
230
244
 
@@ -235,4 +249,4 @@ the panel on 3999 and a Vite dev server on 3998 with `/api` proxied — point yo
235
249
  - [ ] deep links render (the server falls back to `index.html`)
236
250
  - [ ] assets self-hosted, no CDN
237
251
  - [ ] works over plain http on a LAN
238
- - [ ] `ui.json` with a name and version, so Settings can tell you what is installed — plus `repo`/`tag`/`asset` if you want `ui-update` to follow your releases
252
+ - [ ] `ui.json` with a name and version, so Global settings can tell you what is installed — plus `repo`/`tag`/`asset` if you want `ui-update` to follow your releases
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "home-hosted",
3
3
  "type": "module",
4
- "version": "0.6.8",
4
+ "version": "0.7.1",
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>",