i3blocks: book_guard block; gatelock 0.9.0 and newest-stable pins
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
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

[book_guard] shows this month's paper-book pace from the state.json
book-guard publishes (green on pace, yellow behind but unlocked, red when
locked or the timer is disarmed, orange when missing/stale); a click opens
book-guard-desktop -- the same way [screen_locker] opens its UI. Behaviour
test in tests/test_i3blocks_book_guard.sh.

meta/requirements.txt: gatelock 0.9.0 (the shared morning-session reader),
plus today's newest stable for anthropic, cryptography, flake8-bugbear,
geopandas, howlongtobeatpy, pyflakes, selenium, mypy, pylint and ruff.
Prunes the oldest evidence artifact to stay at the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBbkN4ZB4aUPJPzR2prRan
This commit is contained in:
Krzysztof Rudnicki 2026-10-02 14:22:43 +02:00
parent efe343dac4
commit a3728824a1
6 changed files with 269 additions and 45 deletions

View File

@ -0,0 +1,49 @@
{
"intent": "Make book-guard openable from the i3 bar the same way screen-locker is: a [book_guard] block shows this month's paper-book pace, and a click opens the book-guard desktop window. Move the shared pins to gatelock 0.9.0 (the new shared morning-session reader) and today's newest stable releases.",
"scope": [
"linux_configuration/i3blocks/book_guard.sh - new block script",
"linux_configuration/i3blocks/config - [book_guard] entry next to [screen_locker]",
"linux_configuration/tests/test_i3blocks_book_guard.sh - behaviour test",
"meta/requirements.txt - gatelock 0.9.0 and newest-stable pins",
"Non-goal: the stale tracked linux_configuration/i3/config (it already lacks the workout-locker and leetcode-guard exec lines); the live ~/.config/i3/config got the book-guard exec line."
],
"changes": [
"book_guard.sh reads the state.json book-guard publishes into ~/data/cloud/Reading on every inbox pass and gate run (no server, one jq pass): green on pace, yellow behind but unlocked (escaped / free day / unlocked tag), red when locked, orange when missing or older than 45 min, red with DISARMED when book-guard.timer is inactive. A click runs book-guard-desktop via setsid, which opens another window if it is already serving.",
"meta/requirements.txt: gatelock 0.8.2 -> 0.9.0; anthropic 1.11.0, cryptography 50.0.2, flake8-bugbear 26.9.30, geopandas 1.2.0, howlongtobeatpy 1.0.24, pyflakes 4.0.1, selenium 4.50.0, mypy 2.4.0, pylint 4.1.1, ruff 0.16.10."
],
"verification": [
{
"command": "bash linux_configuration/tests/test_i3blocks_book_guard.sh",
"result": "pass",
"evidence": "on pace / locked / escaped / free day / other / stale / disarmed / missing / broken / click all asserted; systemctl and the launcher stubbed on PATH."
},
{
"command": "bash linux_configuration/tests/test_i3blocks_efficiency.sh",
"result": "pass",
"evidence": "All i3blocks efficiency regression tests passed."
},
{
"command": "~/.config/i3blocks/book_guard.sh (live state.json)",
"result": "pass",
"evidence": "'📖 0/300 · 10p behind (escaped)' #F1FA8C; block live in the bar after i3-msg reload."
},
{
"command": ".venv/bin/python -m pytest python_pkg/code_tutor (the only anthropic/geopandas/selenium/cryptography importer)",
"result": "pass",
"evidence": "217 passed on anthropic 1.11.0."
},
{
"command": "pre-commit run --files <changed-files>",
"result": "pass",
"evidence": "all hooks passed, including dependency-freshness."
}
],
"risks": [
"cryptography 50.0.2 stays outside pyopenssl/mitmproxy's declared ranges, as 50.0.1 already was.",
"A click with book-guard-desktop missing does nothing (errors are silenced so the bar never breaks)."
],
"rollback": [
"git revert this commit and remove ~/.config/i3blocks/book_guard.sh; i3-msg reload.",
"Confirm the bar still renders [screen_locker] and the other blocks."
]
}

View File

@ -1,34 +0,0 @@
{
"intent": "Make the Python tests workflow actually trigger when the dependency list changes, so a requirements fix cannot silently skip the workflow it is meant to repair.",
"scope": [
".github/workflows/python-tests.yml (push and pull_request path filters)",
"Non-goal: changing the symlink layout, which the repo documents deliberately"
],
"changes": [
"Added meta/requirements.txt alongside requirements.txt in both path filters. The root file is a symlink into meta/, and GitHub matches filters against the path as git records it, so editing the real file did not match the symlink's name."
],
"verification": [
{
"command": "Observed on commit 32de2215, which changed meta/requirements.txt",
"result": "fail",
"evidence": "Dependency Graph, Shell tests and Pre-commit checks all ran; Python tests did NOT trigger at all, leaving the last Python tests result on the older red commit 5ba2cbd4"
},
{
"command": "python3 -c \"import yaml; yaml.safe_load(open('.github/workflows/python-tests.yml'))\"",
"result": "pass",
"evidence": "YAML valid; both the push and pull_request filter lists were patched (2 occurrences)"
},
{
"command": "gh workflow run 'Python tests' --ref main",
"result": "pass",
"evidence": "Dispatched run 32645075080 to confirm the dependency fix itself, since the filter bug prevented an automatic run"
}
],
"risks": [
"The other root symlinks (pyproject.toml, run.sh, lint_python.sh, .fvmrc) are not referenced by any workflow path filter today, so they carry the same latent trap if one is ever added."
],
"rollback": [
"Remove the meta/requirements.txt lines from both filters.",
"After rollback, a change to meta/requirements.txt will once again skip this workflow."
]
}

View File

@ -0,0 +1,87 @@
#!/bin/bash
# ============================================================================
# book-guard status for i3blocks: this month's paper-book pace.
#
# Sibling of screen_locker.sh, and opened the same way: a click opens the
# book-guard desktop window (book-guard-desktop serves the web app on :8773
# and, if it is already running, just opens another window onto it).
#
# Reads the state.json book-guard publishes into the Reading folder on every
# inbox pass (every 15 min) and every gate run -- no server needed. Like the
# screen-locker block it is never blank and never silently green: a missing
# or stale snapshot, or a disarmed timer, is shown as such.
# ============================================================================
set -euo pipefail
readonly STATE="${BOOK_GUARD_STATE:-$HOME/data/cloud/Reading/state.json}"
readonly LAUNCHER="${BOOK_GUARD_DESKTOP:-$HOME/.local/bin/book-guard-desktop}"
# Three missed inbox passes: past this the numbers describe a different hour.
readonly STALE_SECONDS=2700
# Dracula-ish palette, matching the other blocks in this bar.
readonly RED='#FF5555'
readonly YELLOW='#F1FA8C'
readonly GREEN='#50FA7B'
readonly ORANGE='#FFB86C'
emit() {
# full_text, short_text, color — the three lines i3blocks reads.
printf '%s\n%s\n%s\n' "$1" "$2" "$3"
}
# A click only opens the window; nothing here can clear a lock.
if [[ ${BLOCK_BUTTON:-0} -ne 0 ]]; then
setsid -f "$LAUNCHER" >/dev/null 2>&1 || true
fi
if ! command -v jq >/dev/null 2>&1; then
emit '📖 no jq' '📖 ?' "$ORANGE"
exit 0
fi
if [[ ! -r $STATE ]] || ! jq -e '.pace' "$STATE" >/dev/null 2>&1; then
emit '📖 no status' '📖 ?' "$ORANGE"
exit 0
fi
# One jq pass, tab-separated: locked, pages, target, behind, reason.
IFS=$'\t' read -r locked pages target behind reason < <(
jq -r '[.locked, .pace.pages, .pace.target, .pace.behind, .reason] | @tsv' "$STATE"
)
text="📖 ${pages}/${target}"
short="📖 ${pages}"
if [[ $locked == 'true' ]]; then
text="$text · LOCKED, ${behind}p behind"
color=$RED
elif ((behind > 0)); then
# Behind but unlocked: the bar has room for a tag, not the full reason.
case $reason in
*escape*) why='escaped' ;;
*free\ day*) why='free day' ;;
*) why='unlocked' ;;
esac
text="$text · ${behind}p behind (${why})"
color=$YELLOW
else
text="$text ✓"
color=$GREEN
fi
printf -v now '%(%s)T' -1
age=$((now - $(stat -c %Y "$STATE")))
if ((age > STALE_SECONDS)); then
text="$text (stale $((age / 60))m)"
color=$ORANGE
fi
# Arming beats pace, as in the screen-locker block: a gate that cannot fire
# is the worse problem, and it is the one that hides.
if ! systemctl --user is-active --quiet book-guard.timer; then
text="$text ⚠ DISARMED"
color=$RED
fi
emit "$text" "$short" "$color"

View File

@ -59,6 +59,10 @@ interval=30
command=~/.config/i3blocks/screen_locker.sh
interval=30
[book_guard]
command=~/.config/i3blocks/book_guard.sh
interval=30
[shutdown_countdown]
command=~/.config/i3blocks/shutdown_countdown.sh
interval=60

View File

@ -0,0 +1,118 @@
#!/bin/bash
# Behaviour tests for the i3blocks book_guard block: pace text, colours, every
# failure state and the click. systemctl and the desktop launcher are stubbed,
# and the state file is a fixture, so nothing reads the real Reading folder.
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
BLOCK="$REPO_DIR/i3blocks/book_guard.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 book_guard tests (pacman -S jq)'
# Stub systemctl: the timer is active unless TIMER_DOWN is set.
BIN_DIR="$TMP_DIR/bin"
mkdir -p "$BIN_DIR"
cat >"$BIN_DIR/systemctl" <<'STUB'
#!/bin/bash
[[ -z ${TIMER_DOWN:-} ]]
STUB
cat >"$BIN_DIR/launcher" <<STUB
#!/bin/bash
echo clicked >>"$TMP_DIR/clicks"
STUB
chmod +x "$BIN_DIR/systemctl" "$BIN_DIR/launcher"
STATE="$TMP_DIR/state.json"
# state <locked> <pages> <behind> <reason>
state() {
jq -n --argjson locked "$1" --argjson pages "$2" --argjson behind "$3" \
--arg reason "$4" \
'{locked: $locked, reason: $reason,
pace: {target: 300, pages: $pages, behind: $behind}}' >"$STATE"
}
run_block() {
PATH="$BIN_DIR:$PATH" BOOK_GUARD_STATE="$STATE" \
BOOK_GUARD_DESKTOP="$BIN_DIR/launcher" bash "$BLOCK"
}
line() {
sed -n "${1}p" <<<"$2"
}
printf 'Checking book_guard block states...\n'
state false 120 0 'on pace'
out=$(run_block)
assert_eq '📖 120/300 ✓' "$(line 1 "$out")" 'on pace text'
assert_eq '📖 120' "$(line 2 "$out")" 'short text'
assert_eq '#50FA7B' "$(line 3 "$out")" 'on pace is green'
state true 40 12 'behind the pace line'
out=$(run_block)
assert_eq '📖 40/300 · LOCKED, 12p behind' "$(line 1 "$out")" 'locked text'
assert_eq '#FF5555' "$(line 3 "$out")" 'locked is red'
state false 0 10 "today's lock was skipped with the escape hatch"
assert_eq '📖 0/300 · 10p behind (escaped)' "$(line 1 "$(run_block)")" 'escaped tag'
state false 0 10 'today is a free day'
assert_eq '📖 0/300 · 10p behind (free day)' "$(line 1 "$(run_block)")" 'free-day tag'
state false 0 10 'before the start date'
out=$(run_block)
assert_eq '📖 0/300 · 10p behind (unlocked)' "$(line 1 "$out")" 'other tag'
assert_eq '#F1FA8C' "$(line 3 "$out")" 'behind but unlocked is yellow'
printf 'Checking failure states are never blank or green...\n'
state false 120 0 'on pace'
touch -d '-2 hours' "$STATE"
out=$(run_block)
assert_eq '📖 120/300 ✓ (stale 120m)' "$(line 1 "$out")" 'stale text'
assert_eq '#FFB86C' "$(line 3 "$out")" 'stale is orange'
state false 120 0 'on pace'
out=$(TIMER_DOWN=1 run_block)
assert_eq '📖 120/300 ✓ ⚠ DISARMED' "$(line 1 "$out")" 'disarmed text'
assert_eq '#FF5555' "$(line 3 "$out")" 'disarmed is red'
rm "$STATE"
out=$(run_block)
assert_eq '📖 no status' "$(line 1 "$out")" 'missing state'
assert_eq '#FFB86C' "$(line 3 "$out")" 'missing is orange'
printf '{' >"$STATE"
assert_eq '📖 no status' "$(line 1 "$(run_block)")" 'broken state'
printf 'Checking a click opens the desktop window...\n'
state false 120 0 'on pace'
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 the launcher'
printf 'book_guard block: all checks passed\n'

View File

@ -6,7 +6,7 @@
add-trailing-comma==4.0.0
aiohttp==3.14.3
anthropic==1.8.0
anthropic==1.11.0
autoflake==2.4.0
autopep8==2.3.2
bandit==1.9.4
@ -16,7 +16,7 @@ black==26.5.1
bottle==0.13.4
codespell==2.4.3
coverage==7.16.2
cryptography==50.0.1
cryptography==50.0.2
darglint==1.8.1
dead==2.1.0
defusedxml==0.7.1
@ -25,7 +25,7 @@ fixit==2.2.1
flake8==7.4.1
flake8-annotations==3.2.0
flake8-bandit==4.1.1
flake8-bugbear==26.9.9
flake8-bugbear==26.9.30
flake8-comprehensions==3.17.0
flake8-docstrings==1.7.0
flake8-eradicate==1.5.0
@ -35,10 +35,10 @@ flake8-pyi==26.5.0
flake8-pytest-style==2.2.0
flake8-return==1.2.0
flake8-simplify==0.30.0
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.2#subdirectory=gatelock
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock
genanki==0.13.1
geopandas==1.1.4
howlongtobeatpy==1.0.23
geopandas==1.2.0
howlongtobeatpy==1.0.24
hypothesis==6.168.3
importlib-metadata==9.0.1
interrogate==1.7.0
@ -50,7 +50,7 @@ lxml==6.1.3
markdownify==1.2.3
matplotlib==3.11.2
mccabe==0.7.0
mypy==2.3.1
mypy==2.4.0
numpy==2.5.3
opencv-python==5.0.0.93
pillow==12.3.0
@ -60,9 +60,9 @@ pre-commit==4.6.2
prospector==1.19.1
pycodestyle==2.15.0
pydocstyle==6.3.0
pyflakes==4.0.0
pyflakes==4.0.1
pygame==2.6.1
pylint==4.0.9
pylint==4.1.1
pyright==1.1.414
pytest==9.1.1
pytest-cov==7.1.0
@ -77,12 +77,12 @@ radon==6.0.1
reorder-python-imports==3.17.0
requests==2.34.2
rich==15.0.0
ruff==0.16.9
ruff==0.16.10
# safety is deliberately not pinned: its only reference in this repo is a
# commented-out pre-commit hook, and safety 3.8.1 declares `typer<0.26`,
# which would hold the typer this repo actually imports below latest.
secretstorage==3.5.0
selenium==4.49.0
selenium==4.50.0
typer==0.27.2
types-python-dateutil==2.9.0.20260807
types-PyYAML==6.0.12.20260906