SkateSpots/scripts/ops-report.sh
SkateSpots bf28519e9e feat(ops): push-marked admin alerts, scan-change DMs, deploy/health alerts
- NIP-17 wraps now carry the app's ['skatespots','push'] marker on every admin
  copy (including the sender), so the push service wakes the phone with the app
  closed. Push test wraps 39fbbd4e…/c88c9f55… verified on the community relay.
- The weekly scan-only job DMs only when the OSV summary changes; unchanged
  scans are logged, not messaged.
- auto-deploy alerts on Netlify failure and on a post-deploy production URL
  health-check failure; deploy-failed/health-failed appear in ops-health and
  the daily report.
2026-10-27 09:52:37 +07:00

222 lines
8.8 KiB
Bash
Executable file

#!/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 <<EOF
SkateSpots daily report
$now_human
$overall
App: $app_line.
Website: $web_line.
Dependencies: $deps_line.
Security: $security_line.
Weekly scan: $scan_line.
EOF
)"
if [ "${#problems[@]}" -gt 0 ]; then
report="$report
What needs attention:"
for problem in "${problems[@]}"; do
report="$report
- $problem"
done
fi
report="$report
This message was sent automatically to the SkateSpots admins."
printf '%s\n' "$report"
if [ "$SEND" -eq 1 ]; then
printf '%s\n' "$report" | timeout 90 "$REPO/node_modules/.bin/tsx" "$REPO/scripts/send-ops-report.ts"
fi