i3blocks: automation_tutor block (today's tutor credits, click runs plc-lab run.sh)
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-10-10 15:06:05 +02:00
parent c287f59621
commit 698fdd5c8e
4 changed files with 196 additions and 0 deletions

View File

@ -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 <changed-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"
]
}

View File

@ -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"

View File

@ -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

View File

@ -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" <<STUB
#!/bin/bash
echo clicked >>"$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 <day:kind>... (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'