diff --git a/docs/superpowers/evidence/i3blocks-automation-tutor-2026-10-10.json b/docs/superpowers/evidence/i3blocks-automation-tutor-2026-10-10.json new file mode 100644 index 00000000..28ef5b35 --- /dev/null +++ b/docs/superpowers/evidence/i3blocks-automation-tutor-2026-10-10.json @@ -0,0 +1,31 @@ +{ + "intent": "The i3 bar shows today's Automation tutor progress (tutor n/4), hides at 4/4, and a click opens the tutor via plc-lab's run.sh.", + "scope": [ + "linux_configuration/i3blocks/automation_tutor.sh, the [automation_tutor] stanza in linux_configuration/i3blocks/config, linux_configuration/tests/test_i3blocks_automation_tutor.sh", + "Non-goal: no ledger HMAC check (earned_time owns that); display only" + ], + "changes": [ + "New block counts kind==credit rows dated today in ~/.local/share/automation_tutor/ledger.json; hidden at 4/4; orange message when jq is missing or the ledger is unreadable", + "Click runs ~/src/plc-lab/run.sh (override AUTOMATION_TUTOR_RUN); ledger override AUTOMATION_TUTOR_LEDGER", + "Hermetic test with a stub run.sh and fixture ledgers: no ledger, 2 today + 1 yesterday, 4 today, unreadable, click" + ], + "verification": [ + { + "command": "bash linux_configuration/tests/test_i3blocks_automation_tutor.sh", + "result": "pass", + "evidence": "automation_tutor block: all checks passed" + }, + { + "command": "pre-commit run --files ", + "result": "pass", + "evidence": "shellcheck, codespell, file caps and shell hooks passed" + } + ], + "risks": [ + "i3blocks config is deployed by the installer; the block only appears after the next deploy to ~/.config/i3blocks" + ], + "rollback": [ + "git revert the commit", + "Remove [automation_tutor] from ~/.config/i3blocks/config and reload i3blocks" + ] +} diff --git a/linux_configuration/i3blocks/automation_tutor.sh b/linux_configuration/i3blocks/automation_tutor.sh new file mode 100755 index 00000000..d85e7d11 --- /dev/null +++ b/linux_configuration/i3blocks/automation_tutor.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# ============================================================================ +# Automation tutor status for i3blocks: today's credited 15-minute blocks. +# +# A click runs plc-lab's run.sh, which starts the tutor if it is down and +# opens it in the browser -- nobody has to remember the port. Hidden once all +# four blocks are in: nothing left to earn, nothing to click. +# +# Counts the tutor ledger's credit rows dated today. It does not check their +# HMAC (that needs the key and Python); earned_time does, so this is a +# display, never the authority on what was paid. +# ============================================================================ + +set -euo pipefail + +readonly LEDGER="${AUTOMATION_TUTOR_LEDGER:-$HOME/.local/share/automation_tutor/ledger.json}" +# i3blocks' PATH has no ~/src: absolute path. +readonly RUN="${AUTOMATION_TUTOR_RUN:-$HOME/src/plc-lab/run.sh}" +readonly TARGET=4 + +readonly YELLOW='#F1FA8C' +readonly ORANGE='#FFB86C' + +emit() { + # full_text, short_text, color — the three lines i3blocks reads. + printf '%s\n%s\n%s\n' "$1" "$2" "$3" +} + +if [[ ${BLOCK_BUTTON:-0} -ne 0 ]]; then + setsid -f "$RUN" >/dev/null 2>&1 || true +fi + +if ! command -v jq >/dev/null 2>&1; then + emit '⚙ tutor: no jq' '⚙ ?' "$ORANGE" + exit 0 +fi + +printf -v today '%(%Y-%m-%d)T' -1 +done_blocks=0 +# No ledger yet is a real zero: the tutor creates it on the first block. +if [[ -e $LEDGER ]]; then + if ! done_blocks=$(jq --arg d "$today" \ + '[.entries[] | select(.kind == "credit" and .day == $d)] | length' \ + "$LEDGER" 2>/dev/null); then + emit '⚙ tutor: ledger unreadable' '⚙ ?' "$ORANGE" + exit 0 + fi +fi + +if ((done_blocks >= TARGET)); then + emit '' '' "$YELLOW" + exit 0 +fi +emit "⚙ tutor ${done_blocks}/${TARGET}" "⚙ ${done_blocks}/${TARGET}" "$YELLOW" diff --git a/linux_configuration/i3blocks/config b/linux_configuration/i3blocks/config index aff265a4..322d5fef 100644 --- a/linux_configuration/i3blocks/config +++ b/linux_configuration/i3blocks/config @@ -63,6 +63,10 @@ interval=30 command=~/.config/i3blocks/book_guard.sh interval=30 +[automation_tutor] +command=~/.config/i3blocks/automation_tutor.sh +interval=30 + [issue_dashboard] command=~/.config/i3blocks/issue_dashboard.sh interval=30 diff --git a/linux_configuration/tests/test_i3blocks_automation_tutor.sh b/linux_configuration/tests/test_i3blocks_automation_tutor.sh new file mode 100755 index 00000000..36ff917c --- /dev/null +++ b/linux_configuration/tests/test_i3blocks_automation_tutor.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# Behaviour tests for the i3blocks automation_tutor block: today's credit +# count, the hidden 4/4 state, the unreadable-ledger warning and the click. +# The ledger is a fixture and run.sh is a stub, so nothing reads the real +# ledger and no tutor or browser is ever started. +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) +BLOCK="$REPO_DIR/i3blocks/automation_tutor.sh" +TMP_DIR=$(mktemp -d) + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_eq() { + local expected=$1 + local actual=$2 + local context=$3 + if [[ "$expected" != "$actual" ]]; then + fail "$context (expected '$expected', actual '$actual')" + fi +} + +printf '{}' | jq -e . >/dev/null 2>&1 || + fail 'a working jq is required for the automation_tutor tests (pacman -S jq)' + +BIN_DIR="$TMP_DIR/bin" +mkdir -p "$BIN_DIR" +cat >"$BIN_DIR/run.sh" <>"$TMP_DIR/clicks" +STUB +chmod +x "$BIN_DIR/run.sh" + +LEDGER="$TMP_DIR/ledger.json" + +TODAY=$(date +%Y-%m-%d) +YESTERDAY=$(date -d yesterday +%Y-%m-%d) + +# ledger ... (writes {"entries":[...]}) +ledger() { + local rows=() + local spec + for spec in "$@"; do + rows+=("$(jq -n --arg day "${spec%%:*}" --arg kind "${spec##*:}" \ + '{day: $day, kind: $kind}')") + done + printf '%s\n' "${rows[@]}" | jq -s '{entries: .}' >"$LEDGER" +} + +run_block() { + AUTOMATION_TUTOR_LEDGER="$LEDGER" AUTOMATION_TUTOR_RUN="$BIN_DIR/run.sh" \ + bash "$BLOCK" +} + +line() { + sed -n "${1}p" <<<"$2" +} + +printf 'Checking automation_tutor block states...\n' + +rm -f "$LEDGER" +out=$(run_block) +assert_eq '⚙ tutor 0/4' "$(line 1 "$out")" 'no ledger is 0/4' +assert_eq '⚙ 0/4' "$(line 2 "$out")" 'no ledger short text' +assert_eq '#F1FA8C' "$(line 3 "$out")" 'progress is yellow' + +ledger "$TODAY:credit" "$TODAY:credit" "$YESTERDAY:credit" +out=$(run_block) +assert_eq '⚙ tutor 2/4' "$(line 1 "$out")" 'counts only today' +assert_eq '⚙ 2/4' "$(line 2 "$out")" 'short text' + +ledger "$TODAY:credit" "$TODAY:debit" +assert_eq '⚙ tutor 1/4' "$(line 1 "$(run_block)")" 'only credit rows count' + +ledger "$TODAY:credit" "$TODAY:credit" "$TODAY:credit" "$TODAY:credit" +out=$(run_block) +assert_eq '' "$(line 1 "$out")" '4/4 hides the block' +assert_eq '' "$(line 2 "$out")" '4/4 short text hidden' + +printf 'Checking failure states are never blank or green...\n' + +printf '{' >"$LEDGER" +out=$(run_block) +assert_eq '⚙ tutor: ledger unreadable' "$(line 1 "$out")" 'unreadable text' +assert_eq '#FFB86C' "$(line 3 "$out")" 'unreadable is orange' + +printf 'Checking a click runs run.sh...\n' + +ledger "$TODAY:credit" +BLOCK_BUTTON=1 run_block >/dev/null +for _ in 1 2 3 4 5 6 7 8 9 10; do + [[ -s "$TMP_DIR/clicks" ]] && break + sleep 0.1 +done +assert_eq 'clicked' "$(cat "$TMP_DIR/clicks" 2>/dev/null)" 'click runs run.sh' + +printf 'automation_tutor block: all checks passed\n'