diff --git a/docs/superpowers/evidence/i3blocks-screen-locker-2026-08-29.json b/docs/superpowers/evidence/i3blocks-screen-locker-2026-08-29.json new file mode 100644 index 00000000..a6481068 --- /dev/null +++ b/docs/superpowers/evidence/i3blocks-screen-locker-2026-08-29.json @@ -0,0 +1,40 @@ +{ + "intent": "Put the screen-locker status back on the i3 bar, so a miscounting or disarmed locker is visible without being asked. The GTK tray icon it replaces had been commented out of ~/.config/i3/config since 2026-08-21, and in that window the locker counted one workout twice, believed the week met at 5/5 when the real total was 3, and skipped enforcement for two ISO weeks with nothing on the machine saying so.", + "scope": [ + "linux_configuration/i3blocks/screen_locker.sh (new block script)", + "linux_configuration/i3blocks/config (register the block before shutdown_countdown)", + "Non-goal: the block never mutates locker state; clicking only opens the read-only UI", + "Depends on screen-locker-web.service (127.0.0.1:8770) from the screen-locker repo" + ], + "changes": [ + "New block renders '/ ยท ' from GET /api/status, coloured by compliance_state (green ok / yellow warn / red lock).", + "Arming from GET /api/health overrides the compliance colour: a timer that is disabled or has no next trigger shows 'DISARMED' in red, because a locker that cannot fire is the worse failure and the one that hides.", + "A dead status server renders the last known value marked '(stale)' in orange, or 'status server down' in red when there is no cache โ€” never a blank block.", + "Missing curl/jq degrades to a visible marker rather than an empty block or a bash error." + ], + "verification": [ + { + "command": "~/.config/i3blocks/screen_locker.sh", + "result": "pass", + "evidence": "Prints '๐Ÿ’ช 3/5 ยท 6h' / '๐Ÿ’ช 3/5' / '#F1FA8C' โ€” 3/5 is the corrected weekly count, 6h the cut gaming budget for a day with no workout." + }, + { + "command": "systemctl --user stop screen-locker-web.service && ~/.config/i3blocks/screen_locker.sh", + "result": "pass", + "evidence": "With cache: '๐Ÿ’ช 3/5 ยท 6h (stale)' in orange. After rm of the cache: '๐Ÿ’ช status server down' in red. Restored to '๐Ÿ’ช 3/5 ยท 6h' once the unit was started again." + }, + { + "command": "i3-msg reload && scrot", + "result": "pass", + "evidence": "Block visible in the HDMI-0 bar between PC:warn and the 23:00 shutdown countdown, rendering '๐Ÿ’ช 3/5 ยท 6h' in yellow. i3blocks does not re-read its config on i3-msg reload alone; the running process had to exit first." + } + ], + "risks": [ + "Two curl calls every 30s. Both are loopback with --max-time 2, so a hung server delays the block by at most 4s rather than wedging the bar.", + "The block reports the gaming budget using its own 8h/6h constants. If steam-backlog-enforcer's configured values change, this display drifts from the enforcer until updated." + ], + "rollback": [ + "Remove the [screen_locker] stanza from linux_configuration/i3blocks/config, delete linux_configuration/i3blocks/screen_locker.sh and the ~/.config/i3blocks symlink, then i3-msg reload and kill the running i3blocks so it re-reads the config.", + "After rollback, confirm the bar still renders the remaining blocks (cpu, gpu, memory, disk, volume, ethernet, claude_usage, pc_startup, shutdown_countdown, time)." + ] +} diff --git a/linux_configuration/i3blocks/config b/linux_configuration/i3blocks/config index 86b822a1..b304855e 100644 --- a/linux_configuration/i3blocks/config +++ b/linux_configuration/i3blocks/config @@ -51,6 +51,10 @@ interval=30 color=#FFFFFF +[screen_locker] +command=~/.config/i3blocks/screen_locker.sh +interval=30 + [shutdown_countdown] command=~/.config/i3blocks/shutdown_countdown.sh interval=60 diff --git a/linux_configuration/i3blocks/screen_locker.sh b/linux_configuration/i3blocks/screen_locker.sh new file mode 100755 index 00000000..27d8f3af --- /dev/null +++ b/linux_configuration/i3blocks/screen_locker.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# ============================================================================ +# Screen-locker status for i3blocks. +# +# Replaces the GTK tray icon that sat commented out in ~/.config/i3/config from +# 2026-08-21, during which the locker miscounted the week and nothing on the +# machine said so. The point of this block is that a broken or disarmed locker +# is visible without being asked: it is never blank and never silently green. +# +# Reads the local status API (screen-locker-web.service). Click opens the UI. +# ============================================================================ + +set -euo pipefail + +readonly API='http://127.0.0.1:8770/api/status' +readonly HEALTH='http://127.0.0.1:8770/api/health' +readonly UI='http://127.0.0.1:8770/' +readonly CACHE="${XDG_RUNTIME_DIR:-/tmp}/i3blocks-screen-locker.cache" + +# 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 opens the web UI rather than doing anything destructive; there is no +# action here that could clear a lock. +if [[ ${BLOCK_BUTTON:-0} -ne 0 ]]; then + xdg-open "$UI" >/dev/null 2>&1 & +fi + +if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + emit '๐Ÿ’ช no curl/jq' '๐Ÿ’ช ?' "$ORANGE" + exit 0 +fi + +status_json=$(curl -fsS --max-time 2 "$API" 2>/dev/null || true) +if [[ -z $status_json ]]; then + # The server being down is itself news: it is what the gaming budget reads, + # and it failing closed costs two hours. Say so instead of showing nothing. + if [[ -r $CACHE ]]; then + emit "$(cat "$CACHE") (stale)" '๐Ÿ’ช ?' "$ORANGE" + else + emit '๐Ÿ’ช status server down' '๐Ÿ’ช ?' "$RED" + fi + exit 0 +fi + +count=$(jq -r '.snapshot.week.counted_count' <<<"$status_json") +minimum=$(jq -r '.snapshot.week.minimum' <<<"$status_json") +state=$(jq -r '.compliance_state' <<<"$status_json") +workout_today=$(jq -r '.gaming.workout_today' <<<"$status_json") + +if [[ $workout_today == 'true' ]]; then + hours='8h' +else + hours='6h' +fi + +health_json=$(curl -fsS --max-time 2 "$HEALTH" 2>/dev/null || true) +armed=$(jq -r '.armed' <<<"${health_json:-{\}}" 2>/dev/null || echo 'unknown') + +text="๐Ÿ’ช ${count}/${minimum} ยท ${hours}" +case $state in +lock) color=$RED ;; +warn) color=$YELLOW ;; +*) color=$GREEN ;; +esac + +# Arming beats compliance: a locker that cannot fire is the worse problem, and +# it is the one that hides. Never render it as a green tick. +if [[ $armed != 'true' ]]; then + text="$text โš  DISARMED" + color=$RED +fi + +printf '%s' "$text" >"$CACHE" +emit "$text" "๐Ÿ’ช ${count}/${minimum}" "$color"