chore(ops): plain-English daily report + NIP-17 alerts to both admins
- scripts/ops-report.sh builds a simple daily report (app, website, deps, security, weekly scan); --send DMs it to the two admins from src/lib/admins.ts via NIP-17 (scripts/send-ops-report.ts), sender = app identity. - Delivery discovers kind-10050 inboxes + NIP-65 write relays and always includes the community relay; OPS_DRY_RUN=1 previews without publishing. - scripts/send-ops-alert.sh sends short alerts; wired into auto-deploy (preflight/smoke), update-deps (full+fallback failure) and ops-health --notify. - skatespots-ops-report.timer daily 07:00 + installer. - Signer moved into the 0600 zsp.env; old history copies removed.
This commit is contained in:
parent
39c388a6d7
commit
8473aaa994
10 changed files with 427 additions and 0 deletions
21
AGENTS.md
21
AGENTS.md
|
|
@ -2403,6 +2403,27 @@ Rules:
|
||||||
final `npm test` + boot smoke gates the result. If both the full and fallback
|
final `npm test` + boot smoke gates the result. If both the full and fallback
|
||||||
paths fail, it restores the original tree and schedules the ~3-day retry.
|
paths fail, it restores the original tree and schedules the ~3-day retry.
|
||||||
|
|
||||||
|
### Ops reports + admin alerts (2026-10-27)
|
||||||
|
- `scripts/ops-report.sh` prints a **plain-English** daily report (app version,
|
||||||
|
website state, dependency/security state, weekly scan). `--send` DMs it to
|
||||||
|
the two admins from `src/lib/admins.ts` (founder `npub1yrnuj…`, second admin
|
||||||
|
`npub1e2ay…`) via NIP-17 (`scripts/send-ops-report.ts`).
|
||||||
|
- Delivery mirrors the app: discover each admin's kind-10050 inbox, fall back to
|
||||||
|
NIP-65 write relays, always include `wss://relay.skate-spots.org`. One gift
|
||||||
|
wrap per recipient; the sender gets the copy. `OPS_DRY_RUN=1` prints
|
||||||
|
recipients/relays without publishing.
|
||||||
|
- `scripts/send-ops-alert.sh "…"` sends a short alert; wired into
|
||||||
|
`auto-deploy.sh` (preflight/smoke failure), `update-deps.sh` (full+fallback
|
||||||
|
failure) and `ops-health.sh --notify` (deduped by problem signature).
|
||||||
|
- `skatespots-ops-report.timer` runs daily at 07:00 (Persistent); install with
|
||||||
|
`scripts/install-ops-report-timer.sh`. A test report was DMed to both admins
|
||||||
|
on 2026-10-27 (kind 1059 on the community relay for each).
|
||||||
|
- `~/.config/skatespots/zsp.env` (mode 600) now actually holds `SIGN_WITH` (the
|
||||||
|
template was empty); `scripts/release.sh`, the alert sender and the ops report
|
||||||
|
read it and never print it. Old signer copies were removed from
|
||||||
|
`~/.bash_history`; clear any open shell's in-memory history with
|
||||||
|
`history -c; history -w` if it predates this.
|
||||||
|
|
||||||
### Community relay — self-hosted Newlay (2026-10-12)
|
### Community relay — self-hosted Newlay (2026-10-12)
|
||||||
|
|
||||||
The community relay moved off the hosted relay.tools instance
|
The community relay moved off the hosted relay.tools instance
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ log "preflight: typecheck + lint + i18n + tests…"
|
||||||
if ! "$REPO/scripts/preflight.sh"; then
|
if ! "$REPO/scripts/preflight.sh"; then
|
||||||
log "PREFLIGHT FAILED — not deploying"
|
log "PREFLIGHT FAILED — not deploying"
|
||||||
record_skip preflight-failed "the test gate failed"
|
record_skip preflight-failed "the test gate failed"
|
||||||
|
timeout 60 "$REPO/scripts/send-ops-alert.sh" "The website deploy is paused because the test gate failed. The live site is unchanged." || true
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -160,6 +161,7 @@ log "smoke-testing the built bundle…"
|
||||||
if ! node scripts/smoke-test.mjs; then
|
if ! node scripts/smoke-test.mjs; then
|
||||||
log "SMOKE TEST FAILED — aborting deploy (the built bundle did not boot)"
|
log "SMOKE TEST FAILED — aborting deploy (the built bundle did not boot)"
|
||||||
record_skip smoke-failed "the built bundle failed the smoke test"
|
record_skip smoke-failed "the built bundle failed the smoke test"
|
||||||
|
timeout 60 "$REPO/scripts/send-ops-alert.sh" "The website deploy is paused because the built app did not start. The live site is unchanged." || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
12
scripts/install-ops-report-timer.sh
Executable file
12
scripts/install-ops-report-timer.sh
Executable file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Install the daily plain-English ops report timer.
|
||||||
|
set -euo pipefail
|
||||||
|
SRC="$(cd "$(dirname "$0")/systemd" && pwd)"
|
||||||
|
DEST="$HOME/.config/systemd/user"
|
||||||
|
mkdir -p "$DEST"
|
||||||
|
cp "$SRC/skatespots-ops-report.service" "$DEST/"
|
||||||
|
cp "$SRC/skatespots-ops-report.timer" "$DEST/"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now skatespots-ops-report.timer
|
||||||
|
systemctl --user list-timers skatespots-ops-report.timer --no-pager
|
||||||
|
|
@ -268,6 +268,7 @@ if [ "$NOTIFY" = "1" ]; then
|
||||||
if command -v notify-send >/dev/null 2>&1; then
|
if command -v notify-send >/dev/null 2>&1; then
|
||||||
notify-send -a SkateSpots "SkateSpots ops" "$body" 2>/dev/null || true
|
notify-send -a SkateSpots "SkateSpots ops" "$body" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
timeout 45 "$REPO/scripts/send-ops-alert.sh" "SkateSpots needs attention: $body" || true
|
||||||
printf '%s' "$signature" > "$NOTIFY_FILE"
|
printf '%s' "$signature" > "$NOTIFY_FILE"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|
|
||||||
214
scripts/ops-report.sh
Executable file
214
scripts/ops-report.sh
Executable file
|
|
@ -0,0 +1,214 @@
|
||||||
|
#!/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=""
|
||||||
|
if [ "$deployed_sha" = "$origin_sha" ]; then
|
||||||
|
web_line="the website is up to date"
|
||||||
|
else
|
||||||
|
if [ -f "$skip_file" ]; then
|
||||||
|
IFS='|' read -r sk_at sk_reason sk_sha sk_detail < "$skip_file"
|
||||||
|
case "${sk_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.") ;;
|
||||||
|
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 (reason: ${sk_reason:-unknown})" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
web_line="new changes are waiting; the next check will deploy them"
|
||||||
|
fi
|
||||||
|
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 at 06:15"
|
||||||
|
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
|
||||||
24
scripts/send-ops-alert.sh
Executable file
24
scripts/send-ops-alert.sh
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Send a short plain-English alert to the SkateSpots admins as a NIP-17 DM.
|
||||||
|
# Usage: scripts/send-ops-alert.sh "message"
|
||||||
|
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"
|
||||||
|
|
||||||
|
MSG="${*:-SkateSpots needs attention.}"
|
||||||
|
{
|
||||||
|
echo "SkateSpots alert"
|
||||||
|
echo
|
||||||
|
echo "$MSG"
|
||||||
|
echo
|
||||||
|
echo "$(LC_ALL=C date '+%A %-d %B %Y, %H:%M')"
|
||||||
|
echo
|
||||||
|
echo "This message was sent automatically to the SkateSpots admins."
|
||||||
|
} | timeout 90 "$REPO/node_modules/.bin/tsx" "$REPO/scripts/send-ops-report.ts"
|
||||||
135
scripts/send-ops-report.ts
Normal file
135
scripts/send-ops-report.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Send a plain-text SkateSpots ops report to the two admins as a NIP-17 DM.
|
||||||
|
*
|
||||||
|
* The message is read from stdin. The sender is the app identity in
|
||||||
|
* ~/.config/skatespots/zsp.env (mode 600); the nsec is never printed.
|
||||||
|
*
|
||||||
|
* Recipients come from src/lib/admins.ts (the single source of truth):
|
||||||
|
* npub1yrnuj… (founder) and npub1e2ay… (second admin).
|
||||||
|
*
|
||||||
|
* Delivery mirrors the app's DM logic: discover each admin's kind-10050 inbox,
|
||||||
|
* fall back to their NIP-65 write relays, and always include the community relay
|
||||||
|
* (the push service watches it). One gift wrap is published per recipient.
|
||||||
|
*/
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { SimplePool, useWebSocketImplementation } from 'nostr-tools/pool';
|
||||||
|
import * as nip17 from 'nostr-tools/nip17';
|
||||||
|
import * as nip19 from 'nostr-tools/nip19';
|
||||||
|
import { getPublicKey } from 'nostr-tools/pure';
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
import { ADMIN_PUBKEYS } from '../src/lib/admins';
|
||||||
|
|
||||||
|
useWebSocketImplementation(WebSocket as never);
|
||||||
|
|
||||||
|
const DISCOVERY = [
|
||||||
|
'wss://purplepag.es',
|
||||||
|
'wss://relay.ditto.pub',
|
||||||
|
'wss://relay.skate-spots.org',
|
||||||
|
'wss://nos.lol',
|
||||||
|
];
|
||||||
|
const COMMUNITY = 'wss://relay.skate-spots.org';
|
||||||
|
|
||||||
|
function readSecret(): string {
|
||||||
|
const fromEnv = process.env.SIGN_WITH?.trim();
|
||||||
|
if (fromEnv) return fromEnv;
|
||||||
|
const file = process.env.SKATES_RELEASE_ENV
|
||||||
|
|| join(homedir(), '.config/skatespots/zsp.env');
|
||||||
|
const text = readFileSync(file, 'utf8');
|
||||||
|
const match = text.match(/^\s*(?:export\s+)?SIGN_WITH=(nsec1[0-9a-z]+)\s*$/m);
|
||||||
|
if (!match) throw new Error(`SIGN_WITH not found in ${file}`);
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
|
||||||
|
return Promise.race([
|
||||||
|
p,
|
||||||
|
new Promise<T>((_, reject) => setTimeout(() => reject(new Error('timeout')), ms)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const text = readFileSync(0, 'utf8').trim();
|
||||||
|
if (!text) throw new Error('empty report on stdin');
|
||||||
|
|
||||||
|
const secret = readSecret();
|
||||||
|
const decoded = nip19.decode(secret);
|
||||||
|
if (decoded.type !== 'nsec') throw new Error('SIGN_WITH is not an nsec');
|
||||||
|
const senderSecret = decoded.data;
|
||||||
|
const senderPubkey = getPublicKey(senderSecret);
|
||||||
|
|
||||||
|
// `wrapManyEvents` adds the sender automatically; don't add the founder twice.
|
||||||
|
const recipients = ADMIN_PUBKEYS
|
||||||
|
.filter((pubkey) => pubkey !== senderPubkey)
|
||||||
|
.map((publicKey) => ({ publicKey }));
|
||||||
|
|
||||||
|
const pool = new SimplePool();
|
||||||
|
const authors = [...new Set([...ADMIN_PUBKEYS, senderPubkey])];
|
||||||
|
|
||||||
|
const [inboxes, relayLists] = await Promise.all([
|
||||||
|
pool.querySync(
|
||||||
|
DISCOVERY,
|
||||||
|
{ kinds: [10050], authors, limit: authors.length * 2 },
|
||||||
|
{ maxWait: 4000 },
|
||||||
|
).catch(() => []),
|
||||||
|
pool.querySync(
|
||||||
|
DISCOVERY,
|
||||||
|
{ kinds: [10002], authors: ADMIN_PUBKEYS, limit: ADMIN_PUBKEYS.length },
|
||||||
|
{ maxWait: 4000 },
|
||||||
|
).catch(() => []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const relays = new Set<string>([COMMUNITY]);
|
||||||
|
const haveInbox = new Set<string>();
|
||||||
|
|
||||||
|
for (const event of inboxes) {
|
||||||
|
const urls = event.tags
|
||||||
|
.filter(([name]) => name === 'relay')
|
||||||
|
.map(([, url]) => url)
|
||||||
|
.filter(Boolean);
|
||||||
|
if (urls.length) haveInbox.add(event.pubkey);
|
||||||
|
for (const url of urls) relays.add(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A recipient with no inbox list falls back to their NIP-65 write relays.
|
||||||
|
for (const event of relayLists) {
|
||||||
|
if (haveInbox.has(event.pubkey)) continue;
|
||||||
|
for (const [name, url, marker] of event.tags) {
|
||||||
|
if (name === 'r' && url && (!marker || marker === 'write')) relays.add(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wraps = nip17.wrapManyEvents(senderSecret, recipients, text, 'SkateSpots ops');
|
||||||
|
|
||||||
|
if (process.env.OPS_DRY_RUN === '1') {
|
||||||
|
console.log(`sender: ${nip19.npubEncode(senderPubkey)}`);
|
||||||
|
console.log(`recipients: ${ADMIN_PUBKEYS.map((p) => nip19.npubEncode(p)).join(', ')}`);
|
||||||
|
console.log(`relays: ${[...relays].join(', ')}`);
|
||||||
|
console.log(`dry run: ${wraps.length} gift wrap(s) prepared, nothing published`);
|
||||||
|
pool.close([...relays]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let accepted = 0;
|
||||||
|
for (const wrap of wraps) {
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
pool.publish([...relays], wrap).map((p) => withTimeout(p, 10000)),
|
||||||
|
);
|
||||||
|
accepted += results.filter((result) => result.status === 'fulfilled').length;
|
||||||
|
}
|
||||||
|
pool.close([...relays]);
|
||||||
|
|
||||||
|
console.log(`recipients: ${ADMIN_PUBKEYS.map((p) => nip19.npubEncode(p)).join(', ')}`);
|
||||||
|
console.log(`relays: ${[...relays].join(', ')}`);
|
||||||
|
console.log(`accepted: ${accepted} publish(es)`);
|
||||||
|
console.log(`message ids: ${wraps.map((wrap) => wrap.id).join(', ')}`);
|
||||||
|
|
||||||
|
if (accepted === 0) process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`send-ops-report: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
7
scripts/systemd/skatespots-ops-report.service
Normal file
7
scripts/systemd/skatespots-ops-report.service
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[Unit]
|
||||||
|
Description=SkateSpots daily ops report (NIP-17 to admins)
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
TimeoutStartSec=5min
|
||||||
|
ExecStart=%h/SkateSpots/scripts/ops-report.sh --send
|
||||||
10
scripts/systemd/skatespots-ops-report.timer
Normal file
10
scripts/systemd/skatespots-ops-report.timer
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Daily SkateSpots ops report
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 07:00:00
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=10min
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
@ -278,5 +278,6 @@ else
|
||||||
RETRY_DAYS=3; [ "$INTERVAL_DAYS" -gt 4 ] || RETRY_DAYS=1
|
RETRY_DAYS=3; [ "$INTERVAL_DAYS" -gt 4 ] || RETRY_DAYS=1
|
||||||
echo $(( $(date +%s) - (INTERVAL_DAYS - RETRY_DAYS) * 86400 )) > "$LAST"
|
echo $(( $(date +%s) - (INTERVAL_DAYS - RETRY_DAYS) * 86400 )) > "$LAST"
|
||||||
notify "Dependency update REVERTED — full and patch fallback failed (see $FAIL_DIR). ${OSV_SUMMARY}"
|
notify "Dependency update REVERTED — full and patch fallback failed (see $FAIL_DIR). ${OSV_SUMMARY}"
|
||||||
|
timeout 90 "$REPO/scripts/send-ops-alert.sh" "The dependency update failed and was rolled back. The app is unchanged. The next retry is in about 3 days." || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue