Commit Graph

781 Commits

Author SHA1 Message Date
896d7b1d15 Run Arch-only shell tests in a container; harden worktree stubbing
Adds an arch-tests job on archlinux:base-devel so the pacman wrapper,
makepkg wrapper and hosts-guard tests finally run somewhere. They need
an Arch userland and root, which a disposable container provides.

Four more tests still referenced pre-reorganisation paths and could
never find their targets; repairing them took the container set from
two passing to five. All five were verified with --network=none, so
the job installs nothing beyond git (base-devel ships no git, and
actions/checkout needs it).

Also removes the silent-failure mode behind the earlier
music_parallelism confusion. The test now derives its temp-worktree
path from the script's real position instead of hardcoding the depth,
checks up front that ../../lib/common.sh resolves to the stub, and has
the stub write a marker asserted after every run. Previously a
reorganisation made the script source the real
/usr/local/lib/common.sh, so the test measured the live system and
failed as though the daemon were broken.

Three tests stay out, each for a different reason, documented in the
workflow: test_i3blocks_efficiency.sh needs strace and could not be
verified locally (containers here have no DNS);
test_hosts_guard_pacman_integration.sh fails a real assertion worth
investigating; test_security_hardening.sh asserts live host state and
can never pass in a fresh container.
2026-07-19 19:27:36 +02:00
e7c3300e80 Repair two stale shell tests; CI now runs all six
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Both tests excluded from CI last commit turned out to be stale rather
than catching regressions. Neither covered script needed changing.

test_music_parallelism.sh built its temp worktree at
scripts/digital_wellbeing/, but music_parallelism.sh resolves its
library as $SCRIPT_DIR/../../lib/common.sh — two levels up, correct
for the real scripts/periodic_background/digital_wellbeing/ location.
From the shallower path that resolved outside the worktree, so the
stubbed common.sh was never sourced and the script fell through to the
real /usr/local/lib/common.sh on the machine. The test was exercising
the live library and ignoring its own MOCK_FOCUS_ACTIVE, which is why
idle mode looked like it waited 15s instead of 30s. The daemon was
correct all along.

test_usage_monitoring_installer_efficiency.sh demanded a literal
'sleep 60'. The logger template no longer polls: it sleeps once until
midnight via seconds_until_next_day, then blocks on wait for the pmon
child — strictly fewer wakeups than the assertion required. Replaced
with assertions on that property so a return to polling still fails.

Allowlist extended from four tests to six.
2026-07-19 19:13:51 +02:00
93a466c8e4 Run shell tests in CI and repair three broken by a reorg
The shell tests under linux_configuration/tests/ never ran anywhere:
no workflow executed them, so they rotted silently. Three referenced
script paths that moved under scripts/periodic_background/ in an
earlier reorganisation, and test_android_guardian_service.sh created
its temp worktree at a different path from the one it copied into, so
its cp could never succeed.

The new workflow runs the bats suites plus an explicit allowlist of
side-effect-free tests. Roughly eight others drive pacman, /etc,
systemd or adb and need root plus an Arch container, so they stay out.

Repairing the paths uncovered two genuine failures, deliberately left
out of the allowlist because they look like real regressions in the
scripts they cover rather than stale assertions:

- test_music_parallelism.sh expects idle mode to wait
  IDLE_CHECK_INTERVAL (30s); the daemon waits 15s, so it wakes twice
  as often as intended when idle.
- test_usage_monitoring_installer_efficiency.sh expects the logger
  template to sleep between day-rollover checks.

Both need a fix in the covered scripts, not in the tests.
2026-07-19 19:06:23 +02:00
1b7a364e64 requirements-sync: map bs4 and exempt foreign-runtime imports
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Two fixes the checker needs for archwiki_rag, left uncommitted in the
working tree and therefore stashed away by pre-commit on every push:

- bs4 resolves to the beautifulsoup4 distribution; without the mapping
  the checker reports it as undeclared.
- mcp_server is resolved by the knowledge-rag uv tool venv at runtime,
  not by CI's `pip install -r requirements.txt`. Declaring it would be
  wrong, so it is exempted via _FOREIGN_RUNTIME_IMPORTS with a comment
  naming the owning venv.

Not authored in this session; committed because the pre-push CI mirror
cannot pass while these live only in the working tree.
2026-07-19 18:58:35 +02:00
90305e03c2 Declare markdownify dependency for archwiki_rag
python_pkg/archwiki_rag/convert.py imports markdownify, but the
declaration was left uncommitted in the working tree. Because
pre-commit stashes unstaged changes before running the pre-push gate
while untracked files stay on disk, the gate saw code importing an
undeclared package and failed requirements-sync on every push.

Committing the declaration makes the tree self-consistent. The
archwiki_rag package itself remains untracked and unaffected.
2026-07-19 18:55:27 +02:00
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.
2026-07-19 18:46:18 +02:00
56535695c6 Add change explanation: shared-app-icon-generator
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Walkthrough of the app_icons package with an interactive micro-world that
steps the centring pipeline (draw, measure ink, compute offset, translate)
on the real glyph data, including the oversized cloud that failed the
safe-box check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:16:10 +02:00
a92cf7f226 Format app_icons README with prettier
The repo's pre-push prettier gate rewraps the markdown table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:03:25 +02:00
27f44cad4b Add shared launcher-icon generator for the Flutter apps
Every Flutter app still shipped the stock Flutter "beach" launcher icon, and
none had an adaptive-icon descriptor at all. Rather than hand-drawing five
one-off icons, define the family once and let each app contribute only an
accent colour and a glyph.

The family: a charcoal #1B1D21 field carrying one glyph in that app's own
theme seed colour, at a single stroke weight. Glyphs are optically centred
automatically - render.centre_offset rasterises a glyph alone, measures its
ink box, and translates (never scales) the fragment, so a row of icons lines
up while every icon keeps the exact family stroke weight.

safe_box_overflow warns when a glyph exceeds the 560px adaptive-icon safe
box; it caught three real overflows that a launcher mask would have clipped.

The registry points dufs_client at ~/dufs-cloud/app: ~/dufs_client was a
stale, remote-less checkout with an identical package name, since deleted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 16:55:48 +02:00
071daea982 Add change explanation: remove-focus-mode-daemon
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 13:23:02 +02:00
937c597c80 remove(digital-wellbeing): delete unusable focus-mode daemon entirely
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
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>
2026-07-19 13:15:04 +02:00
632f028537 Add change explanation: claude-tty-fix-hardening
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Interactive walkthrough of commit b57a130 (precmd mouse-heal, always-on
--debug-file logging, on-demand MCP launchers) with a suspend-vs-exit cleanup
simulator and a debiased quiz. Regenerates the explanations index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZ15AWWYN8xGKdwXKn7foM
2026-07-18 16:03:48 +02:00
b57a130e9b claude-tty-fix.zsh: precmd mouse-heal, debug logging, on-demand MCP launchers
Diagnosed the "claude keeps crashing + mouse-command garble" as three stacked
bugs (remoteControlAtStartup end_session collision, SIGTTIN suspend on early
input, and an upstream REPL-exits-on-startup-input). This carries the shell-side
mitigations and the tooling that caught them:

- _claude_mouse_heal precmd hook: disables mouse tracking before every prompt,
  so garble left by a *suspended* claude clears on the next Enter (the on-exit
  reset in claude() can't fire while the function is blocked in a suspend).
- Always-on --debug-file logging in claude() (~/.claude/logs, keeps last 20;
  opt out with CLAUDE_DEBUG_LOG_OFF=1) -- this captured all three root causes.
- On-demand launchers claude-rag/-travel/-sports/-media/-extras load the niche
  MCP servers via --mcp-config, keeping them out of the always-on global fleet.
- claude-debug raw-binary probe (note: zsh printf has no %(...)T directive).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZ15AWWYN8xGKdwXKn7foM
2026-07-18 15:55:28 +02:00
176d17ba3a Add change explanation: night-lockdown-wake-fixes
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4YWZhDcEUMUi8cyw9AzTR
2026-07-18 07:03:11 +02:00
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
2026-07-18 06:51:26 +02:00
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
2026-07-18 06:44:17 +02:00
189cd73747 Add change explanation: claude-tty-fix-wrapper
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Interactive walkthrough of the claude() zsh wrapper (SIGTTIN / job-control
prevention + /dev/tty self-heal), with a startup-race simulator and quiz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fwBQdNPJPgKR3gcDZLRjo
2026-07-18 06:03:31 +02:00
4be57f0996 feat(zsh): add claude() wrapper to survive early terminal input
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Typing during Claude Code's ~5s startup triggered a SIGTTIN job-control
stop ("suspended (tty input)") that left mouse tracking / terminal probes
enabled, spamming SGR mouse (35;44;41M) and DSR (997;1n) sequences.
Upstream bug, closed "not planned" (anthropics/claude-code #7807 #11898
#23581 #50032); no flag/env disables the probes.

Wrapper runs claude with job-control monitoring off (nomonitor) so it
can't take SIGTTIN, and resets mouse/focus/paste/cursor modes to /dev/tty
on exit as a self-heal. Opt-out via CLAUDE_TTY_FIX_OFF=1. Auto-loaded via
an oh-my-zsh custom symlink (same pattern as calc-live.zsh); .zshrc
untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fwBQdNPJPgKR3gcDZLRjo
2026-07-18 05:53:16 +02:00
e19f889989 chore: remove byox_ladder — extracted to its own repo
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Has been cancelled
Moved to https://github.com/kuhyx/build-your-x with full git history, where it
gained a `byox` progress tracker (crdt_sync-backed) and the builds themselves.

- git rm -r python_pkg/byox_ladder/
- CLAUDE.md: add build-your-x to the extracted-repos list
- .pre-commit-config.yaml: drop the now-stale byox_ladder codespell exclude

No remaining package imports byox_ladder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nw9Vw8ufxLjkDox7iXPMH3
2026-07-17 20:06:14 +02:00
577efc0f6b docs: add MCP server security-triage shortlist
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Tiered Keep/Conditional/Skip evaluation of the 33 pasted MCP servers,
weighted on trust-surface then maintenance then workflow fit. Rendered
as a self-contained, theme-aware HTML dashboard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UudRq5TbiBGUK2yz8wviFV
2026-07-17 19:16:31 +02:00
32f0877eff Add change explanation: byox-ladder
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Interactive walkthrough of the byox_ladder difficulty generator (commit 6342eaf):
background, the category-tier + language-nudge scoring model with a live widget,
a grouped code tour, and a debiased quiz. Refreshes the explanations gallery index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01QNRTPNYvoBTQoTEWkYtCJD
2026-07-17 18:42:50 +02:00
6342eaf0e2 feat(byox_ladder): add build-your-own-x difficulty ladder generator
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Ranks the codecrafters build-your-own-x projects by difficulty and emits two
offline, self-contained HTML pages: a per-guide ladder (~360 tutorials) and a
per-category ladder (30 categories), grouped into Beginner/Intermediate/Advanced/
Expert tiers plus an Unsorted bucket. A guide inherits its category's tier;
language only nudges order within a tier. Includes a Makefile pipeline
(fetch -> parse -> build) and 100%-branch-covered tests. Excludes the generated
guide page from codespell since it inlines upstream titles verbatim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01QNRTPNYvoBTQoTEWkYtCJD
2026-07-17 18:31:21 +02:00
678098e7e5 Add change explanation: brother-printer-status-truth
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Interactive walkthrough of a12e696: why a sleeping printer was reported as
a critical paper jam, and how the status table was rebuilt from the HP PJL
reference. Includes a live PJL-code decoder (old table vs new code vs spec)
and a debiased quiz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RG3eEn8rwZbcXaJrKJxdv8
2026-07-17 17:46:24 +02:00
e0f62d699a fix(wellbeing): split focus_mode_daemon to satisfy the 500-line limit
focus_mode_daemon.py has been 509 lines since 22e22c1, over the repo's
cap. It went unnoticed because the Python tests workflow only triggers on
python_pkg/** changes, so that commit never ran CI; the next commit to
touch python_pkg/ exposed it and turned main red.

Extract configuration, logging, notification, /proc process inspection and
the killers into _focus_mode_lib.py, leaving the daemon its policy: the
whitelist, the FocusMode state machine, and main(). log/notify move with
the process code that calls them, so the dependency runs one way and there
is no cycle. The helper is underscore-prefixed and registered in pylint's
init-hook path, mirroring the existing usage_report.py / _usage_report_*.py
sibling-import precedent.

The daemon runs from a symlink in /usr/local/bin, so a sibling import only
works if Python resolves that symlink before setting sys.path[0]; verified
that it does, and the installer symlinks rather than copies, so the sibling
travels with it. Confirmed against the running service: restarts clean and
resumes enforcing, process detection unchanged, status output matches the
pre-split version.

Structural only; no behaviour change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RG3eEn8rwZbcXaJrKJxdv8
2026-07-17 17:36:30 +02:00
69242e4bb3 fix(brother-printer): split oversized files, fix order-dependent entry test
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
a12e696 turned CI red two ways, both missed locally because the suite was
run for the brother_printer package alone while CI runs everything in a
random order.

The repo caps Python files at 500 lines and cups_service.py had reached
659. Rather than cut it arbitrarily, extract consumable life tracking -
page counters, replacement baselines, the scale migration, dropped-page
detection - into consumables.py. The seam is real: none of it depends on
CUPS service control, and cups_service needs only estimate_consumable_life
for its fallback query, so the dependency runs one way. The test files are
split along the same seams.

test_main_entry passed or failed depending on execution order: runpy warns
when the target module is already in sys.modules, a sibling test imports
it, and pytest is configured with filterwarnings=error. Drop the module
before running it so the test no longer depends on who ran first.

Structural only; no behaviour change. 692 tests, 100% branch coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RG3eEn8rwZbcXaJrKJxdv8
2026-07-17 15:06:25 +02:00
a12e696494 fix(brother-printer): report the truth, not invented status codes
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
./run.sh failed outright with ModuleNotFoundError, and once running it
reported a sleeping printer as a critical paper jam. Both are fixed, plus
the defects found while verifying them against the real HL-1110.

Entry point: run.sh ran the script by path, so the absolute
python_pkg.* imports could not resolve. Resolve the repo root and pass
PYTHONPATH as a sudo argument, since sudo's env_reset strips an exported
one. __main__.py had the wrong import prefix and ran main() on import.

Status codes: the table claimed provenance from a "Brother PJL Technical
Reference" that does not appear to exist, and 23 of its 25 entries were
wrong. Rebuilt from the HP PJL Technical Reference Manual, Appendix D,
and verified against six states induced on real hardware (10001 READY,
10003 WARMING UP, 10023 PRINTING, 40000 SLEEP, 40021 TOP COVER OPEN,
41213 NO PAPER) - all six matched the reference, none matched the table.
40000 is SLEEP MODE, which the reference explicitly flags "not an error";
the real jam code 40022 was absent entirely. The inverse also bit: 10006
is TONER LOW, previously shown as a benign "Processing". Codes in no
reference are dropped and now report "Unknown status - check printer
display" rather than a confident wrong answer, and states we infer
ourselves no longer borrow PJL numbers to describe themselves.

Page counts: estimates came from the CUPS page log, which only sees jobs
CUPS logged and counts pages the printer never printed (it logged
total=50 for a job that printed 3). Take them from @PJL INFO PAGECOUNT
instead, migrating stored baselines onto the printer's scale once so the
reported percentages do not jump.

Reading status must not cost anything: a blocking open() hung forever
whenever the printer was unwell - precisely when a status report is
wanted - and the port-status probe stopped CUPS and reset the USB device,
destroying the in-flight job and then misreporting the damage as a
printer error. The fd is now non-blocking with deadline-bounded writes,
and the probe reads the same byte via the free usblp ioctl, skips
entirely while a job prints, and re-attaches the kernel driver.

Dropped pages: the HL-1110 silently discards pages whose 600dpi raster
exceeds its ~1MB memory, stays READY, and CUPS still calls the job
successful - so nothing anywhere reports the failure. Compare CUPS's
claim against the printer's counter between jobs and name the workaround
(lp -o Resolution=300dpi).

402 tests, 100% branch coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RG3eEn8rwZbcXaJrKJxdv8
2026-07-17 14:49:23 +02:00
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 22e22c1 sat in the repo while
a months-old copy at /usr/local/bin/focus-mode-daemon went on entering
GAMING mode and killing browsers. It only surfaced because the deployed
copy was checked by hand.

Link instead of copy, so editing the repo file IS deploying it. Also
chmod +x the source, since a symlink takes its executability from the
target rather than from an install mode.

uninstall now guards with [[ -e || -L ]] rather than [[ -f ]]: both -f
and -e follow the link and report false when the target is gone, which
is exactly the case (repo moved or deleted) where a stale link most
needs clearing.

The link resolves into the user's checkout, so /usr/local/bin/... is only
as trustworthy as that path. Fine here - focus-mode runs as a systemd
*user* service, crossing no privilege boundary. Not a pattern to copy for
anything root runs.

Verified: stat -L reports the same inode (92274745) for the deployed path
and the repo file; appending a marker to the repo file makes it appear at
/usr/local/bin/focus-mode-daemon immediately, with no reinstall; a link
to a missing target is now removed by uninstall where [[ -f ]] skipped it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rN2usTS3pYLJqeVg2ZpgR
2026-07-17 14:02:33 +02:00
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
2026-07-17 13:55:24 +02:00
147ec3f690 Add change explanation: night-lockdown
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Interactive walkthrough of 3ebb56c: why the curfew's power-off was swapped
for a GUI lockdown that leaves the servers running, and the four bugs that
surfaced (wake alarm losing its resume trigger, the 05:00 off-by-one, the
DisplayPort link death, and openrgb's CLI exiting 0 while doing nothing).

Includes a micro-world that drives the schedule clock so the -le vs -lt
divergence at 05:00 is visible rather than described.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dpu8SqqBVm97tCuTGE3nCs
2026-07-17 09:27:23 +02:00
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
2026-07-17 09:09:54 +02:00
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
2026-07-17 08:58:43 +02:00
cc2fcc44d3 fix(checker): stop reporting healthy services as errors; drop thesis_tracker
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
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
2026-07-17 08:32:55 +02:00
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
2026-07-17 08:25:51 +02:00
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
2026-07-17 08:17:51 +02:00
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 6e3d61d (same fix for setup_midnight_shutdown.sh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
2026-07-17 08:06:46 +02:00
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
2026-07-17 07:55:07 +02:00
2a304691cb fix(pacman): wrap makepkg so an orphaned db.lck can't hang makepkg -i
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
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
2026-07-17 07:46:31 +02:00
93308e15ab Add change explanation: personal-website-edge
Some checks failed
Pre-commit checks / pre-commit (push) Has been cancelled
Interactive walkthrough (Background → Intuition → Code → Quiz) of the Gitea
subdomain move, the Caddyfile snippet-import refactor, and the admin-off fix,
with a micro-world simulating the config-clobber bug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KG7yQj4hP8DDpasK2e4cn7
2026-07-13 19:18:12 +02:00
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
2026-07-13 19:10:48 +02:00
e262ea1d35 Add change explanation: setup-syncyomi
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Interactive walkthrough (Background → Intuition → Code → Quiz) of
setup_syncyomi.sh, with a Host-routing micro-world and a debiased quiz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbvNNUYcdNtg5NjAhYc12E
2026-07-13 17:29:07 +02:00
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
2026-07-13 17:20:02 +02:00
8aa3e7d962 Add change explanation: tether-hotspot-block
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Interactive /explain-diff walkthrough of the phone_focus_mode hotspot/tethering
enforcer (commit range HEAD~2..HEAD): background on netfilter OUTPUT vs FORWARD,
the second-phone bypass and fix, a steppable FORWARD-chain simulator, and a
debiased 5-question quiz. Refresh the explanations gallery index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q6i3cdhKBoUrKxy4rmx6hH
2026-07-13 10:59:07 +02:00
dae6be2883 phone_focus_mode: prettier-format the README tethering section
Apply repo prettier formatting (table alignment, emphasis markers) to the
README updated in the previous commit, so the pre-push prettier hook passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q6i3cdhKBoUrKxy4rmx6hH
2026-07-13 10:50:48 +02:00
ef892eb162 phone_focus_mode: block hotspot/tethering to close the second-phone bypass
The focus system only ever filtered this phone's own traffic: /system/etc/hosts
via the system resolver, dns_enforcer and the curfew net layer via OUTPUT. A
tethered second phone's packets are FORWARDed + NAT'd through us on a path none
of that covers, so a second phone on the hotspot browses freely on our mobile
data and defeats focus mode.

Add tether_enforcer.sh: an always-on daemon that acts while focus mode is ON
(at home) or a force-test file is present, converging three levers each tick:
  1. disable Android tether offload (settings global tether_offload_disabled=1,
     snapshot/restore) so forwarded traffic goes through netfilter;
  2. blanket REJECT of the FORWARD chain (iptables + ip6tables, FOCUS_TETHER_BLOCK
     pinned at position 1), rebuilt only when tampered via a chain_intact gate so
     it does not fork an `iptables -L` every second (that overheated the phone
     during curfew-net tuning). Covers WiFi, USB and BT tethering;
  3. best-effort `cmd wifi stop-softap` (Android 11+) so the hotspot visibly
     flips off.
Reverts offload + tears down FORWARD on transition away from home / on SIGTERM.

Wiring: config.sh flags, focus_ctl.sh (tether-status/start/stop/log/test-on/off),
magisk_service.sh boot autostart, deploy.sh push/cp/restart. README section and
a working subprocess regression test (lib/tests/test_tether_enforcer.sh, 17/17).

Verified on-device (BL9000, Android 13): our REJECT jump sits ABOVE Android's
tetherctrl_FORWARD chain, tether_offload_disabled=1 took effect, and
`cmd wifi stop-softap` reports "Soft AP stopped successfully".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q6i3cdhKBoUrKxy4rmx6hH
2026-07-13 10:48:59 +02:00
b500b5daba Extract dufs cloud (web + app + scripts) to github.com/kuhyx/dufs-cloud
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Move cloud_gallery/, the Flutter dufs_client app, and the dufs/cloud setup
scripts into a new public repo (with full git history), leaving pointers here.
The live media-cloud-sync systemd units were re-pointed to ~/dufs-cloud/scripts/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
2026-07-12 18:38:37 +02:00
dfd0e2d70a import_media_archives.sh: fold Downloads media_archive zips into the cloud
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
New reusable utility that extracts each media_archive_*.zip snapshot, MOVEs its
media into ~/cloud/Media/YYYY/MM via sync_media_to_cloud.sh (dedup across the
overlapping snapshots), verifies staging drained, then deletes the zip — only
after its media is confirmed in the cloud. Staging lives on the cloud
filesystem so moves are atomic renames.

Ran over ~/Downloads/media_archive_archive: 44 zips / 27 GB → 44 imported +
deleted, 0 kept. The two 13.7 GB near-duplicate snapshots collapsed (2412
duplicates dropped). Cloud Media 4.9 GB → 19 GB (2949 files); Downloads
reclaimed 27 GB; empty archive dir removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
2026-07-12 17:43:23 +02:00
c6f1b85c78 cloud_gallery: fix build, 100% coverage, aggressive lint; whitelist dufs_client
- dufs-client.test.ts: type fetch mocks as vi.fn<typeof fetch>() (fixes the
  TS2493/TS2352 tuple/undefined build errors that broke `pnpm build`)
- dufs-client.ts: explicit method arg drops the unreachable `?? "GET"` branch
- gallery/grid: -1 viewer-index sentinel + pass media entry, removing three
  unreachable defensive guards; onOpenMedia now takes the entry
- use-hash-path: extract serverPath() so the SSR snapshot is covered
- eslint: strict+stylistic type-checked, react-hooks, reportUnusedDisable
  Directives; drop eslint-plugin-react (incompatible with ESLint 10)
- add grid.test.tsx + use-listing.test.ts; coverage thresholds 100/100/100/100
  (62 tests: statements/branches/functions/lines all 100%)
- phone_focus_mode/config.sh: whitelist com.kuhy.dufs_client

Deployed live via setup_cloud_gallery.sh (render-spa); fixed ffmpeg by
renaming a stale /usr/local/bin shadow linking the removed libvpx.so.11;
regenerated thumbnails (10 made, 0 failed). APK installed + launch-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
2026-07-12 17:29:39 +02:00
76d4e8e0cf cloud_gallery: React media-gallery SPA served by dufs (Phase 3)
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Replace dufs's plain file list with a React 19 + Vite + TypeScript SPA served
same-origin by dufs, so the cloud URL becomes a real gallery behind the existing
auth: thumbnail grid, image lightbox (zoom + keyboard nav), inline video, drag/
pick upload, download, confirm-guarded delete, and a simple .txt/.md editor.

Serving architecture (de-risked against the real dufs 0.46.0): --assets does not
override the UI, so dufs `render-spa` serves the app shell, the SPA lists dirs
via WebDAV PROPFIND (render-flag-immune) and streams files via GET (Range for
video seeking), with HashRouter so navigation stays at "/". Verified render-spa
leaves KeePass file GET/PUT/?hash intact (Phase 1 unaffected).

Adds generate_thumbnails.sh (ImageMagick images + ffmpeg video posters →
~/cloud/.thumbs, idempotent) and setup_cloud_gallery.sh (build + deploy the SPA
into the cloud root, enable render-spa, wire thumbnail refresh into the Phase-2
media sync). setup_dufs_cloud.sh now emits render-spa only when the SPA is
deployed, so a fresh dufs keeps its default listing.

Verified: pnpm build + lint (strictTypeChecked) clean; 38 vitest tests, coverage
92.8% stmts / 96% lines (dufs-client 100%); local dufs --render-spa serves shell
+ assets + thumbnails + PROPFIND (all 200). Live deploy left for the user (needs
sudo; enables render-spa on the public service). Note: host ffmpeg is broken
(libvpx.so.11) so video posters are skipped until it's fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
2026-07-12 06:05:52 +02:00
095587c0de linux_configuration: move Downloads media into the dufs cloud (browsable)
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Phase 2 of the cloud roadmap. sync_media_to_cloud.sh MOVES organized images and
videos out of ~/Downloads into ~/cloud/Media/YYYY/MM (by mtime), unzipped and
browsable via the dufs URL — no duplication, the cloud folder becomes the single
storage. Move is crash-safe (mv: atomic on same fs, copy-then-unlink across fs);
files modified in the last 2 min are skipped (in-progress downloads); identical
files are de-duplicated and name clashes get a numeric suffix.

setup_media_cloud_sync.sh installs a oneshot service + 30-min catch-up timer +
a ~/Downloads path watcher, plus a drop-in ordering the existing media-organizer
after the cloud sync so media is moved before it would be zipped.

Verified: shellcheck clean; 12-assertion MOVE suite on throwaway dirs; real
move of a PNG into ~/cloud/Media fetched via the live URL (HTTP 200 image/png).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
2026-07-12 05:19:31 +02:00
0902a5e1f4 linux_configuration: unified multi-device KeePass sync over dufs
Add "one vault, synced everywhere" for PC + Ubuntu laptop + phone, with an
identical flow and no stored master password. keepass-open prompts the master
password (in memory only), folds in strays, pulls+merges the cloud copy over
WebDAV, opens KeePassXC on ~/Keepass/Passwords.kdbx, and publishes edits on
close. Safety rests on `keepassxc-cli merge` (entry-level union) plus a dufs
?hash compare-and-swap on upload, so concurrent edits merge instead of clobber.

The consolidator enforces exactly one vault per machine: it folds a stray only
if it opens with the same master password, backs up first, never removes the
canonical, and auto-excludes the dufs serve-path (the remote store) — a
data-safety fix after the first real run tried to delete ~/cloud/Keepass.

Only the dufs *server* password (not the vault key) is cached in the keyring.
Single cross-distro installer, no role flag; the phone uses KeePassDX natively.

Verified: shellcheck clean; 8 (reconcile) + 14 (consolidator) + 8 (installer)
assertions on throwaway vaults; real PC apply with live WebDAV GET=200 and
?hash==sha256; a clean keepass-open run ("no strays / already in sync").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
2026-07-12 05:00:27 +02:00