app_icons: daily_limits gets an hourglass in the shared family colours
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

The daily-limits phone app's launcher icon: charcoal #211D1B field,
hourglass glyph in the shared accent #B8862E. Additive only, so every
other app's icon is byte-identical. A registry invariant now refuses a
new app that reuses another app's glyph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcxgxMaQHMSGWzLRpNSkuP
This commit is contained in:
Krzysztof Rudnicki 2026-10-09 12:38:19 +02:00
parent 30721bff7e
commit d792c081e4
7 changed files with 106 additions and 47 deletions

View File

@ -0,0 +1,15 @@
{
"title": "app_icons: daily_limits gets the hourglass glyph in the shared family",
"objective": "The daily-limits phone app ships a family launcher icon (charcoal #211D1B field, shared accent #B8862E) instead of the stock Flutter icon, with no change to any other app's icon.",
"acceptance_criteria": [
"`generate --app daily_limits --android` writes assets/icon and the Android mipmaps for ~/src/daily-limits/app",
"The registry keeps one accent (#B8862E) and one field (style.BACKGROUND) for every app",
"The diff to python_pkg/app_icons is additive only, so every other app renders byte-identically",
"python_pkg/app_icons tests pass at 100% line and branch coverage; pre-commit is clean"
],
"out_of_scope": [
"A per-app background or accent override (decided 2026-10-09: the generator's shared colours are right)",
"Committing the daily-limits app itself"
],
"verifier": ".venv/bin/python -m pytest -q python_pkg/app_icons --cov=python_pkg/app_icons --cov-branch; pre-commit run"
}

View File

@ -0,0 +1,41 @@
{
"intent": "Give the daily-limits phone app an on-style launcher icon: an hourglass (a daily allowance that runs out) in the shared accent on the family charcoal.",
"scope": [
"python_pkg/app_icons/_glyph_art_more.py, glyphs.py: the `hourglass` glyph",
"python_pkg/app_icons/apps.py: the `daily_limits` registry entry",
"python_pkg/app_icons/tests/test_registry.py: no glyph reused across apps; hourglass sand takes the accent marker"
],
"changes": [
"New `hourglass` glyph: capped bars, crossed sides, sand filled in the accent so the monochrome layer recolours it",
"daily_limits registered with accent #B8862E and the default style.BACKGROUND field",
"Invariant test: apps are told apart by glyph, so a new app may not reuse one (track-bars is the pre-existing exception)"
],
"verification": [
{
"command": "PATH=~/sdk/fvm/versions/3.47.7/bin:$PATH python3 -m python_pkg.app_icons generate --app daily_limits --android",
"result": "pass",
"evidence": "daily_limits: wrote 6 asset files; regenerated Android mipmaps; ic_launcher_background #211D1B"
},
{
"command": "phone_deploy.sh ~/src/daily-limits/app (adb install -r on 23181JEGR08034)",
"result": "pass",
"evidence": "Install Success; the APK's mipmap-xxxhdpi ic_launcher.png is byte-identical to the generated one (hourglass in #B8862E on #211D1B); confirmed by kuhy"
},
{
"command": ".venv/bin/python -m pytest -q python_pkg/app_icons --cov=python_pkg/app_icons --cov-branch",
"result": "pass",
"evidence": "47 passed; every python_pkg/app_icons module 100% line and branch"
},
{
"command": "git diff python_pkg/app_icons | grep -c '^-[^-]'",
"result": "pass",
"evidence": "0 removed lines: renderer and other registry entries untouched, so other apps' icons are byte-identical"
}
],
"risks": [
"`generate` runs `dart run`, which needs a Dart SDK satisfying the app's ^3.13.5; the system dart 3.13.3 fails, so put the fvm 3.47.7 SDK first on PATH"
],
"rollback": [
"git revert this commit; daily_limits disappears from the registry and the glyph library"
]
}

View File

@ -1,47 +0,0 @@
{
"intent": "Give a freshly installed machine a working escape hatch from the hosts file-guards. Until now --rollback refused to run on exactly the machines the previous commit created.",
"scope": [
"linux_configuration/fixes/lib/hosts_guard_rollback.sh only.",
"Follow-up to 4c426eae, which made hosts-blocker's install.sh register the guard-lib instances directly.",
"Non-goal: changing what the migration path records; a migrated machine keeps its existing legacy backup and restore behaviour unchanged."
],
"changes": [
"do_rollback no longer exits when /var/lib/guard-lib-migration is absent. That state dir is written by the one-shot migration, but a machine set up by hosts-blocker's install.sh has no legacy layer to back up and therefore no state dir -- so --rollback bailed out while the very instances it exists to remove were live. It now proceeds whenever a guard-lib instance is registered, warns that there is no legacy layer to restore, and still refuses when there is genuinely nothing to roll back.",
"After uninstalling the instances, re-assert chattr +i on /etc/hosts when no legacy unit did it. The existing re-assertion relied on starting hosts-guard.service / hosts-bind-mount.service, which do not exist on a fresh machine, so every systemctl cat skipped and the rollback left /etc/hosts with no immutable flag and no bind mount -- strictly less protected than before the rollback."
],
"verification": [
{
"command": "sudo bash linux_configuration/fixes/migrate_hosts_guard_to_guard_lib.sh --rollback --dry-run (pristine vmbox guest, guards installed by install_core_system.sh --all)",
"result": "pass",
"evidence": "DRYRUN_RC=0, reporting it would uninstall the hosts, nsswitch and resolved instances. Before this change the same command printed 'no rollback state at /var/lib/guard-lib-migration - nothing to roll back to' and exited 1."
},
{
"command": "sudo bash linux_configuration/fixes/migrate_hosts_guard_to_guard_lib.sh --rollback (same guest)",
"result": "pass",
"evidence": "ROLLBACK_RC=0. Uninstalled file-guard hosts, nsswitch and resolved, then logged 're-asserted chattr +i on /etc/hosts (no legacy unit to do it)'."
},
{
"command": "ls /etc/guard-lib/targets; lsattr /etc/hosts; findmnt /etc/hosts; wc -l /etc/hosts (after rollback)",
"result": "pass",
"evidence": "Only shutdown-schedule.conf remains, which belongs to a different feature and must not be touched. /etc/hosts shows ----i----------------- and still holds all 178410 lines; the bind mount is gone, which is the intended post-rollback state."
},
{
"command": "shellcheck -x linux_configuration/fixes/lib/hosts_guard_rollback.sh",
"result": "pass",
"evidence": "Clean. An initial A && B || C form was rewritten as if/else after SC2015 pointed out the C branch can run when A succeeds, which matters because `run` is a no-op under --dry-run."
},
{
"command": "bash ./linux_configuration/fixes/lib/tests/run_all.sh",
"result": "pass",
"evidence": "hosts_guard_rollback: 31 passed, 0 failed; whole suite exits 0. The first attempt failed one case: the refusal message is asserted verbatim by test_hosts_guard_rollback.sh:115, and the new no-instance branch had dropped the trailing 'to'. The wording was restored rather than the assertion loosened, since the test is checking the user-visible explanation."
}
],
"risks": [
"Rollback is now reachable in more situations, so an accidental invocation can unregister live guards. It still refuses when no instance is registered, and it keeps the canonical copy via --keep-canonical, so the file itself is never lost.",
"The re-assertion only covers /etc/hosts. nsswitch.conf and resolved.conf are left to their plugins, matching the behaviour the migration path already had."
],
"rollback": [
"git revert this commit; the previous behaviour (refuse without a state dir) returns.",
"After reverting, confirm with sudo bash linux_configuration/fixes/migrate_hosts_guard_to_guard_lib.sh --status that the instance list matches expectations before relying on it."
]
}

View File

@ -207,3 +207,25 @@ _OPEN_BOOK = """\
<path fill="{{ACCENT}}" stroke="none" d="\
M 232 344 Q 372 290 488 360 L 488 744 Q 372 674 232 728 Z \
M 536 360 Q 652 290 792 344 L 792 728 Q 652 674 536 744 Z"/>"""
# An hourglass with sand still in the top bulb -- daily-limits, today's PC
# shutdown time and gaming budget: a daily allowance that runs out. Chosen over
# a clock-with-a-cap because `clock` is already wake_alarm_sync's glyph.
#
# Geometry, inside SAFE_BOX (ink x=284..740, y=236..788):
# caps bars at y=272 and y=752, x=320..704.
# glass straight sides (352,272)->(512,512)->(352,752) and mirror; the
# sides cross at the neck, so the waist is solid, not a sliver.
# sand filled to the glass centrelines, so it fuses with the outline:
# top bulb from the neck up to y=392, leaving an empty band 84 tall
# and 73+ wide below the cap; bottom bulb a pile up to y=680. Every
# gap clears MIN_NEGATIVE_SPACE=36.
_HOURGLASS = """\
<path d="M 320 272 L 704 272"/>
<path d="M 320 752 L 704 752"/>
<path d="M 352 272 L 672 752"/>
<path d="M 672 272 L 352 752"/>
<path fill="{{ACCENT}}" stroke="none" d="\
M 432 392 L 592 392 L 512 512 Z \
M 400 680 L 624 680 L 672 752 L 352 752 Z"/>"""

View File

@ -211,6 +211,14 @@ APPS: Final[dict[str, AppIcon]] = {
icon_name="book-guard",
linux=False,
),
AppIcon(
key="daily_limits",
repo=_HOME / "src/daily-limits/app",
accent="#B8862E",
glyph="hourglass",
icon_name="daily-limits",
linux=False,
),
)
}

View File

@ -31,6 +31,7 @@ from python_pkg.app_icons._glyph_art import (
)
from python_pkg.app_icons._glyph_art_more import (
_BOWL_STAR,
_HOURGLASS,
_HOUSE_TICK,
_NOTE_ASCENDER,
_OPEN_BOOK,
@ -94,6 +95,7 @@ GLYPHS: Final[dict[str, Glyph]] = {
Glyph("house-tick", "House outline with a tick inside", _HOUSE_TICK),
Glyph("umbrella", "Filled umbrella canopy over a J-handle", _UMBRELLA),
Glyph("open-book", "Open book, two filled pages", _OPEN_BOOK),
Glyph("hourglass", "Capped hourglass with sand still in the top", _HOURGLASS),
Glyph(
"remote-wave",
"Handset with punched buttons under a broadcast arc",

View File

@ -45,6 +45,13 @@ class TestFamilyInvariants:
assert len(app.accent) == 7
int(app.accent[1:], 16)
def test_no_glyph_is_reused(self) -> None:
"""Apps are told apart by glyph; a new app must not borrow one."""
shared_by_design = {"track-bars"} # kuhylog + betting_sim, pre-existing
used = [app.glyph for app in apps.APPS.values()]
reused = {glyph for glyph in used if used.count(glyph) > 1}
assert reused <= shared_by_design
def test_safe_box_fits_inside_canvas(self) -> None:
assert style.SAFE_BOX < style.CANVAS
assert style.MIN_NEGATIVE_SPACE == style.STROKE_WIDTH / 2
@ -64,3 +71,14 @@ class TestFamilyInvariants:
f"{glyph.name} paints style.BACKGROUND; cut the hole with "
f'fill-rule="evenodd" instead'
)
class TestHourglassGlyph:
def test_daily_limits_uses_it(self) -> None:
assert apps.get_app("daily_limits").glyph == "hourglass"
def test_sand_is_filled_in_the_accent(self) -> None:
"""The sand takes the accent marker, so the monochrome layer recolours it."""
body = glyphs.get_glyph("hourglass").body
assert f'fill="{style.ACCENT_MARKER}"' in body
assert 'stroke="none"' in body