SysDeck/web/scripts/make-master-tarball.sh

568 lines
30 KiB
Bash
Executable File

#!/usr/bin/env bash
# SysDeck master tarball builder
#
# Bundles into ONE distributable (sysdeck-<version>-master.tar.bz2):
# / — the cockpit edition (bridge/ + plugins/ + shared/ + docs)
# /web — the SysDeck Web Edition (this Next.js project)
# /web/mini-services/fester — Fester, vendored + pre-integrated
# /klanker-gate — the Frosty Deno LLM gateway by TykoDev
# (https://github.com/TykoDev/klanker-gate, Apache-2.0 —
# NOT SysDeck code), vendored + pre-integrated
# (with arch/ packaging: PKGBUILD + systemd + runbook)
#
# Preconditions:
# - master-build/cockpit/ holds the staged + upgraded cockpit tree
# (fester.py / klanker.py / bridge.js upgraded, Makefile at 0.4.4)
# - master-build/klanker-gate/ holds the vendored gateway tree + arch/
#
# Output:
# public/download/sysdeck-<version>-master.tar.bz2 (+ .sha256)
# download/ (sandbox mirror)
set -euo pipefail
VERSION="0.4.4"
NAME="sysdeck-${VERSION}-master"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
STAGE_PARENT="$ROOT/master-build"
STAGE="$STAGE_PARENT/$NAME"
OUT_DIR="$ROOT/public/download"
OUT="$OUT_DIR/$NAME.tar.bz2"
echo ">>> Building $NAME"
# ── guards: the cockpit tree must be upgraded before packing ──────────
grep -q 'start-build' "$STAGE_PARENT/cockpit/bridge/fester.py" || {
echo "FAIL: master-build/cockpit/bridge/fester.py is not upgraded (no start-build subcommand)"; exit 1; }
grep -q 'VERSION := 0.4.4' "$STAGE_PARENT/cockpit/Makefile" || {
echo "FAIL: master-build/cockpit/Makefile is not bumped to 0.4.4"; exit 1; }
test -f "$STAGE_PARENT/cockpit/bridge/klanker.py" || {
echo "FAIL: master-build/cockpit/bridge/klanker.py missing"; exit 1; }
grep -q '"journal"' "$STAGE_PARENT/cockpit/bridge/klanker.py" || {
echo "FAIL: bridge/klanker.py lacks the journal subcommand"; exit 1; }
test -d "$STAGE_PARENT/cockpit/plugins/sysdeck-klanker" || {
echo "FAIL: master-build/cockpit/plugins/sysdeck-klanker missing"; exit 1; }
test -f "$STAGE_PARENT/klanker-gate/arch/PKGBUILD" || {
echo "FAIL: master-build/klanker-gate/arch/PKGBUILD missing (Arch packaging)"; exit 1; }
grep -q 'klanker' "$STAGE_PARENT/cockpit/shared/bridge.js" || {
echo "FAIL: shared/bridge.js has no klanker surface"; exit 1; }
# klanker-gate upstream attribution (user directive: credit TykoDev —
# https://github.com/TykoDev/klanker-gate — it is not SysDeck code)
test -f "$STAGE_PARENT/klanker-gate/ATTRIBUTION.md" || {
echo "FAIL: klanker-gate/ATTRIBUTION.md missing (upstream credit)"; exit 1; }
grep -q 'TykoDev/klanker-gate' "$STAGE_PARENT/klanker-gate/ATTRIBUTION.md" || {
echo "FAIL: klanker-gate/ATTRIBUTION.md lacks the TykoDev source URL"; exit 1; }
grep -q 'TykoDev/klanker-gate' "$STAGE_PARENT/cockpit/THIRD_PARTY.md" || {
echo "FAIL: THIRD_PARTY.md lacks the klanker-gate/TykoDev entry"; exit 1; }
grep -q 'TykoDev/klanker-gate' "$STAGE_PARENT/klanker-gate/arch/PKGBUILD" || {
echo "FAIL: arch/PKGBUILD lacks upstream attribution (or wrong url=)"; exit 1; }
grep -q 'TykoDev/klanker-gate' "$STAGE_PARENT/cockpit/plugins/sysdeck-klanker/klanker.js" || {
echo "FAIL: sysdeck-klanker panel lacks visible upstream attribution"; exit 1; }
grep -q 'TykoDev/klanker-gate' "$STAGE_PARENT/cockpit/bridge/klanker.py" || {
echo "FAIL: bridge/klanker.py lacks the upstream attribution header"; exit 1; }
grep -q 'TykoDev/klanker-gate' "$ROOT/src/components/sysdeck/panels/klankerPanel.tsx" || {
echo "FAIL: web klankerPanel.tsx lacks visible upstream attribution"; exit 1; }
grep -q 'TykoDev/klanker-gate' "$ROOT/src/lib/sysdeck/bridge/klanker.ts" || {
echo "FAIL: web bridge/klanker.ts lacks the upstream attribution header"; exit 1; }
# local-stack support (klanker localstack + Local stack wiring cards)
grep -q '"localstack"' "$STAGE_PARENT/cockpit/bridge/klanker.py" || {
echo "FAIL: bridge/klanker.py lacks the localstack subcommand"; exit 1; }
grep -q 'localstack' "$STAGE_PARENT/cockpit/plugins/sysdeck-klanker/klanker.js" || {
echo "FAIL: sysdeck-klanker panel lacks the local stack card"; exit 1; }
grep -q 'localstack' "$STAGE_PARENT/cockpit/shared/bridge.js" || {
echo "FAIL: shared/bridge.js lacks the klanker.localstack method"; exit 1; }
grep -q 'localstack' "$ROOT/src/lib/sysdeck/bridge/klanker.ts" || {
echo "FAIL: web bridge klanker.ts lacks the localstack command"; exit 1; }
grep -q 'Local stack wiring' "$ROOT/src/components/sysdeck/panels/klankerPanel.tsx" || {
echo "FAIL: web klankerPanel.tsx lacks the Local stack wiring card"; exit 1; }
grep -q '10.1' "$STAGE_PARENT/cockpit/QUICKSTART.md" || {
echo "FAIL: QUICKSTART lacks the local-stack section (10.1)"; exit 1; }
# module visibility toggles (shell bridge + sidebar power controls)
test -f "$ROOT/src/lib/sysdeck/bridge/shell.ts" || {
echo "FAIL: web shell.ts (module-toggle bridge) missing"; exit 1; }
grep -q 'setToggle' "$ROOT/src/lib/sysdeck/bridge/shell.ts" || {
echo "FAIL: shell.ts lacks the setToggle command"; exit 1; }
grep -q 'setToggle' "$ROOT/src/components/sysdeck/shell.tsx" || {
echo "FAIL: web shell (components/sysdeck/shell.tsx) lacks module toggle wiring"; exit 1; }
grep -q '10.2' "$STAGE_PARENT/cockpit/QUICKSTART.md" || {
echo "FAIL: QUICKSTART lacks the module-toggles section (10.2)"; exit 1; }
# 0.3.0 security audit fixes — cockpit bridge (fail-closed guards)
grep -q '_cgroup_path_ok' "$STAGE_PARENT/cockpit/bridge/policy.py" || {
echo "FAIL: bridge/policy.py lacks the cgroup path bound guard (C-1)"; exit 1; }
grep -q '_CGROUP_CTRL_RE' "$STAGE_PARENT/cockpit/bridge/policy.py" || {
echo "FAIL: bridge/policy.py lacks the cgroup control-name allowlist"; exit 1; }
grep -q '_valid_id' "$STAGE_PARENT/cockpit/bridge/builder.py" || {
echo "FAIL: bridge/builder.py lacks the build-id/profile validation (C-2/H-1)"; exit 1; }
grep -q 'refusing to clear' "$STAGE_PARENT/cockpit/bridge/builder.py" || {
echo "FAIL: bridge/builder.py artifacts-clear lacks the rmtree guard (C-2)"; exit 1; }
if grep -q 'sudo", "sh", "-c' "$STAGE_PARENT/cockpit/bridge/hwalert.py"; then {
echo "FAIL: bridge/hwalert.py still contains the sudo sh -c injection (H-2)"; exit 1; }; fi
grep -q '_device_path_ok' "$STAGE_PARENT/cockpit/bridge/hwalert.py" || {
echo "FAIL: bridge/hwalert.py lacks the device path guard (H-2)"; exit 1; }
grep -q '_engine_registered' "$STAGE_PARENT/cockpit/bridge/db.py" || {
echo "FAIL: bridge/db.py start/stop/restart lack the registry check (M-1)"; exit 1; }
grep -q 'read-only queries only' "$STAGE_PARENT/cockpit/bridge/db.py" || {
echo "FAIL: bridge/db.py query lacks the read-only guard (M-2)"; exit 1; }
grep -q 'refusing to set' "$STAGE_PARENT/cockpit/bridge/themes.py" || {
echo "FAIL: bridge/themes.py set lacks the INI-injection guard (M-6)"; exit 1; }
grep -q '_pkg_name_ok' "$STAGE_PARENT/cockpit/bridge/packages.py" || {
echo "FAIL: bridge/packages.py lacks package-name validation (L-1)"; exit 1; }
grep -q 'invalid sysbench test name' "$STAGE_PARENT/cockpit/bridge/benchmark.py" || {
echo "FAIL: bridge/benchmark.py lacks the test-name guard (L-1)"; exit 1; }
grep -q 'invalid package name' "$STAGE_PARENT/cockpit/bridge/firewall.py" || {
echo "FAIL: bridge/firewall.py lacks package-name validation"; exit 1; }
# 0.3.0 security audit fixes — cockpit panels (XSS escaping + CSP)
# (grep exits 1 on a clean tree — absorb it so pipefail doesn't kill the build)
unsafe_eval_manifests=$({ grep -rl "unsafe-eval" "$STAGE_PARENT"/cockpit/plugins/*/manifest.json 2>/dev/null || true; } | wc -l)
test "$unsafe_eval_manifests" -eq 0 || {
echo "FAIL: $unsafe_eval_manifests plugin manifests still ship unsafe-eval (M-5)"; exit 1; }
for panel in packages benchmark auth sensors vault firmware mesh; do
grep -q 'function escapeHtml' "$STAGE_PARENT/cockpit/plugins/sysdeck-$panel/$panel.js" || {
echo "FAIL: sysdeck-$panel panel lacks escapeHtml (H-3/H-4 XSS)"; exit 1; }
done
if grep -qP 'target="_blank"(?! rel=)' "$STAGE_PARENT"/cockpit/plugins/*/[a-z]*.js 2>/dev/null; then {
echo "FAIL: a plugin still has target=_blank without rel=noopener (L-4)"; exit 1; }; fi
# 0.3.0 security audit fixes — web edition (loopback binding + DoS guards)
grep -q '127.0.0.1' "$ROOT/package.json" || {
echo "FAIL: web package.json does not bind loopback"; exit 1; }
grep -q "hostname: '127.0.0.1'" "$ROOT/mini-services/fester/index.ts" || {
echo "FAIL: fester service does not bind loopback"; exit 1; }
grep -q 'rateLimited' "$ROOT/src/app/api/bridge/route.ts" || {
echo "FAIL: bridge route lacks rate limiting"; exit 1; }
grep -q 'MAX_BODY_BYTES' "$ROOT/src/app/api/bridge/route.ts" || {
echo "FAIL: bridge route lacks the body-size cap"; exit 1; }
# v0.4.0 unix login — PAM helper + SdUser local accounts + user-bound session v2
test -f "$ROOT/src/lib/sysdeck/session.ts" || {
echo "FAIL: web session lib (src/lib/sysdeck/session.ts) missing"; exit 1; }
grep -q 'requireSession' "$ROOT/src/app/api/bridge/route.ts" || {
echo "FAIL: bridge route lacks the session gate"; exit 1; }
grep -q 'requireSession' "$ROOT/src/app/api/fester/route.ts" || {
echo "FAIL: fester proxy route lacks the session gate"; exit 1; }
grep -q 'requireSession' "$ROOT/src/app/api/release/route.ts" || {
echo "FAIL: release route lacks the session gate"; exit 1; }
test -f "$ROOT/src/app/api/auth/login/route.ts" || {
echo "FAIL: login route (src/app/api/auth/login) missing"; exit 1; }
grep -q 'currentSession' "$ROOT/src/app/page.tsx" || {
echo "FAIL: page.tsx lacks the server-side session gate"; exit 1; }
grep -q 'LoginScreen' "$ROOT/src/app/page.tsx" || {
echo "FAIL: page.tsx does not render the login screen"; exit 1; }
grep -q "sessionOk" "$ROOT/mini-services/fester/index.ts" || {
echo "FAIL: fester service lacks the shared-secret session check"; exit 1; }
grep -q 'SYSDECK_AUTH_MODE' "$STAGE_PARENT/cockpit/QUICKSTART.md" || {
echo "FAIL: QUICKSTART lacks the 10.4 unix-login section"; exit 1; }
grep -q 'SYSDECK_SESSION_SECURE' "$ROOT/src/lib/sysdeck/session.ts" || {
echo "FAIL: session lib lacks the session-cookie surface"; exit 1; }
# 0.3.0 security audit — klanker-gate upstream findings + arch mitigations
test -f "$STAGE_PARENT/klanker-gate/arch/SECURITY-UPSTREAM.md" || {
echo "FAIL: klanker-gate/arch/SECURITY-UPSTREAM.md (upstream advisory) missing"; exit 1; }
grep -q 'U-1' "$STAGE_PARENT/klanker-gate/arch/SECURITY-UPSTREAM.md" || {
echo "FAIL: SECURITY-UPSTREAM.md lacks the findings table"; exit 1; }
grep -q 'ExecStartPre' "$STAGE_PARENT/klanker-gate/arch/klanker-gate.service" || {
echo "FAIL: klanker-gate.service lacks the fail-closed token guard (U-1)"; exit 1; }
grep -q 'SECURITY-UPSTREAM.md' "$STAGE_PARENT/klanker-gate/arch/PKGBUILD" || {
echo "FAIL: PKGBUILD does not install SECURITY-UPSTREAM.md"; exit 1; }
grep -q 'REQUIRED' "$STAGE_PARENT/klanker-gate/arch/env.example" || {
echo "FAIL: env.example does not mark FROSTY_ADMIN_TOKEN required"; exit 1; }
grep -q '## 9. Security notes' "$STAGE_PARENT/klanker-gate/arch/INSTALL-ARCH.md" || {
echo "FAIL: INSTALL-ARCH.md lacks the security section"; exit 1; }
# v0.3.0 additions — the web-edition skin for cockpit + the runbook
test -f "$STAGE_PARENT/cockpit/shared/sysdeck-web.css" || {
echo "FAIL: shared/sysdeck-web.css (web-edition skin) missing"; exit 1; }
test -f "$STAGE_PARENT/cockpit/shared/branding.css" || {
echo "FAIL: shared/branding.css (cockpit shell skin) missing"; exit 1; }
grep -q 'sysdeck-web.css' "$STAGE_PARENT/cockpit/Makefile" || {
echo "FAIL: Makefile does not install the web-edition skin"; exit 1; }
grep -q 'install-branding:' "$STAGE_PARENT/cockpit/Makefile" || {
echo "FAIL: Makefile lacks install-branding"; exit 1; }
skins=0
for f in "$STAGE_PARENT"/cockpit/plugins/*/index.html; do
grep -q 'sysdeck-common/sysdeck-web.css' "$f" || {
echo "FAIL: $(basename "$(dirname "$f")")/index.html does not link the skin"; exit 1; }
skins=$((skins+1))
done
test "$skins" -ge 27 || { echo "FAIL: only $skins plugin pages link the skin"; exit 1; }
test -f "$ROOT/src/components/sysdeck/panels/runbookPanel.tsx" || {
echo "FAIL: web edition runbookPanel.tsx missing"; exit 1; }
grep -q "id: 'runbook'" "$ROOT/src/lib/sysdeck/registry.ts" || {
echo "FAIL: web edition registry lacks the runbook module"; exit 1; }
rm -rf "$STAGE"
mkdir -p "$STAGE" "$OUT_DIR" "$ROOT/download"
# ── 1. cockpit edition (staged + upgraded tree) ────────────────────────
tar -C "$STAGE_PARENT/cockpit" -cf - \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='*.tar.bz2' \
. | tar -C "$STAGE" -xf -
# ── 2. web edition (this project; no build output, no node_modules) ───
mkdir -p "$STAGE/web" "$STAGE/web/db"
tar -C "$ROOT" -cf - \
--exclude='node_modules' \
--exclude='.next' \
--exclude='dev.log' \
--exclude='server.log' \
--exclude='worklog.md' \
--exclude='worklog-*.md' \
--exclude='tool-results' \
--exclude='upload' \
--exclude='download' \
--exclude='master-build' \
--exclude='public/download' \
--exclude='.git' \
--exclude='.zscripts' \
--exclude='skills' \
--exclude='examples' \
--exclude='tests' \
--exclude='tsconfig.tsbuildinfo' \
--exclude='mini-services/fester/fester.db' \
--exclude='mini-services/fester/fester.db-shm' \
--exclude='mini-services/fester/fester.db-wal' \
--exclude='mini-services/fester/node_modules' \
src prisma public mini-services scripts package.json bun.lock \
next.config.ts postcss.config.mjs tsconfig.json \
components.json eslint.config.mjs Caddyfile \
| tar -C "$STAGE/web" -xf -
# portable DATABASE_URL for the bundled copy (prisma resolves relative to
# prisma/schema.prisma → <web root>/db/custom.db); KLANKER_URL keeps the
# AI Gateway panel pointed at the gateway (vendored at ../klanker-gate,
# or the Arch-packaged systemd service on the operator's host).
printf 'DATABASE_URL=file:../db/custom.db\n# unix-account login (cockpit-style, v0.4.0+):\n# pam — host PAM only (the cockpit default posture; run the\n# service as root so any unix account can sign in)\n# pam+local — PAM first, SdUser local accounts as fallback (works\n# unprivileged — unix_chkpwd only serves the invoking uid)\n# local — SdUser console accounts only\nSYSDECK_AUTH_MODE=pam+local\n# SYSDECK_PAM_SERVICE=sysdeck # ship /etc/pam.d/sysdeck to tailor the stack\n# SYSDECK_PYTHON=python3\n# cockpit module detection (v0.4.1): extra scan roots beyond\n# /usr/share/cockpit and /usr/local/share/cockpit (colon-separated)\n#SYSDECK_COCKPIT_SCAN=/path/to/staged/cockpit\n# KLANKER_URL=http://127.0.0.1:8080\n# KLANKER_ADMIN_TOKEN=\n# SYSDECK_SESSION_SECURE=1 # add the Secure cookie flag when fronted by TLS\n# mutation authorization (v0.4.3): 'admin' gates mutating bridge commands\n# behind an admin session (wheel/sudo/adm or uid 0); 'any' restores the\n# single-operator posture\n#SYSDECK_MUTATIONS=admin\n# X-Forwarded-For defines rate-limit identity only behind a trusted proxy\n#SYSDECK_TRUST_PROXY=1\n' > "$STAGE/web/.env"
cat > "$STAGE/web/README.md" <<'EOF'
# 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.
## 2. Quickstart — dev
From the extracted master tarball root, one command does everything
(install + migrate + fester + web):
tar xjf sysdeck-0.4.4-master.tar.bz2
cd sysdeck-0.4.4-master
make web-dev # bun install + db:push + fester + next dev :3000
Granular equivalent (what `make web-dev` does):
cd sysdeck-0.4.4-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.
EOF
# ── 3. klanker-gate (vendored gateway + Arch packaging) ───────────────
mkdir -p "$STAGE/klanker-gate"
tar -C "$STAGE_PARENT/klanker-gate" -cf - \
--exclude='.git' \
. | tar -C "$STAGE/klanker-gate" -xf -
# ── 4. pack ───────────────────────────────────────────────────────────
tar -cjf "$OUT" -C "$STAGE_PARENT" "$NAME"
SHA="$(sha256sum "$OUT" | cut -d' ' -f1)"
SIZE="$(stat -c %s "$OUT")"
printf '%s %s\n' "$SHA" "$NAME.tar.bz2" > "$OUT.sha256"
# sandbox mirror + summary
cp -f "$OUT" "$OUT.sha256" "$ROOT/download/"
FILES="$(tar -tjf "$OUT" | wc -l)"
echo ">>> $OUT"
echo " files: $FILES size: $SIZE bytes"
echo " sha256: $SHA"