"""The ``earner-bonus.path`` unit: apply a flat bonus the moment it is earned. The live bonus pass used to run only on the 15-minute ``workout-sync`` timer, so a LeetCode solve could wait a quarter of an hour for its shutdown hour while the gaming hour landed in seconds (2026-10-06). The path unit watches every flat earner's ledger and starts ``earner-bonus.service`` (``--apply-bonuses``) when a gate writes one. The watch list is rendered from the ``earned_time`` registry, not kept by hand. Re-pinning the registry is a pip install that never re-runs the installer, so :func:`sync_path_unit` -- called from every ``--sync-only`` pass -- rewrites an installed unit that has drifted from the registry. A new earner is watched within 15 minutes with no step to remember. ``PathChanged=`` and not ``PathModified=``: the gates write a temp file and rename it into place, which ``PathChanged`` sees, including the first write into a directory that does not exist yet. ``PathModified`` would also fire mid-write on a gate that writes in place, and a half-written ledger reads as "could not check". """ from __future__ import annotations import logging from pathlib import Path import subprocess import sys from typing import Final from screen_locker._earned import ledger_file, watched_earners __all__ = ["PATH_UNIT_NAME", "USER_UNIT_DIR", "render_path_unit", "sync_path_unit"] _logger = logging.getLogger(__name__) PATH_UNIT_NAME: Final = "earner-bonus.path" USER_UNIT_DIR: Final = Path.home() / ".config" / "systemd" / "user" _HEADER: Final = """\ # Generated by screen_locker._earner_units from the earned_time registry. # Do not edit: the next --sync-only pass rewrites a unit that has drifted. [Unit] Description=Apply a flat earner's shutdown bonus as soon as its ledger changes [Path] """ _FOOTER: Final = """ [Install] WantedBy=default.target """ def render_path_unit() -> str: """The unit text: one ``PathChanged=`` per gate earner's ledger. Every registry's gates, not just today's (``watched_earners``), so a dated cutover (the Automation tutor's ``TUTOR_FROM``) needs no re-render on the day. The header keeps saying "flat": changing it would rewrite the installed unit for no behavioural change. """ ledgers = dict.fromkeys(ledger_file(e) for e in watched_earners()) watches = "".join(f"PathChanged={path}\n" for path in ledgers) return _HEADER + watches + _FOOTER def sync_path_unit(unit_dir: Path | None = None) -> bool: """Rewrite an installed path unit that no longer matches the registry. An absent unit is left absent: it was never installed, or it was removed on purpose, and installing units is the installer's job. *unit_dir* defaults to :data:`USER_UNIT_DIR`, read at call time. Returns True if the unit was rewritten. """ unit = (unit_dir or USER_UNIT_DIR) / PATH_UNIT_NAME if not unit.is_file(): return False wanted = render_path_unit() try: if unit.read_text(encoding="utf-8") == wanted: return False unit.write_text(wanted, encoding="utf-8") subprocess.run( ["/usr/bin/systemctl", "--user", "daemon-reload"], check=True, capture_output=True, ) except (OSError, subprocess.CalledProcessError) as exc: _logger.warning( "%s differs from the earner registry and could not be refreshed: %s" " -- a newly registered earner waits for the 15-minute sync", unit, exc, ) return False _logger.warning( "%s was out of date with the earner registry; rewritten and reloaded", unit, ) return True if __name__ == "__main__": # install_systemd.sh / arm.sh: write the unit into the given directory. target = Path(sys.argv[1]) / PATH_UNIT_NAME target.write_text(render_path_unit(), encoding="utf-8")