ba-auto-daily/ba_cron_run.sh
Nik Afiq 68a7ed2529 feat: cafe lowest-affection invite for room 2, exit_game AP-left-unspent alert, login window-flicker fix
- cafe.py: room 2 now invites the lowest-affection candidate instead of
  highest (room 1 unchanged), plus OCR'd invited-student name logging
- exit_game.py: alert (AP_ALERT: log tag, picked up by ba_cron_run.sh's
  Discord alerting) if AP is still unspent above threshold when the game
  closes
- login.py: re-check window_exists() after the stabilization wait to catch
  a startup window-flicker race that could crash a run
- config.py/mapping.md: supporting constants and reference-mapping updates

Also rewrites plan.md: retires the phase-by-phase changelog (now fully
superseded by CLAUDE.md's own documentation) down to forward-looking
backlog items, and adds a performance-improvement plan from a cron-log
time analysis (lesson.py's per-cell screenshot redundancy, event_sweep's
multi-call design).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 19:28:59 +09:00

127 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Cron entry point for ba_dailies.sh presets. Pure process-locking/logging
# plumbing -- no game-automation logic, same "thin wrapper" spirit as
# ba_dailies.sh itself (see CLAUDE.md's Bash policy).
#
# The shared flock lock exists because "daily" fires twice a day (3:30 and
# 4:30) and "q4h" fires every 4 hours -- without a lock, two overlapping
# invocations would both drive xdotool/scrot against the same game window
# at once, which is unsafe. -n (non-blocking): if a previous run is still
# in progress, this invocation is skipped entirely rather than queued, so
# a scheduled fire time can never silently run late.
#
# Logs to ~/ba_logs/<preset>.log -- one file per preset, so "daily" and
# "q4h" never interleave. Every line is one of four explicitly-tagged
# outcomes (OK / FAILED / SKIPPED -- previous run still in progress /
# SKIPPED -- paused) specifically so a real failure can be found with a
# single command, e.g.:
# grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log
# A bare exit code alone can't do this: flock and a genuinely crashed task
# both exit 1, so lock acquisition is checked as its own explicit step
# (via an fd-based flock, not the "flock -n LOCKFILE COMMAND" form used
# originally) rather than folded into the wrapped command's own exit code.
# Likewise, ba_daily.py's manual pause switch (config.PAUSE_FLAG_PATH,
# toggled via `./ba_dailies.sh pause`/`resume`) exits with the distinct
# PAUSE_EXIT_CODE (75) below rather than the generic failure code, so a
# deliberate pause never gets logged as a crash.
#
# Optional Discord alerting (see plan.md Phase 26): if repo-root .env
# (gitignored -- copy .env.example to create it) provides a working
# ALERT_BRIDGE_URL/ALERT_BRIDGE_API_KEY pair, this posts a start alert and
# an OK/FAILED finish alert to alert-bridge for every real run. SKIPPED
# (lock contention or paused) never alerts -- both are benign/expected and
# would just be noise. A missing, incomplete, or malformed .env silently
# disables alerting only -- it can never affect the wrapped ba_dailies.sh
# run or this script's own OK/FAILED/SKIPPED logging.
#
# Separately (see plan.md Phase 30): if the exit_game task ends the run
# with AP still above config.EXIT_AP_ALERT_THRESHOLD, it prints a
# distinctly-tagged "AP_ALERT:" line rather than posting to Discord itself
# (Python has no .env/alert-bridge access by design -- see .env.example).
# This script greps only the lines this run itself appended to LOG_FILE
# (tracked via a before/after line count, not $(...) capture or `>()`
# process substitution, so the log keeps streaming live and neither of
# CLAUDE.md's Bash-safety triggers for those forms applies) and fires an
# extra `error`-level alert (pings you) independent of the run's own
# OK/FAILED outcome.
set -uo pipefail
# Resolves its own directory (same pattern ba_dailies.sh itself already
# uses) rather than assuming a fixed ~/ba_dailies.sh copy -- the runtime
# now runs directly out of this git checkout, so cron's own crontab entry
# points straight at this file's real path (see CLAUDE.md's "Deployment
# model") and this always calls the ba_dailies.sh sitting right next to it,
# wherever the checkout lives.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PRESET="${1:?usage: ba_cron_run.sh <preset>}"
LOG_DIR="$HOME/ba_logs"
LOG_FILE="$LOG_DIR/$PRESET.log"
LOCK_FILE="$LOG_DIR/ba_dailies.lock"
mkdir -p "$LOG_DIR"
# Load optional alert-bridge config. nounset is relaxed only around the
# `source` call so a malformed/typo'd .env (e.g. referencing an unset var,
# or a bad quote) can never abort this wrapper -- it just fails to enable
# alerting, same as a missing file or an unedited "changeme" key.
ALERT_ENABLED=0
ENV_FILE="$SCRIPT_DIR/.env"
if [ -f "$ENV_FILE" ]; then
set +u
source "$ENV_FILE" 2>/dev/null
env_source_status=$?
set -u
if [ "$env_source_status" -eq 0 ] \
&& [ -n "${ALERT_BRIDGE_URL:-}" ] \
&& [ -n "${ALERT_BRIDGE_API_KEY:-}" ] \
&& [ "${ALERT_BRIDGE_API_KEY:-}" != "changeme" ]; then
ALERT_ENABLED=1
fi
fi
alert() {
local level="$1" message="$2"
[ "$ALERT_ENABLED" -eq 1 ] || return 0
curl -sf --max-time 5 -X POST "$ALERT_BRIDGE_URL" \
-H "Authorization: Bearer $ALERT_BRIDGE_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"source\":\"ba-cron-$PRESET\",\"message\":\"$message\",\"level\":\"$level\"}" \
> /dev/null 2>&1 \
|| echo "=== $(date -Iseconds) alert POST failed, continuing ==="
}
{
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "=== $(date -Iseconds) SKIPPED $PRESET -- previous run still in progress ==="
exit 0
fi
echo "=== $(date -Iseconds) starting $PRESET ==="
start_epoch=$(date +%s)
alert info "Starting $PRESET"
start_line=$(wc -l < "$LOG_FILE" 2>/dev/null)
start_line="${start_line:-0}"
"$SCRIPT_DIR/ba_dailies.sh" "$PRESET"
status=$?
elapsed=$(( $(date +%s) - start_epoch ))
duration="$(( elapsed / 60 ))m $(( elapsed % 60 ))s"
if [ "$status" -eq 0 ]; then
echo "=== $(date -Iseconds) finished $PRESET OK (exit 0) ==="
alert info "$PRESET finished OK in $duration"
elif [ "$status" -eq 75 ]; then
echo "=== $(date -Iseconds) SKIPPED $PRESET -- paused ==="
else
echo "=== $(date -Iseconds) finished $PRESET FAILED (exit $status) ==="
alert error "$PRESET FAILED (exit $status) after $duration"
fi
ap_alert_line=$(tail -n +"$((start_line + 1))" "$LOG_FILE" 2>/dev/null | grep -m1 "AP_ALERT:")
if [ -n "$ap_alert_line" ]; then
alert error "$PRESET: $ap_alert_line"
fi
} >> "$LOG_FILE" 2>&1