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/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
+ })