319 lines
15 KiB
Markdown
319 lines
15 KiB
Markdown
# SysDeck — the standalone web console
|
|
|
|
SysDeck's browser-native front end: 31 bridge modules behind one
|
|
console, every one of them reading **real host state** — /proc and /sys
|
|
collectors, systemctl, lsblk, the host's real package manager, live
|
|
service APIs — with honest empty inventories (and install guidance)
|
|
where a backend is absent. Nothing is demo, mock, or seeded. You sign
|
|
in with your Unix account (the host PAM stack, exactly like Cockpit),
|
|
every installed Cockpit module loads into this console's navigation,
|
|
and every SysDeck module can likewise be loaded inside Cockpit — one
|
|
module catalog, two front ends. The Fester DAG orchestrator is vendored
|
|
as a dedicated service (`mini-services/fester`) and the klanker-gate LLM
|
|
gateway alongside with Arch packaging. klanker-gate ("Frosty Deno") is by TykoDev
|
|
(https://github.com/TykoDev/klanker-gate, Apache-2.0) — **not SysDeck
|
|
code**; see `../klanker-gate/ATTRIBUTION.md` and `../THIRD_PARTY.md`.
|
|
|
|
**This is a complete standalone application.** No Cockpit, no Python
|
|
bridge, no systemd, no root — one Next.js process serves the whole
|
|
console. This README is the full standalone runbook; the web console
|
|
ships the same content as the "Run without Cockpit" panel (system group,
|
|
right under Overview) with copy buttons on every command.
|
|
|
|
## 1. Prerequisites
|
|
|
|
- **Bun >= 1.1** — `pacman -S bun` on Arch, or
|
|
`curl -fsSL https://bun.sh/install | bash` elsewhere.
|
|
(Node-only hosts work too — see the node path in §3.)
|
|
- ~200 MB disk for dependencies, ~512 MB RAM. SQLite is bundled via
|
|
Prisma — no database server.
|
|
- **Not required:** Cockpit, systemd, Docker, root. Runs as an
|
|
unprivileged user on any Linux/BSD/macOS.
|
|
|
|
## 1a. Release gates
|
|
|
|
Before any release build, both gates run green from the tree root:
|
|
|
|
```bash
|
|
cd web
|
|
bunx tsc --noEmit # type errors fail the release
|
|
bunx eslint src # hook deps + unused vars fail the release
|
|
```
|
|
|
|
`next.config.ts` enforces the same type gate at build time
|
|
(`typescript.ignoreBuildErrors: false`) — a console that administers
|
|
hosts does not ship on a red typecheck.
|
|
|
|
## 2. Quickstart — dev
|
|
|
|
From the extracted master tarball root, one command does everything
|
|
(install + migrate + fester + web):
|
|
|
|
tar xjf sysdeck-0.4.5-master.tar.bz2
|
|
cd sysdeck-0.4.5-master
|
|
make web-dev # bun install + db:push + fester + next dev :3000
|
|
|
|
Granular equivalent (what `make web-dev` does):
|
|
|
|
cd sysdeck-0.4.5-master/web
|
|
bun install # dependencies
|
|
bun run db:push # create + migrate db/custom.db (SQLite)
|
|
bun run dev # Next.js on :3000
|
|
|
|
# optional — the Fester DAG orchestrator (own service, :3010):
|
|
cd mini-services/fester
|
|
bun install
|
|
bun run dev # bun --hot index.ts
|
|
|
|
Open http://localhost:3000 — you'll get the **login screen**: sign in
|
|
with a **Unix account** (verified by the host PAM stack, the same
|
|
mechanism Cockpit uses; see §10 and `SYSDECK_AUTH_MODE` in §5). The
|
|
Fester panel proxies REST through
|
|
`/api/fester` (server-side) and streams live build events over
|
|
WebSocket through the port gateway (`/?XTransformPort=3010`). Without
|
|
the service running, the Fester panel says so — everything else works.
|
|
|
|
## 3. Production build
|
|
|
|
The build emits a self-contained standalone server
|
|
(`.next/standalone/` with static assets and public/ folded in):
|
|
|
|
cd web
|
|
bun install
|
|
bun run build # next build + fold static/ & public/
|
|
PORT=3000 HOSTNAME=0.0.0.0 bun run start
|
|
|
|
Node-only hosts:
|
|
|
|
bun run build # or: npx next build
|
|
PORT=3000 HOSTNAME=0.0.0.0 node .next/standalone/server.js
|
|
|
|
Run `bun run db:push` once before the first production start — the
|
|
SQLite file lives at `db/custom.db` (path from `DATABASE_URL` in
|
|
.env). Keep the bundle layout intact so `../db` resolves.
|
|
|
|
## 4. systemd services (Arch Linux)
|
|
|
|
Deploy the extracted bundle to `/opt/sysdeck` and run:
|
|
|
|
`/etc/systemd/system/sysdeck-web.service`
|
|
|
|
[Unit]
|
|
Description=SysDeck standalone web console (Next.js)
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=sysdeck
|
|
WorkingDirectory=/opt/sysdeck/web
|
|
Environment=PORT=3000
|
|
Environment=HOSTNAME=0.0.0.0
|
|
Environment=NODE_ENV=production
|
|
ExecStart=/usr/bin/bun run start
|
|
Restart=on-failure
|
|
RestartSec=3
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
|
|
`/etc/systemd/system/sysdeck-fester.service`
|
|
|
|
[Unit]
|
|
Description=SysDeck Fester — DAG build orchestration
|
|
After=network-online.target sysdeck-web.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=sysdeck
|
|
WorkingDirectory=/opt/sysdeck/web/mini-services/fester
|
|
ExecStart=/usr/bin/bun index.ts
|
|
Restart=on-failure
|
|
RestartSec=3
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
|
|
Enable:
|
|
|
|
sudo useradd -r -d /opt/sysdeck -s /usr/sbin/nologin sysdeck || true
|
|
sudo systemctl enable --now sysdeck-web.service sysdeck-fester.service
|
|
|
|
## 5. Environment reference (.env in web/)
|
|
|
|
| variable | default | purpose |
|
|
|----------------------|-----------------------|------------------------------------------------|
|
|
| `DATABASE_URL` | `file:../db/custom.db`| SQLite file for theme, baselines, runs (Prisma)|
|
|
| `KLANKER_URL` | — | AI Gateway panel → live gateway (e.g. http://127.0.0.1:8080) |
|
|
| `KLANKER_ADMIN_TOKEN`| — | admin token for that gateway (FROSTY_ADMIN_TOKEN) |
|
|
| `PORT` / `HOSTNAME` | `3000` / `localhost` | standalone server bind (use 0.0.0.0 for LAN) |
|
|
| `SYSDECK_AUTH_MODE` | `pam` | unix login policy: `pam` (host PAM only — run as root), `pam+local` (PAM first, SdUser scrypt fallback), `local` (console accounts only) |
|
|
| `SYSDECK_PAM_SERVICE` | `sysdeck` | PAM stack to use; falls back to `login` when `/etc/pam.d/sysdeck` is absent |
|
|
| `SYSDECK_PYTHON` | `python3` | interpreter that runs `scripts/pam-auth.py` |
|
|
| `SYSDECK_PAM_TIMEOUT_MS` | `8000` | hard timeout for one PAM authentication |
|
|
| `SYSDECK_COCKPIT_SCAN` | — | extra cockpit module scan roots (colon-separated) for staged/DESTDIR trees — detection also always covers /usr/share/cockpit and /usr/local/share/cockpit |
|
|
| `SYSDECK_SESSION_SECURE` | off | set `1` to add the `Secure` cookie flag (front the console with TLS first) |
|
|
| `SYSDECK_MUTATIONS` | `admin` | mutation policy: `admin` gates mutating bridge commands behind an admin session (wheel/sudo/adm or uid 0) — reads stay open to every signed-in unix account; `any` restores the single-operator posture |
|
|
| `SYSDECK_TRUST_PROXY` | off | set `1` to honor `X-Forwarded-For` for rate-limit identity — only behind a trusted proxy; client-supplied headers are ignored by default |
|
|
|
|
With no `KLANKER_URL`, the AI Gateway panel renders honest empty
|
|
tables (they fill with LIVE data automatically when the gateway
|
|
answers).
|
|
|
|
## 6. Reverse proxy + WebSocket gateway
|
|
|
|
The bundled `Caddyfile` implements the port-gateway pattern: every
|
|
request carrying `?XTransformPort=<port>` is forwarded to that
|
|
localhost port (this is how the Fester event stream crosses the single
|
|
public port). Adapt the listener to your domain:
|
|
|
|
sysdeck.example.com {
|
|
@ws query XTransformPort=*
|
|
handle @ws {
|
|
reverse_proxy localhost:{query.XTransformPort}
|
|
}
|
|
handle {
|
|
reverse_proxy 127.0.0.1:3000
|
|
}
|
|
}
|
|
|
|
nginx equivalent:
|
|
|
|
location / {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade; # websocket (XTransformPort=3010)
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_set_header Host $host;
|
|
}
|
|
|
|
## 7. Troubleshooting
|
|
|
|
- **PrismaClientInitializationError / "database not found"** — run
|
|
`bun run db:push` in `web/`; check `DATABASE_URL` points at
|
|
`db/custom.db` relative to the bundle layout.
|
|
- **Port 3000 already in use** — `PORT=3001 bun run dev`.
|
|
- **Panels show a DEMO badge** — expected: the host lacks that backend
|
|
(docker/kubectl/pacman). Panels badged LIVE read real /proc, /sys,
|
|
lsblk data.
|
|
- **Fester panel says service unreachable** — start it:
|
|
`(cd mini-services/fester && bun run dev)`.
|
|
- **AI Gateway tables are empty** — set `KLANKER_URL` +
|
|
`KLANKER_ADMIN_TOKEN` in `web/.env`, restart, or install the gateway
|
|
via `../klanker-gate/arch/INSTALL-ARCH.md`.
|
|
- **Edits not appearing** — dev recompiles on save (check dev.log);
|
|
production needs `bun run build` again.
|
|
|
|
## 8. Wiring the vendored services
|
|
|
|
- **Fester** — vendored at `mini-services/fester` (independent version
|
|
0.2.1, own package). Start it as in §2; the panel connects on its own.
|
|
- **AI Gateway (klanker-gate)** — the `klanker-gate/` tree sits next to
|
|
this `web/` directory in the master tarball: the Frosty Deno LLM
|
|
gateway (Deno 2 + TypeScript, own version 0.9.0), with complete Arch
|
|
packaging under `arch/` (PKGBUILD, hardened systemd unit, run wrapper,
|
|
`INSTALL-ARCH.md` runbook). **Upstream credit:** klanker-gate is by
|
|
TykoDev — https://github.com/TykoDev/klanker-gate (Apache-2.0) — and
|
|
is not SysDeck code; it is vendored unmodified (SysDeck adds only
|
|
`arch/`), see `../klanker-gate/ATTRIBUTION.md` and `../THIRD_PARTY.md`.
|
|
Point the panel at a running gateway via `.env`:
|
|
|
|
KLANKER_URL=http://127.0.0.1:8080
|
|
KLANKER_ADMIN_TOKEN=<FROSTY_ADMIN_TOKEN>
|
|
|
|
- **Local stack (ollama · llama.cpp · koboldcpp · LM Studio · SGLang ·
|
|
vLLM)** — the gateway is *not* SaaS-only: `ollama`/`lmstudio`/`sgl`
|
|
are native keyless provider types, and llama-server/KoboldCpp/vLLM
|
|
plug in via the generic `openai-compatible` type (base URL + optional
|
|
key). No API key is required anywhere. The AI Gateway panel ships a
|
|
**Local stack wiring** card that live-probes each backend's
|
|
`/v1/models` from the host and shows the env + admin-API recipes with
|
|
copy buttons. Env wiring:
|
|
|
|
OLLAMA_BASE_URL=http://127.0.0.1:11434/v1
|
|
OLLAMA_MODELS=qwen3:14b,llama3.1:8b,nomic-embed-text
|
|
LMSTUDIO_BASE_URL=http://127.0.0.1:1234/v1
|
|
OPENAI_COMPAT_BASE_URL=http://127.0.0.1:8081/v1
|
|
|
|
Env registers ONE openai-compatible account — to run llama.cpp AND
|
|
koboldcpp side by side, register each via the admin API
|
|
(`POST /api/providers`, then `refresh-models` auto-discovers the
|
|
catalog). Port note: llama-server defaults to :8080, the gateway's
|
|
own port — run it on 8081 or move the gateway. Full recipes:
|
|
QUICKSTART §10.1 in the bundle root.
|
|
|
|
## 9. Layout
|
|
|
|
- `src/lib/sysdeck/` — module registry + bridge dispatcher modules
|
|
- `src/app/api/bridge` — the bridge endpoint (POST {module, command, args})
|
|
- `src/app/api/fester` — server-side proxy to the fester service
|
|
- `src/app/api/release` — master-tarball release metadata
|
|
- `src/components/sysdeck/` — panels + shared UI primitives
|
|
- `mini-services/fester` — the vendored Fester service (own package, v0.2.1)
|
|
|
|
**Module toggles:** every sidebar module can be turned off (hover a row
|
|
→ power icon) and back on from the **Disabled (N)** section — hidden
|
|
from navigation and the ⌘K palette while off, persisted in SQLite
|
|
(`shell.disabled`), audited, and survives restarts. Overview is
|
|
protected. Switched away from the AI Gateway to another stack? Turn it
|
|
off in one click; its bridge commands stay available for scripts.
|
|
|
|
`db/custom.db` is created by `bun run db:push` using `DATABASE_URL` from
|
|
`.env`. The master tarball builder lives at `scripts/make-master-tarball.sh`
|
|
in the canonical development tree. The cockpit edition (bundle root,
|
|
`sudo make install`) remains available but is entirely optional; the two
|
|
front ends share one module catalog (every module ships both a web bridge
|
|
and a cockpit manifest). Since 0.3.0 the cockpit plugin pages wear this
|
|
console's skin
|
|
(`shared/sysdeck-web.css`), with `sudo make install-branding` theming the
|
|
Cockpit shell chrome to match (see ../QUICKSTART.md §12).
|
|
|
|
## 10. Security posture
|
|
|
|
This is a **single-operator console**: the web app, the fester service and
|
|
the AI Gateway panel assume the person at the machine is the operator. The
|
|
0.3.0 security audit shaped the shipped defaults around that contract:
|
|
|
|
- **Loopback binding by default.** `bun run dev` starts Next.js on
|
|
`127.0.0.1:3000` and the fester service binds `127.0.0.1:3010` — no
|
|
LAN exposure without an explicit act (change the bind, add a reverse
|
|
proxy, forward the port). The production `start` script pins
|
|
`HOSTNAME=127.0.0.1` the same way.
|
|
- **Unix-account login (since 0.4.0), the Cockpit way.** The username
|
|
+ password pair is verified by the **host's PAM stack**
|
|
(`scripts/pam-auth.py`, a stdlib ctypes client of libpam; service
|
|
`sysdeck` when `/etc/pam.d/sysdeck` exists, else the stock `login`
|
|
stack; credentials over stdin, never argv). Success buys an
|
|
HMAC-signed HttpOnly session cookie **bound to the username**
|
|
(`v2.<exp>.<userB64>.<hmac>`, 12h). The page server-renders a login
|
|
screen until the cookie verifies, every `/api/*` route answers 401
|
|
until signed in, and the fester service verifies the identical token
|
|
on its REST + WebSocket surface straight from the shared SQLite
|
|
secret — no unauthenticated path into the console's data.
|
|
- **Auth modes + root.** pam_unix needs root to verify *arbitrary*
|
|
users (non-root only gets the invoking uid via `unix_chkpwd`), so:
|
|
run the service as root (like cockpit-ws) for any-account login
|
|
(`SYSDECK_AUTH_MODE=pam`, the default), or run unprivileged with
|
|
`pam+local` — PAM first, then the `SdUser` scrypt table managed by
|
|
`bun scripts/manage-users.mjs list|add|passwd|disable|enable|remove`.
|
|
Failures rate limit per-IP **and** per-username (5/min each), wrong
|
|
user and wrong password look identical, and a wedged PAM helper
|
|
fails CLOSED. 0.3.1 v1 tokens still verify as legacy sessions so
|
|
upgrades don't log anyone out. Still LAN-side posture: loopback binds
|
|
stay the outer boundary (see ../QUICKSTART.md §2).
|
|
- **Guards stay layered under the login.** The bridge endpoint
|
|
allowlists every module+command, caps request bodies at 256 KB,
|
|
applies a per-IP rate limit, and returns generic errors (full detail
|
|
goes to the server log, not the response).
|
|
- **SQL is parameterized everywhere** (Prisma on the app, bound
|
|
parameters in fester's bun:sqlite); **no bridge command passes
|
|
client strings to a shell** — the one spawn helper uses fixed argv
|
|
arrays; **no filesystem path is built from client input** — every
|
|
path is a hardcoded constant.
|
|
- **Secrets stay server-side.** `KLANKER_ADMIN_TOKEN` exists only in
|
|
`.env` and the `Authorization` header; it is never echoed to any
|
|
panel, response or log.
|
|
- If you DO expose the console (reverse proxy, tailscale, etc.), put
|
|
authentication in front of it first — the bridge can mutate system
|
|
state (netsec bans, service ports, module toggles) and it trusts
|
|
whoever can reach it.
|