home-hosted 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,128 @@
1
+ # AGENTS.md
2
+
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 `$HHOSTED_HOME/servers.config.json`
5
+ and serves a UI. User docs: `README.md`; UI authors: `UI_CREATION.md`.
6
+
7
+ State lives only in `$HHOSTED_HOME` (default `~/.home-hosted`): `servers.config.json`,
8
+ `.control-secrets.json` (0600), `.logs/`, `.tls/`, `.backups/`, `.ui/`, and `run.json` — the live
9
+ daemon's pid/url/token, 0600. The package ships **no servers**: never commit a config, a seed
10
+ entry, or a path that names one.
11
+
12
+ ## Commands
13
+
14
+ ```sh
15
+ pnpm run up|down|restart|status # detached; `down` asks /_hh/shutdown, signals are the fallback
16
+ pnpm run start # up --foreground (systemd, docker, a foreground shell)
17
+ pnpm dev # tsx-watch panel :3999 + the stock UI's Vite :3998 (proxies /api)
18
+ pnpm run build # dist/cli.js + the stock UI (uis/stock/dist)
19
+ pnpm run build:uis # every UI under uis/, zipped into uis/dist/ (release assets)
20
+ pnpm run quickcheck # eslint + tsc + vue-tsc for every UI under uis/
21
+ pnpm exec vitest run # `pnpm test` is vitest in watch mode
22
+ pnpm run check # quickcheck + vitest run --coverage
23
+ pnpm run set-password # non-interactive through HHOSTED_PASSWORD
24
+ ```
25
+
26
+ Releases are dispatched from `.github/workflows/release.yml` with a version: changelogen writes the
27
+ changelog, bumps, commits and tags, the GitHub release carries the UI zips, and npm publishes through
28
+ trusted publishing.
29
+
30
+ ## Architecture (and why)
31
+
32
+ - `src/cli.ts` — the CLI. Its only static imports are node builtins: `--home`/`--project` must set
33
+ `HHOSTED_HOME`/`HHOSTED_PROJECT` before any `#src` module resolves paths, so every `#src` import is
34
+ dynamic. `up` re-spawns itself detached as `up --foreground`.
35
+ - `src/index.ts` — `runControlPlane()`: wiring, startup guards (exposure, free port, live run.json),
36
+ `run.json`, signals. Wiring belongs here and nowhere else.
37
+ - `src/app.ts` — the Hono root, chained routes only. `/_hh` is mounted *before* the `/api/*` auth
38
+ guard on purpose (a local `down` uses run.json's token, not a session). `AppType` is the route type
39
+ that `hc<AppType>` clients and the OpenAPI document derive from.
40
+ - `src/api/**` — one file per URL group (`$.routes.ts` = several routes), mirroring the path.
41
+ - `src/shared/contracts.ts` — every ArkType schema (config, API and SSE DTOs), shared with the UIs;
42
+ the OpenAPI spec is generated from it, never hand-written.
43
+ - `src/config/` — `schema.ts` (on-disk shape), `store.ts` (validate/merge/atomic commit, reports
44
+ `configError` instead of throwing on a bad file), `secrets.ts`, `seed.ts`.
45
+ - `src/providers/` — stateless leaves: process, port, proc, health-check, host, telegram, archive.
46
+ - `src/services/` — stateful orchestration: supervisor, control-server, state, auth + exposure,
47
+ dependencies, history, log-buffer/log-files, notifications, host-monitor, backups, tls, ui.
48
+ - `src/helpers/` — paths (`dataRoot` vs `projectDir`), daemon (run.json + a loopback probe that
49
+ bypasses `fetch`, so TLS with a self-signed pair still answers), error, validator, atomic,
50
+ template, env-file, openapi, factory.
51
+ - `uis/<name>/` — each UI is a Vite app (Vue 3 + Tailwind v4) built through `uis/vite.shared.ts`;
52
+ `stock` is the one shipped inside the package. Aliases: `@` → that UI's `src`, `@shared` →
53
+ `src/shared`, `@server` → `src` (**types only** — never import runtime server code into a UI).
54
+ - `bin/home-hosted.mjs` — the published bin: `dist/cli.js`, or `src/cli.ts` through tsx when the
55
+ build is missing (a linked checkout).
56
+
57
+ ## Conventions
58
+
59
+ - `#src/*` imports inside `src/`; UIs use `@shared/*`.
60
+ - ArkType at every runtime boundary: routes use `validate('json'|'query'|'param', schema)` then
61
+ `c.req.valid(...)`; ad-hoc payloads use `parseOrThrow`. Schemas reject undeclared keys.
62
+ - Every failure is a `DetailedError` (`@namesmt/utils`), mapped by `src/helpers/error.ts` into one
63
+ envelope `{ message, code, detail }`. Never hand-roll `c.json({ error })`.
64
+ - Document routes with `describeRoute` + `jsonBody(schema)`; `jsonBody` needs a real schema.
65
+ - Patch schemas carry no defaults; nested groups (`restart`/`health`/`stop`/`auth`/`tls`/`telegram`/
66
+ `http`) merge key-by-key, and an explicit `null` clears a key.
67
+ - Two-sided bounds read inclusively (`'1 <= number.integer <= 512'`). `test/shared/contracts.test.ts`
68
+ pins every boundary and the patch/schema parity — update it with any schema change.
69
+ - Conventional commits; ESLint via `@antfu/eslint-config`; sparse comments.
70
+
71
+ ## Rules that matter
72
+
73
+ - **Server-agnostic.** No blessed ids, no `dataDir`-style globals: a server gets only its own
74
+ `command`/`args`/`env`/`dataEnvs`/`envFile`/`bootstrap`. A test fails if a core file learns one.
75
+ - **Paths.** `dataRoot` is state; `projectDir` is the base for relative entry paths. `{id}{port}`
76
+ `{host}{bind}{cwd}{projectDir}{dataRoot}{home}` and `${ENV}` expand in config; there is no
77
+ package-relative state.
78
+ - **Secrets never enter the config.** Password hash, bot token and TLS key live in the 0600 secrets
79
+ file; the config holds policy.
80
+ - **Never expose beyond loopback without auth and a non-default password.** `checkExposure()` is the
81
+ single rule, enforced at startup, on every settings write, and in the UI.
82
+ - **UIs are external clients.** Nothing in `src/**` may know a UI's markup or files;
83
+ `$HHOSTED_HOME/.ui` overrides the packaged UI at runtime.
84
+ - **A restore writes only where a config says** — archive allowlist, `origin` matching first,
85
+ symlinks never recreated.
86
+ - **Cross-platform.** Linux/macOS/Windows: `/proc` vs `ps` vs Win32_Process, process groups vs
87
+ `taskkill /T`, graceful fallbacks, no shell utilities assumed.
88
+ - **Writes are atomic** (`writeFileAtomic`) and validated before commit.
89
+
90
+ ## Gotchas
91
+
92
+ - `run.json` is the daemon's identity and `down`'s credential; keep `runtimeSchema` in sync with the
93
+ `Runtime` written in `src/index.ts`.
94
+ - Moving the listener (host/port/TLS) kills the connection answering that request, so
95
+ `PATCH /api/settings`, the TLS routes and `/_hh/shutdown` defer with `afterResponse()`.
96
+ - `Supervisor.start()` sets `starting` synchronously before its first await, and `stop()` sets
97
+ `stopping` first: overlapping calls would double-spawn or resurrect a stopped process. Tests must
98
+ call `supervisor.dispose()`.
99
+ - Port preflight re-probes after 300 ms — a just-closed listener can still complete a handshake.
100
+ - `stop.killPortHolders` frees a port only from a *listener* that is not our own process tree. Broad
101
+ `lsof -ti:<port>` sweeps and pid-as-text parses have killed supervisors in the field; don't add one.
102
+ - `ServerView.config.port` is normalized to `number | null`; the hand-narrowed types in
103
+ `contracts.ts` are deliberate.
104
+ - ArkType: an optional property (`'x?'`) rejects an explicit `undefined` (omit the key). Fields a UI
105
+ must clear are `'type | null?'`.
106
+ - Server args are logged *before* `${VAR}` expansion, so an expanded secret never reaches the log
107
+ buffer, disk, SSE or Telegram.
108
+ - Backups are zips; a password makes them WinZip AES-256/AE-2, and zero-byte entries stay
109
+ unencrypted on purpose (p7zip 16.02 reports a CRC failure otherwise). `list()` is sync, so
110
+ encryption flags are cached and refreshed in the background.
111
+ - `vite.server.config.ts` targets `node22` while `engines` requires >= 24 — deliberate margin, leave it.
112
+ - `pnpm test` watches; CI runs `vitest run`.
113
+
114
+ ## Where to extend
115
+
116
+ - **Route**: `src/api/<x>.ts` (chained factory) → mount in `src/app.ts` → schemas in
117
+ `src/shared/contracts.ts` → `describeRoute` + `jsonBody`.
118
+ - **Server field**: `serverSchema` + its patch in contracts, merge keys in `config/store.ts` when
119
+ nested, the form in `uis/stock/src/components/settings/`, and the contract tests.
120
+ - **UI**: a new `uis/<name>/` with a `vite.config.ts` from the shared factory;
121
+ `node scripts/build-uis.mjs <name> --zip`. The contract is `UI_CREATION.md`.
122
+ - **Capability**: a stateless `src/providers/*` returning plain data.
123
+
124
+ ## Publishing
125
+
126
+ `pnpm pack` runs `prepack` (a full build) and ships `bin/`, `dist/`, `uis/stock/dist`, `README.md`,
127
+ `UI_CREATION.md`, `AGENTS.md` and `LICENSE`. The bin falls back to tsx so `pnpm link` works before a
128
+ build; `vue`/`vue-router` are devDependencies because the UIs are prebuilt.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 NamesMT <https://github.com/namesmt>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,433 @@
1
+ <div align="center">
2
+
3
+ # 🏠 home-hosted
4
+
5
+ **The control panel for everything you self-host at home.**
6
+
7
+ <sub>The harness for your servers.</sub>
8
+
9
+ Point it at the things you run — a gateway, a media server, a bot, a database — and it
10
+ starts them, watches them, restarts them when they die, and gives you one page to see
11
+ what is actually going on. Or BYOU — [bring your own UI](#-bring-your-own-ui-byou), for a
12
+ specialized panel that fits you exactly.
13
+
14
+ [![npm](https://img.shields.io/npm/v/home-hosted.svg)](https://www.npmjs.com/package/home-hosted)
15
+ [![Downloads](https://img.shields.io/npm/dm/home-hosted.svg)](https://www.npmjs.com/package/home-hosted)
16
+ [![License](https://img.shields.io/npm/l/home-hosted.svg)](./LICENSE)
17
+ [![Node](https://img.shields.io/node/v/home-hosted.svg)](https://nodejs.org)
18
+
19
+ [🚀 Quick start](#-quick-start) · [✨ Features](#-features) · [🧩 Adding a server](#-adding-a-server) · [🛠 CLI](#-cli) · [🔐 Security](#-security) · [💾 Backups](#-backups)
20
+
21
+ </div>
22
+
23
+ ---
24
+
25
+ ## 🤔 Why?
26
+
27
+ Running services on a home machine usually means one of two extremes: `tmux` sessions you
28
+ forget about, or a hand-written systemd unit per service (times six).
29
+
30
+ | | |
31
+ | --- | --- |
32
+ | ❌ **"Is it still running?"** | You check with `ps`, then `curl`, then hope. |
33
+ | ❌ **Silent deaths** | Something crashes at 3am and you notice days later. |
34
+ | ❌ **One terminal per service** | Logs scroll away in tabs you closed. |
35
+ | ❌ **Fragile restarts** | The box reboots and half the stack is gone. |
36
+ | ✅ **home-hosted** | Declare it once, watch it forever, one command to stop it all. |
37
+
38
+ ```text
39
+ ┌──────────────────────────────────────┐
40
+ your browser ──▶│ home-hosted · 127.0.0.1:3999 │
41
+ │ Vue panel + JSON API + SSE logs │
42
+ └───────────────┬──────────────────────┘
43
+ │ supervises
44
+ ┌──────────────────────────┼──────────────────────────┐
45
+ ▼ ▼ ▼
46
+ ┌─────────┐ ┌─────────┐ ┌─────────┐
47
+ │ gateway │ │ files │ │ bot │
48
+ │ :4000 │ │ :4010 │ │ ... │
49
+ └─────────┘ └─────────┘ └─────────┘
50
+ health ✓ health ✓ restarts ↻
51
+ ```
52
+
53
+ It ships with **nothing**. No blessed paths, no "data directory" setting, no opinion about
54
+ what you run — a server is a command, some arguments, and the environment you give it.
55
+
56
+ ---
57
+
58
+ ## ⚡ Quick start
59
+
60
+ ```bash
61
+ npx home-hosted # start it — detached, it stays running
62
+ npx home-hosted status # where is it, is it healthy
63
+ ```
64
+
65
+ That is it. The panel is on **<http://127.0.0.1:3999>**, and it keeps running after the
66
+ terminal closes. Stop it whenever you like:
67
+
68
+ ```bash
69
+ npx home-hosted down # stops the panel *and* everything it started
70
+ ```
71
+
72
+ > [!NOTE]
73
+ > The first boot writes a default password (`hh`) so the panel is never unprotected. Change
74
+ > it under **Settings → Password** — binding beyond `127.0.0.1` stays refused until you do.
75
+
76
+ <details>
77
+ <summary><b>📦 Install it instead of npx-ing it</b></summary>
78
+
79
+ ```bash
80
+ npm install -g home-hosted
81
+ home-hosted up
82
+
83
+ # or keep a project with its own servers *and* its own state directory
84
+ cd ~/my-servers
85
+ HHOSTED_HOME=./state npx home-hosted up
86
+ ```
87
+
88
+ Before the first npm release, the same commands work from a checkout: `pnpm install && pnpm run up`
89
+ in this repo, or `pnpm link` into a project that should depend on it.
90
+
91
+ Everything home-hosted owns — config, secrets, logs, TLS, backups — lives in one directory:
92
+ `$HHOSTED_HOME`, defaulting to `~/.home-hosted`. Delete it and nothing of yours is left behind.
93
+
94
+ </details>
95
+
96
+ <details>
97
+ <summary><b>📁 Keep it in a project you can take anywhere (and share)</b></summary>
98
+
99
+ Pin the version, install the servers next to it, and commit the whole project — it *is* the
100
+ setup:
101
+
102
+ ```bash
103
+ mkdir my-servers && cd my-servers
104
+ npm init -y
105
+ npm install home-hosted@0.2.0 9router serve # pinned, so `git clone && npm ci` is enough
106
+
107
+ npx home-hosted up --home ./state # state lives inside the project
108
+ ```
109
+
110
+ ```jsonc
111
+ // package.json
112
+ {
113
+ "scripts": {
114
+ "up": "home-hosted up --home ./state",
115
+ "down": "home-hosted down --home ./state",
116
+ "status": "home-hosted status --home ./state"
117
+ }
118
+ }
119
+ ```
120
+
121
+ ```gitignore
122
+ state/* # secrets, logs, TLS keys and archives stay local…
123
+ !state/servers.config.json # …but the server definitions are committed
124
+ ```
125
+
126
+ <sub>Call them as `npm run up` — `npm up` is npm's own update, not your script.</sub>
127
+
128
+ One clone, `npm ci`, `npm run up`, and the whole setup is up on any machine with Node.
129
+ **Settings → Backups** exports the same thing as one archive for someone else to restore.
130
+
131
+ </details>
132
+
133
+ <details>
134
+ <summary><b>🧰 Run it under systemd or Docker (no daemon needed)</b></summary>
135
+
136
+ ```bash
137
+ home-hosted up --foreground # stays in the foreground, logs to stderr
138
+ ```
139
+
140
+ ```ini
141
+ [Unit]
142
+ Description=home-hosted
143
+ [Service]
144
+ ExecStart=/usr/local/bin/home-hosted up --foreground
145
+ Environment=HHOSTED_HOME=/srv/home-hosted
146
+ Restart=always
147
+ [Install]
148
+ WantedBy=multi-user.target
149
+ ```
150
+
151
+ </details>
152
+
153
+ ---
154
+
155
+ ## ✨ Features
156
+
157
+ | | |
158
+ | --- | --- |
159
+ | 🚦 **Lifecycle** | Start, stop, restart from the panel or the API; `autostart` entries come up with it. |
160
+ | ♻️ **Auto-restart** | Exponential backoff on crash, with the counter reset once a process stays up. |
161
+ | 🩺 **Health that acts** | TCP or HTTP probes per server: warn on the card, force a restart after a timeout, and check ports before starting. |
162
+ | 🔗 **Ordered startup** | `dependsOn` waits for a dependency to be *healthy* — not merely spawned — and stops in reverse. |
163
+ | 📜 **Logs** | Live per-server stream, buffer plus rotated files on disk, search, download, one click to clear. |
164
+ | 📈 **Resources** | CPU and RSS of the whole process tree, with an optional memory ceiling that triggers a restart. |
165
+ | 🌡️ **Host vitals** | Load, memory, swap, disk and CPU temperature, with thresholds that notify once and again on recovery. |
166
+ | 💾 **Backups** | One click for config, secrets, TLS and your declared data directories — plain `.zip`, or AES-256 with a password, restored per path. |
167
+ | 🚚 **Portable setup** | Restore a shared backup onto a blank instance and the whole server setup is back: definitions, data, secrets and all. |
168
+ | 🎨 **BYOU — Bring Your Own UI** | The panel is a static site you can replace: upload your own build (or ship a whole alternative), and `home-hosted ui-revert` puts the stock one back. See [UI_CREATION.md](./UI_CREATION.md). |
169
+ | 🔔 **Notifications** | Telegram (grammY) on crash, unhealthy, forced restart, recovery and host thresholds. |
170
+ | 🔐 **Security** | httpOnly cookie sessions, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
171
+ | 🧩 **Server-agnostic** | `command` + `args` + `env` + `cwd`. Nothing in the code knows what you run. |
172
+ | 🖥 **Cross-platform** | Linux, macOS and Windows: `/proc`, `ps` or Win32_Process, process groups or `taskkill /T`, no shell dependencies. |
173
+
174
+ ---
175
+
176
+ ## 🧩 Adding a server
177
+
178
+ Use **➕ Add server** in the panel, or write it into `servers.config.json`:
179
+
180
+ ```json
181
+ {
182
+ "id": "myapp",
183
+ "command": "node",
184
+ "args": ["server.js"],
185
+ "cwd": "./apps/myapp",
186
+ "port": 8080,
187
+ "autostart": true,
188
+ "env": { "NODE_ENV": "production" },
189
+ "health": { "mode": "http", "http": { "path": "/healthz" } }
190
+ }
191
+ ```
192
+
193
+ Relative paths resolve against the directory you ran `home-hosted` from, so a project can keep
194
+ its servers and its state together:
195
+
196
+ ```bash
197
+ cd ~/my-servers
198
+ HHOSTED_HOME=./state npx home-hosted up
199
+ ```
200
+
201
+ <details>
202
+ <summary><b>🧾 Field reference</b></summary>
203
+
204
+ | field | what it does |
205
+ | --- | --- |
206
+ | `command`, `args`, `cwd` | what to run, with `{placeholders}` resolved per entry |
207
+ | `env`, `dataEnvs`, `envFile` | environment; `dataEnvs` also marks data directories for backups, `envFile` keeps secrets out of the config |
208
+ | `port`, `bind` | enables the readiness wait, health checks and the conflict preflight; `local` keeps it on `127.0.0.1` |
209
+ | `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body) |
210
+ | `restart.*` | backoff: `maxRetries`, `baseDelayMs`, `factor`, `maxDelayMs`, `resetAfterMs` |
211
+ | `stop.*` | signal, `killGroup`, grace period, and whether to sweep leftover port holders |
212
+ | `dependsOn` | ids that must be healthy first; stopped in reverse order |
213
+ | `resources.maxRssBytes` | restart when the process tree grows past a limit |
214
+ | `bootstrap` | one command to run once before the first start (migrations, warmups) |
215
+ | `backupPaths` | extra paths this entry owns, included in backups |
216
+
217
+ Placeholders: `{id}` `{label}` `{port}` `{host}` `{displayHost}` `{bind}` `{lanIp}` `{cwd}`
218
+ `{projectDir}` `{dataRoot}` `{home}`, plus `${VAR}` from the environment. Unknown placeholders
219
+ stay visible instead of silently becoming empty.
220
+
221
+ Every supervised process also gets `HHOSTED_SERVER_ID` and `HHOSTED_CONTROL_PORT`, so a
222
+ service can tell which entry it is and how to reach the panel.
223
+
224
+ </details>
225
+
226
+ ---
227
+
228
+ ## 🛠 CLI
229
+
230
+ | command | |
231
+ | --- | --- |
232
+ | `home-hosted up` | start the panel detached, and keep it alive in the background |
233
+ | `home-hosted down` | stop it cleanly — supervised processes included |
234
+ | `home-hosted restart` | `down`, then `up` |
235
+ | `home-hosted status` | pid, URL, health, uptime, state and log paths (`--json` for scripts) |
236
+ | `home-hosted set-password` | set the panel password without opening a browser |
237
+ | `home-hosted ui-revert` | go back to the stock panel UI after uploading your own |
238
+
239
+ <details>
240
+ <summary><b>⚙️ Flags</b></summary>
241
+
242
+ ```text
243
+ -c, --config <file> servers config (default: <state>/servers.config.json)
244
+ -p, --port <port> control panel port (default: 3999)
245
+ --host <bind> local | lan | an ipv4 address
246
+ --open open the panel in a browser once it is up
247
+ --no-autostart do not start the entries marked autostart
248
+ --foreground run in this process instead of detaching
249
+ --print-config print the effective config and exit
250
+ --home <dir> state directory (or $HHOSTED_HOME)
251
+ --project <dir> base for relative paths (or $HHOSTED_PROJECT)
252
+ ```
253
+
254
+ </details>
255
+
256
+ ---
257
+
258
+ ## 🔐 Security
259
+
260
+ Everything binds `127.0.0.1` until you say otherwise.
261
+
262
+ - **A password is required to expose the panel.** Replace the default, then bind to `lan` —
263
+ in the UI, in the config, or with `--host lan`. The same guard applies in all three places.
264
+ - **Sessions** live in memory only; the cookie is `HttpOnly` and `SameSite=Strict`, and the
265
+ login route locks out repeated failures per IP.
266
+ - **Secrets never enter the config**: the password hash, the Telegram token and the TLS key
267
+ live in `$HHOSTED_HOME/.control-secrets.json` with mode `0600`.
268
+ - **Behind a proxy** turn on `trustProxy` and let `cookieSecure: auto` add `Secure` on https,
269
+ or upload a PEM pair and let home-hosted terminate TLS itself.
270
+
271
+ ---
272
+
273
+ ## 💾 Backups
274
+
275
+ **Settings → Backups** archives the config, the secrets file, the TLS pair and the data
276
+ directories your entries declare — as an ordinary `.zip`:
277
+
278
+ - no password → **plain zip**, openable by every operating system out of the box
279
+ - with a password → **WinZip AES-256**, openable in 7-Zip, WinRAR, Keka, PeaZip, Ark…
280
+ - restore is selective: check config, secrets, TLS or individual data paths, and nothing
281
+ else is touched
282
+
283
+ ### 🚚 One archive is a whole setup
284
+
285
+ Start a **blank** home-hosted anywhere — another machine, another user, a fresh container —
286
+ open **Settings → Backups**, upload the archive and restore: the server definitions come
287
+ back, their data lands where *this* machine's config says it should, and the entries marked
288
+ `autostart` are brought up right away. Nothing to re-declare by hand.
289
+
290
+ That works because a backup carries its own `servers.config.json`, and paths are matched by
291
+ the **declaration** (`9router:DATA_DIR`), not by the absolute path they had on the source
292
+ machine — so `{home}` stays this user's home. A restore never writes anywhere a config does
293
+ not declare: yours, or the one inside the archive you chose to restore.
294
+
295
+ <details>
296
+ <summary><b>🗂 Declaring data directories</b></summary>
297
+
298
+ ```json
299
+ {
300
+ "dataEnvs": { "DATA_DIR": "{home}/.myapp" },
301
+ "backupPaths": ["{home}/.myapp/uploads"]
302
+ }
303
+ ```
304
+
305
+ `dataEnvs` is exported to the process *and* backed up, so a data directory is declared once.
306
+ A path already covered by a declared parent is skipped.
307
+
308
+ </details>
309
+
310
+ ---
311
+
312
+ Two UIs live in this repo: **stock** (the panel you get by default) and **noc-console**, an
313
+ operations console over the same API. `pnpm run build:uis` builds and zips every one of them, and
314
+ each release attaches them as `home-hosted-ui-<name>.zip` — install one from Settings → Interface.
315
+
316
+ ## 🎨 Bring your own UI (BYOU)
317
+
318
+ The panel is a static site served from `$HHOSTED_HOME/.ui` when one is installed, and from
319
+ the package otherwise. **Settings → Interface** takes a zip; that is the whole install step,
320
+ no restart and no fork.
321
+
322
+ ```bash
323
+ home-hosted ui-revert # if it breaks, the stock panel is one command away
324
+ ```
325
+
326
+ The API is documented at `/openapi/ui` (and `/openapi/spec.json`), generated from the same
327
+ schemas the server validates with, so a UI can be built in any framework — the stock one is
328
+ just the first client. [UI_CREATION.md](./UI_CREATION.md) has the contract, the rules and a
329
+ worked example.
330
+
331
+ ---
332
+
333
+ ## 🌐 HTTP API
334
+
335
+ The panel is a client of its own API, so everything is scriptable:
336
+
337
+ | | |
338
+ | --- | --- |
339
+ | `GET /api/state` | the full snapshot: config, live status, host vitals |
340
+ | `GET /api/servers/:id/stream` | SSE stream of state and log lines |
341
+ | `POST /api/servers/:id/{start,stop,restart}` | lifecycle |
342
+ | `PATCH /api/servers/:id`, `PATCH /api/settings` | edit configuration |
343
+ | `GET /api/logs`, `/api/backups`, `/api/notifications` | logs, archives, Telegram |
344
+ | `GET /healthz` | no session needed — the one an external monitor wants |
345
+ | `GET /api/metrics` | Prometheus text |
346
+
347
+ ---
348
+
349
+ ## ❓ FAQ
350
+
351
+ <details>
352
+ <summary><b>Is it a systemd replacement?</b></summary>
353
+
354
+ No — it is a friendlier layer for the handful of things you run yourself. Keep systemd for the
355
+ panel itself (`--foreground`) and for system services; use home-hosted for the rest.
356
+
357
+ </details>
358
+
359
+ <details>
360
+ <summary><b>What happens to my servers when the panel stops?</b></summary>
361
+
362
+ `home-hosted down` stops them — that is the point of the command. `SIGTERM`/`SIGINT` are handled
363
+ the same way: every supervised process tree is stopped before the panel exits.
364
+
365
+ </details>
366
+
367
+ <details>
368
+ <summary><b>Which ports does it use?</b></summary>
369
+
370
+ Just the control panel, `3999` by default. Supervised servers use the ports you give them.
371
+
372
+ </details>
373
+
374
+ <details>
375
+ <summary><b>Where is my state?</b></summary>
376
+
377
+ `$HHOSTED_HOME`, default `~/.home-hosted`:
378
+
379
+ ```text
380
+ servers.config.json your servers (the UI writes it back atomically)
381
+ servers.config.schema.json regenerated on every start, for editor autocomplete
382
+ .control-secrets.json password hash + Telegram token (mode 0600)
383
+ .logs/ rotated per-server logs + history
384
+ .tls/ an uploaded PEM pair
385
+ .backups/ zip archives
386
+ run.json the running panel (pid, url, token, mode 0600)
387
+ ```
388
+
389
+ `home-hosted status` prints the paths.
390
+
391
+ </details>
392
+
393
+ <details>
394
+ <summary><b>Windows support, really?</b></summary>
395
+
396
+ Yes. Process trees are sampled from Win32_Process, termination uses `taskkill /T`, and the
397
+ shipped examples avoid POSIX-only commands. CPU temperature and swap are best-effort where the
398
+ OS does not expose them to an unprivileged process.
399
+
400
+ </details>
401
+
402
+ <details>
403
+ <summary><b>Nothing starts and the port is busy</b></summary>
404
+
405
+ A supervised server whose port is taken is reported rather than started over — the panel names
406
+ the holder. The control port itself is checked before the listener is opened.
407
+
408
+ </details>
409
+
410
+ ---
411
+
412
+ ## 🗂 Layout
413
+
414
+ ```text
415
+ src/ control plane: config, supervisor, API, providers, services
416
+ src/cli.ts the command line (up/down/status/restart/set-password)
417
+ src/index.ts the control plane itself, used by `up --foreground`
418
+ uis/ UIs: `stock` (shipped) and alternatives, each a Vite app
419
+ bin/ the published entry point
420
+ vite.server.config.ts bundles src/cli.ts into dist/cli.js
421
+ ```
422
+
423
+ Development: `pnpm dev` runs the panel with `tsx watch` plus the Vite dev server (state goes to
424
+ `.dev-state/`), `pnpm build` produces `dist/` and `uis/stock/dist/`, `pnpm quickcheck` is lint plus
425
+ types, and `pnpm test` is vitest.
426
+
427
+ ---
428
+
429
+ <div align="center">
430
+
431
+ **MIT**
432
+
433
+ </div>