feat(packaging/web): bundle the web console into the apt install (punktfunk-web)
android / android (push) Failing after 22s
deb / build-publish (push) Failing after 0s
decky / build-publish (push) Failing after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Failing after 1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Failing after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 1s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Failing after 0s
flatpak / build-publish (push) Failing after 1s
docker / deploy-docs (push) Has been skipped
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 0s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 0s
apple / swift (push) Successful in 53s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 34s
ci / bench (push) Successful in 1m32s
ci / rust (push) Failing after 53s

Every user needs the console for pairing, so ship it via apt, auto-wired to the
host — no manual bun/env setup. New punktfunk-web .deb (Architecture: all,
Depends: nodejs >= 20 — runs the node-server build under apt-native node, no
bundled bun):

- packaging/debian/build-web-deb.sh: stages web/.output (server + public) + a
  /usr/bin/punktfunk-web-server wrapper (node) + the systemd --user units + the
  web.env template + docs. Refuses a bun bundle (Bun.serve) as a wrong-preset guard.
- scripts/punktfunk-web.service: --user unit on :3000, EnvironmentFile sources the
  host's ~/.config/punktfunk/mgmt-token (the shared bearer) + the generated
  web-password; sets PUNKTFUNK_MGMT_URL=https://127.0.0.1:47990 +
  NODE_TLS_REJECT_UNAUTHORIZED=0 (loopback self-signed cert). Restart=on-failure
  rides out the host-writes-token-first ordering.
- scripts/punktfunk-web-init.service + web-init.sh: --user one-shot that generates
  the login password (a .deb postinst runs as root → wrong $HOME) and surfaces it
  to the journal.
- build-deb.sh: punktfunk-host now Recommends punktfunk-web (apt pulls it by
  default; headless boxes opt out with --no-install-recommends).
- deb.yml: build the web console + smoke-boot it under node (gate the .deb on a
  real /login 200) + build-web-deb.sh; the publish loop globs it automatically.
- web/{.env.example,web.env.example}: document the auto-wiring vs a manual deploy.

End state: `apt install punktfunk-host` pulls punktfunk-web; enable both --user
services; the console logs in (password from the journal) and proxies the host's
HTTPS mgmt API with the shared token — zero hand-edited env. Local .deb build +
node smoke-boot verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 08:50:40 +00:00
parent b2a335122e
commit df005e2963
8 changed files with 225 additions and 3 deletions
+18
View File
@@ -62,6 +62,23 @@ jobs:
git config --global --add safe.directory "$PWD" git config --global --add safe.directory "$PWD"
cargo build --release -p punktfunk-host -p punktfunk-client-linux --locked 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 - name: Version
# Tag v1.2.3 -> 1.2.3 (a real release); a main push -> 0.0.1~ciN.g<sha>, which sorts # Tag v1.2.3 -> 1.2.3 (a real release); a main push -> 0.0.1~ciN.g<sha>, which sorts
# BEFORE 0.0.1 (the '~') yet monotonically increases by run number, so `apt upgrade` # BEFORE 0.0.1 (the '~') yet monotonically increases by run number, so `apt upgrade`
@@ -79,6 +96,7 @@ jobs:
run: | run: |
VERSION="$VERSION" bash packaging/debian/build-deb.sh VERSION="$VERSION" bash packaging/debian/build-deb.sh
VERSION="$VERSION" bash packaging/debian/build-client-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 - name: Publish to the Gitea apt registry
env: env:
+4 -1
View File
@@ -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 # 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 # 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. # 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" SUGGESTS="kwin-wayland, mutter"
INSTALLED_KB="$(du -k -s "$STAGE" | cut -f1)" INSTALLED_KB="$(du -k -s "$STAGE" | cut -f1)"
+111
View File
@@ -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_<version>_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" <<EOF
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: punktfunk
Source: https://git.unom.io/unom/punktfunk
Files: *
Copyright: punktfunk contributors
License: MIT or Apache-2.0
Dual-licensed. Full texts in /usr/share/doc/$PKG/LICENSE-MIT and
/usr/share/doc/$PKG/LICENSE-APACHE.
EOF
printf '%s (%s) stable; urgency=medium\n\n * Automated build %s.\n\n -- unom <noreply@anthropic.com> %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" <<EOF
Package: $PKG
Version: $VERSION
Architecture: all
Maintainer: unom <noreply@anthropic.com>
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://<host-ip>: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
+17
View File
@@ -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
+30
View File
@@ -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
+19
View File
@@ -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://<host-ip>:3000 and log in)"
fi
+4 -2
View File
@@ -1,5 +1,7 @@
# punktfunk web — management console (Bun/Nitro server) configuration. # punktfunk web — management console (Nitro/Node server) configuration.
# Copy to `.env` (gitignored) or set these in the environment of `bun run start`. # 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 # 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 # server fails CLOSED (503 on every request) if this is unset, so a LAN-exposed server
+22
View File
@@ -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=