home-hosted 0.3.0 β 0.4.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 +92 -12
- package/NOTIFICATIONS.md +69 -0
- package/README.md +177 -171
- package/SERVERS.md +101 -0
- package/UI_CREATION.md +12 -5
- package/dist/cli.js +1196 -141
- package/dist/cli.js.map +1 -1
- package/package.json +7 -1
- package/uis/stock/dist/assets/index-DOD80X00.css +2 -0
- package/uis/stock/dist/assets/index-Dl2nDThm.js +47 -0
- package/uis/stock/dist/index.html +2 -2
- package/uis/stock/dist/assets/index-CjmgiKjn.js +0 -47
- package/uis/stock/dist/assets/index-CmkrtNU0.css +0 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ them, watches them, restarts what dies, and shows you one page of what is going
|
|
|
16
16
|
[](./LICENSE)
|
|
17
17
|
[](https://nodejs.org)
|
|
18
18
|
|
|
19
|
-
[π Quick start](#-quick-start) Β· [
|
|
19
|
+
[π Quick start](#-quick-start) Β· [π€ Agents & API](#-agents-scripts-and-tools) Β· [β¨ Features](#-features) Β· [π§© Servers](./SERVERS.md) Β· [π CLI](#-cli) Β· [π Notifications](./NOTIFICATIONS.md) Β· [π¨ BYOU](#-bring-your-own-ui-byou)
|
|
20
20
|
|
|
21
21
|
</div>
|
|
22
22
|
|
|
@@ -34,44 +34,12 @@ shortcuts wizards), and UI directions you could build yourself β
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
-
## π€ Why?
|
|
38
|
-
|
|
39
|
-
Running services on a home machine usually means one of two extremes: `tmux` sessions you
|
|
40
|
-
forget about, or a hand-written systemd unit per service (times six).
|
|
41
|
-
|
|
42
|
-
| | |
|
|
43
|
-
| --- | --- |
|
|
44
|
-
| β **"Is it still running?"** | You check with `ps`, then `curl`, then hope. |
|
|
45
|
-
| β **Silent deaths** | Something crashes at 3am and you notice days later. |
|
|
46
|
-
| β **One terminal per service** | Logs scroll away in tabs you closed. |
|
|
47
|
-
| β **Fragile restarts** | The box reboots and half the stack is gone. |
|
|
48
|
-
| β
**home-hosted** | Declare it once, watch it forever, one command to stop it all. |
|
|
49
|
-
|
|
50
|
-
```text
|
|
51
|
-
ββββββββββββββββββββββββββββββββββββββββ
|
|
52
|
-
your browser βββΆβ home-hosted Β· 127.0.0.1:3999 β
|
|
53
|
-
β your UI + JSON API + SSE logs β
|
|
54
|
-
βββββββββββββββββ¬βββββββββββββββββββββββ
|
|
55
|
-
β supervises
|
|
56
|
-
ββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββ
|
|
57
|
-
βΌ βΌ βΌ
|
|
58
|
-
βββββββββββ βββββββββββ βββββββββββ
|
|
59
|
-
β gateway β β files β β bot β
|
|
60
|
-
β :4000 β β :4010 β β ... β
|
|
61
|
-
βββββββββββ βββββββββββ βββββββββββ
|
|
62
|
-
health β health β restarts β»
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
It ships with **nothing**. No blessed paths, no "data directory" setting, no opinion about
|
|
66
|
-
what you run β a server is a command, some arguments, and the environment you give it.
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
37
|
## β‘ Quick start
|
|
71
38
|
|
|
72
39
|
```bash
|
|
73
40
|
npx home-hosted # start it β detached, it stays running
|
|
74
41
|
npx home-hosted status # where is it, is it healthy
|
|
42
|
+
# pnpm instead of npx: `pnpm dlx home-hosted β¦`
|
|
75
43
|
```
|
|
76
44
|
|
|
77
45
|
That is it. The panel is on **<http://127.0.0.1:3999>** and keeps running after the terminal
|
|
@@ -89,7 +57,7 @@ npx home-hosted down # stops the panel *and* everything it started
|
|
|
89
57
|
<summary><b>π¦ Install it instead of npx-ing it</b></summary>
|
|
90
58
|
|
|
91
59
|
```bash
|
|
92
|
-
npm install -g home-hosted
|
|
60
|
+
npm install -g home-hosted # or: pnpm add -g home-hosted
|
|
93
61
|
home-hosted up
|
|
94
62
|
```
|
|
95
63
|
|
|
@@ -99,18 +67,18 @@ Everything it owns β config, secrets, logs, TLS, backups β lives in `$HHOSTE
|
|
|
99
67
|
</details>
|
|
100
68
|
|
|
101
69
|
<details>
|
|
102
|
-
<summary><b>π Keep
|
|
70
|
+
<summary><b>π Keep a whole setup in a project you can take anywhere</b></summary>
|
|
103
71
|
|
|
104
|
-
|
|
72
|
+
Commit the project and it *is* the setup. `init` scaffolds it:
|
|
105
73
|
|
|
106
74
|
```bash
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
npx home-hosted up --home ./state # state lives inside the project
|
|
75
|
+
npx home-hosted init # or: pnpm dlx home-hosted init
|
|
76
|
+
# project directory, package name, package manager, install, git β every step has a default
|
|
77
|
+
# `--yes` takes them all, for an agent or a CI job
|
|
112
78
|
```
|
|
113
79
|
|
|
80
|
+
It writes a manifest whose scripts all pass `--home ./state`:
|
|
81
|
+
|
|
114
82
|
```jsonc
|
|
115
83
|
// package.json
|
|
116
84
|
{
|
|
@@ -118,19 +86,23 @@ npx home-hosted up --home ./state # state lives inside the proje
|
|
|
118
86
|
"up": "home-hosted up --home ./state",
|
|
119
87
|
"down": "home-hosted down --home ./state",
|
|
120
88
|
"status": "home-hosted status --home ./state"
|
|
89
|
+
// β¦and restart, set-password, set-token, migrate
|
|
121
90
|
}
|
|
122
91
|
}
|
|
123
92
|
```
|
|
124
93
|
|
|
125
94
|
```gitignore
|
|
126
|
-
|
|
127
|
-
|
|
95
|
+
node_modules/
|
|
96
|
+
state/* # secrets, logs, TLS keys and archives stay localβ¦
|
|
97
|
+
!state/servers.config.json # β¦but the server definitions are committed
|
|
98
|
+
data/ # per-server data directories, declared through dataEnvs
|
|
128
99
|
```
|
|
129
100
|
|
|
130
|
-
|
|
101
|
+
One clone, `pnpm install --frozen-lockfile`, `pnpm run up` β the setup is up on any machine with Node.
|
|
102
|
+
Worked example, with per-server data inside the project:
|
|
103
|
+
**[hhosted-9router-dsh](https://github.com/NamesMT/hhosted-9router-dsh)**.
|
|
131
104
|
|
|
132
|
-
|
|
133
|
-
it as one archive under **Settings β Backups**.
|
|
105
|
+
<sub>Call them as `pnpm run up` β `pnpm up` is pnpm's own update, not your script.</sub>
|
|
134
106
|
|
|
135
107
|
</details>
|
|
136
108
|
|
|
@@ -156,83 +128,101 @@ WantedBy=multi-user.target
|
|
|
156
128
|
|
|
157
129
|
---
|
|
158
130
|
|
|
131
|
+
## π€ Agents, scripts and tools
|
|
132
|
+
|
|
133
|
+
Everything the panel does, a script or an agent can do: the API takes a **long-lived token** in place
|
|
134
|
+
of the browser cookie. One command for a credential, then plain HTTP to start, stop, inspect, restart
|
|
135
|
+
and read logs.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
home-hosted set-token --generate
|
|
139
|
+
# hh_9uA2β¦ (printed once; only its hash is kept, mode 0600)
|
|
140
|
+
|
|
141
|
+
curl -H "Authorization: Bearer hh_9uA2β¦" http://127.0.0.1:3999/api/state
|
|
142
|
+
curl -H "Authorization: Bearer hh_9uA2β¦" -X POST http://127.0.0.1:3999/api/servers/9router/restart
|
|
143
|
+
curl -N -H "Authorization: Bearer hh_9uA2β¦" 'http://127.0.0.1:3999/api/events?logs=1' # SSE
|
|
144
|
+
home-hosted status --json # machine-readable: pid, url, health, paths
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A token has the same authority as a signed-in browser and outlives restarts; `set-token --clear`
|
|
148
|
+
revokes it instantly.
|
|
149
|
+
|
|
150
|
+
<details>
|
|
151
|
+
<summary><b>π The endpoints worth knowing</b></summary>
|
|
152
|
+
|
|
153
|
+
| | |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `GET /api/state` | the full snapshot: config, live status, host vitals |
|
|
156
|
+
| `GET /api/events` | SSE: the live state, plus logs (`?logs=0`, `?serverId=β¦`) |
|
|
157
|
+
| `GET /api/servers/:id/stream` | SSE: one server's state and logs |
|
|
158
|
+
| `POST /api/servers/:id/{start,stop,restart}` | lifecycle |
|
|
159
|
+
| `POST /api/servers/:id/free-port` | ask whatever holds that server's port to stop |
|
|
160
|
+
| `PATCH /api/servers/:id`, `PATCH /api/settings` | edit configuration |
|
|
161
|
+
| `GET /api/logs`, `/api/backups`, `/api/notifications` | logs, archives, Telegram |
|
|
162
|
+
| `GET /healthz` | no session needed β the one an external monitor wants (its per-server detail needs a credential) |
|
|
163
|
+
| `GET /api/metrics` | Prometheus text |
|
|
164
|
+
|
|
165
|
+
`GET /openapi/spec.json` describes all of it, `/openapi/ui` is the browsable version, and every error
|
|
166
|
+
comes back as one envelope (`{ message, code, detail }`) with a stable `code` a tool can branch on.
|
|
167
|
+
|
|
168
|
+
</details>
|
|
169
|
+
|
|
170
|
+
<details>
|
|
171
|
+
<summary><b>π§βπ» Pointing an agent at it</b></summary>
|
|
172
|
+
|
|
173
|
+
Give the agent four things and it can run your home server without guessing:
|
|
174
|
+
|
|
175
|
+
1. the token (`home-hosted set-token --generate`),
|
|
176
|
+
2. `http://127.0.0.1:3999/openapi/spec.json` β the API it may call,
|
|
177
|
+
3. `home-hosted status --json` β where things are,
|
|
178
|
+
4. [SERVERS.md](./SERVERS.md) β how an entry is declared when it needs a new server.
|
|
179
|
+
|
|
180
|
+
For a UI rather than the API, [UI_CREATION.md](./UI_CREATION.md) is the whole contract, and the panel
|
|
181
|
+
can be told what to be: *"Help me build a UI for home-hosted: nostalgic game theme, including β¦"*.
|
|
182
|
+
|
|
183
|
+
</details>
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
159
187
|
## β¨ Features
|
|
160
188
|
|
|
161
189
|
| | |
|
|
162
190
|
| --- | --- |
|
|
163
191
|
| π¦ **Lifecycle** | Start, stop, restart from the panel or the API; `autostart` entries come up with it. |
|
|
164
192
|
| β»οΈ **Auto-restart** | Exponential backoff on crash, with the counter reset once a process stays up. |
|
|
165
|
-
| π©Ί **Health that acts** | TCP or HTTP probes per server: warn on the card, force a restart after a timeout,
|
|
193
|
+
| π©Ί **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](./SERVERS.md#when-a-program-restarts-itself) a program that restarts itself. |
|
|
166
194
|
| π **Ordered startup** | `dependsOn` waits for a dependency to be *healthy* β not merely spawned β and stops in reverse. |
|
|
167
195
|
| π **Logs** | Live per-server stream, buffer plus rotated files on disk, search, download, one click to clear. |
|
|
168
196
|
| π **Resources** | CPU and RSS of the whole process tree, with an optional memory ceiling that triggers a restart. |
|
|
169
197
|
| π‘οΈ **Host vitals** | Load, memory, swap, disk and CPU temperature, with thresholds that notify once and again on recovery. |
|
|
198
|
+
| π€ **Token API** | Scripts and agents drive it with `Authorization: Bearer` β no browser, no session. [β](#-agents-scripts-and-tools) |
|
|
199
|
+
| π **Notifications** | Telegram on crash, unhealthy, forced restart, recovery and host thresholds β [setup here](./NOTIFICATIONS.md). |
|
|
170
200
|
| πΎ **Backups** | One click for config, secrets, TLS and your declared data directories β plain `.zip`, or AES-256 with a password, restored per path. |
|
|
171
|
-
| π **Portable setup** | Restore a shared backup onto a blank instance and the whole server setup is back: definitions, data, secrets and all. |
|
|
172
201
|
| π¨ **BYOU β Bring Your Own UI** | Upload a static build, `home-hosted ui-revert` to go back. [UI_CREATION.md](./UI_CREATION.md) |
|
|
173
|
-
|
|
|
174
|
-
|
|
|
175
|
-
| π§© **Server-agnostic** | `command` + `args` + `env` + `cwd`. Nothing in the code knows what you run. |
|
|
202
|
+
| π **Security** | Cookie sessions, API tokens, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
|
|
203
|
+
| π§© **No special treatment** | A server is `command` + `args` + `env` + `cwd`; nothing is built in for any particular app. |
|
|
176
204
|
| π₯ **Cross-platform** | Linux, macOS and Windows: `/proc`, `ps` or Win32_Process, process groups or `taskkill /T`, no shell dependencies. |
|
|
177
205
|
|
|
178
206
|
---
|
|
179
207
|
|
|
180
|
-
## π§©
|
|
208
|
+
## π§© Servers
|
|
181
209
|
|
|
182
|
-
|
|
210
|
+
An entry is a few lines. Add one with **β Add server**, or write it into `servers.config.json`:
|
|
183
211
|
|
|
184
212
|
```json
|
|
185
213
|
{
|
|
186
214
|
"id": "myapp",
|
|
187
215
|
"command": "node",
|
|
188
216
|
"args": ["server.js"],
|
|
189
|
-
"cwd": "./apps/myapp",
|
|
190
217
|
"port": 8080,
|
|
191
218
|
"autostart": true,
|
|
192
|
-
"
|
|
193
|
-
"health": { "mode": "http", "http": { "path": "/healthz" } }
|
|
219
|
+
"dataEnvs": { "DATA_DIR": "{projectDir}/data/myapp" }
|
|
194
220
|
}
|
|
195
221
|
```
|
|
196
222
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
<details>
|
|
201
|
-
<summary><b>π§Ύ Field reference</b></summary>
|
|
202
|
-
|
|
203
|
-
| field | what it does |
|
|
204
|
-
| --- | --- |
|
|
205
|
-
| `command`, `args`, `cwd` | what to run, with `{placeholders}` resolved per entry |
|
|
206
|
-
| `env`, `dataEnvs`, `envFile` | environment; `dataEnvs` also marks data directories for backups, `envFile` keeps secrets out of the config |
|
|
207
|
-
| `port`, `bind` | enables the readiness wait, health checks and the conflict preflight; `local` keeps it on `127.0.0.1` |
|
|
208
|
-
| `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body) |
|
|
209
|
-
| `restart.*` | backoff: `maxRetries`, `baseDelayMs`, `factor`, `maxDelayMs`, `resetAfterMs` |
|
|
210
|
-
| `stop.*` | signal, `killGroup`, grace period, and whether to sweep leftover port holders |
|
|
211
|
-
| `dependsOn` | ids that must be healthy first; stopped in reverse order |
|
|
212
|
-
| `resources.maxRssBytes` | restart when the process tree grows past a limit |
|
|
213
|
-
| `bootstrap` | one command to run once before the first start (migrations, warmups) |
|
|
214
|
-
| `backupPaths` | extra paths this entry owns, included in backups |
|
|
215
|
-
|
|
216
|
-
Placeholders, and what each one resolves to:
|
|
217
|
-
|
|
218
|
-
| placeholder | resolves to |
|
|
219
|
-
| --- | --- |
|
|
220
|
-
| `{id}` `{label}` | the entry's id, and its label (falling back to the id) |
|
|
221
|
-
| `{port}` | its configured port, empty when it has none |
|
|
222
|
-
| `{bind}` `{host}` `{displayHost}` | `local`/`lan`/an address; the address it binds (`127.0.0.1`, `0.0.0.0`); the address to *show* (`127.0.0.1`, the LAN IP) |
|
|
223
|
-
| `{lanIp}` | this machine's LAN address, or `127.0.0.1` |
|
|
224
|
-
| `{cwd}` | the entry's working directory (its `cwd`, else the project directory) |
|
|
225
|
-
| `{projectDir}` | where you ran `home-hosted` (or `--project`) β `$HHOSTED_PROJECT` |
|
|
226
|
-
| `{dataRoot}` | the state directory β `$HHOSTED_HOME`, default `~/.home-hosted` |
|
|
227
|
-
| `{home}` | the current user's home directory |
|
|
228
|
-
|
|
229
|
-
Unknown placeholders stay visible instead of silently becoming empty, and `${VAR}` reads from the
|
|
230
|
-
environment (including `envFile`).
|
|
231
|
-
|
|
232
|
-
Every supervised process also gets `HHOSTED_SERVER_ID` and `HHOSTED_CONTROL_PORT`, so a
|
|
233
|
-
service can tell which entry it is and how to reach the panel.
|
|
234
|
-
|
|
235
|
-
</details>
|
|
223
|
+
`dataEnvs` declares a data directory once: it is exported to the process *and* picked up by Backups.
|
|
224
|
+
**Every field, every placeholder, the port-conflict policies (including adopting a server that
|
|
225
|
+
restarts itself), and how hand-edits are validated: [SERVERS.md](./SERVERS.md).**
|
|
236
226
|
|
|
237
227
|
---
|
|
238
228
|
|
|
@@ -245,6 +235,9 @@ service can tell which entry it is and how to reach the panel.
|
|
|
245
235
|
| `home-hosted restart` | `down`, then `up` |
|
|
246
236
|
| `home-hosted status` | pid, URL, health, uptime, state and log paths (`--json` for scripts) |
|
|
247
237
|
| `home-hosted set-password` | set the panel password without opening a browser |
|
|
238
|
+
| `home-hosted set-token` | set the API token scripts and agents use (`--generate`, `--clear`) |
|
|
239
|
+
| `home-hosted migrate` | bring `servers.config.json` up to this release's schema (`--dry-run`, `--yes`) |
|
|
240
|
+
| `home-hosted init` | scaffold a project that keeps `state/` and its data in the repo |
|
|
248
241
|
| `home-hosted ui-revert` | go back to the stock panel UI after uploading your own |
|
|
249
242
|
|
|
250
243
|
<details>
|
|
@@ -264,55 +257,81 @@ service can tell which entry it is and how to reach the panel.
|
|
|
264
257
|
|
|
265
258
|
</details>
|
|
266
259
|
|
|
260
|
+
<details>
|
|
261
|
+
<summary><b>π§ Upgrading, and why the panel sometimes refuses to start</b></summary>
|
|
262
|
+
|
|
263
|
+
`servers.config.json` records what wrote it: `meta.writtenBy` (the release) and `meta.schema` (the
|
|
264
|
+
config shape). That buys two guarantees:
|
|
265
|
+
|
|
266
|
+
- **A newer home-hosted always reads an older config** β every existing key keeps its meaning.
|
|
267
|
+
- **Keys a newer release added are ignored, not fatal.** The panel names them in its log, leaves them
|
|
268
|
+
in the file, and never resets the settings around them.
|
|
269
|
+
|
|
270
|
+
What it will not do is run a config it cannot read. A wrong value, a duplicate id, an unreadable file
|
|
271
|
+
or a config whose schema is newer than the running release stops `up` with the exact problem, rather
|
|
272
|
+
than starting with defaults that quietly differ from your file. Fix the file, or install the release
|
|
273
|
+
that wrote it.
|
|
274
|
+
|
|
275
|
+
When a release changes the shape itself, `home-hosted migrate` applies the steps it ships:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
home-hosted migrate --dry-run # print the steps, write nothing
|
|
279
|
+
home-hosted migrate # ask, then write β keeps servers.config.json.bak
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Unattended, consent comes from `--yes` or `HHOSTED_MIGRATE=allow`; without it the command stops.
|
|
283
|
+
|
|
284
|
+
</details>
|
|
285
|
+
|
|
267
286
|
---
|
|
268
287
|
|
|
269
288
|
## π Security
|
|
270
289
|
|
|
271
290
|
Everything binds `127.0.0.1` until you say otherwise.
|
|
272
291
|
|
|
273
|
-
- **A password is required to expose the panel.** Replace the default, then bind to `lan` β
|
|
274
|
-
|
|
275
|
-
- **Sessions** live in memory only; the cookie is `HttpOnly` and `SameSite=Strict`, and the
|
|
276
|
-
|
|
277
|
-
- **
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
292
|
+
- **A password is required to expose the panel.** Replace the default, then bind to `lan` β in the
|
|
293
|
+
UI, in the config, or with `--host lan`. The same guard applies in all three places.
|
|
294
|
+
- **Sessions** live in memory only; the cookie is `HttpOnly` and `SameSite=Strict`, and the login
|
|
295
|
+
route locks out repeated failures per IP.
|
|
296
|
+
- **API tokens** for scripts and agents: `home-hosted set-token --generate` prints one once, and a
|
|
297
|
+
request proves itself with `Authorization: Bearer β¦` β the same access as a signed-in browser,
|
|
298
|
+
stored as a SHA-256 hash, revoked with `set-token --clear`.
|
|
299
|
+
- **Port conflicts** are named β `port 4010 is already in use (pid 4242)` β and can be resolved from
|
|
300
|
+
a confirmation popover on that banner or card. The process is looked up again at that moment,
|
|
301
|
+
never taken from the message, and anything the panel supervises is refused, not killed. A server
|
|
302
|
+
that [restarts itself](./SERVERS.md#when-a-program-restarts-itself) can be followed, or replaced
|
|
303
|
+
with a supervised copy.
|
|
304
|
+
- **Secrets never enter the config**: the password hash, the API token hash, the Telegram bot token
|
|
305
|
+
and the TLS key live in `$HHOSTED_HOME/.control-secrets.json` with mode `0600`.
|
|
306
|
+
- **Behind a proxy** turn on `trustProxy` and let `cookieSecure: auto` add `Secure` on https, or
|
|
307
|
+
upload a PEM pair and let home-hosted terminate TLS itself.
|
|
281
308
|
|
|
282
309
|
---
|
|
283
310
|
|
|
284
|
-
##
|
|
311
|
+
## π Notifications
|
|
285
312
|
|
|
286
|
-
|
|
287
|
-
|
|
313
|
+
Telegram, when something happens while you are not looking: a server that gave up restarting, a
|
|
314
|
+
failing health check, a forced restart, a recovery, or a host threshold (disk, memory, swap, load,
|
|
315
|
+
temperature). Opt-in, rate-limited per server *and* reason, and the bot token stays in the secrets
|
|
316
|
+
file. **Two minutes of setup: [NOTIFICATIONS.md](./NOTIFICATIONS.md).**
|
|
288
317
|
|
|
289
|
-
|
|
290
|
-
- with a password β **WinZip AES-256**, openable in 7-Zip, WinRAR, Keka, PeaZip, Arkβ¦
|
|
291
|
-
- restore is selective: check config, secrets, TLS or individual data paths, and nothing
|
|
292
|
-
else is touched
|
|
318
|
+
---
|
|
293
319
|
|
|
294
|
-
|
|
320
|
+
## πΎ Backups
|
|
295
321
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
machine's config says, and `autostart` entries come up immediately.
|
|
299
|
-
|
|
300
|
-
It works because an archive carries its own `servers.config.json` and paths are matched by the
|
|
301
|
-
**declaration** (`9router:DATA_DIR`), not by an absolute path from the source machine. A restore
|
|
302
|
-
never writes where no config declares.
|
|
322
|
+
**Settings β Backups** archives the config, secrets, TLS pair and every data directory your entries
|
|
323
|
+
declare β an ordinary `.zip`, or WinZip AES-256 with a password, restored per path.
|
|
303
324
|
|
|
304
325
|
<details>
|
|
305
|
-
<summary><b
|
|
326
|
+
<summary><b>π One archive is a whole setup</b></summary>
|
|
306
327
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"backupPaths": ["{home}/.myapp/uploads"]
|
|
311
|
-
}
|
|
312
|
-
```
|
|
328
|
+
Start a **blank** home-hosted anywhere β another machine, another user, a fresh container β upload the
|
|
329
|
+
archive and restore. Definitions come back, data lands where *this* machine's config says, and
|
|
330
|
+
`autostart` entries come up immediately.
|
|
313
331
|
|
|
314
|
-
|
|
315
|
-
|
|
332
|
+
It works because an archive carries its own `servers.config.json` and paths are matched by the
|
|
333
|
+
**declaration** (`9router:DATA_DIR`), not by an absolute path from the source machine. A restore never
|
|
334
|
+
writes where no config declares.
|
|
316
335
|
|
|
317
336
|
</details>
|
|
318
337
|
|
|
@@ -326,44 +345,22 @@ panel if yours breaks.
|
|
|
326
345
|
|
|
327
346
|
Two ship in this repo: `uis/stock`, and `uis/noc-console` for TUI and shortcuts wizards; a release
|
|
328
347
|
attaches both as `home-hosted-ui-<name>.zip`. Yours can be anything that compiles to static files β
|
|
329
|
-
the server never cares what built it.
|
|
330
|
-
has the rules and a worked example.
|
|
348
|
+
the server never cares what built it.
|
|
331
349
|
|
|
332
350
|
<details>
|
|
333
351
|
<summary><b>π€ Or have an agent build the UI you actually want</b></summary>
|
|
334
352
|
|
|
335
353
|
The whole contract fits in one file, so a coding agent can do this. Point it at this repo and be
|
|
336
|
-
specific
|
|
354
|
+
specific:
|
|
337
355
|
|
|
338
|
-
> Help me build
|
|
339
|
-
> HP bars, logs in a text-box pane, keyboard navigation, and a save-state corner for backups.
|
|
340
|
-
> Follow `UI_CREATION.md`.
|
|
356
|
+
> Help me build a UI for `home-hosted`: nostalgic game theme, including β¦ features.
|
|
341
357
|
|
|
342
|
-
|
|
343
|
-
you change your mind. [`docs/mockups/`](./docs/mockups) holds example directions to borrow from, or
|
|
344
|
-
ignore all of them.
|
|
358
|
+
[UI_CREATION.md](./UI_CREATION.md) has the endpoints, the SSE frames, the auth rules and a checklist.
|
|
345
359
|
|
|
346
360
|
</details>
|
|
347
361
|
|
|
348
362
|
---
|
|
349
363
|
|
|
350
|
-
## π HTTP API
|
|
351
|
-
|
|
352
|
-
The panel is a client of its own API, so everything is scriptable:
|
|
353
|
-
|
|
354
|
-
| | |
|
|
355
|
-
| --- | --- |
|
|
356
|
-
| `GET /api/state` | the full snapshot: config, live status, host vitals |
|
|
357
|
-
| `GET /api/events` | SSE: the live state, plus logs (`?logs=0`, `?serverId=β¦`) |
|
|
358
|
-
| `GET /api/servers/:id/stream` | SSE: one server's state and logs |
|
|
359
|
-
| `POST /api/servers/:id/{start,stop,restart}` | lifecycle |
|
|
360
|
-
| `PATCH /api/servers/:id`, `PATCH /api/settings` | edit configuration |
|
|
361
|
-
| `GET /api/logs`, `/api/backups`, `/api/notifications` | logs, archives, Telegram |
|
|
362
|
-
| `GET /healthz` | no session needed β the one an external monitor wants |
|
|
363
|
-
| `GET /api/metrics` | Prometheus text |
|
|
364
|
-
|
|
365
|
-
---
|
|
366
|
-
|
|
367
364
|
## β FAQ
|
|
368
365
|
|
|
369
366
|
<details>
|
|
@@ -383,9 +380,12 @@ the same way: every supervised process tree is stopped before the panel exits.
|
|
|
383
380
|
</details>
|
|
384
381
|
|
|
385
382
|
<details>
|
|
386
|
-
<summary><b>
|
|
383
|
+
<summary><b>Nothing starts and the port is busy</b></summary>
|
|
387
384
|
|
|
388
|
-
|
|
385
|
+
A supervised server whose port is taken is reported rather than started over β the panel names the
|
|
386
|
+
holder and offers to free it, and a program that restarts itself can be followed or reclaimed instead
|
|
387
|
+
([SERVERS.md](./SERVERS.md#a-busy-port)). The control port itself is checked before the listener is
|
|
388
|
+
opened.
|
|
389
389
|
|
|
390
390
|
</details>
|
|
391
391
|
|
|
@@ -395,9 +395,9 @@ Just the control panel, `3999` by default. Supervised servers use the ports you
|
|
|
395
395
|
`$HHOSTED_HOME`, default `~/.home-hosted`:
|
|
396
396
|
|
|
397
397
|
```text
|
|
398
|
-
servers.config.json your servers
|
|
398
|
+
servers.config.json your servers, plus meta: which release and schema wrote it
|
|
399
399
|
servers.config.schema.json regenerated on every start, for editor autocomplete
|
|
400
|
-
.control-secrets.json password hash + Telegram token (mode 0600)
|
|
400
|
+
.control-secrets.json password hash + API token hash + Telegram token (mode 0600)
|
|
401
401
|
.logs/ rotated per-server logs + history
|
|
402
402
|
.tls/ an uploaded PEM pair
|
|
403
403
|
.backups/ zip archives
|
|
@@ -409,19 +409,18 @@ run.json the running panel (pid, url, token, mode 0600)
|
|
|
409
409
|
</details>
|
|
410
410
|
|
|
411
411
|
<details>
|
|
412
|
-
<summary><b>
|
|
412
|
+
<summary><b>Which ports does it use?</b></summary>
|
|
413
413
|
|
|
414
|
-
|
|
415
|
-
shipped examples avoid POSIX-only commands. CPU temperature and swap are best-effort where the
|
|
416
|
-
OS does not expose them to an unprivileged process.
|
|
414
|
+
Just the control panel, `3999` by default. Supervised servers use the ports you give them.
|
|
417
415
|
|
|
418
416
|
</details>
|
|
419
417
|
|
|
420
418
|
<details>
|
|
421
|
-
<summary><b>
|
|
419
|
+
<summary><b>Windows support, really?</b></summary>
|
|
422
420
|
|
|
423
|
-
|
|
424
|
-
|
|
421
|
+
Yes. Process trees are sampled from Win32_Process, termination uses `taskkill /T`, and the shipped
|
|
422
|
+
examples avoid POSIX-only commands. CPU temperature and swap are best-effort where the OS does not
|
|
423
|
+
expose them to an unprivileged process, and adopting a self-restarted process is Linux/macOS only.
|
|
425
424
|
|
|
426
425
|
</details>
|
|
427
426
|
|
|
@@ -429,12 +428,9 @@ the holder. The control port itself is checked before the listener is opened.
|
|
|
429
428
|
|
|
430
429
|
## π Working on it
|
|
431
430
|
|
|
432
|
-
<details>
|
|
433
|
-
<summary><b>Layout, scripts and conventions</b></summary>
|
|
434
|
-
|
|
435
431
|
```text
|
|
436
432
|
src/ control plane: config, supervisor, API, providers, services
|
|
437
|
-
src/cli.ts the command line (up/down/status/restart/set-password)
|
|
433
|
+
src/cli.ts the command line (up/down/status/restart/set-password/set-token/migrate)
|
|
438
434
|
src/index.ts the control plane itself, used by `up --foreground`
|
|
439
435
|
uis/ UIs: `stock` (shipped) and alternatives β any framework, static output
|
|
440
436
|
bin/ the published entry point
|
|
@@ -442,8 +438,18 @@ bin/ the published entry point
|
|
|
442
438
|
|
|
443
439
|
`pnpm dev` runs the panel with `tsx watch` plus the stock UI's dev server (state goes to
|
|
444
440
|
`.dev-state/`); `pnpm build` produces `dist/` and `uis/stock/dist/`; `pnpm quickcheck` is lint plus
|
|
445
|
-
types; `pnpm test` is vitest; `pnpm run media` regenerates the GIF above.
|
|
446
|
-
|
|
441
|
+
types; `pnpm test` is vitest; `pnpm run media` regenerates the GIF above.
|
|
442
|
+
|
|
443
|
+
<details>
|
|
444
|
+
<summary><b>π The docs, and which one you want</b></summary>
|
|
445
|
+
|
|
446
|
+
| file | for |
|
|
447
|
+
| --- | --- |
|
|
448
|
+
| [SERVERS.md](./SERVERS.md) | declaring a server: every field, placeholders, port conflicts |
|
|
449
|
+
| [NOTIFICATIONS.md](./NOTIFICATIONS.md) | Telegram alerts, end to end |
|
|
450
|
+
| [UI_CREATION.md](./UI_CREATION.md) | building a UI against the API |
|
|
451
|
+
| [AGENTS.md](./AGENTS.md) | the architecture and the rules worth knowing before changing anything |
|
|
452
|
+
| [/openapi/ui](http://127.0.0.1:3999/openapi/ui) | the live API, on your own panel |
|
|
447
453
|
|
|
448
454
|
</details>
|
|
449
455
|
|
package/SERVERS.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Servers
|
|
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.
|
|
5
|
+
|
|
6
|
+
```json
|
|
7
|
+
{
|
|
8
|
+
"id": "myapp",
|
|
9
|
+
"command": "node",
|
|
10
|
+
"args": ["server.js"],
|
|
11
|
+
"cwd": "./apps/myapp",
|
|
12
|
+
"port": 8080,
|
|
13
|
+
"autostart": true,
|
|
14
|
+
"env": { "NODE_ENV": "production" },
|
|
15
|
+
"health": { "mode": "http", "http": { "path": "/healthz" } }
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Relative paths resolve against the directory you ran `home-hosted` from, so a project can keep its
|
|
20
|
+
servers and its state together.
|
|
21
|
+
|
|
22
|
+
## Field reference
|
|
23
|
+
|
|
24
|
+
| field | what it does |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `command`, `args`, `cwd` | what to run, with `{placeholders}` resolved per entry |
|
|
27
|
+
| `env`, `dataEnvs`, `envFile` | environment; `dataEnvs` also marks data directories for backups, `envFile` keeps secrets out of the config |
|
|
28
|
+
| `port`, `bind` | enables the readiness wait, health checks and the conflict preflight; `local` keeps it on `127.0.0.1` |
|
|
29
|
+
| `onPortConflict` | `block` (default), `warn`, or `adopt` β see below |
|
|
30
|
+
| `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body) |
|
|
31
|
+
| `health.unhealthyThreshold`, `forceRestartAfterMs` | how many failed probes before the card warns, and when to restart anyway |
|
|
32
|
+
| `restart.*` | backoff: `maxRetries`, `baseDelayMs`, `factor`, `maxDelayMs`, `resetAfterMs` |
|
|
33
|
+
| `stop.*` | `signal`, `killGroup`, `graceMs`, and `killPortHolders` to sweep a leftover listener |
|
|
34
|
+
| `dependsOn` | ids that must be healthy first; stopped in reverse order |
|
|
35
|
+
| `resources.maxRssBytes` | restart when the process tree grows past a limit |
|
|
36
|
+
| `bootstrap` | one command to run once before the first start (migrations, warmups) |
|
|
37
|
+
| `backupPaths` | extra paths this entry owns, included in backups |
|
|
38
|
+
|
|
39
|
+
### Placeholders
|
|
40
|
+
|
|
41
|
+
| placeholder | resolves to |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `{id}` `{label}` | the entry's id, and its label (falling back to the id) |
|
|
44
|
+
| `{port}` | its configured port, empty when it has none |
|
|
45
|
+
| `{bind}` `{host}` `{displayHost}` | `local`/`lan`/an address; the address it binds (`127.0.0.1`, `0.0.0.0`); the address to *show* (`127.0.0.1`, the LAN IP) |
|
|
46
|
+
| `{lanIp}` | this machine's LAN address, or `127.0.0.1` |
|
|
47
|
+
| `{cwd}` | the entry's working directory (its `cwd`, else the project directory) |
|
|
48
|
+
| `{projectDir}` | where you ran `home-hosted` (or `--project`) β `$HHOSTED_PROJECT` |
|
|
49
|
+
| `{dataRoot}` | the state directory β `$HHOSTED_HOME`, default `~/.home-hosted` |
|
|
50
|
+
| `{home}` | the current user's home directory |
|
|
51
|
+
|
|
52
|
+
Unknown placeholders stay visible instead of silently becoming empty, and `${VAR}` reads from the
|
|
53
|
+
environment (including `envFile`). Every supervised process also gets `HHOSTED_SERVER_ID` and
|
|
54
|
+
`HHOSTED_CONTROL_PORT`, so a service can tell which entry it is and how to reach the panel.
|
|
55
|
+
|
|
56
|
+
`dataEnvs` is the one that pays for itself twice: the value is exported to the process *and* the path
|
|
57
|
+
is picked up by Backups, so a data directory is declared once.
|
|
58
|
+
|
|
59
|
+
## A busy port
|
|
60
|
+
|
|
61
|
+
Preflight runs before every start, so two servers cannot silently fight over one port.
|
|
62
|
+
|
|
63
|
+
| `onPortConflict` | what happens when something already listens |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `block` *(default)* | the entry goes to **conflict** with `port 4000 is already in use (pid 4242)`, and does not start |
|
|
66
|
+
| `warn` | it starts anyway β useful when the listener is a leftover you are replacing |
|
|
67
|
+
| `follow` | if the listener is a **detached restart of this same entry**, the panel adopts it as-is; anything else blocks, exactly like `block` |
|
|
68
|
+
| `reclaim` | same detection, but it stops that successor and starts a fully supervised process of its own |
|
|
69
|
+
|
|
70
|
+
### When a program restarts itself
|
|
71
|
+
|
|
72
|
+
Some programs restart themselves by launching a new detached process and exiting β a plugin doing an
|
|
73
|
+
update, a `re-exec` on config change. The panel used to see the successor's port as a conflict and sit
|
|
74
|
+
there blocked while the service was actually up.
|
|
75
|
+
|
|
76
|
+
Every process gets `HHOSTED_SERVER_ID` in its environment and a successor inherits it, so the preflight
|
|
77
|
+
can tell a successor from a stranger (Linux `/proc`, macOS `ps -E`; Windows has no per-process
|
|
78
|
+
environment, so there you get the conflict banner and the free-port button). Two policies act on that:
|
|
79
|
+
|
|
80
|
+
| policy | what it does | trade-off |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `follow` | adopts the successor: pid, health probe, CPU/RSS, stop, and it starts its own process again when the successor exits | **keeps exactly what the program set up**, but its output is not captured β the pipe belongs to whoever spawned it, so that entry's log in the panel goes quiet |
|
|
83
|
+
| `reclaim` | stops the successor, then starts a fully supervised process of its own | **full features** β live logs, resources, stop semantics all behave like any other entry β at the cost of one restart |
|
|
84
|
+
|
|
85
|
+
Both are shown as **detached** in the panel while adopted, both can be stopped and restarted like any
|
|
86
|
+
other entry, and neither will ever start a second copy on top of a **stranger**: that still blocks,
|
|
87
|
+
exactly like `block`.
|
|
88
|
+
|
|
89
|
+
If the holder is ours under a different policy, the conflict message says so, which is how the setting
|
|
90
|
+
is discovered:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
port 4374 is already in use (pid 912) β pid 912 is a detached restart of this entry:
|
|
94
|
+
set onPortConflict to "follow" to adopt it, or "reclaim" to replace it with a supervised process
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Editing fields
|
|
98
|
+
|
|
99
|
+
Changes from the panel are atomic and validated before they are written. Editing an entry in the file
|
|
100
|
+
by hand is picked up without a restart; a value the schema rejects is refused with the exact path, and
|
|
101
|
+
the panel keeps running on the config it already had.
|