diff --git a/.gitea/workflows/deb.yml b/.gitea/workflows/deb.yml index 1e45d98..b7b284c 100644 --- a/.gitea/workflows/deb.yml +++ b/.gitea/workflows/deb.yml @@ -62,6 +62,23 @@ jobs: git config --global --add safe.directory "$PWD" cargo build --release -p punktfunk-host -p punktfunk-client-linux --locked + - name: Build + smoke-boot web console (node-server preset) + # Gate the .deb on a real node boot: the punktfunk-web .deb runs `node .output/server`, + # so prove the node-server build exists, isn't a bun bundle, and actually serves /login. + run: | + cd web + bun install --frozen-lockfile + bun run build + if grep -q 'Bun\.serve' .output/server/index.mjs; then + echo "ERROR: web build is a bun bundle (Bun.serve) — need the node-server preset"; exit 1 + fi + PORT=3009 HOST=127.0.0.1 PUNKTFUNK_UI_PASSWORD=ci node .output/server/index.mjs & + NP=$!; sleep 3 + code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3009/login || echo 000) + kill "$NP" 2>/dev/null || true + echo "web console smoke: /login -> $code" + [ "$code" = 200 ] || { echo "ERROR: web console failed to boot under node"; exit 1; } + - name: Version # Tag v1.2.3 -> 1.2.3 (a real release); a main push -> 0.0.1~ciN.g, which sorts # BEFORE 0.0.1 (the '~') yet monotonically increases by run number, so `apt upgrade` @@ -79,6 +96,7 @@ jobs: run: | VERSION="$VERSION" bash packaging/debian/build-deb.sh VERSION="$VERSION" bash packaging/debian/build-client-deb.sh + VERSION="$VERSION" bash packaging/debian/build-web-deb.sh - name: Publish to the Gitea apt registry env: diff --git a/packaging/debian/build-deb.sh b/packaging/debian/build-deb.sh index 64cdcdc..0bd0184 100755 --- a/packaging/debian/build-deb.sh +++ b/packaging/debian/build-deb.sh @@ -103,7 +103,10 @@ DEPENDS="$SHDEPS, libei1, pipewire, wireplumber" # ffmpeg: Ubuntu's ffmpeg ships the NVENC-enabled libav* the binary links AND is the encoder # runtime; the libav* sonames are already hard Depends via shlibdeps, so the ffmpeg metapackage # is a Recommends. gamescope = a ready compositor backend; pipewire-pulse = desktop audio. -RECOMMENDS="ffmpeg, gamescope, pipewire-pulse" +# punktfunk-web = the management web console (pairing + status) every user needs — a separate +# Architecture:all .deb; Recommends so `apt install punktfunk-host` pulls it by default, while a +# headless/encoding-only box can opt out with --no-install-recommends. +RECOMMENDS="ffmpeg, gamescope, pipewire-pulse, punktfunk-web" SUGGESTS="kwin-wayland, mutter" INSTALLED_KB="$(du -k -s "$STAGE" | cut -f1)" diff --git a/packaging/debian/build-web-deb.sh b/packaging/debian/build-web-deb.sh new file mode 100755 index 0000000..fc0ff38 --- /dev/null +++ b/packaging/debian/build-web-deb.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# Build the punktfunk-web .deb — the management web console (Nitro/Node SSR + React). +# +# Architecture: all — the .output is pre-built JS (no compiled binary, so NO dpkg-shlibdeps). +# Runtime is apt-native: Depends on nodejs (>= 20). The host's punktfunk-host .deb Recommends this, +# so a default `apt install punktfunk-host` pulls the console too. It is auto-wired to the host's +# mgmt token via the systemd --user units (no env editing on a packaged install). +# +# Usage: VERSION=0.0.1~ci42.gdeadbee bash packaging/debian/build-web-deb.sh +# Output: dist/punktfunk-web__all.deb +set -euo pipefail + +VERSION="${VERSION:?set VERSION (e.g. 0.0.1 or 0.0.1~ci42.gdeadbee)}" +PKG="punktfunk-web" +ROOTDIR="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOTDIR" + +# Build the console if not already built (.output is gitignored — CI builds it each run). +if [ ! -f web/.output/server/index.mjs ]; then + echo "==> building web console" + (cd web && bun install --frozen-lockfile && bun run build) +fi +# The build MUST be the node-server preset (runnable by apt-native node) — never bun. +if grep -rq 'Bun\.serve' web/.output/server/index.mjs 2>/dev/null; then + echo "ERROR: web/.output contains Bun.serve — wrong nitro preset (need 'node-server')" >&2 + exit 1 +fi + +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT +SHAREDIR="$STAGE/usr/share/$PKG" +DOCDIR="$STAGE/usr/share/doc/$PKG" + +# --- file layout ------------------------------------------------------------- +mkdir -p "$SHAREDIR/.output" +cp -r web/.output/server "$SHAREDIR/.output/server" +cp -r web/.output/public "$SHAREDIR/.output/public" +# Stable PATH-independent ExecStart wrapper. +install -d "$STAGE/usr/bin" +cat > "$STAGE/usr/bin/punktfunk-web-server" <<'WRAP' +#!/bin/sh +exec /usr/bin/node /usr/share/punktfunk-web/.output/server/index.mjs "$@" +WRAP +chmod 0755 "$STAGE/usr/bin/punktfunk-web-server" +install -Dm0644 scripts/punktfunk-web.service "$STAGE/usr/lib/systemd/user/punktfunk-web.service" +install -Dm0644 scripts/punktfunk-web-init.service "$STAGE/usr/lib/systemd/user/punktfunk-web-init.service" +install -Dm0755 scripts/web-init.sh "$SHAREDIR/web-init.sh" +install -Dm0644 web/web.env.example "$SHAREDIR/web.env.example" +install -Dm0644 LICENSE-MIT "$DOCDIR/LICENSE-MIT" +install -Dm0644 LICENSE-APACHE "$DOCDIR/LICENSE-APACHE" +install -Dm0644 web/README.md "$DOCDIR/README.md" + +cat > "$DOCDIR/copyright" < %s\n' \ + "$PKG" "$VERSION" "$VERSION" "$(date -uR 2>/dev/null || echo 'Thu, 01 Jan 1970 00:00:00 +0000')" \ + | gzip -9n > "$DOCDIR/changelog.Debian.gz" + +INSTALLED_KB="$(du -k -s "$STAGE" | cut -f1)" + +install -d "$STAGE/DEBIAN" +cat > "$STAGE/DEBIAN/control" < +Installed-Size: $INSTALLED_KB +Section: net +Priority: optional +Homepage: https://git.unom.io/unom/punktfunk +Depends: nodejs (>= 20) +Description: punktfunk management web console (Nitro/Node SSR + React) + The browser console for a punktfunk streaming host: status, paired devices, and the + SPAKE2 PIN pairing flow every client needs. Runs as a systemd --user service on port + 3000, login-gated (a password generated on first start), proxying the host's loopback + HTTPS management API with a bearer token injected server-side (never sent to the browser). + . + Auto-wired to the host on a packaged install: it sources the host's + ~/.config/punktfunk/mgmt-token and a generated login password — no env editing. Enable + the systemd user service punktfunk-web; read the login password from the --user journal. +EOF + +cat > "$STAGE/DEBIAN/postinst" <<'EOF' +#!/bin/sh +set -e +if [ "$1" = "configure" ]; then + echo "punktfunk-web installed. Enable it for your user:" + echo " systemctl --user enable --now punktfunk-web" + echo "A login password is generated on first start — read it with:" + echo " journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'" + echo " (or: sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password)" + echo "Then open http://:3000" +fi +exit 0 +EOF +chmod 0755 "$STAGE/DEBIAN/postinst" + +mkdir -p dist +OUT="dist/${PKG}_${VERSION}_all.deb" +dpkg-deb --root-owner-group --build "$STAGE" "$OUT" >/dev/null +echo "built $OUT" +dpkg-deb -I "$OUT" | sed -n 's/^/ /p' | grep -E 'Version|Installed-Size|Depends' || true diff --git a/scripts/punktfunk-web-init.service b/scripts/punktfunk-web-init.service new file mode 100644 index 0000000..818656d --- /dev/null +++ b/scripts/punktfunk-web-init.service @@ -0,0 +1,17 @@ +# punktfunk web console first-run setup — systemd USER one-shot. +# +# Generates the console login password (PUNKTFUNK_UI_PASSWORD) in the streaming user's +# ~/.config/punktfunk on first start, surfaced to the --user journal for retrieval. A .deb postinst +# runs as root (wrong $HOME), so credential generation must happen as the user — hence this unit. +# Pulled in by punktfunk-web.service (Wants=); also runnable directly. +[Unit] +Description=punktfunk web console first-run setup (login password) +ConditionPathExists=!%h/.config/punktfunk/web-password + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/share/punktfunk-web/web-init.sh + +[Install] +WantedBy=default.target diff --git a/scripts/punktfunk-web.service b/scripts/punktfunk-web.service new file mode 100644 index 0000000..11e2559 --- /dev/null +++ b/scripts/punktfunk-web.service @@ -0,0 +1,30 @@ +# punktfunk management web console — systemd USER unit (Nitro/Node SSR, port 3000). +# +# Installed by the punktfunk-web .deb to /usr/lib/systemd/user/. AUTO-WIRED — no env editing: +# it sources the host's mgmt token + the generated login password, and points at the host's +# loopback HTTPS mgmt API (self-signed cert → NODE_TLS_REJECT_UNAUTHORIZED for the proxy's only +# outbound hop, which is loopback). Enable per user: +# systemctl --user enable --now punktfunk-web +[Unit] +Description=punktfunk management web console +# web-init generates the login password; the host writes the mgmt token. Order after both. +After=punktfunk-web-init.service punktfunk-host.service +Wants=punktfunk-web-init.service + +[Service] +Type=simple +# Both are KEY=VALUE files. mgmt-token is REQUIRED (written by the host's `serve`); if absent the +# unit fails + Restart retries until the host has created it. web-password is '-' optional (web-init +# creates it first, but a manual operator may inject PUNKTFUNK_UI_PASSWORD another way). +EnvironmentFile=%h/.config/punktfunk/mgmt-token +EnvironmentFile=-%h/.config/punktfunk/web-password +Environment=PUNKTFUNK_MGMT_URL=https://127.0.0.1:47990 +Environment=NODE_TLS_REJECT_UNAUTHORIZED=0 +Environment=PORT=3000 +Environment=HOST=0.0.0.0 +ExecStart=/usr/bin/punktfunk-web-server +Restart=on-failure +RestartSec=2 + +[Install] +WantedBy=default.target diff --git a/scripts/web-init.sh b/scripts/web-init.sh new file mode 100755 index 0000000..ee86d43 --- /dev/null +++ b/scripts/web-init.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# First-run setup for the punktfunk web console (run by punktfunk-web-init.service as the user): +# generate the login password once, in the streaming user's config dir, and surface it to the +# journal. The mgmt token is NOT created here — the host owns it (~/.config/punktfunk/mgmt-token). +set -eu + +DIR="${XDG_CONFIG_HOME:-$HOME/.config}/punktfunk" +mkdir -p "$DIR" +chmod 700 "$DIR" 2>/dev/null || true +PWFILE="$DIR/web-password" + +if [ ! -s "$PWFILE" ]; then + # URL/shell-safe password (no /+= so it's a clean EnvironmentFile value). + PW=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | cut -c1-20) + (umask 077; printf 'PUNKTFUNK_UI_PASSWORD=%s\n' "$PW" > "$PWFILE") + chmod 600 "$PWFILE" 2>/dev/null || true + echo "punktfunk web console login password generated: $PW" + echo "(stored in $PWFILE — open http://:3000 and log in)" +fi diff --git a/web/.env.example b/web/.env.example index bbfde43..404f60c 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,5 +1,7 @@ -# punktfunk web — management console (Bun/Nitro server) configuration. -# Copy to `.env` (gitignored) or set these in the environment of `bun run start`. +# punktfunk web — management console (Nitro/Node server) configuration. +# Copy to `.env` (gitignored) or set these in the environment of `node .output/server/index.mjs`. +# NOTE: on a packaged install (the punktfunk-web .deb) you edit NOTHING — the systemd --user units +# auto-wire these from the host's ~/.config/punktfunk/{mgmt-token,web-password}. See web.env.example. # REQUIRED in production: the shared login password for the console. The built Nitro # server fails CLOSED (503 on every request) if this is unset, so a LAN-exposed server diff --git a/web/web.env.example b/web/web.env.example new file mode 100644 index 0000000..717ab95 --- /dev/null +++ b/web/web.env.example @@ -0,0 +1,22 @@ +# punktfunk web console — packaged config reference. +# +# On a `apt install punktfunk-web` install you DO NOT edit anything: the systemd --user units wire +# everything automatically — +# punktfunk-web.service sets PUNKTFUNK_MGMT_URL=https://127.0.0.1:47990, NODE_TLS_REJECT_UNAUTHORIZED=0, +# PORT=3000, HOST=0.0.0.0, and sources: +# ~/.config/punktfunk/mgmt-token (written by the host's `serve` — the shared bearer token) +# ~/.config/punktfunk/web-password (written by punktfunk-web-init — the console login password) +# +# This file documents the variables for a MANUAL deploy (running `node .output/server/index.mjs` +# yourself). The mgmt API is HTTPS with the host's self-signed loopback cert, so the proxy needs +# NODE_TLS_REJECT_UNAUTHORIZED=0 (its only outbound TLS hop is that loopback connection). +PUNKTFUNK_MGMT_URL=https://127.0.0.1:47990 +NODE_TLS_REJECT_UNAUTHORIZED=0 +PORT=3000 +HOST=0.0.0.0 + +# Match the host's ~/.config/punktfunk/mgmt-token (auto-generated by the host if unset): +PUNKTFUNK_MGMT_TOKEN= + +# Console login password (fails closed if unset on the built server): +PUNKTFUNK_UI_PASSWORD=