mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-13 21:50:47 +01:00
b27790596b
65 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| d72f03ea93 |
setup_midnight_shutdown: split into lib/ — the cap is now zero
1734 -> 109 lines across eight libs plus one payload file. This was the last file over the 250-line cap; check_file_length.sh --all now reports "all checked files are within 250 lines". This file needed a different proof from the others. Its product is scripts generated into /usr/local/bin via heredocs, so "did the functions move verbatim" is not the question that matters — "does it still emit the same bytes" is. Every heredoc body was hashed before the split: 11 of 12 are byte-identical afterwards, and the twelfth (the 224-line config-unlock body) now lives in lib/payloads/shutdown-config-unlock.sh.in at hash 1a5c67cfd0244101, exactly matching what it replaced. No new bodies appeared. The "~39 top-level functions" this file is documented as having are not real. log(), now_epoch(), cmd_add() and friends sit at column 0 because they are inside the generated scripts. The brace-matching extractor keeps each heredoc with its owning generator, so they moved as part of it. That is also why verify_shell_split.sh reports a bare "-log": it cannot tell a heredoc-internal definition from a top-level one. log() is not lost — it is line 17 of the payload, which is where it always ran from. $_MS_PAYLOAD_DIR is defined in the same file as its use, directly above it, following the _NL_PAYLOAD_DIR lesson from earlier in this campaign. --help is byte-identical, shellcheck is clean with no suppression added. `setup` itself is not executed — it installs systemd units, makes the config immutable and arms a shutdown timer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV |
|||
| fbe9134c22 |
pacman_wrapper: split into flat libs under the 250-line cap
929 -> 217 lines across five pw_*.sh libraries. This file IS /usr/bin/pacman here (the symlink points at the deployed copy), so the design constraint was not readability but that no failure mode may leave the machine unable to install packages. Fail OPEN, unlike the other splits in this campaign. A missing lib warns on stderr and execs real pacman unwrapped. Failing closed would mean a missing lib leaves no way to install anything, including the missing lib. This is the same choice the existing pacman_lock_lib.sh block already makes, and it is verified rather than asserted: with pw_guard.sh removed from a scratch-dir copy, the wrapper warns, execs, and returns real `-Qi bash` output at rc=0. The libs load at the very top, before the fast path, because the fast path calls needs_unlock() to decide whether to exec straight through, and that function now lives in pw_guard.sh. They are deployed flat beside the wrapper — the shape pacman_lock_lib.sh and heavy_job_lock.sh already use in /usr/local/bin — as required copies, and are added to the integrity manifest. Tamper detection was round-tripped in a fake root: 8 manifest entries verify clean, and appending one line to pw_challenge.sh is detected and the transaction denied. Verified by execution, not just textually: real `-Qi bash` and `-Q` through the split wrapper are byte-identical to the pre-split baseline, same 2824 packages. verify_shell_split.sh reports all 14 top-level functions moved verbatim. The deployed copy is deliberately NOT updated by this commit and still hashes 548fc2b5... exactly as before the work started; live pacman is unaffected. A live `pacman -S` remains unverified — the Bash tool deadlocks on db.lck and a failed experiment costs the package manager. Cap is now down to one file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV |
|||
| 79a49846b9 |
setup_night_lockdown: define _NL_PAYLOAD_DIR where it is used
The split referenced $_NL_PAYLOAD_DIR from both install functions but
defined it nowhere, so `setup_night_lockdown.sh setup` would have died
with an unbound variable partway through installing the lockdown. My
earlier scratch verification set the variable by hand, which is exactly
what hid the bug: the emitted artifacts hashed correctly while the real
entry path could not have reached them.
It is now defined in each lib that uses it, eight lines above the use,
via `: "${_NL_PAYLOAD_DIR:=...}"` so an explicit override still wins. A
definition placed in the entry script was silently stripped by an
autoformat pass three times during this work, and a half-run lockdown
installer is not an acceptable failure mode.
Also drops two zero-byte payload files left behind by the .sh -> .sh.in
rename.
Re-verified without presetting anything: install_enter_script and
install_unlock_script emit mode-0755 files hashing 4baaf405... and
eafce28d..., byte-identical to the live /usr/local/bin copies. help and
an unknown command match their baselines; status differs only by the
05:00 countdown ticking 13h to 12h between runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| f6bfdff76a |
setup_night_lockdown: split into lib/ under the 250-line cap
918 -> 138 lines, across five libs plus two data-file payloads. The two generated scripts became data files rather than heredocs. ENTER_EOF alone was 279 lines, so any lib containing it would have been over the cap however the rest was arranged. Both heredocs were quoted, so their bodies were already literal text with no build-time interpolation, and copying them verbatim is exactly equivalent. The payloads are named *.sh.in on purpose. The pre-commit hook globs staged files by '\.(sh|bash|zsh)$', runs shfmt -w on them and re-stages the result -- and shfmt does reformat these bodies. That would have changed their bytes and destroyed the property this split is verified by. The .in extension keeps them out of every .sh-globbing hook without a suppression or an exclude regex. cat + chmod 0755 became a single `install -m 0755`, closing the window the original left between creating the file and making it executable. The load-bearing check: running install_enter_script and install_unlock_script against a scratch directory produces mode-0755 files whose sha256 sums are identical to the copies already deployed in /usr/local/bin on this machine. The split provably emits the exact bytes that are running today, and it touched no system file -- sha256sum -c on both deployed artifacts passes unchanged. help, status and an unknown command are byte-identical to their pre-split baselines. A line-multiset check leaves exactly six lines unaccounted for: the two cat/heredoc openers, the two terminators and the two chmod calls that install -m now performs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 72e1b6a8b1 |
block_compulsive_opening: test the allow/block decision
21 assertions for cco_state.sh, the lib that decides whether a launch is
permitted. Coverage measured: 62/66 lines = 93.94%. The only uncovered lines
are kill_app's pkill/pkill -9 pair, which must never run in a test on a
machine whose apps are live. cco_wrapper/cco_install/cco_report stay at 0%:
every function in them moves system binaries, writes /etc/pacman.d/hooks or
kills processes.
The assertion that matters most is the one in the negative direction: a
PREVIOUS hour's stamp must NOT keep blocking. If that decision inverted, a
legitimately-closed app could never be reopened, and the failure would look
like the tool working. Both directions are pinned, plus the empty-stamp case
and stale suspend-marker cleanup.
Also generalises run_all.sh from test_leechblock_*.sh to test_*.sh. This
directory now holds suites for more than one entry script, and a prefix glob
silently skips every suite added after it — including inside the CI discovery
step added in
|
|||
| f5edebae4a |
block_compulsive_opening: split into lib/ under the 250-line cap
705 -> 182 lines, with state, the launch gate, install/uninstall and reporting moved into cco_state.sh, cco_wrapper.sh, cco_install.sh and cco_report.sh. The entry script keeps its exact deployed path and stays a real file there, not a symlink: the app wrappers already installed on disk hard-code /usr/local/bin/block-compulsive-opening.sh, and the pacman hook invokes it. _CCO_LIB_DIR probes for lib/ beside the script and falls back to the script's own directory, so the repo tree and the flat /usr/local/bin deploy layout both work; both branches were exercised. install_all deploys the libs and uninstall_all removes them in this same commit, so there is never a deployed copy that is split but missing its libs -- the failure that had to be repaired for install_leechblock. The libs deploy flat into /usr/local/bin rather than into /usr/local/share/digital_wellbeing/lib, because install_leechblock_payload does an rm -rf mirror of that directory and would delete them. Verified beyond the usual gates: a line-multiset check reports zero missing functional lines, and all three heredoc bodies -- the generated app wrapper, the generated pacman hook and the help text -- are byte-identical, which verify_shell_split.sh cannot check. status output matches the still-monolithic deployed copy byte for byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| baeb1119d0 |
install_leechblock: deploy lib/ alongside the entry script
The pacman-wrapper installer copied install_leechblock.sh to
/usr/local/share/digital_wellbeing/ along with three data files, but no
lib/. After the split in
|
|||
| 63454fa544 |
install_leechblock: first test suite for the split libs
29 assertions across two files, run via lib/tests/run_all.sh. Coverage measured, not waived: leechblock_fetch.sh 38.57% (27/70), leechblock_browsers.sh 36.47% (31/85), config and firefox 0%. The uncovered lines are exactly the bodies that must never run under test — curl/tar/unzip downloads, pkill + sudo binary patching, npm install, and writes to /etc/*/policies.json. Closing the gap needs those externals shimmed. What the tests actually pin down: - all three get_latest_tag fallbacks (releases API, tags API, Location header), plus JSON-null-is-not-a-tag and the all-sources-exhausted non-zero return. A silent failure there installs the wrong version. - the 8+3 browser tables including every hyphenated key, and the command -v gating that decides whether a binary is patched at all. Also fixes a latent set -u abort: wire_up_browsers interpolates VERSION_DIR, VERSION and TAG, which only resolve_version sets. Calling it on its own aborted on an unbound VERSION_DIR; they are now read through locals with defaults, leaving production output unchanged. Harness note: PATH must be narrowed to the shim dir for "no browser installed" to be testable on a machine that has five, but every external the code under test calls has to be seeded there too — a missing mkdir aborted the suite under set -e with no stderr, which under kcov looked like a coverage-tool bug rather than a PATH bug. The hyphenated array subscripts in the test are quoted for the same reason they are in the lib: shfmt -w rewrites an unquoted [google-chrome-stable] into a subtraction expression. Reproduced live in this file during the work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV |
|||
| 1dceca6600 |
install_leechblock: split into lib/ under the 250-line cap
485 -> 114 lines, with the six install phases moved into four sourced
libs: leechblock_fetch.sh, leechblock_config.sh, leechblock_browsers.sh
and leechblock_firefox.sh.
detect_browsers, replace_browser_in_place and wire_up_browsers stay in
one lib on purpose: detect_browsers writes the BROWSERS/FIREFOXES arrays
that wire_up_browsers reads, and splitting them would create a
write-only cross-file global.
Four justified body changes, all recorded in the evidence:
- Quoted the six hyphenated associative-array keys. shfmt 3.13.1 parses
an unquoted [google-chrome-stable] as arithmetic and rewrites it to
[google - chrome - stable], which would break browser detection
entirely. The pre-commit hook runs shfmt -w on staged files and
re-stages them, so this corruption would have landed in the commit
unseen -- this is what caused the previous attempt at this split to be
reverted. Behaviour-preserving: quoted and unquoted are the same key.
- SCRIPT_DIR is assigned in the entry script rather than inside
inject_default_config. Left in the lib, ${BASH_SOURCE[0]} resolves to
lib/, so the defaults JSON lookup would miss and default blocking
rules would silently never be seeded.
- install_firefox_policy takes ff_found as a parameter instead of
reading it as a cross-file global, fixing a genuine SC2154 without a
suppression.
- Added require_cmd unzip. It was used bare under set -Eeuo pipefail, so
a missing unzip would abort after the version dir was populated but
before the 'current' symlink was made, leaving a half-installed tree.
verify_shell_split.sh reports additions only -- no -/+ pair -- so
get_latest_tag and replace_browser_in_place moved byte-identically.
--help and unrecognized-flag output are byte-identical to the baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 47478791d8 |
hosts/install: split into lib/ under the 250-line cap
913 lines down to 150. The ten functions were only 202 lines; the other 712 were a single top-level block, now eight phase functions across six libs. As in the previous split the bodies were wrapped WITHOUT re-indenting, because five heredocs use quoted terminators that must stay at column 0 (shfmt indented them afterwards, which is safe). The ~320-line custom-blocklist heredoc became custom_entries.hosts. It was 321 of the 370 lines of hosts_write.sh and cannot be split, so that lib could not otherwise fit the cap; a hosts-format data file is also far easier to audit and diff than a quoted heredoc. That move broke a guard, which is the important part of this commit: extract_custom_entries_from_script sliced the blocklist out of the script between "# Custom blocking entries" and the heredoc terminator. With the entries in a data file there is no terminator, so it would have extracted NOTHING -- and check_custom_entries_protection, the guard whose entire job is refusing to drop blocked domains, would have compared against an empty list and passed everything. It now reads the data file, and the extracted set is identical: 254 domains, diffed. A second, pre-existing bug surfaced while testing: extract_unblock_entries_from_script matched its OWN marker lines. PROTECTED_UNBLOCK_LIST_START and _END are word characters, so the '^# [a-zA-Z0-9._-]+$' pattern pulled both into the whitelist as if they were domains. Both are in this machine's live state file right now. Renaming either marker would have read as the whitelist growing and blocked the install outright. There is no dry-run or status mode, so this file cannot be proven by running it: an aborted run leaves /etc/hosts mutable with the guard's bind mount down. The proof is textual instead. Every one of the 8 wrapped phase bodies is byte-identical to its original line range; a line-multiset check found 0 lines lost; and an indentation-insensitive comparison against the pre-split file leaves exactly 8 absent lines, each an intentional edit (the rewritten extractor, three `readlink -f "$0"` made overridable, the /etc/hosts fallback made overridable, and the heredoc markers). /etc/hosts is byte-unchanged across the whole session, and `--help` output is identical. 36 assertions. Both anti-tamper guards at 100% line coverage (75/75 and 49/49), covering both directions of each: removing a blocked domain is refused and adding one allowed; growing the whitelist is refused and shrinking it allowed. A same-count swap is refused too, which a count-only check would have let through. The install phases themselves are deliberately untested -- running them would take down the protection they install. That is weaker evidence than the two previous splits had, and it is inherent to this file rather than a gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV |
|||
| 08f3c764b2 |
check_and_enable_services: a warning must never mask an error
Eight checks assigned status="warning" unconditionally, so a check running
later in the same function overwrote the status="error" an earlier one had
set. The five other sites in this family already guard the assignment with
`if [[ $status != "error" ]]`, so this was an inconsistency, not a design.
Only half of it was load-bearing, and the two halves fail differently:
services_units.sh (4 sites) reports through report_and_fix, which repairs
ONLY on "error". A downgrade there meant no repair ran at all. The is-active
check is the LAST status assignment in each of the three functions, so it
could mask any error above it -- in check_periodic_systems, three separate
is-enabled checks. A timer that was both disabled and inactive, which is the
ordinary shape of a broken timer, was reported and then left broken.
services_hosts.sh (4 sites) gates its own repair on `status != "ok"`, so the
repair still ran. There the bug printed a real error as a yellow [!] warning
and understated the summary row.
Demonstrated rather than argued, against a simulated broken machine (timer
disabled AND inactive, unit files present):
without the guards: status=warning, repair ran 0 times
with the guards: status=error, repair ran 1 time
The seven assertions that previously pinned the broken behaviour now assert
the correct behaviour; they were rewritten first and failed, then the guards
made them pass. Two further cases cover the masking shape directly: a broken
guard-lib instance must still be repaired when a later check warns.
`--status --dry-run` output is still byte-identical to rev
|
|||
| 0a69d18390 |
check_and_enable_services: 100% coverage via a redirectable path prefix
Follow-up to |
|||
| 9085a4e508 |
check_and_enable_services: split into lib/ under the 250-line cap
1225 lines down to 202, with the 21 functions moved into eight sourced libraries grouped by subject. The entry script now holds only the configuration, the argument parsing and the order the checks run in. check_hosts was the one function that could not move verbatim: at 260 lines it would have stayed over the cap on its own, so its repair half is now hosts_fix_nsswitch / hosts_fix_resolved / hosts_repair_all in services_hosts_fix.sh. Repair order is preserved (resolver config, then the hosts file, then the guard-lib migration), and hosts_repair_all returns non-zero under --dry-run so a dry run can no longer promote a status to "ok" on the strength of repairs that never happened. SERVICE_STATUS was a write-only global across five libs, which is a real SC2034 rather than noise: the pre-commit hook runs shellcheck with no -x and genuinely cannot see the one reader. It now goes through set_service_status / get_service_status, which also rejects a duplicate write — each check owns exactly one summary row, so a second write means two checks are fighting over it. No suppression was added anywhere. Proof the behaviour is unchanged: `--status --dry-run` run as root against this machine is byte-identical to the same command from a worktree at the pre-split revision, modulo the Date: line. verify_shell_split.sh reports check_hosts as the single expected difference; the other 20 functions hash identically. 72 assertions across three test files pass, with 100% line coverage on services_common.sh (54/54) and services_report.sh (27/27). services_units.sh measures 84.51% (60/71). The 11 uncovered lines are the else-branches of [[ -f /etc/systemd/system/*.service ]] and [[ -f /usr/local/bin/*.sh ]] probes — absolute root-owned paths that exist on this machine and that no PATH shim can redirect. Reaching them needs a root-prefix rewrite of live enforcement logic, which is not a line-count fix and would forfeit the verbatim-move proof for everything else. Found while testing, deliberately NOT fixed here: in all three unit checks the is-active branch assigns status="warning" unconditionally, overwriting the status="error" the is-enabled branch set just above it. A timer that is both disabled and inactive therefore never gets repaired, because report_and_fix only repairs on "error". It is identical in the pre-split file, so this split neither caused nor hides it. The tests pin the current behaviour and name it; fixing it changes enforcement and earns its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV |
|||
| 327d48badd |
Archive legacy hosts-guard subsystem: superseded by guard-lib
setup_hosts_guard.sh was NEXT_SESSION_PROMPT.md's next split target, but
live-machine inspection showed it (and 16 supporting files - enforce-*.sh,
6 systemd units, install_pacman_hooks.sh, pacman-hooks/, psychological/,
both READMEs) is fully retired in favor of guard-lib, migrated 2026-07-04.
User confirmed archive over split, then confirmed the larger 17-file scope
once the directory was surveyed. Full history preserved via git filter-repo,
live at github.com/kuhyx/testsAndMisc-archive/tree/main/linux_configuration/scripts/periodic_background/hosts/guard.
plugins/{nsswitch,resolved}-plugin.sh stay - they're guard-lib's live
plugins, referenced by absolute path from /etc/guard-lib/targets/*.conf.
check_and_enable_services.sh's check_hosts() rewritten to check guard-lib
state (guardctl file-guard status) instead of the retired units/scripts it
used to check and self-repair - verified live, now reports the hosts
service ok against the actual migrated machine instead of a false error.
copilot-instructions.md and one install.sh comment updated to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
|
|||
| 76e73830f6 |
shell: split clean_audio, enforce_vbox_hosts and migrate_hosts_guard
clean_audio.sh 419 -> 230 + 204
enforce_vbox_hosts.sh 443 -> 220 + 236
migrate_hosts_guard.sh 444 -> 167 + 162 + 141
migrate_hosts_guard needed two libraries: with all ten work functions moved
the single lib was 295 lines, so migrate and rollback were separated.
The extractor's brace matcher required index > start and so silently skipped
one-line `warn() { ...; }` definitions. Fixed, and regression-checked against
the committed fresh-install/main.sh split byte-for-byte.
The harness earned its keep on a fourth file: adding `export` to libre_translate
to satisfy SC2034 changed two function bodies, and verify_shell_split.sh caught
what lint had accepted. Those exports were reverted, and libre_translate.sh is
parked - what remains after its clean seams both assigns and reads the same
globals, so getting it under the cap needs parse_args restructured rather than
relocated, which is a refactor rather than a verbatim move.
That cost is now written down: a lib must contain the readers of every global
it assigns. The precedent at install_pacman_wrapper.sh:29 is about
definitions-only libs, not about assignments as such, and the pre-commit hook
runs shellcheck with no -x, so a source= directive will not make it follow the
link. This is the shell analogue of the monkeypatch-target trap.
Verified: all 8, 13 and 19 functions verbatim; all four libraries source with
no side effects; shellcheck passes standalone on every file with no
suppression; bash -n passes on all seven. None of the three was executed -
they drive ffmpeg over real media, mutate VirtualBox guest disks, and rewrite
systemd units.
Over-cap files 29 -> 26.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
|
|||
| 57b9005a6b |
usage_report: delete the dead _compute_window shim
Documented as "kept for backwards import compatibility", it had zero production callers - only its own two tests. Removed the function, those two tests, and the pathlib.Path import that only its signature used. _SEC_PER_DAY stays: two live callers remain at _usage_report_run.py:135 and :181, so only the function was dead, not the constant. Verified: usage_report.py --help exits 0 (the unit tests stub subprocesses, so only a real run proves the import graph), 202 tests pass, and _usage_report_run.py holds 100% line+branch coverage after removing both the code and the tests that covered it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| ff1a1b6a0e |
usage_report: split usage_report.py 426 -> 101 + 195 + 117 + 68
The entry point now holds only the parser, mode dispatch and main. Three modules take one concern each: _usage_report_logs (where the logs are, the state file, per-day segment planning), _usage_report_emit (preflight, stdout, clipboard), _usage_report_run (the two run paths and the aggregation they drive). Verified by the 100%-coverage tests from the previous commit. An AST comparison of all 22 top-level defs and classes reports 22 identical, 0 mismatches. All four modules hold 100% line+branch. 204 tests before and after. Both CLI modes were run against the machine's real logs, which is the check the unit tests cannot make: they stub the collaborators, so only a real run proves the new import graph resolves. Two lessons, recorded because the next target is a 498-line file: This cut was too big. Two names had to be relocated after their first placement created an import cycle, and four modules' worth of monkeypatch targets needed re-pointing. A smaller first slice would have been cheaper. Patching a moved function must target the module whose namespace resolves it - the caller's, not the one it was moved to. Getting that wrong on _run_since made main() call the real function, which read the actual atop logs and hung the suite rather than failing. Three import bugs (Path and _fmt_h left under TYPE_CHECKING but used at runtime, dataclass missing where _Aggregates landed) were caught immediately by the coverage from the previous commit. Over-cap 38 -> 37. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| b5dbe1e67a |
usage_report: split _usage_report_parsing.py 425 -> 243 + 206
Lifts the atop subprocess machinery into _usage_report_atop.py: running atop, streaming its parseable output, building/caching the atop_agg C helper, and reading its TSV. What stays is record parsing, epoch windows and per-day merging. Verified by the 100%-coverage tests from the previous commit. An AST comparison of all 16 functions against HEAD reports 16 identical, 0 mismatches. _usage_report_render.py imported _run from the parsing module; it now imports from the module that owns it. That is the same re-export barrel the last split shipped and had to fix in a follow-up - caught in the same commit this time. usage_report.py needed no edit at all. Both modules hold 100% line+branch (94/28 and 130/34, 0 missed). 179 tests before and after; the five new test files collect 8+6+4+14+5 = 37, which is last commit's 36 plus the one re-homed out of test_usage_report_modes.py, where it referenced a constant that had moved out from under it. The two test files added last commit were themselves over cap (373, 272) so they are split five ways here - the DropZone lesson from earlier today, where a lint fix silently added an over-cap file. Ran the program: a real report from today's logs, through the native atop_agg path, which is precisely the code this commit moved. Over-cap 39 -> 38. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| efb471fc8b |
usage_report: import _fmt_h from its owner, not through the render module
The previous commit's message claimed "no re-export barrel". That was
wrong: usage_report.py still did
from _usage_report_render import _fmt_h, _render_report
and after the split _fmt_h lives in _usage_report_format - the render
module only re-imports it for _methodology_section. So the import reached
the owner through an intermediary, which is the exact pattern the brief
says was vetoed last session. Now each name comes from its own module.
Also untracks bucket_catch/packages/frontend/tsconfig.tsbuildinfo and
gitignores *.tsbuildinfo. It is a generated incremental-build cache
tracked in a repo that blocks binaries, and it has been dirty in every
commit this session, forcing pre-commit to stash each time.
143 tests pass; usage_report.py still generates a real report.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
|
|||
| ad646541ad |
usage_report: split _usage_report_render.py 310 -> 181 + 123 + 24
Verified by the 100%-coverage tests from the previous commit, written before this seam existed. _cpu_table/_dedupe_ram/_ram_table/_gpu_table move to _usage_report_tables.py with the two constants they own. _md_escape and _fmt_h move to _usage_report_format.py: both the tables module and the render module need them, so a third module is what avoids a cycle. Import sites name the owning module; no re-export barrel. usage_report.py needed no edit - it imports _fmt_h and _render_report from the render module, and both names still resolve there. Evidence the move was verbatim: an AST comparison of all 10 functions against HEAD reports 10 identical, 0 mismatches. That matters because I retyped rather than cut-and-pasted, and did briefly introduce a literal × where the original had × - ruff's RUF001 caught it. Coverage stays 100% line+branch on all three (15/55/54 stmts, 4/10/14 branches, 0 missed). Test count 43 before and after, summed across the five files it now lives in - the check that matters for a test split, since a file outside the runner's glob is silently never collected. Directory total 143 passed, unchanged. Ran the real program: a full report from today's atop + pmon logs, every section rendering through the new boundaries including the RAM sibling dedupe and its (+20 more) truncation. Over-cap 41 -> 39. No suppressions; a shared test-helper module was tried and abandoned because name-tests-test would have needed an exclusion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 5dce82f2c7 |
Remove the dead Thorium/Fitatu autostart instead of splitting it
setup_thorium_startup.sh (443 lines) was queued for a split under the 250-line cap. It turned out the feature it installs has been broken since Thorium was removed in favour of chromium/librewolf. /usr/local/bin/thorium-browser is now only a leftover symlink to browser-preexec-wrapper, which routes by basename to /usr/bin/<name>. With no /usr/bin/thorium-browser it falls into its testing branch and treats the URL as the binary, so every boot logged: thorium-browser: line 33: /https://www.fitatu.com/app/planner: No such file while systemd reported status=0/SUCCESS and "Finished". The success status is why this went unnoticed. Splitting a 443-line script to keep a dead feature under a line cap is the wrong work, so it is deleted instead, along with its checker. Removing check_thorium_startup() from check_and_enable_services.sh is the part that makes this stick: its report_and_fix RE-RAN the installer whenever the service was not enabled, so deleting only the deployed units would have let them come back. Verified afterwards that the hourly maintenance timer runs a deployed script with no thorium reference, and that nothing on a timer invokes the checker at all. Also removed from fresh-install/main.sh so new machines skip it. Units disabled and all four deployed files removed. The generic browser-candidate lists that merely mention thorium are left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 4eedc17b17 |
Split install_pacman_wrapper.sh into an entry plus four libs
The 316-line installer was 66 over the 250-line cap. The managed-copy helpers, the /usr/local/bin install step, the integrity + drift manifest, and the immutability/LeechBlock/VirtualBox payloads move into a sibling lib/, leaving a 128-line entry. What stays in the entry is chosen by what the harness can execute, not by taste. `--bind-abs /usr/bin` removes the directory bwrap needs to exec anything, so the trace stops at `ln -sf ... /usr/bin/pacman` with exit 1 on EPERM. That line onwards, plus the `if [ ! -L /usr/bin/pacman ]` backup branch whose guard is false on this machine, therefore never run under trace — moving them would have shipped code no diff could check. They stay put. Everything else is proven: the before/after traces are identical, including all 16 generated-file hashes. pacman_wrapper at 548fc2b53ee4cea6 is the one that matters most — it is post-`sed -i`, so it catches a reorder of the rewrite relative to the copy. The path definitions stay in the entry too, for a linter reason rather than a runtime one: a definitions-only lib assigns without referencing, which is SC2034 on 36 variables, and suppressions are not allowed. Libs that only reference globals are fine. `export` would have silenced it while leaking 40 variables into every child process, which the trace cannot see. Eight assertions across two test libs grep the installer's source text; five needed widening to search entry+lib/. TEST 9's `chattr +i` grep caught the move as a genuine failure first, which is what prompted enumerating the rest. No assertion was weakened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 26965bad32 |
Split install_usage_monitoring.sh into an entry plus five libs
The 290-line installer was 40 over the 250-line cap. Its five banner-marked steps move into a sibling lib/, leaving a 53-line entry that owns log()/die(), the globals that cross between steps, and the call order. Proven by execution, not inspection: meta/scripts/trace_shell_split.sh --prefix was captured at HEAD in place (tracing from a detached worktree changes REPO_DIR, which is interpolated into a generated file, and fakes a diff), then re-run after the split. The traces are identical -- same exit status, same ordered stubbed calls, and all five generated-file hashes unchanged, including usage-report-catchup.sh at sha=9f4803ab157c5527. REPO_DIR is computed once in the entry and read as a global rather than recomputed inside a lib one directory deeper; recomputing it would have pointed the generated catch-up script at the wrong repo while still exiting 0. test_usage_monitoring_installer_efficiency.sh greps the installer's source text for the pmon heredoc, so it follows that heredoc to lib/nvidia_pmon.sh. Its extraction anchor also had to become whitespace-insensitive: the pre-commit hook runs `shfmt -w` on staged shell files and rewrites `cat > "x" << 'SCRIPT'` to `cat >"x" <<'SCRIPT'`, which silently broke extraction. All five property assertions are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| d889c8e655 |
Split music_parallelism.sh into an entry script and three libs
412 lines -> 135, with the nine functions in lib/music_detect.sh (133), lib/music_monitor.sh (91) and lib/music_status.sh (85). find_music_services and kill_music_services stay together: the first writes MUSIC_FOUND_PROCESS/MUSIC_FOUND_WINDOW and the second reads them, so splitting between them would thread mutable state across files. The test suite caught what the static checks could not. The harness copies the script into a temp worktree to stub common.sh, and copied exactly one file -- so after the split every source line failed and the script died before any behaviour under test ran. Teaching it to copy lib/ too is a harness fix under decision 8: no assertion changed, the same five checks run, exit 0 before and after. bash -n also earned its keep: the first splice dropped notify_user's closing brace, and shellcheck reported that only as SC1094 "parsing of sourced file failed" against the entry. One rename, not a suppression: monitor_loop's local `music_services` became `active_services` after shellcheck flagged SC2153 against the MUSIC_SERVICES config array now visible in the same file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| cffd4587cf |
Apply prettier to two split prose files
prettier only runs on pre-push, so the split commit passed the per-file gate and then failed the push. Both files stay under the cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 346eb1f00b |
Bring every markdown file under the cap
Twelve prose violations to zero. Eight files split into per-topic subdirectories with the original left as an index; both SKILL.md files keep their frontmatter so discovery still works, with detail moved to references/. Deleted docs/cleanup-2026-05/ (7 files) and linux_configuration/docs/ VERIFICATION.md on the user's ruling: unreferenced records of finished work whose rules are now enforced by the polling, shellcheck and shfmt hooks. PACMAN_WRAPPER_SECURITY.md was split instead - unlike those, it documents live guarantees. Every split verified by a sorted diff against HEAD: zero content lines lost, only the new headings and index links added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 9e62430b8d |
Split two more linux_configuration scripts; revert a third
control_from_mobile.sh 415 -> 180 and setup_periodic_system.sh 408 -> 228. setup_periodic_system.sh has no main(): it defines functions then runs a block of top-level calls, and the splitter moved that block into the lib, changing when it ran. Redone by hand moving only definitions. It also asks for sudo at source time, so the stubbed run is off-limits - verified instead that the source line precedes the first call and all eleven invoked functions are defined exactly once across the two files. clean_audio.sh is reverted and stays over the cap. Two probe-result globals are read from what would be two different libs, and renaming them to satisfy SC2154 only moved the warning along. That is a refactor, not a line-count split, and forcing it would have cost a suppression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 47371ac9a1 |
Wire uBlock Origin into every real Chromium launch, alongside LeechBlock
install_ublock.sh patches the same wrapper install_leechblock.sh already targets, always rewriting --load-extension into a single comma-joined flag rather than appending a second occurrence — verified empirically that Chromium is last-wins across separate --load-extension flags and would otherwise silently drop LeechBlock. seed_ublock_storage.js previously hardcoded uBlock Origin's Chrome Web Store extension ID, but --load-extension (unpacked) loads get a synthetic, install-path-derived ID instead. It now reads the real ID back out of the profile's own Preferences file, with a --ext-path override for installers whose package path differs from Arch's. install_ungoogled_chromium_ubuntu.sh's UBLOCK_EXT_PATH was an unverified guess (/usr/share/webext/ublock-origin-chromium); corrected to the real Debian/Ubuntu package path confirmed via packages.ubuntu.com. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015yfDYbaw1D1WhVMqWhsSDA |
|||
| 0c28f9988f |
Fix pacman whitelist matching for yay's -U package-file installs
is_blocked_package_name compared raw pacman -U <file>.pkg.tar.zst arguments against whitelist entries, which never matched since yay always installs AUR packages this way. Added strip_pkgfile_suffix() to derive the bare package name first, plus a regression test. Also swaps the browser whitelist entries to ungoogled-chromium (Chromium 151 dropped MV2, breaking uBlock Origin's full/classic build). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015yfDYbaw1D1WhVMqWhsSDA |
|||
| 2d4d2a2eb2 |
Block by hiding apps, which the phone cannot undo
The network layer was the wrong place for this. Tested remotely today: opening RethinkDNS and switching off its device blocklists un-blocked YouTube in a few taps, and no device owner API can stop that -- an on-device filter app always owns its own rules. App hiding is different. 'pm unhide' from adb shell throws a SecurityException and the package stays hidden; only the device owner can reverse it, and reaching that costs a factory reset. So the block moves there, scoped to an explicit allowlist and the home geofence. Both whitelists are rewritten from the apps that actually exist on this phone. The old ones described the rooted Blackview. Every browser is excluded now, so at home a link does not open at all -- confirmed as intended. LAUNCHER_PACKAGE was still the Blackview's launcher, which is not installed here. The exporter refused to render the policy twice over it -- precisely what that check is for. The network layer stays a soft deterrent by choice: pinned and uninstall-blocked, but without lockdown, and its blocklists remain switchable. Private DNS is implemented but ships disabled. stunnel logged zero connections from the handset across two pin attempts, so the router does not forward 853. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| 58c1df573c |
i3blocks: add Claude usage block, fix icon rendering, drop dead blocks
Add a [claude_usage] block showing the 5h and 7d rate-limit windows. It
reads ~/.claude/cache/limit-state/*.json, which Claude Code's statusLine
hook already writes to disk, so the data costs nothing to display: the
script only runs jq against a local file and makes no API call. Limits
are account-wide while the cache is sharded one file per project, so it
takes the newest file and ignores cwd. Percentages arrive as floats and
are truncated; a stale cache is labelled rather than shown as current,
and a rolled-over window reads "?%" instead of a misleading value.
Replace Font Awesome private-use-area icons with plain Unicode. Both
fonts named in the bar's pango string ("System San Francisco Display",
"FontAwesome") are not installed, so PUA codepoints fell through to
whatever font claimed them: ethernet rendered as a star, wifi as
Cyrillic, gpu as Hangul, disk as a chess pawn. Emoji resolve through
fontconfig consistently, which the pre-existing volume block already
relied on, and need no change to ~/.config/i3/config.
Remove the bluetooth, battery, wifi, activitywatch, warp and
network_monitor blocks: this desktop has no BT adapter, no battery, no
wireless interface, and no warp-cli. This also removes a rendering bug,
since activitywatch_status.sh and warp_status.sh were interval=persist
yet emitted three lines per update, leaking their colour line into the
bar as literal "#FF0000" text.
Fix ethernet reporting "down" on a live wired link:
find_ethernet_interface returned the first non-loopback interface, which
with docker installed is a br-* bridge (permanently down, and sorted
before enp*). It now requires a real /sys/class/net/*/device and prefers
an interface that is up.
Tests drop the assertions and fixtures for the removed blocks and add
claude_usage coverage (fresh, warn, critical, stale, rolled-over, null
reset, unparsable, missing dir), a guard that removed blocks cannot
return, a repo-wide check that no block script uses a PUA codepoint, and
an ethernet physical-NIC regression check. The fork budget is asserted
against a populated cache directory, which caught a stat call inside the
scan loop that cost one fork per cached project (170 -> 2 execs).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
|
|||
| b0c7baf22e |
Log why Chromium browsers die; whitelist librewolf
Thorium was "closing and dying randomly" with no evidence anywhere.
Two independent structural causes, both now fixed:
1. Crashpad was never enabled -- no crash_reporting consent key and an
empty crash DB, so no minidump was ever written.
2. stderr from a dmenu/.desktop launch reaches no terminal and no
journal. The only Thorium lines ever seen in the journal came from a
systemd-launched instance, which is why they survived.
browser-preexec-wrapper.sh now gives every Chromium-family launch its own
log under $XDG_STATE_HOME/browser-crash-logs: a VRAM/RAM/kernel/driver
snapshot before start, the browser's stderr, and on exit the exit code,
decoded signal, and a SECOND VRAM snapshot. Those two snapshots plus the
signal are the discriminator between the live hypotheses -- a VRAM death
shows NVRM/NV_ERR_NO_MEMORY or near-full VRAM at exit, a stale-binary
death shows SIGSEGV with the GPU idle. Root cause is deliberately NOT
asserted here; the next crash decides it from evidence.
Three details that are load-bearing rather than incidental:
- The exit record is written from a signal trap, not inline after
`wait`. If the wrapper itself is killed, an inline block never runs
and the log ends mid-stream with no verdict -- indistinguishable
from a browser that is still running.
- The browser is a direct redirect, NOT a pipe into a size filter: in
a pipeline `wait` reports the last element's status, discarding the
browser's signal, which is the one fact worth capturing.
- Logs are capped by bytes as well as count. Measured: ~2 MB per 45s
of navigation (65% VERBOSE1 net/TLS chatter), ~0 while idle. Count
alone would not bound disk use, and a full disk causes exactly the
random deaths under investigation.
setup_periodic_system.sh: rewrite Chromium "app mode" .desktop entries
(Exec=/opt/<browser>/...) to go through /usr/local/bin. These are
auto-generated and bypass the wrapper entirely, so neither the flags file
nor the logging applied to them -- five Thorium entries were found still
launching without the --disable-gpu-compositing mitigation added on
2026-07-25, two weeks after the fact. Runs on every install because
Chromium regenerates the files.
pacman_whitelist.txt: allow the exact package name `librewolf`. The
`librewolf` keyword stays blocked, as do all other Firefox forks. It is
LeechBlock-enforced via /etc/librewolf/policies (force_installed), so
this is not an unfiltered escape hatch; it is needed because Chromium's
MV2 sunset ends full uBlock Origin with all filter lists, which the Gecko
build keeps.
Verified: shellcheck clean, no lint suppressions; fake-browser SIGSEGV
and wrapper-SIGTERM both produce a verdict; trim watchdog bounds a
runaway log; kill -SEGV on the real /opt/thorium-browser/thorium wrote
"exit code: 139 / died on signal: 11 (SEGV)"; librewolf installs and
loads with uBO + LeechBlock active.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME5ZkgWjFvPZrUwMpVZM3e
|
|||
| b83ce5d3c7 |
Fail the build on any shellcheck finding; fix the 222 the old gate hid
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
billsplit CI / quality (push) Has been cancelled
reverse-survivors CI / check (push) Has been cancelled
The shellcheck hook ran with --severity=warning, so info- and style-level
findings never failed a commit. 222 had accumulated across 94 of 244 shell
files. They were not all style noise -- six were bugs:
- Five scripts in single_use/misc/testsAndMisc-bash/ sourced
$SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists
at that path, so all five failed at runtime.
- setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage()
(which ends in `exit 0`) and then `exit 1` for an unknown command. The
exit 1 was unreachable, so a typo'd subcommand reported SUCCESS.
- Six i3blocks scripts had a `# shellcheck source=` directive missing the
scripts/periodic_background/ path segment. The stale path had silently
disabled source-following for all of them.
- diagnose_arch_performance.sh word-split a newline-joined file list; when
the find came back empty, awk got no file operands, fell through to
reading stdin, and the diagnostic hung.
- lib/common.sh's handle_arg_help_or_unknown ended in an unreachable
`return 0` while documenting "Returns: 0 if handled" -- a contract no code
path could satisfy.
- deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top
level, and config.sh can repoint it at the on-device path
/data/local/tmp/focus_mode. Renamed to DEPLOY_DIR.
The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc
sets external-sources + source-path=SCRIPTDIR so sourced files are actually
followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs.
17 `# shellcheck disable=` directives remain, each with a written reason, and
only where the finding is provably wrong: injection-test payloads that must not
expand, literal source text being grepped for, POSIX sh scripts on the phone
where arrays do not exist, and source paths that only exist at runtime.
Everything else was fixed in code.
Verified in a CLEAN worktree, not just my own tree. config.sh sources the
gitignored config_secrets.sh, which exists locally but not in a fresh checkout;
a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a
clean worktree, exactly as CI does -- failed on it.
The gate is verified to bite: a file whose only finding is SC2016 (info) fails
the hook, while the old --severity=warning gate exits 0 on it.
third_party/ is excluded -- vendored upstream, matching the prettier hook.
Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing
SC2034 disable that hides a real bug -- its `patterns` array is never used, so
the checker detects nothing. Confirmed unchanged by this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 79829ec8a2 |
Diagnose the pacman first-hook stall; fix the drift that hid a stale wrapper
pacman intermittently freezes at its FIRST PreTransaction hook - 5 abandoned
transactions since 2026-07-29, stalls of 4 minutes to overnight, always fine on
retry. This investigates it and repairs the real defects found on the way.
The stall is NOT fixed, and deliberately so: no verified defect was found, so
nothing pacman-side was changed.
- Not guard-lib. /etc/guard-lib/targets was empty, so that hook script
provably exits in milliseconds. The failures share POSITION (first hook of
the transaction), not identity: one stall hit the hosts hook back when that
one ran first.
- Not reproducible on demand. 65 real transactions (40 idle, 25 under 2.6 GB
of memory pressure) produced zero stalls. Against the historical 4-in-56
rate that is ~0.9% likely, so the trigger is conditional on something a
synthetic load does not recreate. Memory pressure is untested at the load
that actually occurred - not refuted.
So diagnose_pacman_hook_stall.sh drives real transactions with a watchdog, and
its --watch mode passively captures /proc/<pid>/{stack,wchan,syscall}, the
process tree, meminfo and PSI from the next REAL stall. No pacman-side fix
should ship before that dump exists.
Three real defects were found and fixed:
1. The deployed pacman wrapper was 19916 B against a 27510 B source, never
sourcing the shared pacman_lock_lib.sh that makepkg_wrapper already used.
It hid because check_pacman_wrapper() only tested that files EXIST. Both
installers now write a drift manifest covering source AND installed hashes;
the checker replays it with sha256sum -c and fails closed when it cannot
verify. Manifest paths are absolute - relative ones resolve to nothing under
systemd's cwd=/ and would have looped reinstalling forever.
2. The guard-lib migration (
|
|||
| d91c0ab0e9 |
fix(i3): repair the dead idle-inhibitor autostart and make it a singleton
The i3 `exec` line pointed at
/home/kuchy/linux-configuration/scripts/utils/turn_off_auto_idle_screen_shutdown.sh
- a typo'd home ("kuchy") *and* a path that stopped existing when the repo
was reorganised in
|
|||
| 4f2c82fee7 |
Add overnight power-save to night-lockdown; harden dnsmasq with a watchdog
Caps CPU (amd-pstate-epp) and GPU (nvidia-smi power limit) during the existing night-lockdown window instead of migrating services to a Raspberry Pi (tried, abandoned after SSH auth failed). A more aggressive GPU PCI-unbind option is built but left disabled by default. Also fixes an unrelated live incident found while testing: dnsmasq (this PC's LAN DHCP/DNS server) had died with zero error logged, breaking WiFi for every device on the network. Added a watchdog timer mirroring the existing shutdown-timer-monitor-watchdog pattern, and whitelisted the Tapo smart plug app so the overnight power reading is checkable during the phone's curfew window. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 5d4d3b9e24 |
night-lockdown: stop firing the morning alarm on a fixed hour
The unlock script started morning-routine.service on every staggered trigger (05:00/05:15/05:30/06:00/07:00) on alarm days. That was a workaround for the always-on lockdown removing the hibernate/resume event the alarm used to ride on — reasonable when nothing else could fire it. Something else fires it now. wake-alarm-trigger.timer ticks every minute and starts morning-routine.service at the time the phone synced, with a catch-up window for a suspended or just-booted machine. Keeping both meant two independent triggers, one of which ignores the phone entirely. That is not hypothetical: on 2026-07-20 the phone was set to 08:00 and the alarm went off at 07:00, started here, with no wake-alarm-trigger "Firing" line in the journal at all. The 08:00 fire then landed as a no-op because this 07:00 run had left morning-routine.service still active. Drop maybe_start_morning_alarm() and both call sites, plus the config that went dead with it (ALARM_DAYS_DOW, MORNING_ROUTINE_UNIT, the alarm_started_date marker). MIN_WAKE_AFTER_LOCKDOWN_HOURS stays — the wake-floor trigger uses it. The lockdown's actual job — console/GUI restore, unmute, unmasking the login surface — is untouched. The only starters of morning-routine.service left are wake-alarm-trigger.timer (primary) and the system-sleep hook (resume backstop). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| 937c597c80 |
remove(digital-wellbeing): delete unusable focus-mode daemon entirely
Steam/browser mutual-exclusion daemon was unusable in its current state. Removed all code, install tooling, and every reference across install_core_system.sh, check_and_enable_services.sh, the security hardening test suite, and CLAUDE.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 88eda8dc51 |
fix(night-lockdown): schedule a dynamic wake-floor trigger for any shutdown time
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
The alarm floor (lockdown-entry + 8h) from the previous commit still relied on the fixed 05:00/05:15/05:30/06:00/07:00 unlock triggers to ever re-check it. Shutdown time is configurable (/etc/shutdown-schedule.conf, changed by the screen_locker sick-day feature) and can vary night to night, so a late enough lockdown (e.g. 23:30) pushes the floor past every remaining trigger (07:30 > 07:00) and the alarm would silently never fire that morning. Enter script now schedules a transient systemd-run --on-calendar one-shot timer at exactly lockdown-entry + MIN_WAKE_AFTER_LOCKDOWN_HOURS, computed from the real lockdown timestamp rather than assumed shutdown hours. It re-runs the idempotent, self-dedup'd unlock script, guaranteeing the alarm gets a correctly-timed chance regardless of what time lockdown started. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S4YWZhDcEUMUi8cyw9AzTR |
|||
| e393207e3a |
fix(night-lockdown): close console-blank gap, recover DP link, floor wake alarm
The 23:00 lockdown -> 05:00 unlock cycle exposed three live bugs: a visible console-text flash before the screen went black (blank_console only ran once systemctl stop lightdm.service fully returned, leaving a gap), DP-0 needing a manual xrandr/replug after wake, and the morning alarm firing only 6h after a late lockdown entry instead of waiting a full 8h. - Blank the console continuously in a background poller for the whole GUI teardown instead of a single after-the-fact call. - Retry autorandr --change against a saved profile after lightdm restarts. - Stamp lockdown-entry time and gate the alarm on entry + 8h, decoupled from the console/GUI-restore idempotency so it can fire on a later staggered trigger instead of only the first one. - Add DRY_RUN NOTE banners so a clean dry run is never mistaken for proof the console-blank and autorandr fixes work for real (they need a live cycle). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S4YWZhDcEUMUi8cyw9AzTR |
|||
| e0f62d699a |
fix(wellbeing): split focus_mode_daemon to satisfy the 500-line limit
focus_mode_daemon.py has been 509 lines since
|
|||
| 28779586eb |
fix(wellbeing): symlink focus-mode-daemon so edits actually deploy
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
install_focus_mode_daemon.sh copied the script to /usr/local/bin, so the
deployed daemon went stale the instant the repo file was edited: what you
run stops matching what you read, with nothing to tell you. That is not
hypothetical - the zombie-detection fix in
|
|||
| 22e22c16a7 |
fix(wellbeing): stop focus mode reading zombie processes as running Steam
Focus mode flapped in and out of GAMING mode every few seconds and killed
browsers while no Steam was running. The pattern matching was not at
fault: the daemon was correctly seeing a process named "steam". That
process was a root-owned zombie.
A total gaming block on 2026-07-04 purged the Steam client and it was
never reinstalled, but steam-backlog-enforcer kept trying to launch it
through an orphaned /usr/local/bin/steam wrapper still pointing at the
deleted /usr/bin/steam. Each launch died on exec and, never being reaped,
lingered as a dead process that still carried the name "steam" - which
this daemon counted as a live Steam.
get_running_processes() now reads /proc/*/stat rather than /proc/*/comm
and skips state Z. Both cost one read per PID, so the poll loop stays
fork-free, but stat also carries the process state. _parse_stat() uses
rpartition(')') because a process name can itself contain spaces and
')', which naive whitespace splitting would corrupt. Names are truncated
to 15 chars in stat exactly as in comm, so the existing patterns match
unchanged.
Verified against the live zombie, before the enforcer was fixed:
zombies on system now : ['steam']
OLD (/proc/*/comm) sees 'steam' : True
NEW (zombie-filtered) sees 'steam' : False
dropped by filter : ['steam']
and non-regression on real processes: a synthetic live 'steam' and a
'steam_app_29687' game both still return True. It skips zombies, not
Steam. Running `focus-mode-daemon` now reports "Browser detected -
entering BROWSING mode" with no GAMING flapping.
The zombie source is fixed separately in steam-backlog-enforcer d54df3a;
this is the durable guard against any future one.
Note: install_focus_mode_daemon.sh copies rather than symlinks, so
/usr/local/bin/focus-mode-daemon was a stale copy without this fix.
Reinstalled. Editing this file alone never changes what runs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rN2usTS3pYLJqeVg2ZpgR
|
|||
| 3ebb56ccbb |
feat(shutdown): night lockdown instead of power-off, so servers stay up
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
The digital-wellbeing curfew powered the PC off at 21:00, which also took down every service this machine hosts (Gitea, the Caddy TLS edge, SyncYomi, the personal site, Open WebUI, Joplin, dufs, ollama, dnsmasq, wg-quick, sshd). Keep the curfew's lockout, drop the power-off: the machine now stays on with the GUI torn down, the TTY login surface masked, the console blanked and the RGB killed, and restores itself at 05:00. The fortress still owns the schedule (immutable config, ratchet, monitor, watchdog); only its terminal action is swapped for night-lockdown-enter.sh. The reversal deliberately lives outside the fortress so a bug in the lock path can never take the recovery path down with it. Verified in production overnight: lightdm down 21:00:04 -> 05:00:00 while all 7 containers stayed "Up 14 hours" and kept serving; ticks idempotent all night; 05:00 unlock fired. Notable fixes found along the way: - Wake alarm regression: it only ever fired on resume-from-hibernate, so an always-on machine silently killed it. The unlock now re-fires morning-routine.service on alarm days (the Mon/Fri/Sat/Sun gate lived in the removed rtcwake branch, not in the alarm). - Morning boundary -le -> -lt, so 05:00 no longer races the unlock. - Mask getty@ only; autovt@ is an alias of it and masking it errors. - Persist LOCKED before the session-killing teardown, else a kill mid-lock leaves state=UNLOCKED and the unlock no-ops, stranding the machine. - Blank the console without DPMS: FB_BLANK_POWERDOWN drops the DisplayPort link and the GPU never re-trains it (DP-0 needed a physical re-plug). - RGB: openrgb-git 0.9.2148's CLI returns before detection finishes, so every call silently no-op'd while exiting 0. Stable openrgb 1.0rc3 fixes it. Use static+black, not "off" (the ZOTAC has no Off mode; the ASRock ignores it). - Validate the generated config with bash -n: its heredoc must stay unquoted, so a stray backtick executes and corrupts the file. Thu-Sun 22->21 aligns the source with the canonical schedule screen_locker's sick-day feature ratcheted in on 2026-07-03; the ratchet only permits same/stricter, so the script was otherwise un-re-runnable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dpu8SqqBVm97tCuTGE3nCs |
|||
| 9c2abe2520 |
fix(wellbeing): clear every remaining error/warning; drop thesis_tracker
Three of the four remaining items were scripts hardcoding assumptions that
stopped being true — not real drift.
hosts (phantom warning): check_hosts looked for 10-unlock-etc-hosts.hook /
90-relock-etc-hosts.hook, gone since the guard migrated to guard-lib's generic
unlock-all/relock-all hooks. Match what pacman_wrapper.sh already treats as
authoritative (legacy pair still accepted).
focus_mode (never installable): install_focus_mode_daemon.sh calls
`systemctl --user`, but the checker invokes it via `sudo -u <user>` from root —
sudo changes the UID but grants no session, so XDG_RUNTIME_DIR /
DBUS_SESSION_BUS_ADDRESS were unset and it died at daemon-reload on every
automated run. Derive them from the effective UID. Now enabled+active.
compulsive_blocker (discord never limited): REAL_BINARIES hardcoded
/opt/discord/Discord — another discord package's layout. This one ships only
/usr/bin/discord, a bootstrap that downloads the app into ~/.config/discord/
and execs ~/.config/discord/Discord (a version symlink its updater maintains),
so the `[[ -x $real_binary ]]` gate failed forever. Exec the preserved LAUNCHER
(/usr/bin/discord.orig) so Discord's bootstrap/self-update still runs. Two
supporting fixes: the gate skips -x when the exec target IS the wrapper's own
.orig (the wrap creates it — chicken-and-egg), and PROCESS_MATCH is added
because the launcher exec()s away, so pgrep -f on '.orig' matches nothing and
auto-close/kill/running-state would have silently no-opped.
vbox_hosts: skipped -> n/a with a reason. VirtualBox is absent AND blocked by
pacman policy ("can bypass /etc/hosts restrictions"), so the check is not
applicable, not ducked. Installing it to satisfy a hosts check would open the
hole that check exists to close.
Also removed thesis_tracker (unused; unit not-found, script never installed).
Verified: every service ok, vbox_hosts n/a, zero errors, zero warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
|
|||
| cc2fcc44d3 |
fix(checker): stop reporting healthy services as errors; drop thesis_tracker
check_and_enable_services.sh was lying about two things and tracking a third the author no longer uses. 1. workout_locker: paths still pointed at python_pkg/screen_locker/, which stopped existing when screen-locker was extracted to its own repo (~/screen-locker). The service was installed, enabled and active the whole time, but the checker reported a red error and its "fix" silently did nothing. Now resolves ~/screen-locker via SUDO_USER + getent (the script re-execs under sudo, so $HOME is /root). 2. User-service queries could never work: `sudo -u <user> systemctl --user` has no XDG_RUNTIME_DIR/DBUS_SESSION_BUS_ADDRESS, so it always failed with "Failed to connect to user scope bus" and reported EVERY user service as not-enabled. Added user_systemctl() using `systemctl --user --machine=<user>@.host` (systemd's own suggested fix) and rerouted all 6 call sites (focus-mode, thorium, workout-locker). 3. Removed thesis_tracker entirely — not used; unit not-found, script never installed. Help list renumbered. focus_mode and guest_mode_removal keep reporting error: those are GENUINE (focus-mode.service is not-found; the guest-mode policies are absent). Verified: ground truth (as kuhy) workout-locker.service = enabled+active. Before: checker said "not enabled". After: workout_locker ok, all 3 checks ✓. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1 |
|||
| 779ace0c87 |
fix(hosts-guard): stop setup aborting silently, so the guards actually enable
setup_hosts_guard.sh died partway on every run under `set -euo pipefail`, which is why hosts-guard.path / hosts-bind-mount.service / nsswitch-guard.path sat installed-but-disabled since 2026-02-02 and resolved-guard.path was never installed at all — /etc/hosts had no watcher to restore it if tampered. Two aborts, both silent: 1. `local_dot=$(grep -E '^\s*DNSOverTLS\s*=' ... | tail -1 | sed ...)` — grep exits 1 when the key is absent, pipefail propagates, the assignment returns 1, set -e kills the script. DNSOverTLS is absent from a default resolved.conf, so this fired on EVERY run, right before resolved-guard.path was enabled. The fix-if-missing branch below it was unreachable because detecting "missing" aborted first. Same trap on the ReadEtcHosts probe. check_and_enable_services.sh already guarded its copy with `|| true`. 2. `systemctl restart systemd-resolved` — resolved is MASKED here (dnsmasq serves DNS), so the restart failed and aborted the script on its last step, after everything was already configured. Now skipped with a note. Before: exit 1, resolved-guard disabled, nsswitch-guard start-limit-hit. After: exit 0; hosts-guard.path, hosts-bind-mount.service, nsswitch-guard.path and resolved-guard.path all enabled+active. DNS still resolves; getent ahosts on doubleclick/googlesyndication/facebook returns 0.0.0.0; /etc/hosts still immutable at 176888 lines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1 |
|||
| aba288d418 |
fix(maintenance): make repair failures loud instead of silently exiting 0
Both tools swallowed failures. execute_with_log appended one line to a log nobody reads and returned 0, so the systemd oneshot reported SUCCESS. check_and_enable_services printed one err line among ~100 and exited 0. That is why the 2026-05-15 reorg silently killed the pacman-wrapper and hosts self-repair for five months with no visible symptom: check_hosts detected the disabled hosts guards on every single run, could not find setup_hosts_guard.sh at the stale $CONFIG_DIR/hosts path, and still exited 0. Driver — any failure now surfaces four ways: 1. journal at ERROR priority (logger -p user.err) 2. the systemd unit FAILS (exit 1) -> systemctl --failed / red timer 3. a critical desktop notification that never auto-dismisses 4. /var/lib/periodic-system-maintenance/LAST_RUN_FAILED breadcrumb It also collects ALL failures rather than aborting on the first. check_and_enable_services — a missing repair script means this tool cannot do its job, so all 12 "script not found" sites now route through err_missing_script(): journal ERROR priority, a terminal banner, and exit 2. Healthy runs still exit 0 and clear the breadcrumb. Verified: driver fed nonexistent paths -> exit 1, all 3 failures on stderr, breadcrumb written, 3 journal ERROR entries. Live regenerate + real run -> rc=0, all steps ok, no breadcrumb. Checker --status -> rc=0, no banner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1 |
|||
| daa712a122 |
fix(hosts): stop chattr +i'ing install.sh and generate_hosts_file.sh sources
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Immutable git-tracked files break git tooling. The pre-push ci-mirror gate
runs `pre-commit run --all-files`; end-of-file-fixer opens every file rb+
and dies with "PermissionError: Operation not permitted", so no push from a
normal checkout could ever succeed — every push had to be routed through a
throwaway worktree (a fresh checkout gets new, non-immutable inodes). The
same immutability also breaks pre-commit's unstaged-changes stash: the
`git checkout -- .` unlink fails, the restore never runs, and unrelated
unstaged edits are silently reverted.
Enforcement does not depend on these sources being locked: /etc/hosts is
itself chattr +i, hosts-guard.path watches and re-enforces it via
enforce-hosts.sh against the locked-hosts canonical snapshot, and
hosts-bind-mount.service pins it. Editing the sources changes nothing until
install.sh is re-run as root, which regenerates the guarded artifacts.
Companion to
|
|||
| 6e3d61daa2 |
fix(shutdown): stop chattr +i'ing this script's own git-tracked source
An immutable tracked file with uncommitted edits breaks every commit/push:
pre-commit clears unstaged changes with `git checkout -- .`, that unlink
fails ("Operation not permitted"), the context manager aborts, and the
restore never runs — silently reverting the user's OTHER unstaged edits
(seen twice today; an unrelated i3/config WIP had to be recovered from
pre-commit's rescue patch).
Enforcement is unaffected and does not depend on the source being locked:
/etc/shutdown-schedule.conf stays chattr +i, guard-lib keeps a canonical
snapshot + path watcher that re-enforces it, the monitor service protects
the timer, and screen_locker's ratchet only accepts same-or-stricter
values. Editing this source changes nothing until setup is re-run as root,
which rewrites the guarded config anyway.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
|