From 856866e8b82b7048f2e94ff6efc312ad503e9dda Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 5 Oct 2026 14:27:43 +0200 Subject: [PATCH] =?UTF-8?q?i3blocks=20gaming=5Fbudget:=20list=20every=20ea?= =?UTF-8?q?rner=20from=20.rules.earners=20(anki=20=F0=9F=97=82),=20sub-hou?= =?UTF-8?q?r=20bonuses=20as=20minutes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- .../extract-digital-wellbeing-2026-08-23.json | 103 ------------------ ...ocks-gaming-budget-earners-2026-10-05.json | 36 ++++++ linux_configuration/i3blocks/gaming_budget.sh | 20 +++- .../tests/test_i3blocks_gaming_budget.sh | 15 +++ 4 files changed, 65 insertions(+), 109 deletions(-) delete mode 100644 docs/superpowers/evidence/extract-digital-wellbeing-2026-08-23.json create mode 100644 docs/superpowers/evidence/i3blocks-gaming-budget-earners-2026-10-05.json diff --git a/docs/superpowers/evidence/extract-digital-wellbeing-2026-08-23.json b/docs/superpowers/evidence/extract-digital-wellbeing-2026-08-23.json deleted file mode 100644 index 6778cf64..00000000 --- a/docs/superpowers/evidence/extract-digital-wellbeing-2026-08-23.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "intent": "Extract the last over-cap unit into github.com/kuhyx/digital-wellbeing (cloned at ~/src/digital-wellbeing), then delete --warn from the directory-depth-cap hook so the cap enforces for real. This is the closing step of the restructure campaign.", - "scope": [ - "REMOVED: linux_configuration/periodic_background/digital_wellbeing/ (80 tracked files)", - "REMOVED: 6 test suites + 2 test libs from linux_configuration/tests/", - "REWIRED: check_and_enable_services.sh, setup_periodic_system.sh, install_core_system.sh, fresh-install/main.sh, installers/install_ungoogled_chromium_ubuntu.sh, tests/test_security_hardening.sh, tests/lib/security_apps.sh, tests/lib/security_pacman.sh", - "CONFIG: .pre-commit-config.yaml (--warn removed, 2 stale excludes dropped), .github/workflows/shell-tests.yml, meta/shell-coverage-allowlist.txt, meta/pyproject.toml, .gitignore, meta/scripts/check_directory_depth.sh", - "LIVE: /usr/local/bin/periodic-system-maintenance.sh", - "NON-GOAL: the unwrapped discord/signal-desktop binaries (pre-existing) and the workout_locker error (different repo)" - ], - "changes": [ - "Filtered history across all three paths the unit has lived at (63 commits). The current path alone holds 2 -- the same trap as hosts-blocker, and the reason the count was checked before filtering rather than after.", - "The six suites that test this code lived in linux_configuration/tests/, not beside it. They were filtered separately (24 commits) and merged as a second root, then their hardcoded periodic_background/digital_wellbeing/ prefixes rewritten for the new root-level layout. A path filter of the unit alone would have left them in the monorepo testing code that had left it.", - "Vendored linux_configuration/lib/common.sh into vendor/ rather than trimming it: set_actual_user_vars calls get_actual_user and get_actual_user_home, so a 4-function trim breaks silently. It went to vendor/ and not lib/ because test_music_parallelism.sh copies the unit's lib/ wholesale into a scratch worktree while stubbing the shared lib -- with both in one directory the real common.sh overwrote the stub and the test measured the wrong library.", - "Generalised that test's layout-drift guard from a required ../ prefix to (../)*; the script now sits at the repo root so the prefix is empty. The guard caught this move, which is what it was written for.", - "Dropped the two .pre-commit-config.yaml excludes for setup_midnight_shutdown.sh instead of moving them: lsattr shows nothing in the unit is chattr +i any more, and the fixer hooks leave the file untouched when allowed to run.", - "Removed --warn from the directory-depth-cap hook and reworded its comment from 'deliberately --warn for now' to state that re-adding it re-opens the hole." - ], - "verification": [ - { - "command": "./meta/scripts/check_directory_depth.sh --all", - "result": "pass", - "evidence": "'all checked paths are within 2 directories', exit 0. 46 -> 41 (after hosts) -> 0. The cap now enforces without --warn." - }, - { - "command": "per-root commit counts before filtering", - "result": "pass", - "evidence": "current path 2, scripts/periodic_background/ 41, scripts/digital_wellbeing/ 23; 63 on main. The 94 an --all union reported included unreachable refs and one merge commit that filter-repo correctly prunes." - }, - { - "command": "cmp each of the 80 tracked files against the monorepo copy", - "result": "pass", - "evidence": "ALL 80 TRACKED FILES IDENTICAL. node_modules (340 files) and __pycache__ correctly excluded -- the repo has 80 tracked files, not the 423 on disk." - }, - { - "command": "~/src/digital-wellbeing/tests/run_all.sh", - "result": "pass", - "evidence": "All six regression suites plus the discovered lib/tests/run_all.sh pass: 'All digital-wellbeing suites passed.'" - }, - { - "command": "env -i PATH=/usr/bin:/bin HOME=/tmp bash ./tests/run_all.sh", - "result": "pass", - "evidence": "Passes under a minimal environment, and no suite needs root or invokes pacman -- so the new repo needs no arch-tests job even though five of these ran in the monorepo's Arch container." - }, - { - "command": "sudo check_and_enable_services.sh (first attempt)", - "result": "fail", - "evidence": "'line 61: DW_REPO: unbound variable' -- the definition sat below its first use. shellcheck and grep both passed the file; only running it caught this." - }, - { - "command": "sudo check_and_enable_services.sh (after moving the block)", - "result": "pass", - "evidence": "Runs to completion: pacman_wrapper ok, makepkg_wrapper ok, midnight_shutdown ok, startup_monitor ok, periodic_systems ok, hosts ok, guest_mode_removal ok. Applied 3 fix(es)." - }, - { - "command": "sudo sha256sum -c --status /var/lib/pacman-wrapper/source.sha256", - "result": "pass", - "evidence": "Exit 0. The manifest embeds ABSOLUTE paths and had named deleted scripts/ paths since the flatten, so the drift check reported a permanent false 'stale or tampered'. It now names ~/src/digital-wellbeing and verifies -- with unchanged hashes, confirming the extracted bytes are identical." - }, - { - "command": "report_live_state.sh --check | grep -c linux_configuration/scripts/", - "result": "pass", - "evidence": "0, down from the 8 pre-existing stale references at session start. Removals only; none added at any point." - }, - { - "command": "grep the six moved test names in .github/workflows/shell-tests.yml", - "result": "pass", - "evidence": "0 matches, and all four names still listed (test_i3blocks_persist_common, test_setup_dns_blocker, test_i3blocks_efficiency, test_security_hardening) resolve to files that exist." - }, - { - "command": "test_security_hardening.sh, current tree vs a detached worktree at HEAD", - "result": "pass", - "evidence": "18 passed / 1 failed / 5 skipped in BOTH. The one failure is 'Compulsive block wrappers installed' -- discord and signal-desktop lack their /usr/bin wrapper. Live-system state, identical before and after." - }, - { - "command": "shellcheck on all nine changed shell files", - "result": "pass", - "evidence": "ALL CLEAN, no severity filter." - }, - { - "command": "source vendor/common.sh in a subshell and inspect stderr (post-push audit)", - "result": "fail", - "evidence": "Three 'No such file or directory' errors: common.sh sources common_packages.sh, common_services.sh and common_datetime.sh unconditionally, and only common.sh had been vendored. Every suite still passed -- the four functions used are defined above that block, and a failed source of a sibling is non-fatal -- so nothing caught it. Fixed in digital-wellbeing c5eb028 by vendoring all four and adding tests/test_vendor_closure.sh, verified to fail when any sibling is removed." - }, - { - "command": "sudo grep -rl 'testsAndMisc/linux_configuration/(scripts|periodic_background)' /etc /usr/local /usr/bin", - "result": "pass", - "evidence": "Broader than report_live_state.sh, which greps only /usr/local/{bin,lib} plus units and symlinks. Three hits, none from this work: pw_guard.sh names a path that still exists, and /usr/local/bin/dwm-session carries two pre-existing single_use/ paths from the flatten. dwm is dormant (lightdm user-session=i3, no dwm process) and one of its targets no longer exists anywhere -- reported, not changed, since dwm was deliberately abandoned." - } - ], - "risks": [ - "vendor/common.sh duplicates 163 lines of the monorepo lib. The two copies may drift; that is accepted, and the README says so, so a future session does not 're-fix' it by re-coupling the repos.", - "The pacman wrapper's drift manifest is regenerated by the checker's own fix path. A machine that clones digital-wellbeing to a non-default location must re-run install_pacman_wrapper.sh or the manifest will name the wrong paths.", - "install_core_system.sh on a fresh machine now hard-stops at three modules until both extracted repos are cloned. Deliberate and loud; INSTALLER_FIX_TASK.md tracks fresh-machine installer defects.", - "The depth cap is now enforcing, so any future path more than two directories deep is a hard commit failure rather than a warning.", - "digital_wellbeing/data/ (2 untracked files) went with the rm -rf. Verified safe to lose: it was never tracked on any historical path, and no script in the unit -- blocker, seeders or libs -- references data/ at all." - ], - "rollback": [ - "git revert this commit to restore the unit, the six suites, the two test libs and every rewired caller, and to put --warn back on the depth hook.", - "Restore /var/tmp/periodic-system-maintenance.sh.pre-dw to /usr/local/bin/.", - "After rollback validate: check_directory_depth.sh --all reports 41 violations again, and sudo check_and_enable_services.sh still completes without an unbound-variable error." - ] -} diff --git a/docs/superpowers/evidence/i3blocks-gaming-budget-earners-2026-10-05.json b/docs/superpowers/evidence/i3blocks-gaming-budget-earners-2026-10-05.json new file mode 100644 index 00000000..484c788d --- /dev/null +++ b/docs/superpowers/evidence/i3blocks-gaming-budget-earners-2026-10-05.json @@ -0,0 +1,36 @@ +{ + "intent": "The i3 gaming-budget block lists every earned_time earner the enforcer reports (now including Anki), so a new earner needs only an icon here, and sub-hour bonuses read as minutes.", + "scope": [ + "linux_configuration/i3blocks/gaming_budget.sh", + "linux_configuration/tests/test_i3blocks_gaming_budget.sh", + "Non-goal: no change to how the budget is computed; the block still only reads steam-backlog-enforcer's /api/budget" + ], + "changes": [ + "Unearned bonuses come from .rules.earners ([{name, label, earned_seconds, bonus_seconds}], steam-backlog-enforcer 7ab65dd) with an icon map {workout ๐Ÿ’ช, leetcode ๐Ÿงฉ, reading ๐Ÿ“–, anki ๐Ÿ—‚}; an unmapped earner shows its label", + "A daemon without .rules.earners falls back to the old three fixed fields and the configured sizes", + "bonus_hm prints '30m' for sizes under an hour instead of '0h30'" + ], + "verification": [ + { + "command": "bash linux_configuration/i3blocks/gaming_budget.sh (live enforcer API)", + "result": "pass", + "evidence": "'๐ŸŽฎ 2h10 left ยท 4h53/7h03 +๐Ÿ“–1h +๐Ÿ—‚30m' / '๐ŸŽฎ 2h10' / #50FA7B; /api/budget lists workout and leetcode earned, reading 0/3600, anki 0/1800" + }, + { + "command": "user looked at the bar", + "result": "pass", + "evidence": "kuhy confirmed the '+๐Ÿ—‚30m' bar 'looks ok' (2026-10-05)" + }, + { + "command": "bash linux_configuration/tests/test_i3blocks_gaming_budget.sh", + "result": "pass", + "evidence": "All gaming_budget checks passed, incl. the new .rules.earners case: registry order, earned omitted, daemon sizes win over config, label for an unmapped earner, 30m and 1h30 sizes; old-daemon fallback cases unchanged" + } + ], + "risks": [ + "An earner whose name is missing from the icon map shows its text label, which is wider in the bar." + ], + "rollback": [ + "git revert this commit; the block goes back to the three fixed bonuses and Anki disappears from the bar" + ] +} diff --git a/linux_configuration/i3blocks/gaming_budget.sh b/linux_configuration/i3blocks/gaming_budget.sh index bf1fa203..c492247c 100755 --- a/linux_configuration/i3blocks/gaming_budget.sh +++ b/linux_configuration/i3blocks/gaming_budget.sh @@ -2,7 +2,7 @@ # ============================================================================ # Gaming time budget for i3blocks: time left today, used/total, and which -# bonuses (workout, LeetCode, reading) are still there to be earned. +# bonuses (workout, LeetCode, reading, Anki, ...) are still there to be earned. # # Reads steam-backlog-enforcer's /api/budget, which already folds in the # screen-locker, leetcode-guard and book-guard bonuses. Like screen_locker.sh @@ -61,7 +61,7 @@ rendered=$(jq -r \ --arg red "$RED" --arg orange "$ORANGE" --arg yellow "$YELLOW" --arg green "$GREEN" ' def hm: (. / 60 | round) as $m | "\($m / 60 | floor)h\($m % 60 | tostring | if length < 2 then "0" + . else . end)"; - def bonus_hm: hm | sub("h00$"; "h"); + def bonus_hm: if . < 3600 then "\(. / 60 | round)m" else hm | sub("h00$"; "h") end; ($cfg[0] // {}) as $c | if .state_status != "ok" then ["nocache", "๐ŸŽฎ state \(.state_status)", "๐ŸŽฎ !", $red] elif .ok != true then ["nocache", "๐ŸŽฎ budget error", "๐ŸŽฎ !", $red] @@ -70,10 +70,18 @@ rendered=$(jq -r \ | .rules.bonuses as $b | ([$t.seconds_remaining, 0] | max) as $left | "\($t.seconds_used | hm)/\($t.budget_seconds | hm)" as $ratio - # Unearned bonuses: [earned, configured size (enforcer default), icon]. - | [ [$b.workout, ($c.workout_bonus_seconds // 7200), "๐Ÿ’ช"], - [$b.leetcode, ($c.leetcode_bonus_seconds // 3600), "๐Ÿงฉ"], - [$b.reading, ($c.reading_bonus_seconds // 3600), "๐Ÿ“–"] ] + # Unearned bonuses: [earned, size, icon]. A daemon that lists the + # registry (.rules.earners) is the source of truth, so a new earner + # needs only an icon here; an older daemon falls back to the three + # fixed fields and the configured sizes. + | {workout: "๐Ÿ’ช", leetcode: "๐Ÿงฉ", reading: "๐Ÿ“–", anki: "๐Ÿ—‚"} as $icon + | (if .rules.earners then + [.rules.earners[] | [.earned_seconds, .bonus_seconds, ($icon[.name] // .label)]] + else + [ [$b.workout, ($c.workout_bonus_seconds // 7200), "๐Ÿ’ช"], + [$b.leetcode, ($c.leetcode_bonus_seconds // 3600), "๐Ÿงฉ"], + [$b.reading, ($c.reading_bonus_seconds // 3600), "๐Ÿ“–"] ] + end) | map(select((.[0] // 0) == 0 and .[1] > 0) | "+\(.[2])\(.[1] | bonus_hm)") | (if length > 0 then " " + join(" ") else "" end) as $todo | if $t.blocked or $left <= 0 then diff --git a/linux_configuration/tests/test_i3blocks_gaming_budget.sh b/linux_configuration/tests/test_i3blocks_gaming_budget.sh index ae3b9512..3b72e704 100755 --- a/linux_configuration/tests/test_i3blocks_gaming_budget.sh +++ b/linux_configuration/tests/test_i3blocks_gaming_budget.sh @@ -90,6 +90,21 @@ write_budget 0 18000 18000 false 0 0 0 assert_eq '๐ŸŽฎ 5h00 left ยท 0h00/5h00 +๐Ÿ’ช1h30 +๐Ÿ“–1h' "$(line 1)" 'configured sizes, zero-size bonus omitted' rm -f "$CONFIG_FILE" +printf 'Checking a daemon that lists the registry (.rules.earners)...\n' +# earner NAME LABEL EARNED SIZE -> one JSON object for .rules.earners. +earner() { + jq -nc --arg n "$1" --arg l "$2" --argjson e "$3" --argjson s "$4" \ + '{name: $n, label: $l, earned_seconds: $e, bonus_seconds: $s}' +} +jq -n --argjson earners "[$(earner workout workout 7200 7200),$(earner leetcode LeetCode 0 3600),$(earner reading reading 0 3600),$(earner anki Anki 0 1800),$(earner piano Piano 0 5400)]" \ + '{ok: true, state_status: "ok", + today: {seconds_used: 0, budget_seconds: 14400, seconds_remaining: 14400, blocked: false}, + rules: {bonuses: {}, earners: $earners}}' >"$FIXTURE" +printf '{"leetcode_bonus_seconds":0}\n' >"$CONFIG_FILE" +assert_eq '๐ŸŽฎ 4h00 left ยท 0h00/4h00 +๐Ÿงฉ1h +๐Ÿ“–1h +๐Ÿ—‚30m +Piano1h30' "$(line 1)" \ + 'registry order, earned omitted, sizes from the daemon not the config, label for an unmapped earner' +rm -f "$CONFIG_FILE" + printf 'Checking a dead server shows the cached value as stale...\n' write_budget 21600 28800 7200 false 7200 3600 0 run_block >/dev/null