feat(web): dark Gitea UI, systemd installer, WEB-UI docs (W7)

Restyles every template with a hand-written dark Gitea-style theme
(web/static/app.css, ~270 lines: badges, buttons, cards, focus rings,
prefers-reduced-motion) and completes the htmx polling wiring for all
four dashboard cards. Adds the repeatable setup-web-linux.sh installer
(idempotent: pip install '[web]' non-editable, render ci-web.service,
template [web]/[web.oidc] into config.toml, prompt for keyring secrets,
print Gitea OIDC registration steps, enable/restart the unit) plus
deploy/systemd/ci-web.service and docs/WEB-UI.md.
This commit is contained in:
2026-07-23 18:36:15 +02:00
parent 43925e6746
commit 60cee5418e
14 changed files with 953 additions and 128 deletions
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env bash
# setup-web-linux.sh — install/upgrade the ci-orchestrator web dashboard (W7b)
#
# Repeatable / idempotent: safe to re-run any time (e.g. after `git pull` +
# a code change — see CLAUDE.md: the production venv must be reinstalled
# non-editable after every change). Every mutating step checks its own
# state first and skips (with a message) when already satisfied.
#
# What it does:
# 1. pip install '<repo>[web]' into /opt/ci/venv, non-editable, as ci-runner
# 2. Render deploy/systemd/ci-web.service into /etc/systemd/system/,
# substituting the LAN bind IP; write /etc/ci/web.env (CI_CONFIG) if the
# config path was overridden
# 3. Ensure a commented [web]/[web.oidc] template exists in config.toml
# (never overwrites an existing [web] section)
# 4. Prompt to store WebSessionSecret / WebOidcClientSecret in the keyring
# (interactive prompt only — never accepts a secret as a CLI argument)
# 5. Print the manual Gitea OIDC application registration steps
# 6. systemctl daemon-reload && enable (+ restart if already running)
#
# Usage: sudo bash setup-web-linux.sh [--host <lan-ip>] [options]
# See --help for the full option list.
set -euo pipefail
# ── Defaults ─────────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CI_USER="ci-runner"
OPT_CI="/opt/ci"
VENV="$OPT_CI/venv"
REPO_DIR="$SCRIPT_DIR"
UNIT_SRC="$REPO_DIR/deploy/systemd/ci-web.service"
UNIT_DST="/etc/systemd/system/ci-web.service"
ENV_FILE="/etc/ci/web.env"
CONFIG_PATH="/var/lib/ci/config.toml"
WEB_PORT=8800
WEB_HOST="" # resolved below (arg > prompt > 127.0.0.1)
SKIP_PIP=false
SKIP_SECRETS=false
ASSUME_DEFAULT_HOST=false
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
info() { echo -e "${GREEN}==>${NC} $*"; }
warn() { echo -e "${YELLOW} ! $*${NC}"; }
die() { echo -e "${RED}ERROR: $*${NC}" >&2; exit 1; }
usage() {
cat <<EOF
Usage: sudo bash $(basename "$0") [options]
Options:
--host <ip> LAN IP the dashboard binds to (written into the unit
and into the config.toml template). Default: prompt,
falling back to 127.0.0.1 (loopback-only) on Enter.
--config-path <p> Target config.toml (default: $CONFIG_PATH).
--skip-pip Skip step 1 (pip install '[web]' extra).
--skip-secrets Skip step 4 (interactive keyring secret prompts).
-h, --help Show this help.
Never binds 0.0.0.0. Binding a non-loopback IP without [web.oidc]
configured is refused at runtime by 'ci web serve' itself (open mode is
localhost-only) -- see docs/WEB-UI.md.
Examples:
sudo bash setup-web-linux.sh --host 127.0.0.1
sudo bash setup-web-linux.sh --host 192.168.1.50
sudo bash setup-web-linux.sh --skip-pip --skip-secrets # re-render unit only
EOF
exit 1
}
# ── Parse args ───────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--host) WEB_HOST="$2"; shift 2 ;;
--config-path) CONFIG_PATH="$2"; shift 2 ;;
--skip-pip) SKIP_PIP=true; shift ;;
--skip-secrets) SKIP_SECRETS=true; shift ;;
-h|--help) usage ;;
*) echo "Unknown option: $1" >&2; usage ;;
esac
done
[[ $EUID -eq 0 ]] || die "run as root (sudo bash $(basename "$0") ...)"
[[ -f "$UNIT_SRC" ]] || die "unit template not found: $UNIT_SRC (run from the repo root)"
id "$CI_USER" &>/dev/null || die "user '$CI_USER' does not exist -- run setup-host-linux.sh first"
[[ -x "$VENV/bin/python" ]] || die "$VENV/bin/python not found -- run setup-host-linux.sh first"
if [[ -z "$WEB_HOST" ]]; then
read -r -p "LAN IP to bind the dashboard to [127.0.0.1]: " WEB_HOST
WEB_HOST="${WEB_HOST:-127.0.0.1}"
fi
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ ci-orchestrator web dashboard — installer ║"
echo "╚══════════════════════════════════════════════════════╝"
echo " Bind : ${WEB_HOST}:${WEB_PORT}"
echo " ci_user : $CI_USER"
echo " venv : $VENV"
echo " config.toml : $CONFIG_PATH"
echo ""
# ── Step 1: pip install '[web]' extra (non-editable, per CLAUDE.md) ─────────
if [[ "$SKIP_PIP" == false ]]; then
info "[1/6] Installing ci_orchestrator[web] into $VENV (non-editable)"
sudo -u "$CI_USER" "$VENV/bin/pip" install -q "${REPO_DIR}[web]"
verify_out=$(sudo -u "$CI_USER" "$VENV/bin/python" -m ci_orchestrator web serve --help 2>&1) \
|| die "'ci web serve --help' failed after install -- see output above"
echo " Verify OK: ${verify_out%%$'\n'*}"
else
info "[1/6] pip install -- SKIPPED (--skip-pip)"
fi
# ── Step 2: render the systemd unit + optional CI_CONFIG env file ───────────
info "[2/6] Rendering ci-web.service (bind host: $WEB_HOST)"
RENDERED="$(mktemp)"
trap 'rm -f "$RENDERED"' EXIT
sed "s#__CI_WEB_BIND_HOST__#${WEB_HOST}#" "$UNIT_SRC" > "$RENDERED"
if [[ -f "$UNIT_DST" ]] && cmp -s "$RENDERED" "$UNIT_DST"; then
warn "$UNIT_DST already up to date -- skip"
UNIT_CHANGED=false
else
install -m 644 "$RENDERED" "$UNIT_DST"
echo " Wrote $UNIT_DST"
UNIT_CHANGED=true
fi
mkdir -p /etc/ci
if [[ "$CONFIG_PATH" != "/var/lib/ci/config.toml" ]]; then
if [[ -f "$ENV_FILE" ]] && grep -q '^CI_CONFIG=' "$ENV_FILE"; then
warn "$ENV_FILE already sets CI_CONFIG -- skip (edit manually to change it)"
else
echo "CI_CONFIG=$CONFIG_PATH" >> "$ENV_FILE"
chmod 640 "$ENV_FILE"
chown "root:$CI_USER" "$ENV_FILE" 2>/dev/null || true
echo " Wrote CI_CONFIG=$CONFIG_PATH to $ENV_FILE"
fi
else
echo " Using the auto-discovered default config.toml path -- no $ENV_FILE needed"
fi
# ── Step 3: ensure a [web]/[web.oidc] template exists in config.toml ────────
info "[3/6] config.toml [web] / [web.oidc] template"
WEB_MARKER="# --- appended by setup-web-linux.sh ---"
if [[ -f "$CONFIG_PATH" ]] && { grep -qF "$WEB_MARKER" "$CONFIG_PATH" || grep -q '^\[web\]' "$CONFIG_PATH"; }; then
warn "[web] section (or a previously appended template) already present in $CONFIG_PATH -- skip, not overwriting"
else
[[ -f "$CONFIG_PATH" ]] || die "$CONFIG_PATH does not exist -- run setup-host-linux.sh first (it creates the base config.toml)"
cat >> "$CONFIG_PATH" <<EOF
$WEB_MARKER
# Uncomment and fill in [web.oidc] once you have registered the Gitea
# OAuth2 application (see the installer's step 5 output / docs/WEB-UI.md).
# session_secret_cred / client_secret_cred are KEYRING KEY NAMES only --
# the secrets themselves are never written to this file.
#
# [web]
# enabled = true
# bind_host = "${WEB_HOST}" # never 0.0.0.0
# bind_port = ${WEB_PORT}
# audit_log = "/var/lib/ci/logs/web-audit.jsonl"
# allowed_users = ["CHANGE-ME"] # Gitea usernames allowed to log in
# session_secret_cred = "WebSessionSecret"
#
# [web.oidc]
# issuer = "https://CHANGE-ME.example"
# client_id = "CHANGE-ME"
# redirect_url = "http://${WEB_HOST}:${WEB_PORT}/auth/callback"
# client_secret_cred = "WebOidcClientSecret"
# scopes = ["openid", "profile"]
EOF
echo " Appended a commented [web]/[web.oidc] template to $CONFIG_PATH"
fi
# ── Step 4: keyring secrets (interactive prompt only, never argv) ───────────
info "[4/6] Keyring secrets (WebSessionSecret, WebOidcClientSecret)"
if [[ "$SKIP_SECRETS" == true ]]; then
echo " SKIPPED (--skip-secrets)"
else
_secret_present() {
sudo -u "$CI_USER" "$VENV/bin/python" -c "
from ci_orchestrator.credentials import KeyringCredentialStore
KeyringCredentialStore().get('$1')
" &>/dev/null
}
_set_secret_if_needed() {
local name="$1" label="$2"
if _secret_present "$name"; then
warn "Keyring key '$name' already set -- skip"
return
fi
read -r -p " Set keyring key '$name' now? [Y/n] " ans
case "$ans" in
[nN]*)
warn "Skipped -- set it later with:" \
"sudo -u $CI_USER $VENV/bin/python -m ci_orchestrator creds set --target $name --user $label"
;;
*)
# Delegates straight to the CLI's own interactive prompt
# (hide_input + confirmation) -- the secret never touches
# this script's variables, argv, or any log.
sudo -u "$CI_USER" "$VENV/bin/python" -m ci_orchestrator creds set \
--target "$name" --user "$label"
;;
esac
}
_set_secret_if_needed "WebSessionSecret" "web-session"
_set_secret_if_needed "WebOidcClientSecret" "web-oidc-client"
fi
# ── Step 5: manual Gitea OIDC app registration instructions ─────────────────
info "[5/6] Gitea OIDC application registration (manual step)"
cat <<EOF
In Gitea: Settings -> Applications -> "Manage OAuth2 Applications" -> Create
Application name : ci-web
Redirect URI : http://${WEB_HOST}:${WEB_PORT}/auth/callback
(must match [web.oidc].redirect_url EXACTLY --
host, port and path, or login will loop)
After creating it, Gitea shows a Client ID and Client Secret ONCE:
1. Client ID -> set [web.oidc].client_id in $CONFIG_PATH
2. Client Secret -> store in the keyring (never in config.toml):
sudo -u $CI_USER $VENV/bin/python -m ci_orchestrator creds set \\
--target WebOidcClientSecret --user web-oidc-client
3. Add your Gitea username(s) to [web].allowed_users in $CONFIG_PATH
4. Uncomment [web] and [web.oidc] in $CONFIG_PATH
EOF
# ── Step 6: enable + (re)start the service ───────────────────────────────────
info "[6/6] systemctl daemon-reload + enable/restart ci-web.service"
systemctl daemon-reload
if systemctl is-active --quiet ci-web.service; then
systemctl restart ci-web.service
echo " ci-web.service was already running -- restarted to pick up any changes"
else
systemctl enable --now ci-web.service
echo " ci-web.service enabled and started"
fi
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Install complete ║"
echo "╚══════════════════════════════════════════════════════╝"
echo ""
echo " Status : systemctl status ci-web.service"
echo " Logs : journalctl -u ci-web.service -f"
if [[ "$WEB_HOST" == "127.0.0.1" || "$WEB_HOST" == "localhost" ]]; then
echo " URL : http://${WEB_HOST}:${WEB_PORT}/ (loopback only -- open mode works without OIDC)"
else
echo " URL : http://${WEB_HOST}:${WEB_PORT}/"
echo " NOTE : a LAN bind refuses to start until [web.oidc] is configured"
echo " and enabled in $CONFIG_PATH (see step 5 above) -- this"
echo " matches the same guard 'ci web serve' enforces itself."
fi
echo " Docs : $REPO_DIR/docs/WEB-UI.md"
echo ""