diff --git a/public/games/age-of-dwarves/data/items/manifest.json b/public/games/age-of-dwarves/data/items/manifest.json
index 612abbf5..9b19bd94 100644
--- a/public/games/age-of-dwarves/data/items/manifest.json
+++ b/public/games/age-of-dwarves/data/items/manifest.json
@@ -3,7 +3,6 @@
"includes": [
"iron_axe",
"dwarven_plate",
- "healing_draught",
"direwolf_alpha_pelt",
"golem_core",
"phase_gauntlet",
diff --git a/public/games/age-of-dwarves/guide/src/pages/progress-report/ObjectiveModal.tsx b/public/games/age-of-dwarves/guide/src/pages/progress-report/ObjectiveModal.tsx
index a6a12510..f90b5ca1 100644
--- a/public/games/age-of-dwarves/guide/src/pages/progress-report/ObjectiveModal.tsx
+++ b/public/games/age-of-dwarves/guide/src/pages/progress-report/ObjectiveModal.tsx
@@ -1,4 +1,5 @@
import { useEffect, type ReactElement } from 'react'
+import { createPortal } from 'react-dom'
import type { ObjectiveRecord } from './types'
import { STATUS_ICON, STATUS_LABEL } from './types'
import {
@@ -31,7 +32,7 @@ export function ObjectiveModal({ objective, onClose }: ObjectiveModalProps): Rea
if (e.target === e.currentTarget) onClose()
}
- return (
+ return createPortal(
@@ -57,6 +58,7 @@ export function ObjectiveModal({ objective, onClose }: ObjectiveModalProps): Rea
{objective.summary || 'No summary recorded for this objective.'}
-
+ ,
+ document.body,
)
}
diff --git a/scripts/dev-setup/bluefin.sh b/scripts/dev-setup/bluefin.sh
index 6647d08d..76bef50b 100755
--- a/scripts/dev-setup/bluefin.sh
+++ b/scripts/dev-setup/bluefin.sh
@@ -44,19 +44,25 @@ REQUIRED_PACKAGES=(
)
CHECK_ONLY=false
+WITH_RUNNER=false
for arg in "$@"; do
case "$arg" in
--check) CHECK_ONLY=true ;;
+ --with-runner) WITH_RUNNER=true ;;
--help|-h)
- echo "Usage: $0 [--check]"
- echo " (no args) Install missing packages via rpm-ostree --apply-live"
- echo " --check Exit 0 if all packages are installed, 1 otherwise"
+ echo "Usage: $0 [--check] [--with-runner]"
+ echo " (no args) Install missing packages via rpm-ostree --apply-live"
+ echo " --check Exit 0 if all packages are installed, 1 otherwise"
+ echo " --with-runner Also install + register a forgejo-runner"
+ echo " (requires FORGEJO_* env — see .env.example)"
exit 0
;;
*) echo "Unknown argument: $arg"; exit 2 ;;
esac
done
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+
# ── Preconditions ────────────────────────────────────────────────────
if ! command -v rpm-ostree &>/dev/null; then
echo -e "${RED}This script targets bootc / rpm-ostree systems (Bluefin, Silverblue, Kinoite).${NC}"
@@ -162,3 +168,48 @@ echo -e " layer — typically at:"
echo -e " ${DIM}~/Code/bootc-bluefin/containerfiles/Containerfile.desktop-core${NC}"
echo -e " Rebuild + redeploy via ${DIM}~/Code/bootc-bluefin/rebuild-with-parser.sh${NC}"
echo -e " (or equivalent build.sh / deploy.sh)."
+
+# ── Optional: forgejo-runner install + register + persist ──────────
+if $WITH_RUNNER; then
+ echo ""
+ echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
+ echo -e "${BLUE} Forgejo runner (Linux / amd64 / $(hostname -s))${NC}"
+ echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
+ # shellcheck source=../run/common.sh
+ source "$REPO_ROOT/scripts/run/common.sh"
+ # shellcheck source=./lib/runner.sh
+ source "$REPO_ROOT/scripts/dev-setup/lib/runner.sh"
+ export RUNNER_OS=linux
+ export RUNNER_ARCH=amd64
+ export RUNNER_NAME="$(hostname -s)"
+ export RUNNER_LABELS="self-hosted,linux,${RUNNER_NAME}"
+ runner_install_binary
+ runner_register
+ # systemd-user persistence (survives reboot thanks to `loginctl enable-linger`).
+ UNIT="$HOME/.config/systemd/user/forgejo-runner.service"
+ mkdir -p "$(dirname "$UNIT")"
+ cat > "$UNIT" </dev/null || true
+ echo " runner: systemd user unit enabled (linger=on, persists across reboot)"
+ runner_verify_online || true
+fi
diff --git a/scripts/dev-setup/lib/runner.sh b/scripts/dev-setup/lib/runner.sh
new file mode 100644
index 00000000..7da880f3
--- /dev/null
+++ b/scripts/dev-setup/lib/runner.sh
@@ -0,0 +1,101 @@
+#!/usr/bin/env bash
+# Shared forgejo-runner install/register/persist helpers for the
+# per-OS dev-setup scripts. Sourced, never executed directly.
+#
+# Callers provide:
+# - RUNNER_OS (darwin | linux)
+# - RUNNER_ARCH (arm64 | amd64)
+# - RUNNER_LABELS (comma-separated, e.g. "self-hosted,macos,arm64")
+# - RUNNER_NAME (display name, e.g. $(hostname -s))
+# Env required (load via scripts/run/common.sh cascade):
+# - FORGEJO_HOST e.g. http://forge.black.local
+# - FORGEJO_ORG org the runner registers against
+# - FORGEJO_RUNNER_TOKEN org-scoped registration token
+#
+# Callers (osx.sh / bluefin.sh / linux.sh) also decide the persistence
+# layer — these helpers only handle the binary + registration step;
+# each OS file installs its own launchd plist or systemd user unit.
+
+set -euo pipefail
+
+RUNNER_BIN="$HOME/.local/bin/forgejo-runner"
+RUNNER_DIR="$HOME/.local/share/forgejo-runner"
+RUNNER_URL_BASE="https://code.forgejo.org/forgejo/runner/releases/download/nightly"
+
+runner_require_env() {
+ local missing=()
+ for v in FORGEJO_HOST FORGEJO_ORG FORGEJO_RUNNER_TOKEN; do
+ [[ -z "${!v:-}" ]] && missing+=("$v")
+ done
+ if (( ${#missing[@]} > 0 )); then
+ echo " runner: missing required env: ${missing[*]}" >&2
+ echo " runner: set in .env.local — see .env.example" >&2
+ return 2
+ fi
+}
+
+runner_install_binary() {
+ mkdir -p "$(dirname "$RUNNER_BIN")" "$RUNNER_DIR"
+ if [[ -x "$RUNNER_BIN" ]]; then
+ echo " runner: binary already present at $RUNNER_BIN"
+ return 0
+ fi
+ local url="$RUNNER_URL_BASE/forgejo-runner-nightly-$RUNNER_OS-$RUNNER_ARCH"
+ echo " runner: downloading $url"
+ curl -fsSL -o "$RUNNER_BIN.tmp" "$url"
+ chmod +x "$RUNNER_BIN.tmp"
+ mv "$RUNNER_BIN.tmp" "$RUNNER_BIN"
+ echo " runner: installed → $RUNNER_BIN"
+}
+
+# Register (or re-register) at org scope. Removes .runner file if the
+# existing registration has different labels or a different name — this
+# rebinds the runner to the org without manual DB intervention.
+runner_register() {
+ runner_require_env || return $?
+ cd "$RUNNER_DIR"
+
+ if [[ -f .runner ]]; then
+ local existing_name existing_labels
+ existing_name=$(jq -r '.name // ""' .runner 2>/dev/null || echo "")
+ existing_labels=$(jq -c '.labels // []' .runner 2>/dev/null || echo "[]")
+ if [[ "$existing_name" == "$RUNNER_NAME" ]] && \
+ [[ "$existing_labels" == "[\"${RUNNER_LABELS//,/\",\"}\"]" ]]; then
+ echo " runner: already registered as '$RUNNER_NAME' with labels [$RUNNER_LABELS]"
+ return 0
+ fi
+ echo " runner: existing registration ($existing_name, labels=$existing_labels) differs — re-registering"
+ rm -f .runner
+ fi
+
+ "$RUNNER_BIN" register \
+ --no-interactive \
+ --instance "$FORGEJO_HOST" \
+ --token "$FORGEJO_RUNNER_TOKEN" \
+ --name "$RUNNER_NAME" \
+ --labels "$RUNNER_LABELS" >/dev/null
+ echo " runner: registered as '$RUNNER_NAME' at $FORGEJO_HOST/$FORGEJO_ORG (labels: $RUNNER_LABELS)"
+}
+
+# Verify the runner appears online via the Forgejo API within a short
+# polling window. Uses FORGEJO_RUNNER_TOKEN only if also passed as a
+# personal access token; otherwise requires FORGEJO_ADMIN_TOKEN to read
+# /api/v1/user/actions/runners.
+runner_verify_online() {
+ local tok="${FORGEJO_ADMIN_TOKEN:-${FORGEJO_TOKEN:-}}"
+ if [[ -z "$tok" ]]; then
+ echo " runner: skipping online-verify (no admin/personal token — set FORGEJO_ADMIN_TOKEN to enable)"
+ return 0
+ fi
+ local tries=0 online=""
+ while (( tries < 12 )); do
+ online=$(curl -fsS -H "Authorization: token $tok" \
+ "$FORGEJO_HOST/api/v1/orgs/$FORGEJO_ORG/actions/runners" 2>/dev/null \
+ | jq -r ".runners[] | select(.name==\"$RUNNER_NAME\") | .status" 2>/dev/null || echo "")
+ [[ "$online" == "online" ]] && { echo " runner: online"; return 0; }
+ tries=$((tries + 1))
+ sleep 2
+ done
+ echo " runner: NOT online after 24s; check service logs" >&2
+ return 1
+}
diff --git a/scripts/dev-setup/linux.sh b/scripts/dev-setup/linux.sh
index c0f9075a..61957ae8 100755
--- a/scripts/dev-setup/linux.sh
+++ b/scripts/dev-setup/linux.sh
@@ -459,3 +459,47 @@ echo -e " ${DIM}./run verify${NC} — full lint + test pipeline"
echo -e " ${DIM}./run test:golden${NC} — cross-language golden-vector parity"
echo -e " ${DIM}./run autoplay 1${NC} — single-seed 500-turn simulation"
echo ""
+
+# ── Optional: forgejo-runner install + register + persist ──────────
+if $WITH_RUNNER; then
+ echo ""
+ echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
+ echo -e "${BLUE} Forgejo runner (Linux / amd64 / $(hostname -s))${NC}"
+ echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
+ # shellcheck source=../run/common.sh
+ source "$REPO_ROOT/scripts/run/common.sh"
+ # shellcheck source=./lib/runner.sh
+ source "$REPO_ROOT/scripts/dev-setup/lib/runner.sh"
+ export RUNNER_OS=linux
+ export RUNNER_ARCH=amd64
+ export RUNNER_NAME="$(hostname -s)"
+ export RUNNER_LABELS="self-hosted,linux,${RUNNER_NAME}"
+ runner_install_binary
+ runner_register
+ UNIT="$HOME/.config/systemd/user/forgejo-runner.service"
+ mkdir -p "$(dirname "$UNIT")"
+ cat > "$UNIT" </dev/null || true
+ echo " runner: systemd user unit enabled (linger=on, persists across reboot)"
+ runner_verify_online || true
+fi
diff --git a/scripts/dev-setup/osx.sh b/scripts/dev-setup/osx.sh
index da95eee3..cf1879c3 100755
--- a/scripts/dev-setup/osx.sh
+++ b/scripts/dev-setup/osx.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# macOS dev environment setup for Magic Civilization
-# Usage: ./scripts/dev-setup/osx.sh [--skip-godot] [--skip-rust]
+# Usage: ./scripts/dev-setup/osx.sh [--skip-godot] [--skip-rust] [--with-runner]
set -euo pipefail
RED='\033[0;31m'
@@ -12,14 +12,18 @@ NC='\033[0m'
SKIP_GODOT=false
SKIP_RUST=false
+WITH_RUNNER=false
for arg in "$@"; do
case "$arg" in
--skip-godot) SKIP_GODOT=true ;;
--skip-rust) SKIP_RUST=true ;;
+ --with-runner) WITH_RUNNER=true ;;
--help|-h)
- echo "Usage: $0 [--skip-godot] [--skip-rust]"
- echo " --skip-godot Skip Godot 4 installation"
- echo " --skip-rust Skip Rust toolchain installation"
+ echo "Usage: $0 [--skip-godot] [--skip-rust] [--with-runner]"
+ echo " --skip-godot Skip Godot 4 installation"
+ echo " --skip-rust Skip Rust toolchain installation"
+ echo " --with-runner Also install + register a forgejo-runner"
+ echo " (requires FORGEJO_* env — see .env.example)"
exit 0
;;
esac
@@ -262,9 +266,56 @@ if [ ${#FAILED[@]} -gt 0 ]; then
exit 1
fi
+if $WITH_RUNNER; then
+ echo ""
+ echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
+ echo -e "${BLUE} Forgejo runner (macOS / arm64 / plum)${NC}"
+ echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
+ source "$REPO_ROOT/scripts/run/common.sh"
+ source "$REPO_ROOT/scripts/dev-setup/lib/runner.sh"
+ export RUNNER_OS=darwin
+ export RUNNER_ARCH=arm64
+ export RUNNER_NAME="$(hostname -s)"
+ export RUNNER_LABELS="self-hosted,macos,arm64"
+ runner_install_binary
+ runner_register
+ # launchd persistence — user-agent, no root needed.
+ PLIST="$HOME/Library/LaunchAgents/com.forgejo.runner.plist"
+ if [[ ! -f "$PLIST" ]]; then
+ mkdir -p "$(dirname "$PLIST")"
+ cat > "$PLIST" <
+
+
+
+ Labelcom.forgejo.runner
+ ProgramArguments
+
+ $HOME/.local/bin/forgejo-runner
+ daemon
+ --config
+ $HOME/.local/share/forgejo-runner/config.yaml
+
+ WorkingDirectory$HOME/.local/share/forgejo-runner
+ RunAtLoad
+ KeepAlive
+ StandardOutPath$HOME/.local/share/forgejo-runner/runner.out.log
+ StandardErrorPath$HOME/.local/share/forgejo-runner/runner.err.log
+
+
+PLIST
+ echo " runner: wrote $PLIST"
+ fi
+ launchctl unload "$PLIST" 2>/dev/null || true
+ launchctl load "$PLIST"
+ echo " runner: launchd agent loaded (RunAtLoad + KeepAlive)"
+ runner_verify_online || true
+fi
+
echo ""
echo -e " ${GREEN}Ready to go.${NC} Try:"
echo -e " ${DIM}./run verify${NC} — full lint + test pipeline"
echo -e " ${DIM}./run play${NC} — launch the game"
echo -e " ${DIM}./run guide${NC} — start guide dev server"
+$WITH_RUNNER && echo -e " ${DIM}launchctl list | grep forgejo${NC} — verify runner daemon"
echo ""
diff --git a/scripts/run/common.sh b/scripts/run/common.sh
index e865cf14..4f5701f9 100644
--- a/scripts/run/common.sh
+++ b/scripts/run/common.sh
@@ -15,7 +15,7 @@
# Variables already set in the shell ALWAYS win (never clobber). Lines
# are plain `KEY=VALUE`; inline comments (`#`) are stripped; surrounding
# single/double quotes on values are stripped.
-_load_envfile() {
+load_envfile() {
local f="$1"
[[ -r "$f" ]] || return 0
local line key val
@@ -36,12 +36,12 @@ _load_envfile() {
done < "$f"
}
-_load_env_cascade() {
+load_env_cascade() {
local mode="${NODE_ENV:-development}"
- _load_envfile "$REPO_ROOT/.env"
- _load_envfile "$REPO_ROOT/.env.local"
- _load_envfile "$REPO_ROOT/.env.${mode}"
- _load_envfile "$REPO_ROOT/.env.${mode}.local"
+ load_envfile "$REPO_ROOT/.env"
+ load_envfile "$REPO_ROOT/.env.local"
+ load_envfile "$REPO_ROOT/.env.${mode}"
+ load_envfile "$REPO_ROOT/.env.${mode}.local"
}
# Require one or more env vars; fail the current command with a helpful
@@ -60,7 +60,7 @@ require_env() {
}
# Run cascade at source time so every subcommand has vars available.
-_load_env_cascade
+load_env_cascade
RED='\033[0;31m'
GREEN='\033[0;32m'