#!/usr/bin/env bash # # Plain-English SkateSpots ops report. # # scripts/ops-report.sh # print it # scripts/ops-report.sh --send # print it and DM it to both admins (NIP-17) # # The daily timer runs `--send` at 07:00. The sender is the app identity in # ~/.config/skatespots/zsp.env (mode 600); recipients are src/lib/admins.ts. set -euo pipefail REPO="/home/sovereignsk8/SkateSpots" cd "$REPO" if [ -d "$HOME/.nvm/versions/node" ]; then NODE_BIN="$(ls -d "$HOME"/.nvm/versions/node/*/bin 2>/dev/null | sort -V | tail -1)" [ -n "$NODE_BIN" ] && export PATH="$NODE_BIN:$PATH" fi export PATH="$HOME/go/bin:$PATH" SEND=0 [ "${1:-}" = "--send" ] && SEND=1 now="$(date +%s)" now_human="$(LC_ALL=C date '+%A %-d %B %Y, %H:%M')" state_deploy="$HOME/.local/state/skatespots-deploy" state_deps="$HOME/.local/state/skatespots-deps" deployed_sha="$(cat "$state_deploy/last-deployed-sha" 2>/dev/null || echo unknown)" origin_sha="$(git rev-parse origin/main 2>/dev/null || echo unknown)" deploy_epoch="$(cat "$state_deploy/last-deploy-epoch" 2>/dev/null || echo 0)" skip_file="$state_deploy/last-skip" deps_last_file="$state_deps/last-run" deps_fail_file="$state_deps/last-failure" human_left() { local s="$1" d h m [ "$s" -lt 0 ] && s=0 d=$((s / 86400)); h=$(((s % 86400) / 3600)); m=$(((s % 3600) / 60)) if [ "$d" -gt 0 ]; then printf '%s day%s %s hour%s' "$d" "$([ "$d" -eq 1 ] || echo s)" "$h" "$([ "$h" -eq 1 ] || echo s)" elif [ "$h" -gt 0 ]; then printf '%s hour%s %s minute%s' "$h" "$([ "$h" -eq 1 ] || echo s)" "$m" "$([ "$m" -eq 1 ] || echo s)" else printf '%s minute%s' "$m" "$([ "$m" -eq 1 ] || echo s)" fi } version="$(node -p "require('./package.json').version")" problems=() # ── app ───────────────────────────────────────────────────────────────────── app_line="version $version" if command -v nak >/dev/null 2>&1; then if timeout 8 nak req -k 30063 -d "org.skatespots.app@$version" wss://relay.zapstore.dev 2>/dev/null \ | grep -qE "\"version\"[[:space:]]*,[[:space:]]*\"$version\""; then app_line="version $version is published on Zapstore" else app_line="version $version; the Zapstore release could not be confirmed just now" fi else app_line="version $version" fi # ── website ───────────────────────────────────────────────────────────────── web_line="" skip_reason=""; sk_at=""; sk_sha=""; sk_detail="" if [ -f "$skip_file" ]; then IFS='|' read -r sk_at skip_reason sk_sha sk_detail < "$skip_file" fi if [ "$skip_reason" = "health-failed" ]; then web_line="deployed but did not answer its health check" problems+=("The website deployed but did not answer its health check.") elif [ "$deployed_sha" = "$origin_sha" ]; then web_line="the website is up to date" else case "${skip_reason:-}" in throttled) # `sk_at` is the last CHECK time (the timer polls every 3 min); the 24h # window is measured from the last successful DEPLOY. remain=$((86400 - (now - ${deploy_epoch:-0}))) if [ "$remain" -gt 0 ]; then web_line="new changes are waiting; it will update automatically in about $(human_left "$remain")" else web_line="new changes are waiting; it should update at the next check" fi ;; non-site-only|dist-unchanged) web_line="up to date (the latest changes did not affect the website)" ;; dirty-tree) if [ -n "$(git status --porcelain --untracked-files=no)" ]; then web_line="paused because the server has uncommitted changes" problems+=("The website deploy is paused because the server has uncommitted changes.") else remain=$((86400 - (now - ${deploy_epoch:-0}))) if [ "$remain" -gt 0 ]; then web_line="new changes are waiting; it will update automatically in about $(human_left "$remain")" else web_line="new changes are waiting; the next check will deploy them" fi fi ;; preflight-failed) web_line="paused because the tests failed" problems+=("The website deploy is paused because the tests failed.") ;; smoke-failed) web_line="paused because the built app did not start" problems+=("The website deploy is paused because the built app did not start.") ;; deploy-failed) web_line="the deploy failed at Netlify" problems+=("The website deploy failed at Netlify.") ;; diverged) web_line="paused because the server code is out of sync" problems+=("The website deploy is paused because the server code is out of sync.") ;; fetch-failed) web_line="waiting because the server could not reach the code host" problems+=("The website deploy could not reach the code host.") ;; "") web_line="new changes are waiting; the next check will deploy them" ;; *) web_line="new changes are waiting (reason: ${skip_reason})" ;; esac fi deploy_timer="$(systemctl --user is-active skatespots-deploy.timer 2>/dev/null || echo unknown)" if [ "$deploy_timer" != "active" ]; then web_line="$web_line; the website deploy timer is stopped" problems+=("The website deploy timer is stopped.") fi # ── dependencies ──────────────────────────────────────────────────────────── deps_line="" deps_timer="$(systemctl --user is-active skatespots-deps.timer 2>/dev/null || echo unknown)" scan_timer="$(systemctl --user is-active skatespots-deps-scan.timer 2>/dev/null || echo unknown)" if [ "$deps_timer" = "active" ]; then if [ -f "$deps_last_file" ]; then deps_last="$(cat "$deps_last_file" 2>/dev/null || echo 0)" deps_due=$((deps_last + 16 * 86400)) if [ "$deps_due" -gt "$now" ]; then deps_line="the next automatic dependency check is in about $(human_left $((deps_due - now)))" else deps_line="the next automatic dependency check runs at the next daily check (around midnight)" fi else deps_line="the first automatic check will run at the next daily check" fi else deps_line="the dependency timer is stopped" problems+=("The dependency update timer is stopped.") fi if [ -f "$deps_fail_file" ]; then deps_line="$deps_line. The last update failed and was rolled back; the next retry is in about 3 days" problems+=("The last dependency update failed and was rolled back.") fi if [ "$scan_timer" != "active" ]; then problems+=("The weekly security scan timer is stopped.") fi # ── security ──────────────────────────────────────────────────────────────── security_line="no security scan report is available yet" latest_report="$(ls -1t "$state_deps"/report-*.txt 2>/dev/null | head -1 || true)" if [ -n "$latest_report" ]; then osv_line="$(grep -E '^Total [0-9]+ packages affected' "$latest_report" | tail -1 || true)" if [[ "$osv_line" =~ Total\ ([0-9]+)\ packages\ affected\ by\ ([0-9]+)\ known\ vulnerabilities\ \(([0-9]+)\ Critical ]]; then pkgs="${BASH_REMATCH[1]}"; vulns="${BASH_REMATCH[2]}"; crit="${BASH_REMATCH[3]}" if [ "$crit" -eq 0 ]; then security_line="the latest security scan found $vulns known issues in $pkgs packages; none are critical. They are in build/development tools and the Excel export, which only writes files" else security_line="the latest security scan found $vulns known issues in $pkgs packages, including $crit critical" problems+=("The security scan found $crit critical issue(s).") fi else security_line="the latest security scan finished; check the server report for details" fi fi scan_line="every Monday morning" if [ "$scan_timer" != "active" ]; then scan_line="the timer is stopped" fi # ── compose ───────────────────────────────────────────────────────────────── if [ "${#problems[@]}" -eq 0 ]; then overall="Overall: all good." else overall="Overall: needs attention." fi report="$(cat <