Shared infra, not a deployable app (provisioned by stack-init's
deploy_mqtt_broker(), no Forgejo repo of its own) — listed in INFRA_HEALTH
only so it's a valid host for /deploy/exec (used by the portal's new MQTT
inspector tab) and shows in the Shell tab's container list. Its /health
check will always read "unreachable" since Mosquitto doesn't speak HTTP —
expected, not a fault.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Calls Forgejo compare API when an update is available to get the exact
number of commits between deployed and latest — exposed as commitsBehind
in the status payload. Degrades gracefully to null if the compare call fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the deploy directory has no .git (e.g. after push_dir stripped it),
re-clone via HTTPS preserving the existing .env, then continue with the
normal pull + up flow. Also sets GIT_SSH_COMMAND on all git calls so the
inner Forgejo SSH connection uses UserKnownHostsFile=/dev/null instead of
trying to write to /root/.ssh/known_hosts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backup container: builds from Dockerfile (adds openssh-client, curl, jq)
fetches Nextcloud creds from Settings API at runtime, pg_dumps all DBs
and tars Docker volumes on each app LXC via SSH, uploads to Nextcloud
WebDAV, writes runs.ndjson log, heartbeats Uptime Kuma
- updater: gains docker-cli, Docker socket mount, /backup/runs,
/backup/status, /backup/trigger endpoints; reads runs.ndjson for status
- kuma_data mounted read-only into backup container for local backup
- .env.example updated with SETTINGS_URL, SETTINGS_SECRET, PG_SUPERPASS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/root/.ssh is mounted read-only in the management container, causing stderr
noise on every SSH connection. Routing to /dev/null avoids the write attempt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sshGet() and deploy() built SSH commands via template literal string
interpolation, which would allow shell injection if host/path values
from the app registry were tampered with. Replaced with spawnAsync()
using shell: false and explicit argv arrays. Added path validation
guard (/^\/opt\/[a-z0-9-]+$/) before both SSH calls. Also removes
the event-loop-blocking execSync in deploy().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fastify-raw-body was never registered, so HMAC ran over re-serialised
JSON and every Forgejo delivery failed with 401
- accept X-Forgejo-Signature / X-Gitea-Signature (bare hex) as well as
the GitHub-style sha256= prefix, and reject length mismatches instead
of crashing timingSafeEqual with a 500
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
updater now fetches app registry from auth /api/auth/internal/registry
and uses internal_host/internal_port for SSH status checks and deploys.
Removes hardcoded host map; platform infra (auth, portal, settings)
kept in INFRA_DEPLOY/INFRA_HEALTH constants.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apps without a /health route return 404 from nginx, but the service IS
running. Treat any HTTP response as up; only connection errors are down.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parallel-fetches /health on each app LXC, returns up/down + response
time. Used by the portal Uptime tab instead of embedding Uptime Kuma.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>