mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 20:14:02 +01:00
8a8a91aec3
121 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 194b006453 |
Split fix_bluetooth.sh 720 -> 186; defer steam_compatibility.sh
Four libs written in one pass rather than four sequential splits, which is what stops the libs chaining - the entry gets one source block naming all four and no lib sources a sibling. steam_compatibility.sh is reverted and stays at 663. Its CACHE_MAP associative array is declared and filled in what would be the ProtonDB lib but read by main, so that lib fails SC2034 linted alone. Third file deferred for the same reason: shared mutable state means the seam is not there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| d9c315e3ef |
Split setup_searxng.sh 912 -> 184
Six libs: the script has four phases and write_stack alone was 342 lines because it emits three config files from heredocs. That became three functions, one per file, each keeping the comment that explains it - including why settings.yml is mode 640 and not 600. For a script whose job is writing config, "the functions still exist" is not enough. Compared the sorted non-comment lines of the original write_stack against its three successors: identical hash, so every line of emitted compose, settings.yml and limiter.toml is byte-for-byte the same and only the wrappers were added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 2527fb37e6 |
Split three large linux_configuration scripts, defer a fourth
organize_downloads.sh 513 -> 72, setup_kcd2_dice_solver.sh 497 -> 112, fix_ubuntu_performance.sh 494 -> 150. First 490+ line scripts done; all three needed two or three seams. Repeated splitting chained the libs - each sourced the next, because the tool inserts its source line relative to the file it just wrote. Flattened so the entry sources all three and no lib sources a sibling. diagnose_pacman_hook_stall.sh is reverted and stays at 493. Its transaction runner assigns LAST_ELAPSED that the caller reads and reads PACMAN_BIN that the caller defines, so linted alone it is SC2034 plus SC2153. The pre-commit hook lints each lib in isolation and fails, while shellcheck -x on the entry passes - suppressing that would be papering over a seam that does not exist. organize_downloads.sh is the ExecStart of media-organizer.service; the unit still resolves and the script still parses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 7e967de79b |
Split seven more linux_configuration scripts
10-recon.sh, install_exercism.sh, fix_arch_performance.sh, format_sd_card.sh, setup_dot_resolver.sh, fix_unity.sh and 20-dump-stock.sh, all now under the cap. Splitting a file twice is the risky move: the second cut on 20-dump-stock.sh dropped the first cut's source line, leaving verify_dump called but never defined, and the repair then landed the line inside the lib instead of the entry script. Both fixed - the entry sources both libs and no lib sources another. Grep the source lines after any double split. fix_unity.sh's desktop_dir is read across the seam; renamed to DESKTOP_DIR to match the file's own convention for globals. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 58fdc6c9de |
Split five more linux_configuration single_use scripts
install_unreal_mcp.sh 395 -> 155, diagnose_arch_performance.sh 377 -> 123 (two cuts), setup_local_llm.sh 370 -> 163, repo_to_study.sh 365 -> 160, setup_dwm.sh 359 -> 147. The SCRIPT_DIR fix from the last batch held on all five. Fixed a second unguarded cd, this time before the study-material generator ran - the same latent bug class as build_website, surfaced because a moved function gets linted in isolation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 1a75ed5bf5 |
Split three more linux_configuration feature and fix scripts
setup_syncyomi.sh 408 -> 138 (two cuts), setup_personal_website.sh 396 -> 192, fix_thorium.sh 377 -> 208. All three already declared SCRIPT_DIR and the splitter added a second one, which aborts at runtime with "readonly variable" while bash -n and shellcheck stay clean. The stubbed run caught it; the splitter now reuses an existing declaration. Fixed SC2164 in build_website rather than suppressing it: a bare cd "$WEBSITE_SRC" inside a subshell, with pnpm install and pnpm run build after it. A failed cd would have built in the caller's directory. 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 |
|||
| 658988ef9b |
Split three near-miss linux_configuration scripts
turn_off_auto_idle_screen_shutdown.sh 289 -> 180, mtk_root/install.sh 262 -> 141, setup_gitea.sh 255 -> 198. A deliberately small batch to check jscpd before scaling; it stayed well under 2%. The stubbed run earned its place again: mtk_root/install.sh aborted with "SCRIPT_DIR: readonly variable" because it already declared one, and both bash -n and shellcheck were clean on that broken file. Two shellcheck findings the split exposed are fixed at the source, not suppressed: persist_with_systemd_logind now takes the flag as an argument instead of reading the caller's global, and a dead python_bin assignment is gone. Neither was introduced here - the pre-split file already had three SC2034 warnings. No script was executed; all three are installers or enforcement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| dadd33f82d |
Split install_joplin.sh and establish the shell-split pattern
299 -> 103, with the --with-server half in lib/joplin_server.sh. First
of the 84 linux_configuration files, done slowly to settle the pattern.
Three things bash -n and shellcheck do not catch, all found here:
- SCRIPT_DIR from "$0" breaks when the entry script is itself sourced.
Use "${BASH_SOURCE[0]}"; keep readlink -f for the one symlinked script.
- A lib needs a shebang AND +x: without the shebang shellcheck raises
SC2148, with it the shebang-executable hook fails. Match common.sh.
- Only a stubbed run proves the source line resolves. Copy entry+lib to
a mktemp -d, sed `main` into a declare -F probe, run that. No
installer step executes.
Also enumerates which scripts escape the repo: three copied out by
install_pacman_hooks.sh, one symlink, everything else generated. All
recorded in docs/shell-split-recipes.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
|
|||
| 17b20e5fae |
Gate the no-inline-Python rule instead of asking people to remember it
CLAUDE.md's longest and most emphatic code rule -- never embed Python program logic in a shell script -- was enforced by nothing. A repo-wide scan found it had already been violated: install_plagiarism_tools.sh ran a 6-line NLTK download as an inline `python3 -c`, invisible to ruff, mypy, pylint, bandit and pytest, exactly the failure mode the rule exists to prevent. check_no_inline_python.sh flags heredocs and multi-line `python -c` while permitting the single-line availability probes the rule explicitly allows, so the ~250 shell files here produce zero false positives. Commented-out examples are skipped; otherwise the rule's own documentation would fail the hook. The extracted download_nltk_data.py also fixes a real defect: the inline block ignored nltk.download()'s return value, so a partial corpus download reported success. It now returns non-zero and the caller's `set -e` sees it. nltk is imported via importlib inside main(), not at module scope: it belongs to the plagiarism toolchain, not this repo, so it is absent from requirements.txt and unresolvable to pylint as a static import. No suppressions anywhere: T201 avoided with sys.stderr.write, INP001 with a shebang (matching sibling fast_count.py), E0401 with the dynamic import. pylint 10.00/10, ruff clean, mypy clean. Verified the gate fires rather than passing vacuously -- a staged canary with a real heredoc exits non-zero -- and that the extracted script runs. 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 |
|||
| 91061edc14 |
feedback: local-only pacman package build, and ignore build artifacts
fee[dB]ack has no AUR entry, so install_feedback.sh builds a local package from the PKGBUILD tracked beside it, the same way hypersomnia-appimage and jackify-bin are installed here. The script only builds and then prints the `sudo pacman -U` line to run, since interactive sudo prompts belong to the user rather than an agent. The built .pkg.tar.zst is ~640 MB, so it is gitignored along with the Bottles prefixes and the archwiki_rag vector index — all three are local runtime state that is regenerable and must never enter the history (see memories/binary-files.md). The PKGBUILD and the installer are the source and stay tracked. Exclude PKGBUILDs from shellcheck. A PKGBUILD is a makepkg manifest, not a standalone script: it has no shebang by design, pkgname/pkgver/ source/sha512sums are read by makepkg, and pkgdir/srcdir are injected by it, so shellcheck flags every one as unused or undefined. The same reasoning already excludes zsh — the dialect is outside what shellcheck can judge — and the alternative was a directive on nearly every line. Also drops three stray files: a saved search-results page, a 0-byte file from a mistyped redirect, and a June i3 config backup superseded by git. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J |
|||
| cef72fbab0 |
syncyomi_guard: refuse a collapsed payload as the new good copy
TachiyomiSY 1.13.2 can report a restore as SUCCESS while writing almost nothing, then sync that near-empty library up and replace the only good copy. On 2026-08-09 a 14 MB payload holding 2182 manga became a 267 KB stub, and the real library survived only as stale pages in the SQLite write-ahead log. The upstream bug is Kotlin in an APK and cannot be fixed here, so this targets the fixable half: decode the payload, count what is actually in it, and refuse to treat a collapse as normal drift. A pure is-it-zero check would have caught this incident but not the harder case upstream describes, where a restore keeps some entries and drops the rest, so the threshold is proportional. Categories get their own rule because all 2182 manga survived the round trip and all 18 categories did not. Adds the setup script and a runbook covering the WAL-page salvage used to recover the library on the day. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J |
|||
| 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
|
|||
| 7cfe188bbe |
Fix engine pruning: disabled -> inactive (they were still running)
The previous commit's pruning did not actually take effect. Per enginelib,
"disabled" only sets the DEFAULT for a fresh visitor -- "will allow the user
to manually activate it in the settings" -- so any browser session with an
existing preferences cookie kept running every pruned engine. yandex images
was still throwing JSONDecodeErrors in the log minutes after being
"disabled". "inactive" is the key that actually removes an engine.
Converted 84 entries. Verified behaviourally rather than by reading config:
an images search now serves from google cse images ALONE (20 results,
unresponsive=[]), and no pruned engine appears in the logs after a restart.
brave and qwant deliberately keep "disabled". Sibling engines reference
their outgoing connection pool by name (network: brave <- brave.images/
.videos/.news; network: qwant <- qwant news/images/videos), and "inactive"
removes the definition entirely, so the container dies at startup with
KeyError: 'qwant'. "disabled" keeps the pool defined while hiding the engine.
Pool owners in this image: brave, qwant, yandex, piped, yacy.
Also fixes two status checks that could report a healthy instance as broken.
Public /search was returning 429 while loopback returned 200, which looked
like a regression but was a missing Accept-Language header in the request:
botdetection runs one check per header and rejects on the first failure.
Setting "debug: true" under general: makes SearXNG log the exact rejecting
method ("NOT OK (searx.botdetection.http_accept_language)"), which settled it
in one run after several wrong theories. wait_for_site and check_headers had
the same omission and are fixed; the full required header set is documented
at check_http.
Public path confirmed working end to end: general 200 with 21 results in
0.71s, images 200 with 40 proxied thumbnails in 0.58s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb77nxXGwFYFM4fyXEQkzG
|
|||
| dc45779d38 |
Prune 84 SearXNG engines; images category now sub-1s
kuhy went through the /preferences engine table and selected every erroring engine (CAPTCHA / access denied / timeout / parsing error / crash) plus the slow tail, across all nine categories. Applied all of them, keeping only google cse images for the images category as requested. Result: unresponsive_engines is now [] in EVERY category. Per-category latency (q=linux): general 572ms, images 406ms, videos 1378ms, news 455ms, music 461ms, it 1698ms, science 1338ms, files 584ms, social media 1074ms. Images went 2.1s -> 406-900ms across 12 queries, still 20 results. The root cause was not google cse images being slow (1.1s): SearXNG waits for the SLOWEST engine in a category, so quark images at 2.9s set the floor. Cutting the others removed the ceiling. Three engines that were not in the list surfaced as unresponsive only under load and were pruned with the rest: brave.images, startpage images, kickass. Corrects a wrong claim from the first pass. I had said general search retained three working sources because !go returned 20 results and !bi returned 10. Upstream marks both google and bing "inactive: true", so they answer ONLY to an explicit bang and were never part of the merged view -- which google cse has been serving alone all along. The pruning therefore removed only engines already contributing zero, but general search does have a single point of failure, so status now prints the !go/!bi fallback when a live search returns nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cb77nxXGwFYFM4fyXEQkzG |
|||
| 8f2c46de7c |
Add setup_searxng.sh: self-hosted SearXNG at searx.kuhy.duckdns.org
Deploys a SearXNG metasearch instance behind the existing shared Caddy edge. Graded A+ (score 120, 10/10) by the MDN Observatory API, and "Vanilla" in the sense that every change is settings-only -- no template or static-file edits. Public but deliberately not submitted to the searx.space directory. Both containers run network_mode: host because Docker bridge networking has zero egress on this box (nftables' default-drop forward chain is terminal at NF_INET_FORWARD regardless of Docker's legacy-iptables ACCEPTs), so a stock bridge compose cannot reach a single search engine. Non-obvious things established empirically against the pinned image, all of which cost a debugging cycle and are commented at their use sites: - granian binds :: (all interfaces) by default, NOT the 127.0.0.1 its own docs claim; with host networking that would have LAN-exposed the instance. GRANIAN_HOST/PORT are the only things pinning it. settings.yml's server.bind_address is inert (read only under __main__). - Caddy's `> Header "v"` is a substring-replace, not a header setter -- it silently set nothing. The bare name is "set"; `+` appends and would duplicate the headers SearXNG already sends (X-Content-Type-Options, Referrer-Policy). - server.public_instance force-enables link_token, a browser-proof challenge no scripted client can pass, which makes the JSON API unusable. Dropped it and kept limiter/image_proxy via their own keys. - Without limiter.toml there are no trusted_proxies, so botdetection 429s every request including the homepage. - The entrypoint chowns config to uid 977 and needs DAC_OVERRIDE to read it, so cap_drop breaks startup and mode 600 breaks the worker's read (640 is right). - `compose up -d` is a no-op when only a bind-mounted config file changed, so an explicit restart is required or every check tests the previous config. - All curl checks must send browser-shaped headers; bare curl gets 429, which had the status command reporting a healthy instance as broken. Engine pruning is measured, not guessed: brave/duckduckgo/startpage CAPTCHA on 100% of queries, flickr is the 1.0s tail, and pinterest was failing because this host's own /etc/hosts blackholes it -- a local cause that would otherwise have been misdiagnosed as a dead upstream. Measured latency, reported honestly: front page median 19ms, but search median 486ms (233-623ms). Sub-200ms search is not achievable by any configuration -- a metasearch cannot return faster than the slowest upstream it queries. The ~200ms figure on searx.space measures initial front-page response, which is met. Image pinned by digest: the workarounds above are only known-true for 2026.8.4. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cb77nxXGwFYFM4fyXEQkzG |
|||
| 143d260c1c |
Park phone DNS filtering: Private DNS fails closed, so the PC cannot host it
Measured on the Pixel 6a, twice: with private_dns_mode=hostname and the resolver unreachable, `ping example.com` returns "unknown host". Android's Private DNS does not fall back to the carrier's resolver -- hostname mode is RFC 8310's strict profile, and failing to establish TLS on 853 is a hard error. So a resolver living on this PC means "PC off" equals "phone has no internet", not merely "no filtering". That is disqualifying for a daily driver, and it disqualifies the whole home-hosted delivery path regardless of how well it works while the machine is up -- which it does; it was verified end to end. DNS_TRUSTED_DOT_HOST/IPS go back to empty, which restores the previous enforcer behaviour byte for byte. No code revert is needed, which is exactly why the flag was built to default empty. Two things found while testing are worth keeping. The nftables input chain is policy drop and admitted only ssh over wg0, so binding stunnel to the tunnel address was not sufficient on its own: ICMP passed while TCP 853 was dropped, which presents confusingly as "ping works but the port does not". That rule is now added explicitly and scoped to wg0. And setup_phone_wireguard.sh automates the client setup the tunnel needed, verifying handshake, reachability and DNS rather than assuming success, and deleting the pushed private key afterwards. The deeper reason the home endpoint could not work even with the tunnel up: dns.kuhy.duckdns.org resolves to the public IP while stunnel listens only on 10.8.0.1, and with AllowedIPs=10.8.0.0/24 that routes outside the tunnel to a closed port. Android never opened a connection at all. Split-horizon DNS does not fix it either, since on mobile data the phone uses the carrier's resolver. Research also ruled out the hosted-hybrid option that looked most attractive: no provider can ingest a 185k-domain list (NextDNS none, AdGuard 1-5K, ControlD 10K), and the free tiers fail OPEN -- they stop filtering past 300k queries a month while still resolving, which for a self-control tool is the worst failure mode available. Details and the remaining options are in the parked doc. The stunnel endpoint is left running deliberately: WireGuard-only, verified unreachable from LAN and WAN, and it makes the next attempt testable immediately. Removal commands are documented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| e3d5e1dd18 |
Serve the blocklist over DoT and hand the policy to the app as an asset
Two gaps stopped the unrooted path working end to end. The phone had no way to read the focus policy, and the blocklist was only reachable as a hosts file or over plain UDP/53 on the LAN -- while the rooted enforcer actively rejected every DoT connection, so a self-hosted resolver could not have worked even if one existed. focus_owner now loads a generated policy.json instead of hardcoding anything, so config.sh stays the single source of truth across Python, shell and Kotlin. The committed asset is rendered with --redact-home and a test fails if home coordinates ever appear in it. An unknown schema_version is rejected outright: silently accepting a newer schema means enforcing a policy the app has misread, and a misread allowlist is one that blocks the dialer. dns_enforcer.sh rejected ALL port 853 traffic and pinned Private DNS off, every 20 seconds. That is right when DoT is a bypass channel and exactly wrong when DoT is the enforcement channel, which is what an unrooted phone needs -- it has no hosts file to fall back to. With DNS_TRUSTED_DOT_HOST set it now ACCEPTs the trusted resolver before the blanket REJECT, and pins Private DNS to that hostname so the user cannot pick an unfiltered one. Rule order is load-bearing: iptables takes the first match, so an ACCEPT appended after the REJECT would never be reached. expected_rule_count() counts the new rules, because a mismatch there makes chain_intact() rebuild the chain every 20s -- a silent fork storm that also drops the connection each time. With the variable empty the behaviour is byte-identical, so the rooted phone is untouched until this is deployed. setup_dot_resolver.sh terminates DoT with stunnel rather than Caddy: DoT is a raw TLS stream and stock caddy:2.8 has no layer4 module, so using Caddy would have meant an xcaddy rebuild of a container serving five live sites. It reuses the certificate Caddy already renews, so nothing competes for port 80. It binds to the WireGuard address, not 0.0.0.0 -- a public DoT endpoint is an open resolver, and over the existing tunnel it needs no port-forward, no firewall rule and no rate limiting. Verified: example.com resolves, pornhub.com returns 0.0.0.0, TLS 1.3 against the LE cert, and both the LAN and public addresses refuse. The Device Owner checklist is planning only. Checking the device rather than the config settled the one open risk: Google Wallet is not installed and no NFC payment default is set, so the unconfirmable tap-to-pay question does not apply here. The same check found that Aegis, MS Authenticator, Oracle IDM and PKO IKO are absent -- they are in config.sh's whitelist, which describes the rooted phone, not this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| 9dd9aeb50e |
Fix the 11 shellcheck findings that kept CI red since b83ce5d
"Pre-commit checks" has failed on every push since
|
|||
| 1b43ba093c |
mtk_root: build the rooting toolkit before the phone lands
Prep for a Ulefone Armor X12 Pro (MediaTek MT6765) that has not arrived, validated read-only against the Pixel 6a that has. Partition discovery never hardcodes boot vs init_boot. It reads /dev/block/by-name/, derives the slot scheme from observed _a suffixes, and picks the ramdisk carrier by preference order init_boot then boot over what is actually present. The AOSP launch-version rule explains why that ordering is right; the code consults the filesystem, not the API level. One path therefore serves both a Pixel (no init_boot, falls to boot_a) and a 13-launch MediaTek device (init_boot_a taken). Patching the wrong one yields a phone that will not boot. Classification refuses rather than guesses: ULEFONE needs a positive MediaTek signal AND a vendor match, with no "not a Pixel therefore Ulefone" fallback. Carrier lock is always reported UNDETERMINED, because sys.oem_unlock_allowed, ro.oem_unlock_supported and settings global oem_unlock_allowed read identically whether a device is carrier-locked or merely has the toggle off; asserting a negative would wrongly rule out a path. No flashing script, deliberately. Automating irreversible partition writes defeats the confirmation steps that catch a wrong partition or missing slot suffix. Three bugs found by running the code rather than only linting it: - SIGPIPE: `adb version | head -1` under pipefail exited 141 and killed 00-preflight.sh mid-run - 20-dump-stock.sh exited silently when no recon had run, because find on a nonexistent cache dir fails under set -e - the read-only static scan had a hole: stripping all quoted strings to avoid false positives on printf text also hid `adb shell "reboot bootloader"`. Now only printf/echo/cat arguments are stripped, and a negative self-test asserts the scan fails on injected commands Also narrowed a world-writable udev catch-all (idVendor=="*", MODE="0666") that granted every local process read/write access to every USB device; commented with a backup, adb connectivity re-verified afterwards. Validated: 58/58 offline fixture tests; 00-preflight.sh HOST READY (12/12); 10-recon.sh read-only against the Pixel reports PIXEL, boot_a, A/B, no init_boot, path C-or-D indeterminate, with the device unchanged afterwards. Untested and labelled as such in READY.md: the init_boot branch on real hardware, mtkclient over BROM, and the whole flash/verify sequence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ReVsw7qovYkkGbqcU9tn7 |
|||
| 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 (
|
|||
| ed1a224ce7 |
Format last three markdown files; cap billsplit's Android build memory
Brings the repo to prettier-clean across all 525 tracked md/json/yaml files. The markdown edits are cosmetic: escaping `L*` so it stops rendering as emphasis, normalising `*em*` to `_em_`, and aligning tables. billsplit/android/gradle.properties shipped from the Flutter template asking for `-Xmx8G -XX:MaxMetaspaceSize=4G` — more than this machine has. Lowered to 768m/512m and, more importantly, cut the number of JVMs: Kotlin compiles in-process, no persistent daemon, no parallel execution, one worker. Also repairs a regression an earlier prettier pass introduced. That pass pretty-printed test/fixtures/sample_receipt.json, but `accepts bare JPK payload and bare JWS token` reads the fixture as raw TEXT with a regex matching `"data":"..."` with no space after the colon. The inserted space made the match null and the null-check operator threw. Comparing the parsed JSON for equality had passed and proved nothing about that raw-text contract. The fixture is restored to its original minified bytes — a real e-paragon arrives as single-line JSON — and test/fixtures/ is now in .prettierignore so captured payloads stay byte-faithful. The regex also tolerates whitespace now and asserts the match is non-null with a reason, so the same change would fail readably instead of crashing. Verified against both the minified and the pretty-printed fixture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 8e24ff093f |
Add boot-repair: offline recovery for a stale EFI System Partition
On 2026-07-30 a kernel upgrade left the machine in systemd emergency mode on three consecutive boots. The upgrade replaced /usr/lib/modules/<old> with <new>, but the new kernel never reached the ESP, so the firmware kept booting the OLD vmlinuz whose module tree had just been deleted. Nothing modular could load after pivot-root — including vfat, which meant /boot itself could not be mounted, local-fs.target failed, and systemd dropped to an emergency shell. pacman reported the transaction as successful. Root cause: pacman-git with ParallelDownloads > 1 segfaults every forked alpm hook child, silently skipping 90-mkinitcpio-install and depmod. boot-repair is self-contained (no repo libs — it must run in the emergency shell) and fully offline (missing modules are recovered from /var/cache/pacman/pkg). It repairs by default; --dry-run inspects. It detects and fixes: read-only root, noauto/nofail fstab drift, orphaned kernel files shadowing an unmounted ESP, unloadable vfat, unmounted ESP, missing modules.dep, ESP kernel != installed tree, stale initramfs, and ParallelDownloads > 1. Two pacman hooks make the failure impossible rather than merely recoverable: 05-boot-mounted-guard aborts a kernel transaction when the ESP is unmounted (AbortOnFail), and 99-boot-autorepair fixes ESP drift after the transaction, before the machine is ever rebooted. Safety: a module tree counts as usable only if it has BOTH vmlinuz and kernel/, so the metadata-only wreckage this bug leaves — which sorts newer — is never chosen as the target. Files under /boot are deleted only while it is not a mountpoint AND contains no EFI/ or loader/; deleting after mounting would destroy the real kernel. Verified: - 31/31 fixture tests (--root against throwaway trees, no privileges) - 11/11 reversible live tests on the real machine: unmounted the real ESP, planted shadow files, confirmed they were deleted and the ESP then mounted with EFI/, loader/ and kernel 7.1.5-arch1-2 intact - Gate proven against a real transaction with the ESP unmounted: "failed to commit transaction", no packages upgraded, nothing written - shellcheck + shfmt clean, pre-commit passing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 9d7751cebb |
kcd2 dice solver: serve it at dice.kuhy.duckdns.org, fit all 43 dice, move inventories between devices
The solver only ran behind ./run.sh, had one media query in the whole
stylesheet, and kept the inventory in a 46vh scroll box that showed maybe a
third of the 43 dice. Setting it up on the PC did nothing for the phone.
Deployment
dice.kuhy.duckdns.org, behind the existing gitea-caddy edge. DuckDNS already
wildcards *.kuhy.duckdns.org (checked against 1.1.1.1 and 8.8.8.8), so no
dashboard step. deploy_build.sh builds to dist.staging and rsyncs into dist/
with --delete-after --delay-updates; plain --delete removes stale assets
before writing the new ones, which 404s a client mid-load. A post-commit hook
starts a MemorySwapMax=0 systemd user unit, so a rebuild can neither slow a
commit nor thrash zram.
Three traps found by driving the live URL in a browser rather than curling it:
a site address of "127.0.0.1:8089" reads as a HOSTNAME, so Caddy enabled
auto-HTTPS and started an HTTP->HTTPS redirect listener that bound *:80 next
to the edge -- SO_REUSEPORT would then split inbound HTTP and intermittently
break ACME for every domain here. The same address also made a Host matcher,
so the edge's forwarded Host matched nothing and served an empty 200 while the
local health check passed. And watching dist/'s own inode could never detect a
detached bind mount: rsync into an existing directory never changes it. Each
now has a guard that asserts positively rather than failing open.
Dice list
All 43 on screen at every width, no inner scrollbar. Column count and size
tier are chosen together from a container query on the panel -- not the
viewport, which mispredicts by 2x at the point the columns split. Two
invariants govern it: the list fits above the fold, and the name column stays
wide enough to tell Painter's die B, G and R apart. The left column holds the
grid alone, so it is sized to fill the height it owns: one --die-row value
drives the type, buttons and bar strip, giving a 52px row and 44px buttons on
a 1296px-tall screen and 20px rows on a phone.
Three safe-design rules are broken inside .die-list deliberately: sub-16px
body text, square icon buttons, sub-8px intra-cell gaps. 43 items with a name,
a chart and a counter in one viewport is arithmetically incompatible with 16px.
Import / export
A file, the clipboard, or a link carrying the inventory in the URL fragment.
One validator behind all four entry points including localStorage; it keeps
what it can and reports what it dropped, so a die added in a later game patch
costs you that entry, not the inventory. A link never silently overwrites what
is saved here -- it offers Keep or Discard, and only a link that actually
carries dice is applied at all, because one encoding a count of zero parsed
cleanly, blanked the screen, disabled Clear and left Keep writing the emptiness
over the visitor's own loadout.
session_autopsy
parse.py (551) and report.py (504) were over the repo's 500-line limit, so
test_file_length has been failing on main since
|
|||
| 071963b92b |
Fix: stop the Flutter SDK reading your own repo as itself inside git hooks
`git commit`/`git push` export GIT_DIR to every hook they run, and the
flutter tool shells out to git to identify its own SDK. `git -C
"$FLUTTER_ROOT" rev-parse HEAD` in bin/internal/shared.sh does not beat an
inherited GIT_DIR -- `-C` only changes directory, GIT_DIR overrides
repository discovery outright:
$ git -C /opt/flutter rev-parse HEAD
058e0af2c2... <- Flutter's HEAD
$ GIT_DIR=~/diet-guard/.git git -C /opt/flutter rev-parse HEAD
84bd550331... <- diet-guard's HEAD
So inside a hook, flutter reports the project as the SDK:
Flutter 3.47.0-0.1.pre • channel main • https://github.com/kuhyx/diet-guard
pub then resolves the SDK as 0.0.0-unknown and every version constraint
becomes unsatisfiable. flutter also writes that into its own version cache,
so the wrong answer outlives the hook and follows the next caller into an
unrelated repository -- which is why the identical command passes when you
re-run it by hand and fails again on the next push.
diet-guard's pre-push gate hit this today. Clearing the variables per call in
each hook works, and screen-locker and diet-guard now do, but only for as
long as every future hook author remembers -- deployment hygiene, not a fix.
This installs a wrapper ahead of /usr/bin on PATH that strips them for every
caller in every repo, covering both flutter and dart. It removes its own
directory from PATH before exec so the Flutter entry point's internal `which
flutter` cannot loop back into it, and it verifies itself by running flutter
against a throwaway poisoned GIT_DIR and failing unless the real SDK comes
back.
Not patching bin/internal/shared.sh: that file belongs to the flutter-bin
package, so the next update would silently restore the bug. Upstream already
unsets these variables in content_aware_hash.sh and update_engine_version.sh,
just not in the version path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 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> |
|||
| 836f4ef2de |
Add network diagnostics: duty-cycle and line-speed probes
Diagnosing "Steam downloads are slow" kept pointing at the ISP because every averaged measurement showed a steady ~170 Mbps. Sampling at 1s instead of 5s showed that plateau does not exist: the link alternates between ~450 Mbps bursts and multi-second stalls. Effective rate is burst x duty_cycle, and only the second factor was wrong. steam-download-duty.sh reports those two factors separately, which is what the Steam UI cannot do. line-speed-probe.sh separates burst from sustained throughput, because a single stream tops out near 180 Mbps on this line and reads convincingly as ISP shaping when it is not — a many-connection swarm reaches 986 Mbps with zero retransmits. Arithmetic is extracted into pure helpers with main() behind a BASH_SOURCE guard so the bats suite can source them without starting a measurement. install.sh installs bats and links both scripts onto PATH. Note: no CI workflow runs .sh/.bats tests, including the four shell tests that predate this one, so the suite is manual-only for now. |
|||
| 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
|
|||
| 2a304691cb |
fix(pacman): wrap makepkg so an orphaned db.lck can't hang makepkg -i
Vendored makepkg's run_pacman() waits on /var/lib/pacman/db.lck existence
with no timeout BEFORE invoking pacman, so an orphaned lock hangs
`makepkg -i` forever ("Pacman is currently in use, please wait...") and
the pacman wrapper's stale-lock cleanup never gets a turn. Wrap makepkg
(mirroring the pacman-wrapper pattern) to clear an orphaned lock up front.
- Extract stale-lock logic into shared pacman_lock_lib.sh; pacman_wrapper
sources it after integrity verification and it is in the SHA256 manifest
- makepkg_wrapper: pre-flight clear for -i/--install; bypass on FAKEROOTKEY
and non-install; FAIL OPEN if the lib is missing (never brick makepkg)
- Cross-user pacman_process_running() pgrep guard: unprivileged makepkg can
detect a root pacman its own fuser cannot see (avoids removing a live lock)
- Upgrade survival: rewrap hook (Install/Upgrade only) + check_makepkg_wrapper
drift verifier + both wrappers wired into the hourly periodic driver
- Fix reorg path bugs: stale ~/linux-configuration periodic path,
$CONFIG_DIR/hosts -> scripts/periodic_background/hosts, test WRAPPER_DIR;
refresh .orig only when target isn't already our symlink (no exec loop)
- Tests: test_makepkg_wrapper.sh (18) incl. hook Operation validity; add
lib-integrity coverage to test_pacman_wrapper_security.sh (also path-fixed)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
|
|||
| 0bdaecd281 |
Add personal website deploy + move Gitea to a subdomain
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Serve the personal website (extracted to github.com/kuhyx/personal-website) at the root kuhy.duckdns.org behind the shared gitea-caddy edge, moving Gitea to gitea.kuhy.duckdns.org. - setup_gitea.sh: relocate Gitea to gitea.kuhy.duckdns.org and refactor the shared Caddyfile into per-service sites/*.caddy imports (fixes the latent bug where re-running dropped the dufs/syncyomi blocks). - setup_personal_website.sh (new): idempotent setup/status/help; migrates existing Caddy blocks to snippets, relocates Gitea, builds ~/personal-website, serves dist/ from a loopback caddy (admin off) on 127.0.0.1:8088 fronted on the root domain, reuses the DuckDNS/firewall edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KG7yQj4hP8DDpasK2e4cn7 |
|||
| 59430f91a1 |
Add setup_syncyomi.sh: self-hosted SyncYomi manga sync
Stands up SyncYomi (ghcr.io/syncyomi/syncyomi:v1.1.11) in Docker behind the existing shared gitea-caddy edge at kuhy-sync.duckdns.org, so TachiyomiSY syncs its library from a mobile network. Follows the setup_dufs_cloud.sh pattern: loopback-bound container, one appended Caddy site block (auto Let's Encrypt), the shared DuckDNS updater extended, and 80/443 opened via setup_wireguard_ssh.sh. Auto-generates stored 0600 credentials, onboards the first account, and mints/reuses an API key over the local REST API, printing host + key. Idempotent setup/status/help. Verified live: container up (restart=unless-stopped), external HTTPS reachable, and a real phone sync landed a 13.9 MB sync_data row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbvNNUYcdNtg5NjAhYc12E |