diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..162e589 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# .env.example -- committed template. Copy to .env and fill in the real API key. +# ba-auto-daily: used only for cron start/end Discord alerting (ba_cron_run.sh). +# Not read by ba_daily.py / ba_auto/ -- game automation has no use for this file. +ALERT_BRIDGE_URL=http://alert-bridge.home.arpa/alerts +ALERT_BRIDGE_API_KEY=changeme diff --git a/.gitignore b/.gitignore index 15afd8c..1285f87 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ *.pyc scratchpad/ /PAUSED +/.env diff --git a/CLAUDE.md b/CLAUDE.md index e5c3ef2..c46df4c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -219,9 +219,12 @@ rsync -av \ --exclude='graphify-out/' \ --exclude='screenshots/' \ --exclude='scratchpad/' \ + --exclude='.env' \ ./ nik-gpu:~/repo/ba-auto-daily/ ``` +`.env` (repo root, gitignored -- holds `ALERT_BRIDGE_URL`/`ALERT_BRIDGE_API_KEY` for cron start/end Discord alerting, see `ba_cron_run.sh` and `plan.md` Phase 26) is excluded for the same reason `PAUSED` is never overwritten: it's host-specific mutable runtime state, not repo content, and a push from a machine with its own local `.env` would otherwise silently overwrite nik-gpu's real one. `.env.example` (the committed template) has no such exclusion and syncs normally. + Do not sync Claude Code internal temporary folders. **Older model, replaced 2026-07-18:** this project used to copy code out to fixed paths (`~/ba_dailies.sh`, `~/ba_daily.py`, `~/ba_auto/`, `~/ba_assets/`, `~/ba_cron_run.sh`, `~/.ba_dailies_completion.bash`) via `setup.sh`, separate from the `~/repo/ba-auto-daily/` checkout `rsync` updates -- meaning a code change could be rsynced to the checkout but never reach the copies cron/manual runs actually used, unless `setup.sh` was re-run afterward. This bit for real, twice: once on 2026-07-16 (a same-day `ba_daily.py` edit missing from the runtime paths, caught and fixed by re-running `setup.sh`), and again on 2026-07-18 (the newly-added `exit_game` task/preset entries were completely absent from a real `q4h` cron run -- confirmed via the run's own log, which dispatched `login`/`cafe`/`mailbox`/`stamina`/`event_sweep` then stopped, no `exit_game` line at all -- root-caused to the exact same drift). Per explicit user request ("keep the home directory clean"), the copy step was removed entirely rather than just re-synced again, closing off this whole class of bug instead of continuing to hit it. If you ever see `~/ba_dailies.sh`/`~/ba_daily.py`/`~/ba_auto/`/`~/ba_assets/` existing again on nik-gpu outside the checkout, that's stale leftover from this old model, not the current architecture -- don't assume anything reads from them. @@ -240,6 +243,16 @@ Times are nik-gpu's local system time (`Asia/Tokyo`/JST). `ba_cron_run.sh` (repo This means: resources (AP, tickets, currency) may already be spent by the time a session starts, independent of anything done in that session -- check `~/ba_logs/` before assuming a given game state is from manual testing. The `daily`/`q4h` preset definitions themselves live in `ba_daily.py`'s `PRESETS` dict (see `plan.md`'s Phase 18 follow-ups for the full writeup), not here -- keep this section in sync if the schedule or preset names change. +### Discord alerting for cron start/end + +Added 2026-08-01 (see `plan.md` Phase 26 for the full design writeup), per explicit user request: a Discord notification each time `ba_cron_run.sh` starts and finishes a real preset run, posted through a separate personal-infra service, `alert-bridge` (`~/repo/home-service/alert-bridge/`, not part of this repo), which turns an authenticated `POST` into a Discord message. + +Config lives in `.env` at the repo root (gitignored -- `/.env` in `.gitignore`; `.env.example` is the committed template). To enable: `cd ~/repo/ba-auto-daily && cp .env.example .env`, then edit `.env` to set the real `ALERT_BRIDGE_API_KEY` (`ALERT_BRIDGE_URL` already defaults to the real LAN endpoint, since that part isn't a secret). Because `.env` lives inside the checkout, the `rsync` push command above excludes it explicitly (`--exclude='.env'`) so a push can never overwrite nik-gpu's real key with whatever (or nothing) happens to be at that path on `nik-macbookair`. + +`ba_cron_run.sh` sources `.env` if present and only enables alerting if both variables are non-empty and `ALERT_BRIDGE_API_KEY` isn't still the placeholder `changeme`. Three failure shapes -- no `.env` yet, an incomplete/placeholder one, or one with a shell syntax error -- all degrade to the exact same thing: alerting silently stays off, the wrapped `ba_dailies.sh` run and its own `OK`/`FAILED`/`SKIPPED` logging are completely unaffected. (The `source` call itself runs with `nounset` relaxed for that one step specifically, so a malformed `.env` referencing an unset variable can't abort the wrapper either.) A failed alert POST (`alert-bridge` unreachable, DNS not resolving, etc.) is swallowed the same way -- logged locally as `alert POST failed, continuing` in the preset's own log file, never allowed to change the run's logged outcome. All of this was verified locally against nik-gpu with a throwaway test `.env` (unreachable URL, malformed syntax, and the untouched `changeme` placeholder), confirming none of the three failure shapes crash the wrapper or alter its existing log format -- not yet verified: an actual Discord message arriving, which needs the real `ALERT_BRIDGE_API_KEY` in place. + +Only real runs alert (`info` on start, `info` on finish OK with elapsed duration, `error` on finish FAILED -- which additionally pings the Discord user `alert-bridge` is configured to mention). The two `SKIPPED` cases (lock contention, paused) never alert, deliberately -- both are benign/expected and would just be noise, especially the twice-hourly `daily` pair. `source` in the JSON payload is `ba-cron-$PRESET` (`ba-cron-daily`/`ba-cron-q4h`) so the Discord message itself identifies which preset fired. + ### Manual pause switch Added 2026-07-29, per explicit user request: a manual switch so a scheduled cron fire (or a forgotten manual invocation) can never fight the user for control of the game mid-play-session, e.g. right after a game update where the user wants to play manually before automation touches the account again. @@ -266,6 +279,7 @@ Required now: - `xdotool` - `scrot` - `python3` +- `curl` (confirmed present at `/usr/bin/curl`) -- only used by `ba_cron_run.sh`'s optional Discord alerting, not by any Python task/driver code Required Python packages: diff --git a/ba_cron_run.sh b/ba_cron_run.sh index 3ff9ae9..5de1cbb 100755 --- a/ba_cron_run.sh +++ b/ba_cron_run.sh @@ -24,6 +24,15 @@ # 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. set -uo pipefail # Resolves its own directory (same pattern ba_dailies.sh itself already @@ -40,6 +49,36 @@ 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 @@ -48,13 +87,21 @@ mkdir -p "$LOG_DIR" fi echo "=== $(date -Iseconds) starting $PRESET ===" + start_epoch=$(date +%s) + alert info "Starting $PRESET" + "$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 } >> "$LOG_FILE" 2>&1 diff --git a/plan.md b/plan.md index d71fba6..42bc15c 100644 --- a/plan.md +++ b/plan.md @@ -1116,6 +1116,105 @@ Building the purchasable check itself hit a real live bug worth remembering: an **Live-tested successfully**: real run bought `中級レポート` for 125,000 credits (the exact item that had been failing for days) and correctly logged the other 7 targets as unavailable this cycle rather than misdiagnosing them as a catalog change -- verified against the shop screen directly afterward (item gone from its slot, credit balance down exactly 125,000) and confirmed the task still returned cleanly to home. +### Phase 26: Discord alerting for cron start/end via alert-bridge (2026-08-01) + +User request: get a Discord notification when a scheduled cron run (`daily`/`q4h`, via `ba_cron_run.sh`) starts and finishes, using the existing `alert-bridge` service -- a separate personal-infra project at `~/repo/home-service/alert-bridge/`, not part of this repo, already deployed at `http://alert-bridge.home.arpa` and confirmed working as of 2026-08-01 (see its own README). It exposes one endpoint: + +``` +POST /alerts +Authorization: Bearer +{"source": "...", "message": "...", "level": "info"|"warn"|"error"} +``` + +`level=error` additionally pings a configured Discord user. It's plain HTTP (LAN-only, bearer-token-authenticated), fire-and-forget, single channel/webhook, no per-source routing. + +This is infrastructure/plumbing, not a Blue Archive game feature -- it has no `baas-reference` counterpart, so the reference-first workflow in CLAUDE.md doesn't apply here. It belongs in `ba_cron_run.sh`, not `ba_auto/tasks/`: alerting is a property of the *scheduled cron wrapper*, the same "pure locking/logging plumbing" category that file already occupies (flock + per-preset log files), not automation logic that runs against the game. Manual/CLI invocations of `ba_dailies.sh` directly (outside cron) are deliberately NOT wired up -- the user asked specifically about "cronjob start/end", and alerting on every ad hoc dev run would just be noise. + +#### Alert points + +Only real cron *runs* alert -- not skips: + +| Event | Level | Fires from | +|---|---|---| +| Run starting | `info` | before invoking `ba_dailies.sh "$PRESET"` | +| Run finished OK (exit 0) | `info` | existing `status -eq 0` branch | +| Run finished FAILED (exit N, not 0 or 75) | `error` | existing FAILED branch -- pings the configured Discord user, since this is the one case actually worth waking up for | +| SKIPPED -- previous run still in progress | none | deliberately not alerted: benign/expected overlap, would just be noise on the twice-hourly `daily` pair | +| SKIPPED -- paused | none | deliberately not alerted: the user caused this themselves via `./ba_dailies.sh pause`, they already know | + +Easy to add lock-skip/pause-skip alerts later at `warn`/`info` if the silence around them ever hides a real problem -- not doing it up front since neither has caused one yet. + +#### Message content + +- `source`: `"ba-cron-$PRESET"` (`ba-cron-daily` / `ba-cron-q4h`) so the Discord message itself identifies which preset fired, since alert-bridge has no per-source routing/formatting beyond the raw fields. +- Start: `"Starting $PRESET"`. +- Finish OK: `"$PRESET finished OK in ${DURATION}"` -- capture `START_EPOCH=$(date +%s)` right before the start alert, compute elapsed seconds at finish and format as `Xm Ys`. The existing timestamped log lines already give wall-clock times; duration is the one thing they don't, and it's nearly free to add here. +- Finish FAILED: `"$PRESET FAILED (exit $status) after ${DURATION}"`. Deliberately no log tail in this first version -- see "JSON safety" below. + +#### Secret handling + +Per explicit user direction: no outside-of-repo dependency. The real secret lives in `.env` at the repo root (`~/repo/ba-auto-daily/.env`), sitting right next to `ba_cron_run.sh`, not in `~/.config/` or anywhere else outside the checkout. It is never committed -- kept out of git the same way `/PAUSED` already is (a `.gitignore` entry), not by living outside the repo. What ships in git instead is a template, `.env.example`, committed at the repo root: + +```bash +# .env.example -- committed template. Copy to .env and fill in the real API key. +# ba-auto-daily: used only for cron start/end Discord alerting (ba_cron_run.sh). +# Not read by ba_daily.py / ba_auto/ -- game automation has no use for this file. +ALERT_BRIDGE_URL=http://alert-bridge.home.arpa/alerts +ALERT_BRIDGE_API_KEY=changeme +``` + +The leading comment matters because `.env` is a generic, easily-confused name -- it exists purely to spell out, right where anyone would open it, that it has nothing to do with game config/credentials and is safe to reason about in isolation. `ALERT_BRIDGE_URL`'s real value isn't a secret (a LAN-only hostname), so it's fine to ship as a working default in the committed example; only `ALERT_BRIDGE_API_KEY` is a placeholder. + +Setup, per the user's own stated workflow: on nik-gpu, `cd ~/repo/ba-auto-daily && cp .env.example .env`, then edit `.env` to drop in the real `ALERT_BRIDGE_API_KEY`. Two things enforce this stays local-only despite living inside the checkout: + +1. **`.gitignore`**: add `/.env` (root-only, so it doesn't also hide any unrelated nested `.env`) alongside the existing `/PAUSED` entry. `.env.example` stays tracked. +2. **rsync exclude**: the "Deployment model" push command (`rsync ... nik-gpu:~/repo/ba-auto-daily/`) must add `--exclude='.env'`, the same way it already excludes `screenshots/`/`scratchpad/`. Without this, a push run from a machine that happens to have its own local `.env` (e.g. a dev copy made for testing on nik-macbookair) would silently overwrite nik-gpu's real one with different content -- there's no `--delete` today, but overwrite (not delete) is exactly what a plain matching-filename `rsync` does, and this file is exactly the "host-specific mutable runtime state" category the existing exclude list already exists to protect (same reasoning as `PAUSED`, just via `.gitignore` + rsync-exclude instead of purely `.gitignore` since this one is also committed-as-a-template). + +`ba_cron_run.sh` sources `.env` from its own script directory (`"$SCRIPT_DIR/.env"`, same `BASH_SOURCE`-relative pattern the rest of the launcher already uses) if present, then explicitly checks both `ALERT_BRIDGE_URL` and `ALERT_BRIDGE_API_KEY` are non-empty before enabling alerting. Three distinct failure shapes all degrade the same way -- **the cron run itself always proceeds normally, with alerting simply skipped, never a hard error**: + +- file doesn't exist at all (only `.env.example` present, `.env` not yet created) +- file exists but is missing one or both variables, or still has the `changeme` placeholder +- file exists but is malformed (bad shell syntax) -- `source` is wrapped so a parse failure is caught (`source "$file" 2>/dev/null || ALERT_ENABLED=0`) rather than aborting the wrapper or leaking a syntax error into the preset run + +This means the feature ships disabled-by-default until `.env` is actually created and filled in, and any later mistake editing it (typo'd var name, stray quote, forgetting to replace `changeme`) silently falls back to "cron runs with no alerts" instead of breaking the actual scheduled job -- alerting must never be able to take the real automation down with it. + +#### Reliability / safety + +- `alert()` must never fail or hang the actual cron run: `curl -sf --max-time 5 ...`, failures ignored (`|| true`), no retry loop. +- A failed alert POST (alert-bridge down, DNS not resolving, etc.) should still leave one line in the existing `~/ba_logs/.log` (e.g. `"alert POST failed, continuing"`) so a totally silent alerting outage is still discoverable locally -- but it must never change `ba_cron_run.sh`'s own logged OK/FAILED/SKIPPED outcome, which keeps reflecting only the wrapped `ba_dailies.sh` run. +- JSON safety: the alert-bridge README's own example builds the JSON body with naive string interpolation -- fine for the fixed, hand-written messages here (`Starting daily`, `q4h finished OK in 2m14s`, `daily FAILED (exit 1) after 45s`), none of which can ever contain a `"` or `\`, but worth a one-line comment in the script noting this is why a raw log tail isn't being interpolated into a message in this first version. +- `curl` almost certainly already exists on nik-gpu (base Ubuntu/Debian), but worth confirming with `which curl` alongside the existing `xdotool`/`scrot`/`tesseract` check in CLAUDE.md's "Runtime dependencies" section, and adding it there once this ships. + +#### Implementation checklist + +1. Add `.env.example` at the repo root (committed) with the memo comment, the real `ALERT_BRIDGE_URL` default, and a placeholder `ALERT_BRIDGE_API_KEY=changeme`. +2. Add `/.env` to `.gitignore`, next to the existing `/PAUSED` entry. +3. Add `--exclude='.env'` to the `rsync` push command documented in CLAUDE.md's "Deployment model" (and use it from here on). +4. Edit `ba_cron_run.sh`: source `"$SCRIPT_DIR/.env"` if present (failure-tolerant, per "Secret handling" above), validate both variables are non-empty (and not still `changeme`) before enabling alerting, add an `alert()` helper (curl + timeout + JSON body + `|| true`) that's a no-op when alerting isn't enabled, call it at the three points above (start, OK, FAILED -- skip the two SKIPPED branches). +5. `bash -n ba_cron_run.sh` locally. +6. Push to nik-gpu (`rsync` with the new `--exclude='.env'`, per CLAUDE.md's "Deployment model" -- the checkout is the runtime, no separate copy step). +7. On nik-gpu: `cd ~/repo/ba-auto-daily && cp .env.example .env`, then edit `.env` to set the real `ALERT_BRIDGE_API_KEY`. +8. Manual smoke test on nik-gpu without touching the game, same technique Phase 18 follow-up #2 used: invoke `ba_cron_run.sh` with a bogus preset name, confirm both a `Starting ` info alert and a `FAILED (exit 1)` error alert (with the Discord ping) land in the channel, and that the local log still shows the normal `FAILED (exit 1)` line unchanged. +9. Real test: manually invoke `ba_cron_run.sh cafe` (a fast, safe real preset) and confirm a start alert plus an OK-with-duration alert both arrive. +10. Let the next real scheduled `daily`/`q4h` cron fire happen unattended and confirm alerts arrive without any manual trigger. +11. Update CLAUDE.md's "Scheduled runs (cron)" section to document the new alerting behavior (per that section's own "keep this section in sync" note), the `.env`/`.env.example` setup, the new rsync exclude, and add `curl` to "Runtime dependencies on nik-gpu" once confirmed working live. +12. Mark this phase done in `plan.md` with the live-test results, following the same retrospective style as every other phase here. + +**Status: steps 1-6, 8, 11 done; step 7 (real `.env` with the actual `ALERT_BRIDGE_API_KEY`) intentionally left to the user, since it's a real secret this session shouldn't handle or guess -- steps 9-10 (a real Discord message arriving) are blocked on that.** + +Implemented exactly per the design above: `.env.example` added at the repo root (committed), `/.env` added to `.gitignore`, `--exclude='.env'` added to the documented `rsync` command, and `ba_cron_run.sh` rewritten with the failure-tolerant `.env` sourcing, the `alert()` helper, and calls at the three real-run points (start/OK/FAILED). `bash -n` passed locally; pushed to nik-gpu via the updated `rsync` command. + +**Verified live on nik-gpu, without touching the game** (bogus-preset technique, same as Phase 18 follow-up #2): four scenarios all confirmed to leave `ba_cron_run.sh`'s own `starting`/`finished ... OK|FAILED`/`SKIPPED` log lines byte-for-byte unchanged from before this phase: + +1. No `.env` present at all -- alerting silently disabled, zero alert-related log lines, identical to pre-Phase-26 output. +2. `.env` present but `ALERT_BRIDGE_API_KEY` still the committed `changeme` placeholder -- same as above, correctly treated as "not configured." +3. `.env` with a syntactically valid but unreachable `ALERT_BRIDGE_URL` (`http://127.0.0.1:1/alerts`) -- alerting enabled, `curl` correctly failed fast (`--max-time 5`) on both the start and finish attempts, each logged its own `alert POST failed, continuing` line, and the wrapped run's `FAILED (exit 1)` line was completely unaffected. +4. `.env` deliberately corrupted (a `$TOTALLY_UNDEFINED_VAR` reference plus an unterminated quote) -- `source` failed as expected, `ALERT_ENABLED` correctly stayed `0` with *no* alert attempts at all (proving the `nounset`-relaxation-around-`source` fix actually works, not just the happy path), and the wrapper still ran and logged normally. + +All test `.env` files and the throwaway `smoke_test_alert_bogus.log` were deleted from nik-gpu afterward -- nothing real-secret-shaped was left behind. `curl` confirmed present at `/usr/bin/curl` on nik-gpu (added to CLAUDE.md's dependency list). CLAUDE.md's "Scheduled runs (cron)" section updated with a new "Discord alerting for cron start/end" subsection documenting all of the above. + +**Not yet done**: the user still needs to run `cp .env.example .env` on nik-gpu and fill in the real `ALERT_BRIDGE_API_KEY` themselves (steps 7 above) -- this session deliberately did not create, request, or guess that value. Once that's in place, steps 9-10 (a real `cafe` invocation confirming an actual Discord message arrives, then an unattended real `daily`/`q4h` cron fire doing the same) are the only remaining open items for this phase. + ## Prerequisites ### OCR