Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Thumbs.db

# Railguard per-session traces and snapshots
.railguard/
# Stray `railguard init` starter in the repo root — the real policy lives in
# agentic-ai/Claude/railguard.yaml. Anchored so only the root file is ignored.
/railguard.yaml

linux-server/adguard/work/
linux-server/adguard/conf/
Expand Down Expand Up @@ -39,3 +42,6 @@ linux-server/qbittorrent/downloads/

# Backup status JSON written at runtime (served to the homepage card)
linux-server/backup/status/

# Forgejo runner status JSON + last-state written at runtime (homepage card)
linux-server/forgejo/runner-status/
18 changes: 18 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,21 @@ phases.
primary cross-platform test mechanism (only one platform can run live).
- App-store packages and `priority: "none"` entries are reminders only — never
auto-installed.

## Privacy & Security

This repo is **public**. Never commit identifying or secret information.

- Keep these out of tracked files entirely: tailnet names / MagicDNS suffixes
(`tailXXXXXX.ts.net`), real hostnames, server IPs, usernames, emails, tokens,
auth keys, and personal absolute paths.
- Put any machine-specific or private value in a `.env` file (gitignored
repo-wide) and ship a committed `.env.example` with placeholders instead —
e.g. `linux-server/forgejo/.env.example`, `macOS/forgejo-runner/.env.example`.
Scripts read these via `${VAR:-<placeholder>}` and source a local `.env` when
present; they never hardcode the real value.
- In docs and configs use placeholders: `<tailnet>`, `<server-ip>`,
`<username>`, `<hostname>`. Default to `.env` whenever a value is
identifying — prefer one more env var over leaking a real value.
- When editing, scan the diff for accidentally introduced real identifiers
before committing.
4 changes: 4 additions & 0 deletions agentic-ai/Claude/railguard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ allowlist: []

fence:
enabled: true
# Opt in to project-local `.railguard.local.yaml` files, which may only ADD
# to allowed_paths (never remove denies; denied_paths always win). Grants
# nothing by itself — each project must ship its own .railguard.local.yaml.
allow_local_overrides: true
allowed_paths:
- "~/.claude"
- "/tmp"
Expand Down
10 changes: 5 additions & 5 deletions linux-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ The Tailscale widget uses a local OAuth proxy (`linux-server/tailscale-proxy`) t
Get a TLS cert from Tailscale and configure NPM to terminate HTTPS:

```sh
tailscale cert <your-tailscale-hostname> # e.g. ollie-server.<tailnet>.ts.net
tailscale cert <your-tailscale-hostname> # e.g. <hostname>.<tailnet>.ts.net
```

In NPM admin (`http://<server-ip>:81`):
Expand Down Expand Up @@ -289,13 +289,13 @@ In NPM admin (`http://<server-ip>:81`):
2. Deploy:
```sh
cd linux-server/forgejo
cp .env.example .env # set FORGEJO_DOMAIN to your Tailscale hostname; optionally set FORGEJO_DATA_PATH for external drive
cp .env.example .env # set FORGEJO_DOMAIN (forgejo.<tailnet>.ts.net) and TS_AUTHKEY; optionally FORGEJO_DATA_PATH for external drive
docker compose up -d
```
3. Web UI at `http://<server-ip>:3300` — complete the setup wizard on first visit, create admin account
4. Git over SSH on port `2222`:
3. Runs behind a Tailscale sidecar (its own tailnet device, HTTPS via `tailscale serve`), so it's reachable only on the tailnet — no host port. Web UI at `https://forgejo.<tailnet>.ts.net/` — complete the setup wizard on first visit, create admin account
4. Git over SSH on port `22` (the sidecar's own tailnet device, no conflict with the host's sshd):
```sh
git clone ssh://git@<tailscale-hostname>:2222/<username>/<repo>.git
git clone ssh://git@forgejo.<tailnet>.ts.net:22/<username>/<repo>.git
```
5. Add your SSH public key in **Settings → SSH / GPG Keys** after creating your account
6. To migrate from GitHub: use Forgejo's built-in migration (**+ → New Migration → GitHub**), then optionally configure a push mirror back to GitHub under repo **Settings → Push Mirrors** while validating the setup
Expand Down
27 changes: 25 additions & 2 deletions linux-server/forgejo/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,35 @@ TS_AUTHKEY=

# Forgejo's own MagicDNS name = the sidecar `hostname:` (forgejo) + your tailnet.
# Find your tailnet: tailscale status --json | jq -r '.Self.DNSName' | sed 's/\.$//'
# (that prints ollie-server.<tailnet>.ts.net — swap the host part for `forgejo`)
FORGEJO_DOMAIN=forgejo.<tailnet>.ts.net
# (that prints <hostname>.<tailnet>.ts.net — swap the host part for `forgejo`)
# Quoted because runner-status.sh sources this file (the <...> placeholder would
# otherwise be read as a shell redirection).
FORGEJO_DOMAIN="forgejo.<tailnet>.ts.net"

# Where to store repositories and Forgejo config.
# Default: ./data (same directory as docker-compose.yml)
# External drive example: FORGEJO_DATA_PATH=/mnt/external/forgejo
FORGEJO_DATA_PATH=./data

TZ=America/Los_Angeles

# --- Runner status monitor (runner-status.sh) ------------------------------
# Server-side check that the Mac mini Actions runner is connected, surfaced on
# the homepage card, Uptime Kuma, and ntfy. The runner itself lives in
# macOS/forgejo-runner/. Leave the optional vars blank to skip that surface.

# Forgejo token that can read runners. The default API URL is the instance
# (admin) scope, so this needs an admin token; create one at
# Forgejo → Settings → Applications → Generate Token.
FORGEJO_RUNNER_API_TOKEN=
# Runner name as registered in Forgejo (Settings → Actions → Runners).
RUNNER_NAME=m4-mini
# Override only if your token is org/repo-scoped rather than instance/admin:
# FORGEJO_RUNNER_API_URL="https://forgejo.<tailnet>.ts.net/api/v1/repos/<owner>/<repo>/actions/runners"

# Uptime Kuma push monitor (optional). Create a Push monitor, paste its URL.
KUMA_PUSH_URL=
# ntfy down/recovery alerts (optional). Quoted: sourced by runner-status.sh.
NTFY_URL="https://ntfy.<tailnet>.ts.net"
NTFY_TOPIC=server-runner
NTFY_TOKEN=
13 changes: 13 additions & 0 deletions linux-server/forgejo/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,16 @@ services:
interval: 30s
timeout: 5s
retries: 3

# Loopback static server for the homepage "forgejo-runner" card. The host
# systemd timer (forgejo-runner-status.timer → runner-status.sh) writes
# runner-status.json here; homepage is host-networked, so localhost reaches it.
# Not user-facing — bound to loopback, no Tailscale sidecar.
forgejo-runner-status:
image: nginx:alpine
container_name: forgejo-runner-status
restart: unless-stopped
ports:
- "127.0.0.1:8098:80"
volumes:
- ./runner-status:/usr/share/nginx/html:ro
8 changes: 8 additions & 0 deletions linux-server/forgejo/forgejo-runner-status.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Unit]
Description=Forgejo Actions runner status check (Mac mini)
After=network-online.target docker.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/home/ollie/github/Computer-Setup/linux-server/forgejo/runner-status.sh
10 changes: 10 additions & 0 deletions linux-server/forgejo/forgejo-runner-status.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Poll the Forgejo runner status every 2 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=2min
Unit=forgejo-runner-status.service

[Install]
WantedBy=timers.target
103 changes: 103 additions & 0 deletions linux-server/forgejo/runner-status.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -euo pipefail

# Server-side check that the Mac mini Forgejo Actions runner is connected.
# Surfaces the result three ways: a JSON file for the homepage card, an Uptime
# Kuma push, and an ntfy alert when the runner transitions offline (and when it
# recovers). Run on a 2-minute systemd timer (forgejo-runner-status.timer).
#
# "Up" means Forgejo's API currently reports the runner as idle/active — i.e.
# the server actually sees it connected, not merely that the host pings. The
# runner itself lives in macOS/forgejo-runner/.

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"

if [[ -f "$SCRIPT_DIR/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "$SCRIPT_DIR/.env"
set +a
fi

: "${RUNNER_NAME:=m4-mini}"
: "${FORGEJO_DOMAIN:?set FORGEJO_DOMAIN in .env}"
# Endpoint that lists runners. Defaults to the instance (admin) scope; override
# in .env if your token is org/repo-scoped instead.
: "${FORGEJO_RUNNER_API_URL:=https://${FORGEJO_DOMAIN}/api/v1/admin/actions/runners}"
: "${FORGEJO_RUNNER_API_TOKEN:?set FORGEJO_RUNNER_API_TOKEN in .env (a Forgejo token that can read runners)}"
: "${STATUS_JSON:=$SCRIPT_DIR/runner-status/runner-status.json}"
: "${STATE_FILE:=$SCRIPT_DIR/runner-status/.last-state}"
: "${KUMA_PUSH_URL:=}"
: "${NTFY_TOPIC:=server-runner}"

command -v jq >/dev/null || { printf 'error: jq not installed (apt install jq)\n' >&2; exit 1; }

notify() {
local title="$1" priority="$2" tags="$3" msg="$4"
[[ -n "${NTFY_URL:-}" ]] || return 0
local args=(-fsS -H "Title: $title" -H "Priority: $priority" -H "Tags: $tags" -d "$msg")
[[ -n "${NTFY_TOKEN:-}" ]] && args+=(-H "Authorization: Bearer $NTFY_TOKEN")
curl "${args[@]}" "$NTFY_URL/$NTFY_TOPIC" >/dev/null 2>&1 || true
}

kuma_push() {
local status="$1" msg="$2"
[[ -n "$KUMA_PUSH_URL" ]] || return 0
curl -fsS -G \
--data-urlencode "status=$status" \
--data-urlencode "msg=$msg" \
"$KUMA_PUSH_URL" >/dev/null 2>&1 || true
}

# --- query Forgejo ----------------------------------------------------------
resp="$(curl -fsS -m 10 -H "Authorization: token $FORGEJO_RUNNER_API_TOKEN" \
"$FORGEJO_RUNNER_API_URL" 2>/dev/null || true)"

# The list endpoint may return a bare array or a {runners|entries:[...]} wrapper.
runners='[]'
if [[ -n "$resp" ]]; then
runners="$(jq -c 'if type=="array" then . else (.runners // .entries // .data // []) end' <<<"$resp" 2>/dev/null || echo '[]')"
fi

runner="$(jq -c --arg n "$RUNNER_NAME" 'map(select(.name==$n)) | .[0] // {}' <<<"$runners")"
status="$(jq -r '.status // "unknown"' <<<"$runner")"
busy="$(jq -r 'if .busy == true then true else false end' <<<"$runner")"

state=down
case "$status" in
idle | active) state=up ;;
esac

# --- homepage JSON ----------------------------------------------------------
mkdir -p "$(dirname "$STATUS_JSON")"
jq -n \
--arg state "$state" \
--arg status "$status" \
--arg runner "$RUNNER_NAME" \
--argjson busy "$busy" \
--arg checked "$(date -u +%FT%TZ)" \
'{state:$state, status:$status, runner:$runner, busy:$busy, checked:$checked}' \
>"$STATUS_JSON"

# --- Uptime Kuma ------------------------------------------------------------
if [[ "$state" == up ]]; then
kuma_push up "$RUNNER_NAME: $status"
else
kuma_push down "$RUNNER_NAME: $status — Forgejo does not see the runner"
fi

# --- ntfy, only on a state change ------------------------------------------
prev="$(cat "$STATE_FILE" 2>/dev/null || true)"
if [[ -z "$prev" ]]; then
printf '%s\n' "$state" >"$STATE_FILE" # seed on first run, no alert
elif [[ "$state" != "$prev" ]]; then
if [[ "$state" == down ]]; then
notify "Forgejo runner DOWN" urgent rotating_light \
"$RUNNER_NAME is $status — Forgejo no longer sees the runner. Check the Mac mini: bash macOS/forgejo-runner/run.sh status"
else
notify "Forgejo runner recovered" default white_check_mark "$RUNNER_NAME is back ($status)"
fi
printf '%s\n' "$state" >"$STATE_FILE"
fi

printf '[runner-status] %s: %s (%s)\n' "$RUNNER_NAME" "$state" "$status"
6 changes: 3 additions & 3 deletions linux-server/homepage/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Used in services.yaml for service hrefs — use hostname (avahi mDNS) to avoid hardcoding an IP
# Access the dashboard at http://<hostname>.local:3000
HOMEPAGE_VAR_SERVER_IP=ollie-server.local
HOMEPAGE_VAR_SERVER_IP=<hostname>.local

# Tailscale widget uses a local proxy (linux-server/tailscale-proxy) — configure credentials there

Expand Down Expand Up @@ -77,8 +77,8 @@ HOMEPAGE_VAR_NPM_DOMAIN=npm.<tailnet>.ts.net
TS_AUTHKEY=

# Server hostname — used to build HOMEPAGE_ALLOWED_HOSTS in docker-compose.yml
HOSTNAME=ollie-server
HOSTNAME=<hostname>
# Server LAN IP — used in HOMEPAGE_ALLOWED_HOSTS to allow direct IP access
SERVER_IP=192.168.1.72
SERVER_IP=<server-ip>
# Tailscale HTTPS hostname — run: tailscale status --json | jq -r '.Self.DNSName' | sed 's/\.$//'
TAILSCALE_HOSTNAME=<hostname>.<tailnet>.ts.net
Loading
Loading