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:
Executable
+263
@@ -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 ""
|
||||
Reference in New Issue
Block a user