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 +128 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/UI_CREATION.md +148 -0
- package/bin/home-hosted.mjs +34 -0
- package/dist/cli.js +7238 -0
- package/dist/cli.js.map +1 -0
- package/package.json +112 -0
- package/uis/stock/dist/assets/index-CmkrtNU0.css +2 -0
- package/uis/stock/dist/assets/index-DqcdQi-W.js +47 -0
- package/uis/stock/dist/assets/jetbrains-mono-latin-B9CIFXIH.woff2 +0 -0
- package/uis/stock/dist/assets/jetbrains-mono-latin-ext-DBQx-q_a.woff2 +0 -0
- package/uis/stock/dist/assets/plex-sans-latin-IvpUvPa2.woff2 +0 -0
- package/uis/stock/dist/assets/plex-sans-latin-ext-CIII54If.woff2 +0 -0
- package/uis/stock/dist/index.html +25 -0
package/UI_CREATION.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Building a UI for home-hosted
|
|
2
|
+
|
|
3
|
+
The panel's UI is **a static site, and it is replaceable**. The stock one ships in the
|
|
4
|
+
package; yours is a folder of files that the control plane serves instead — same API, same
|
|
5
|
+
authentication, no server changes and no fork.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
$HHOSTED_HOME/.ui/ ← your build lives here
|
|
9
|
+
index.html ← required, at the root
|
|
10
|
+
assets/…
|
|
11
|
+
ui.json ← optional: { "name": "my-panel", "version": "2.1.0" }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install it
|
|
15
|
+
|
|
16
|
+
**Settings → Interface** → pick a `.zip` and *Install UI* (refresh to see it). Or drop the
|
|
17
|
+
files in `$HHOSTED_HOME/.ui` yourself. The zip may contain the files at its root, or inside
|
|
18
|
+
one wrapper directory (`zip -r ui.zip dist` also works).
|
|
19
|
+
|
|
20
|
+
There is no build step on the server side: whatever you upload is served as-is. So ship
|
|
21
|
+
plain HTML/JS/CSS, or the output of your own Vite/Next/Astro build with relative asset paths.
|
|
22
|
+
|
|
23
|
+
**If it breaks:** `home-hosted ui-revert` puts the stock panel back (or *Revert to stock* in
|
|
24
|
+
the settings page). The CLI also prints a reminder on startup while a custom UI is active,
|
|
25
|
+
because a broken UI must never lock you out of your own server.
|
|
26
|
+
|
|
27
|
+
## Rules of the road
|
|
28
|
+
|
|
29
|
+
1. **Static only.** No server code, no environment variables, no filesystem. Anything you
|
|
30
|
+
need comes from the API.
|
|
31
|
+
2. **Same origin, relative paths.** Call `/api/...` (never an absolute host): the panel may
|
|
32
|
+
be reached over loopback, a LAN address, TLS or a proxy.
|
|
33
|
+
3. **`index.html` at the root** (or inside a single wrapper directory). Deep links work:
|
|
34
|
+
unknown paths fall back to `index.html`, so client-side routing is fine.
|
|
35
|
+
4. **Keep the login flow.** The API answers `401` with `{"code":"AUTH_REQUIRED"}` when a
|
|
36
|
+
session is missing — route to your login screen and `POST /api/auth/login`.
|
|
37
|
+
5. **Self-host your assets.** An offline home server should not need a CDN.
|
|
38
|
+
6. **Limits:** a zip of at most 20 000 entries / 512 MB uncompressed, no absolute paths, no
|
|
39
|
+
`..`, no symlinks, no drive letters.
|
|
40
|
+
|
|
41
|
+
## The API
|
|
42
|
+
|
|
43
|
+
Two ways to consume it, both generated from the same ArkType schemas the server validates
|
|
44
|
+
with:
|
|
45
|
+
|
|
46
|
+
| | |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| approach | how |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| **OpenAPI** | `GET /openapi/spec.json` (no session needed); browse it at `GET /openapi/ui` |
|
|
51
|
+
| **Typed RPC** | building inside this repo: `import type { AppType } from '@server/app'` + `hc<AppType>()`, as `uis/stock/src/lib/rpc.ts` does |
|
|
52
|
+
| **Generated types** | `npx openapi-typescript http://127.0.0.1:3999/openapi/spec.json -o src/api.d.ts` |
|
|
53
|
+
|
|
54
|
+
The stock UI (`uis/stock/src/lib/api.ts`) is the reference client: `fetch` for everything, with
|
|
55
|
+
ArkType validating the responses at runtime. Either style is fine.
|
|
56
|
+
|
|
57
|
+
### The endpoints you will actually use
|
|
58
|
+
|
|
59
|
+
| endpoint | what it gives you |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `GET /api/state` | everything: panel settings, servers with live status, host vitals. Live clients should use SSE instead |
|
|
62
|
+
| `GET /api/events` | **the live feed.** `event: hello` carries the full state, then `state`, `server` and `log` frames |
|
|
63
|
+
| `GET /api/servers/:id/stream` | one server's `server` + `log` frames |
|
|
64
|
+
| `POST /api/servers/:id/{start,stop,restart}`, `/api/servers/{start-all,stop-all}` | lifecycle |
|
|
65
|
+
| `GET /api/servers`, `POST /api/servers`, `PATCH` / `DELETE /api/servers/:id` | the entries themselves |
|
|
66
|
+
| `GET /api/logs`, `GET /api/logs/:id?tail=&search=&stream=`, `GET /api/logs/:id/download?file=` | persisted logs |
|
|
67
|
+
| `GET` / `PATCH /api/settings`, `POST` / `DELETE /api/settings/tls` and `/api/settings/ui` | the panel's own configuration |
|
|
68
|
+
| `POST /api/backups`, `GET /api/backups`, `POST /api/backups/restore`, `GET /api/backups/:name/download`, `DELETE /api/backups/:name` | archives |
|
|
69
|
+
| `POST` / `DELETE /api/notifications/token`, `POST /api/notifications/test`, `POST /api/notifications/detect-chats` | Telegram |
|
|
70
|
+
| `GET /healthz` | liveness, **no session** — 503 when an autostart server has crashed |
|
|
71
|
+
| `GET /api/metrics` | Prometheus text |
|
|
72
|
+
|
|
73
|
+
`GET /healthz` and `GET /openapi/*` are the only unauthenticated reads; the SPA shell itself
|
|
74
|
+
is public too, so your app can load before a session exists.
|
|
75
|
+
|
|
76
|
+
### Failures
|
|
77
|
+
|
|
78
|
+
Every failing request answers with one envelope:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{ "message": "unknown server \"web\"", "code": "UNKNOWN_SERVER", "detail": { "…": "…" } }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`code` is stable and machine-readable (`AUTH_REQUIRED` drives the login redirect); `detail`
|
|
85
|
+
carries validation issues or context when there is any. Status codes are the usual ones
|
|
86
|
+
(400 bad input, 401 no session, 403 bad token/origin, 404 unknown id, 409 conflict, 413 too
|
|
87
|
+
large).
|
|
88
|
+
|
|
89
|
+
### SSE frames
|
|
90
|
+
|
|
91
|
+
| `event:` | `data:` |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `hello` | `{ ts, state }` — the first frame, with the complete snapshot |
|
|
94
|
+
| `state` | `{ ts, state }` — anything changed: a status, a resource sample, the host vitals |
|
|
95
|
+
| `server` | `{ ts, serverId, server }` — one entry, after an action or a probe |
|
|
96
|
+
| `log` | `{ ts, serverId, lines }` — new output (dropped under backpressure, never state) |
|
|
97
|
+
| `ping` | the current time, every 15 s |
|
|
98
|
+
|
|
99
|
+
The exact frames are in `src/shared/contracts.ts` (`sseMessageSchema`) — the server validates
|
|
100
|
+
against them before writing, so that schema is also your best type source.
|
|
101
|
+
|
|
102
|
+
Send `?logs=0` to skip log frames, or `?serverId=<id>` for one server. The server pings every
|
|
103
|
+
15 s.
|
|
104
|
+
|
|
105
|
+
## Adding a UI to this repo
|
|
106
|
+
|
|
107
|
+
`uis/<name>/` is a Vite app: `index.html`, `src/`, a `tsconfig.json` (copy a sibling's) and a 2-line
|
|
108
|
+
`vite.config.ts` calling `createUiConfig` from `uis/vite.shared.ts`. `public/ui.json` names it.
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
node scripts/build-uis.mjs <name> --zip # builds it and writes uis/dist/home-hosted-ui-<name>.zip
|
|
112
|
+
pnpm run build:uis # every UI, zipped; the release attaches them as assets
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Only `stock` ships inside the npm package — the others are release assets you upload from
|
|
116
|
+
Settings → Interface. `pnpm run quickcheck` type-checks every UI, and `pnpm test` (vitest) picks up
|
|
117
|
+
any `test/*.test.ts` you add (use relative imports; the `@` alias points at `stock`).
|
|
118
|
+
|
|
119
|
+
## A worked example
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# 1. any static framework; the only requirement is a static output
|
|
123
|
+
npm create vite@latest my-panel -- --template vue-ts
|
|
124
|
+
cd my-panel && npm install
|
|
125
|
+
npm run build # → dist/
|
|
126
|
+
|
|
127
|
+
# 2. make sure the API base is relative, then zip the build
|
|
128
|
+
cd dist && zip -r ../my-panel.zip . && cd ..
|
|
129
|
+
|
|
130
|
+
# 3. Settings → Interface → Install UI, and refresh
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Your client needs the session cookie, which the browser sends automatically once you log in
|
|
134
|
+
on that origin. For local development, `pnpm dev` in this repo runs the panel on 3999 and a
|
|
135
|
+
Vite dev server on 3998 with `/api` proxied, so you can point your own dev server at
|
|
136
|
+
`http://127.0.0.1:3999` the same way.
|
|
137
|
+
|
|
138
|
+
## Checklist
|
|
139
|
+
|
|
140
|
+
- [ ] `index.html` at the root of the zip, assets referenced relatively
|
|
141
|
+
- [ ] only `/api/...` calls, no absolute origins, no hard-coded port
|
|
142
|
+
- [ ] `401 { code: 'AUTH_REQUIRED' }` handled with a login screen
|
|
143
|
+
- [ ] live data from SSE (a panel that only polls feels broken)
|
|
144
|
+
- [ ] deep links render (the server falls back to `index.html`)
|
|
145
|
+
- [ ] assets self-hosted; no CDN dependencies
|
|
146
|
+
- [ ] works offline over plain http on a LAN (no `Secure`-only cookies, no https assumptions)
|
|
147
|
+
- [ ] `ui.json` with a name and version, so *Settings → Interface* can tell you what is
|
|
148
|
+
installed
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The published entry point. It prefers the built CLI and falls back to the
|
|
4
|
+
* TypeScript sources, so a linked checkout works before its first build.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import process from 'node:process'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
const root = path.resolve(fileURLToPath(new URL('..', import.meta.url)))
|
|
13
|
+
const built = path.join(root, 'dist', 'cli.js')
|
|
14
|
+
const args = process.argv.slice(2)
|
|
15
|
+
const hasBuild = fs.existsSync(built)
|
|
16
|
+
|
|
17
|
+
const commandArgs = hasBuild
|
|
18
|
+
? [built, ...args]
|
|
19
|
+
: ['--import', 'tsx', path.join(root, 'src', 'cli.ts'), ...args]
|
|
20
|
+
|
|
21
|
+
const child = spawn(process.execPath, commandArgs, {
|
|
22
|
+
// The caller's directory is the project: relative entry paths resolve there.
|
|
23
|
+
cwd: hasBuild ? process.cwd() : root,
|
|
24
|
+
env: { ...process.env, HHOSTED_PROJECT: process.env.HHOSTED_PROJECT ?? process.cwd() },
|
|
25
|
+
stdio: 'inherit',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
child.once('error', (error) => {
|
|
29
|
+
process.stderr.write(`home-hosted could not start: ${error.message}\n`)
|
|
30
|
+
process.exit(1)
|
|
31
|
+
})
|
|
32
|
+
child.once('exit', (code, signal) => {
|
|
33
|
+
process.exit(code ?? (signal === null ? 0 : 1))
|
|
34
|
+
})
|