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/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
|
|
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
|
|
8
|
-
The block is saved by the page's own **Save settings**, like every other section on it; the
|
|
9
|
-
button saves on its own, being a secret. Provider credentials never enter
|
|
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 `.
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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
|
package/docs/NOTIFICATIONS.md
CHANGED
|
@@ -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 **
|
|
53
|
-
Dynamic DNS carries one message per pass that changed a record, and one per failed pass —
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
package/docs/UI_CREATION.md
CHANGED
|
@@ -22,15 +22,15 @@ care what your UI looks like.
|
|
|
22
22
|
## Install it
|
|
23
23
|
|
|
24
24
|
```text
|
|
25
|
-
$HHOSTED_HOME/.ui/
|
|
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
|
-
**
|
|
32
|
-
`$HHOSTED_HOME/.ui` yourself. The zip may hold the files at its root or inside one wrapper
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
120
|
-
| `
|
|
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;
|
|
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
|
|
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,
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
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.
|
|
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
|
|
227
|
-
`http://127.0.0.1:
|
|
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
|
|
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.
|
|
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>",
|