home-hosted 0.4.1 → 0.6.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 +101 -26
- package/README.md +113 -43
- package/dist/cli.js +2153 -765
- package/dist/cli.js.map +1 -1
- package/{SERVERS.md → docs/SERVERS.md} +61 -8
- package/{UI_CREATION.md → docs/UI_CREATION.md} +34 -2
- package/package.json +9 -5
- package/uis/stock/dist/assets/index-4_Q69j7h.js +46 -0
- package/uis/stock/dist/assets/index-DMo6krK0.css +2 -0
- package/uis/stock/dist/index.html +2 -2
- package/uis/stock/dist/assets/index-DOD80X00.css +0 -2
- package/uis/stock/dist/assets/index-Dl2nDThm.js +0 -47
- /package/{NOTIFICATIONS.md → docs/NOTIFICATIONS.md} +0 -0
|
@@ -26,7 +26,7 @@ servers and its state together.
|
|
|
26
26
|
| `command`, `args`, `cwd` | what to run, with `{placeholders}` resolved per entry |
|
|
27
27
|
| `env`, `dataEnvs`, `envFile` | environment; `dataEnvs` also marks data directories for backups, `envFile` keeps secrets out of the config |
|
|
28
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 `
|
|
29
|
+
| `onPortConflict` | `block` (default), `warn`, `follow`, `reclaim`, or `kill` — see below |
|
|
30
30
|
| `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body) |
|
|
31
31
|
| `health.unhealthyThreshold`, `forceRestartAfterMs` | how many failed probes before the card warns, and when to restart anyway |
|
|
32
32
|
| `restart.*` | backoff: `maxRetries`, `baseDelayMs`, `factor`, `maxDelayMs`, `resetAfterMs` |
|
|
@@ -34,7 +34,7 @@ servers and its state together.
|
|
|
34
34
|
| `dependsOn` | ids that must be healthy first; stopped in reverse order |
|
|
35
35
|
| `resources.maxRssBytes` | restart when the process tree grows past a limit |
|
|
36
36
|
| `bootstrap` | one command to run once before the first start (migrations, warmups) |
|
|
37
|
-
| `backupPaths` | extra paths this entry owns, included in backups |
|
|
37
|
+
| `backupPaths`, `backupIgnoreGenerated` | extra paths this entry owns, included in backups; the flag (on by default) skips the known build and dependency directories inside them — `node_modules`, `dist`, `.next`, framework caches |
|
|
38
38
|
|
|
39
39
|
### Placeholders
|
|
40
40
|
|
|
@@ -66,6 +66,7 @@ Preflight runs before every start, so two servers cannot silently fight over one
|
|
|
66
66
|
| `warn` | it starts anyway — useful when the listener is a leftover you are replacing |
|
|
67
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
68
|
| `reclaim` | same detection, but it stops that successor and starts a fully supervised process of its own |
|
|
69
|
+
| `kill` | stops **whatever holds the port** — no ownership test — then starts. The blunt one: see below |
|
|
69
70
|
|
|
70
71
|
### When a program restarts itself
|
|
71
72
|
|
|
@@ -74,14 +75,32 @@ update, a `re-exec` on config change. The panel used to see the successor's port
|
|
|
74
75
|
there blocked while the service was actually up.
|
|
75
76
|
|
|
76
77
|
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
|
|
78
|
-
|
|
78
|
+
can tell a successor from a stranger — read from `/proc` on Linux and `ps -E` on macOS. **Windows
|
|
79
|
+
cannot read another process's environment**, so there the entry's own argv answers instead: a process
|
|
80
|
+
holding the port is the successor when its image and its arguments are both the entry's own. That
|
|
81
|
+
fallback also covers a Linux or macOS program that restarted itself in a way that dropped the marker.
|
|
82
|
+
Two policies act on the result:
|
|
79
83
|
|
|
80
84
|
| policy | what it does | trade-off |
|
|
81
85
|
| --- | --- | --- |
|
|
82
86
|
| `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
87
|
| `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
88
|
|
|
89
|
+
Identification is deliberately strict, because `reclaim` kills what it identifies: the image has to be
|
|
90
|
+
the entry's resolved command **and** the arguments have to be the entry's own, compared literally — an
|
|
91
|
+
argument sharing a basename with another program's is not a match. When **more than one** holder
|
|
92
|
+
matches, the panel refuses to guess and blocks, naming the pids. These are the cases that still block
|
|
93
|
+
rather than being identified:
|
|
94
|
+
|
|
95
|
+
- a successor that re-execs under a **different image**, or rewrites its own arguments;
|
|
96
|
+
- an argument read back ambiguously — on macOS `ps` joins argv with spaces, so an argument that itself
|
|
97
|
+
contains a space cannot be told from two arguments;
|
|
98
|
+
- a Windows entry launched through an `npm`/`pnpm` **`.cmd` shim**: the shim runs under `cmd.exe`, so
|
|
99
|
+
the process actually holding the port is `node.exe` and its argv never mentions the shim;
|
|
100
|
+
- a Windows entry whose `CommandLine` PowerShell cannot read.
|
|
101
|
+
|
|
102
|
+
`kill` exists precisely for those cases: it does not need to identify anything.
|
|
103
|
+
|
|
85
104
|
Both are shown as **detached** in the panel while adopted, both can be stopped and restarted like any
|
|
86
105
|
other entry, and neither will ever start a second copy on top of a **stranger**: that still blocks,
|
|
87
106
|
exactly like `block`.
|
|
@@ -91,11 +110,45 @@ is discovered:
|
|
|
91
110
|
|
|
92
111
|
```text
|
|
93
112
|
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,
|
|
113
|
+
set onPortConflict to "follow" to adopt it, "reclaim" to replace it with a supervised process,
|
|
114
|
+
or "kill" to stop whatever holds the port
|
|
95
115
|
```
|
|
96
116
|
|
|
117
|
+
### The `kill` policy
|
|
118
|
+
|
|
119
|
+
`follow` and `reclaim` need to know *whose* port it is. When they cannot answer that — a program that
|
|
120
|
+
re-execs under a different image, or two holders that both look like the entry — `kill` skips the
|
|
121
|
+
question:
|
|
122
|
+
|
|
123
|
+
1. SIGTERM every listener on the port, escalating to SIGKILL after `stop.graceMs`.
|
|
124
|
+
2. Wait for the port to actually accept nothing, then start as usual.
|
|
125
|
+
3. If the port is somehow still held, the entry goes to **conflict** with the pids it tried —
|
|
126
|
+
it does **not** retry in a loop.
|
|
127
|
+
|
|
128
|
+
It is the one policy that will stop a **stranger** whose port you have configured, so the log says
|
|
129
|
+
which pids it stopped and why. It will not touch the panel itself or any process the panel
|
|
130
|
+
supervises: a port held by a sibling entry is a config mistake and still blocks, exactly as it does
|
|
131
|
+
under `block`. If it is a stray leftover you would rather inspect first, the **Free port** button on
|
|
132
|
+
the server card does the same thing on demand.
|
|
133
|
+
|
|
134
|
+
Because a detached successor is not something the panel supervises, `kill` ends up doing what
|
|
135
|
+
`reclaim` does for your own restarted process — stops it and starts a supervised child — without ever
|
|
136
|
+
asking whether it was yours. Only `follow` ever adopts, and only `reclaim` will refuse to act on a
|
|
137
|
+
holder it cannot prove is yours.
|
|
138
|
+
|
|
97
139
|
## Editing fields
|
|
98
140
|
|
|
99
|
-
Changes from the panel are atomic and validated before they are written. Editing
|
|
100
|
-
|
|
101
|
-
|
|
141
|
+
Changes from the panel are atomic and validated before they are written. Editing the file by hand —
|
|
142
|
+
an editor, a `git checkout`, a config-management tool — is picked up within a couple of seconds, no
|
|
143
|
+
restart needed:
|
|
144
|
+
|
|
145
|
+
- **A definition you changed** takes effect on that entry's next start; a running process is not
|
|
146
|
+
restarted under you.
|
|
147
|
+
- **A definition you added** appears (and starts itself if it is `autostart`); **one you removed** is
|
|
148
|
+
stopped and forgotten.
|
|
149
|
+
- **A file the schema rejects, or one that cannot be parsed**, is reported in the panel with the
|
|
150
|
+
exact path and the running config is left alone. Fix it and it reloads on its own — a typo never
|
|
151
|
+
stops a server.
|
|
152
|
+
|
|
153
|
+
A save from the panel's own UI is the panel's write: it replaces the file, so hand-edits made while
|
|
154
|
+
it runs are lost by that next save. Edit, then let the panel read it back, before using the UI.
|
|
@@ -11,12 +11,12 @@ specific about what you want:
|
|
|
11
11
|
|
|
12
12
|
> Help me build a UI for `home-hosted`: a nostalgic game theme. Servers as a party menu, health as
|
|
13
13
|
> HP bars, logs in a text-box pane, keyboard navigation, and a save-state corner for backups.
|
|
14
|
-
> Follow `UI_CREATION.md`.
|
|
14
|
+
> Follow `docs/UI_CREATION.md`.
|
|
15
15
|
|
|
16
16
|
Then zip the build and install it (below). Useful constraints to include in the prompt: the API is
|
|
17
17
|
same-origin (relative `/api/...`), `401 { code: 'AUTH_REQUIRED' }` means "show the login screen",
|
|
18
18
|
live data should come from SSE, and assets must be self-hosted. Existing directions to borrow from
|
|
19
|
-
live in [`
|
|
19
|
+
live in [`mockups/`](./mockups) — or ask for something else entirely; the server does not
|
|
20
20
|
care what your UI looks like.
|
|
21
21
|
|
|
22
22
|
## Install it
|
|
@@ -33,6 +33,9 @@ $HHOSTED_HOME/.ui/ ← where your build lives
|
|
|
33
33
|
(`zip -r ui.zip dist` works too). `ui.json` is optional; it is what the settings page shows as
|
|
34
34
|
installed.
|
|
35
35
|
|
|
36
|
+
Without the settings page, `home-hosted ui-switch` installs one from a GitHub release asset (its
|
|
37
|
+
default), from a local `.zip` (`--file ./ui.zip`), or from a URL (`--file https://…/ui.zip`).
|
|
38
|
+
|
|
36
39
|
Nothing is built on the server side: whatever you upload is served as-is, so ship plain
|
|
37
40
|
HTML/JS/CSS or the output of your own Vite/Next/Astro build with relative asset paths.
|
|
38
41
|
|
|
@@ -102,6 +105,11 @@ signed-in session. The browser app you ship should still use the cookie.
|
|
|
102
105
|
Two settings worth reflecting: `control.label` is the panel's own name (the stock shell shows it),
|
|
103
106
|
and `GET /api/settings` includes `ui` — which UI is being served, and its metadata.
|
|
104
107
|
|
|
108
|
+
An entry body is partial by design: `POST /api/servers` and `PATCH /api/servers/:id` take only the
|
|
109
|
+
fields the person decided, and the panel's **Server defaults** fill the rest. Sending a value the
|
|
110
|
+
person never chose freezes it against those defaults, so build the body as a diff
|
|
111
|
+
(`inheritBaseline` and `diffFields` in `src/shared/patch-diff.ts`).
|
|
112
|
+
|
|
105
113
|
### Failures
|
|
106
114
|
|
|
107
115
|
Every failing request answers with one envelope:
|
|
@@ -128,6 +136,30 @@ Send `?logs=0` to skip log frames, or `?serverId=<id>` to follow one server. The
|
|
|
128
136
|
`sseMessageSchema` in `src/shared/contracts.ts` — the server validates against it before writing, so
|
|
129
137
|
that schema is also your best type source.
|
|
130
138
|
|
|
139
|
+
## Editing settings without fighting the stream
|
|
140
|
+
|
|
141
|
+
Two things the API will not tell you, and both are what people report as bugs:
|
|
142
|
+
|
|
143
|
+
- **A live frame must never overwrite a half-typed edit.** Every frame carries freshly built
|
|
144
|
+
objects, so a watcher that copies state into a form on each change reverts whatever is being
|
|
145
|
+
typed. The settings are also not one payload: host thresholds and the backups policy come from
|
|
146
|
+
`GET /api/settings`, which lands *after* the first SSE frame — a single "has anything changed?"
|
|
147
|
+
guard reads that late arrival as a pending edit, leaves those fields showing schema defaults
|
|
148
|
+
forever, and offers to "revert" a change nobody made. Fill each block on its own, and only while
|
|
149
|
+
that block is untouched since it was last filled (`uis/stock/src/components/settings/settingsForm.ts`).
|
|
150
|
+
- **A boolean setting is a switch, not a checkbox.** Keep checkboxes for picking items out of a set
|
|
151
|
+
(a restore plan), where the control is the list entry. `uis/stock/src/components/ui/ToggleSwitch.vue`
|
|
152
|
+
is the reference.
|
|
153
|
+
- **Decide to open the stream from the session, not from the auth flags.** With authentication off,
|
|
154
|
+
`authRequired` and `authenticated` are both `false` from the first paint to the last, so a watcher
|
|
155
|
+
on those two never re-runs when the session lands: no event stream is opened, the connection badge
|
|
156
|
+
sits on "Connecting", and the dashboard shows one stale snapshot forever. Turn the two flags into a
|
|
157
|
+
single decision (`wait` | `connect` | `login`) and watch that — see
|
|
158
|
+
`uis/stock/src/composables/useSession.ts`.
|
|
159
|
+
|
|
160
|
+
A pending-edit summary pays for itself: show how many fields changed, and let the list be opened —
|
|
161
|
+
`describeChanges` and `countLeaves` in `src/shared/patch-diff.ts` turn a patch into those rows.
|
|
162
|
+
|
|
131
163
|
## Building one in this repo
|
|
132
164
|
|
|
133
165
|
The repo's own UIs are Vite apps: `uis/<name>/` holds `index.html`, `src/`, a `tsconfig.json`
|
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.6.0",
|
|
5
5
|
"packageManager": "pnpm@12.5.1",
|
|
6
6
|
"description": "A self-hosted control panel that keeps your home server processes alive - with a UI you can replace.",
|
|
7
7
|
"author": "NamesMT <dangquoctrung123@gmail.com>",
|
|
@@ -33,17 +33,18 @@
|
|
|
33
33
|
},
|
|
34
34
|
"main": "./dist/cli.js",
|
|
35
35
|
"bin": {
|
|
36
|
-
"home-hosted": "bin/home-hosted.mjs"
|
|
36
|
+
"home-hosted": "bin/home-hosted.mjs",
|
|
37
|
+
"hh": "bin/home-hosted.mjs"
|
|
37
38
|
},
|
|
38
39
|
"files": [
|
|
39
40
|
"AGENTS.md",
|
|
40
41
|
"LICENSE",
|
|
41
|
-
"NOTIFICATIONS.md",
|
|
42
42
|
"README.md",
|
|
43
|
-
"SERVERS.md",
|
|
44
|
-
"UI_CREATION.md",
|
|
45
43
|
"bin",
|
|
46
44
|
"dist",
|
|
45
|
+
"docs/NOTIFICATIONS.md",
|
|
46
|
+
"docs/SERVERS.md",
|
|
47
|
+
"docs/UI_CREATION.md",
|
|
47
48
|
"uis/stock/dist"
|
|
48
49
|
],
|
|
49
50
|
"engines": {
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"up": "tsx src/cli.ts up",
|
|
54
55
|
"down": "tsx src/cli.ts down",
|
|
55
56
|
"status": "tsx src/cli.ts status",
|
|
57
|
+
"restart": "tsx src/cli.ts restart",
|
|
56
58
|
"start": "tsx src/cli.ts up --foreground",
|
|
57
59
|
"dev": "node scripts/dev.mjs",
|
|
58
60
|
"build": "pnpm run build:ui && pnpm run build:server",
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
"@scalar/hono-api-reference": "^0.12.4",
|
|
78
80
|
"@zip.js/zip.js": "^2.17.0",
|
|
79
81
|
"arktype": "^2.2.3",
|
|
82
|
+
"citty": "^0.2.2",
|
|
80
83
|
"consola": "^3.4.2",
|
|
81
84
|
"grammy": "^1.46.0",
|
|
82
85
|
"hono": "^4.13.8",
|
|
@@ -102,6 +105,7 @@
|
|
|
102
105
|
"pngjs": "^7.0.0",
|
|
103
106
|
"reka-ui": "^2.10.5",
|
|
104
107
|
"simple-git-hooks": "^2.14.0",
|
|
108
|
+
"splitpanes": "^4.1.2",
|
|
105
109
|
"tailwind-merge": "^3.7.0",
|
|
106
110
|
"tailwindcss": "^4.3.3",
|
|
107
111
|
"tsx": "^4.23.15",
|