i3blocks/tutor: count credited minutes (cap 60), not ledger rows vs 4
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

Since earned_time 0.8.0 each tutor credit row carries detail.minutes, so
counting rows against 4 hid the block after 4 credited minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YADHKXzZhhePzQ2rNoXFcK
This commit is contained in:
Krzysztof Rudnicki 2026-10-12 16:55:06 +02:00
parent a511403e0f
commit 66160cd6da
3 changed files with 61 additions and 26 deletions

View File

@ -0,0 +1,29 @@
{
"intent": "The i3 tutor block shows today's credited tutor minutes against the 60-minute daily cap; it used to count ledger rows against 4 and hid after 4 credited minutes since earned_time 0.8.0 made each row a minute count.",
"scope": [
"linux_configuration/i3blocks/tutor.sh, linux_configuration/tests/test_i3blocks_tutor.sh",
"Non-goal: no HMAC check (earned_time stays the authority)"
],
"changes": [
"Sum detail.minutes of today's credit rows (legacy rows without it count 15, as earned_time does); TARGET 4 -> 60",
"Test fixture takes day:kind[:minutes]; new cases for summing, legacy rows and the 60/60 hidden state"
],
"verification": [
{
"command": "bash linux_configuration/tests/test_i3blocks_tutor.sh",
"result": "pass",
"evidence": "tutor block: all checks passed"
},
{
"command": "bash linux_configuration/i3blocks/tutor.sh",
"result": "pass",
"evidence": "⚙ tutor 0/60 on the live ledger; same jq on 2026-10-11 gives 60, matching earned_time's 60 units that day"
}
],
"risks": [
"A ledger row with non-numeric minutes would make jq fail and show the orange 'ledger unreadable' state"
],
"rollback": [
"git revert the commit; the block goes back to counting rows / 4"
]
}

View File

@ -1,13 +1,14 @@
#!/bin/bash
# ============================================================================
# Tutor status for i3blocks: today's credited 15-minute blocks.
# Tutor status for i3blocks: today's credited tutor minutes.
#
# A click runs ~/src/tutor/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.
# opens it in the browser -- nobody has to remember the port. Hidden once the
# daily cap is in: nothing left to earn, nothing to click.
#
# Counts the tutor ledger's credit rows dated today. It does not check their
# Sums detail.minutes of the ledger's credit rows dated today (legacy rows
# without it count 15, as earned_time does). 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.
# ============================================================================
@ -17,7 +18,7 @@ set -euo pipefail
readonly LEDGER="${TUTOR_LEDGER:-$HOME/.local/share/tutor/ledger.json}"
# i3blocks' PATH has no ~/src: absolute path.
readonly RUN="${TUTOR_RUN:-$HOME/src/tutor/run.sh}"
readonly TARGET=4
readonly TARGET=60 # earned_time daily cap, minutes
readonly YELLOW='#F1FA8C'
readonly ORANGE='#FFB86C'
@ -37,19 +38,19 @@ if ! command -v jq >/dev/null 2>&1; then
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.
done_minutes=0
# No ledger yet is a real zero: the tutor creates it on the first credit.
if [[ -e $LEDGER ]]; then
if ! done_blocks=$(jq --arg d "$today" \
'[.entries[] | select(.kind == "credit" and .day == $d)] | length' \
if ! done_minutes=$(jq --arg d "$today" \
'[.entries[] | select(.kind == "credit" and .day == $d) | (.detail.minutes // 15)] | add // 0 | floor' \
"$LEDGER" 2>/dev/null); then
emit '⚙ tutor: ledger unreadable' '⚙ ?' "$ORANGE"
exit 0
fi
fi
if ((done_blocks >= TARGET)); then
if ((done_minutes >= TARGET)); then
emit '' '' "$YELLOW"
exit 0
fi
emit "⚙ tutor ${done_blocks}/${TARGET}" "⚙ ${done_blocks}/${TARGET}" "$YELLOW"
emit "⚙ tutor ${done_minutes}/${TARGET}" "⚙ ${done_minutes}/${TARGET}" "$YELLOW"

View File

@ -1,6 +1,6 @@
#!/bin/bash
# Behaviour tests for the i3blocks tutor block: today's credit
# count, the hidden 4/4 state, the unreadable-ledger warning and the click.
# Behaviour tests for the i3blocks tutor block: today's credited
# minutes, the hidden 60/60 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
@ -45,13 +45,15 @@ LEDGER="$TMP_DIR/ledger.json"
TODAY=$(date +%Y-%m-%d)
YESTERDAY=$(date -d yesterday +%Y-%m-%d)
# ledger <day:kind>... (writes {"entries":[...]})
# ledger <day:kind[:minutes]>... (writes {"entries":[...]}; no minutes = legacy row)
ledger() {
local rows=()
local spec
for spec in "$@"; do
rows+=("$(jq -n --arg day "${spec%%:*}" --arg kind "${spec##*:}" \
'{day: $day, kind: $kind}')")
local day kind minutes
IFS=: read -r day kind minutes <<<"$spec"
rows+=("$(jq -n --arg day "$day" --arg kind "$kind" --arg m "$minutes" \
'{day: $day, kind: $kind} + (if $m == "" then {} else {detail: {minutes: ($m | tonumber)}} end)')")
done
printf '%s\n' "${rows[@]}" | jq -s '{entries: .}' >"$LEDGER"
}
@ -69,22 +71,25 @@ printf 'Checking 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 '⚙ tutor 0/60' "$(line 1 "$out")" 'no ledger is 0/60'
assert_eq '⚙ 0/60' "$(line 2 "$out")" 'no ledger short text'
assert_eq '#F1FA8C' "$(line 3 "$out")" 'progress is yellow'
ledger "$TODAY:credit" "$TODAY:credit" "$YESTERDAY:credit"
ledger "$TODAY:credit:3" "$TODAY:credit:1" "$YESTERDAY:credit:50"
out=$(run_block)
assert_eq '⚙ tutor 2/4' "$(line 1 "$out")" 'counts only today'
assert_eq '⚙ 2/4' "$(line 2 "$out")" 'short text'
assert_eq '⚙ tutor 4/60' "$(line 1 "$out")" 'sums only today'
assert_eq '⚙ 4/60' "$(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:2"
assert_eq '⚙ tutor 17/60' "$(line 1 "$(run_block)")" 'legacy row counts 15'
ledger "$TODAY:credit" "$TODAY:credit" "$TODAY:credit" "$TODAY:credit"
ledger "$TODAY:credit:5" "$TODAY:debit:9"
assert_eq '⚙ tutor 5/60' "$(line 1 "$(run_block)")" 'only credit rows count'
ledger "$TODAY:credit:59" "$TODAY:credit:1"
out=$(run_block)
assert_eq '' "$(line 1 "$out")" '4/4 hides the block'
assert_eq '' "$(line 2 "$out")" '4/4 short text hidden'
assert_eq '' "$(line 1 "$out")" '60/60 hides the block'
assert_eq '' "$(line 2 "$out")" '60/60 short text hidden'
printf 'Checking failure states are never blank or green...\n'