From 4c7fa8f81289bebeca0442291ae69de106c3446b Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Mon, 10 Aug 2026 02:31:33 -0400 Subject: [PATCH] Purge stale contacts: a one-day rung at the top of the ladder The tightest rung was a week, which is no help to a device whose contact table filled up in one busy afternoon. Nothing else moves: the rung sweeps only contacts actually heard and since gone quiet, the never-heard bucket still stands apart, and the typed `delete` gate still names the span. Co-Authored-By: Claude Opus 5 (1M context) --- meshterm/ui/contacts_screen.py | 3 ++- tests/test_device_config.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/meshterm/ui/contacts_screen.py b/meshterm/ui/contacts_screen.py index a11b673..1c41ed6 100644 --- a/meshterm/ui/contacts_screen.py +++ b/meshterm/ui/contacts_screen.py @@ -11,7 +11,7 @@ and backing out of that page returns to the list right where it stood. The one-s :func:`~meshterm.ui.widgets.contacts_table`; only the menu gets the live list. Below the contacts sits one maintenance action — **Purge stale contacts** — that sweeps out -the contacts a device has accumulated but no longer hears. It offers an age ladder (a week up +the contacts a device has accumulated but no longer hears. It offers an age ladder (a day up to a year, plus a *never heard* bucket), each rung showing a live count of how many contacts it would remove, so the impact is visible before anything is chosen; the pick is then gated behind a typed ``delete`` confirm. A purge deletes the contacts from the device's table and @@ -55,6 +55,7 @@ _DAY = 86400 #: bucket, so an age rung never quietly deletes a freshly-added contact that hasn't had time #: to advert yet. PURGE_RUNGS: tuple[tuple[str, int], ...] = ( + ("Older than 1 day", _DAY), ("Older than 1 week", 7 * _DAY), ("Older than 1 month", 30 * _DAY), ("Older than 3 months", 90 * _DAY), diff --git a/tests/test_device_config.py b/tests/test_device_config.py index 624687b..733441d 100644 --- a/tests/test_device_config.py +++ b/tests/test_device_config.py @@ -327,11 +327,16 @@ def test_purge_buckets_split_stale_from_never_heard() -> None: now = utcnow() fresh = Contact(name="Fresh", public_key="aa" * 32, last_seen=now - timedelta(hours=1)) + quiet = Contact(name="Quiet", public_key="ee" * 32, last_seen=now - timedelta(days=3)) old = Contact(name="Old", public_key="bb" * 32, last_seen=now - timedelta(days=45)) ancient = Contact(name="Ancient", public_key="cc" * 32, last_seen=now - timedelta(days=400)) unheard = Contact(name="Unheard", public_key="dd" * 32) # no last_seen - contacts = [fresh, old, ancient, unheard] + contacts = [fresh, quiet, old, ancient, unheard] + # A day catches everything heard but not heard *today* — the tightest rung the ladder + # offers (JP, 2026-08-10), so a contact three days quiet is swept where the week rung + # would still have kept it. + assert {c.name for c in stale_past(contacts, _DAY)} == {"Quiet", "Old", "Ancient"} # A week catches the two aged ones but not the fresh — and never the never-heard one. week = {c.name for c in stale_past(contacts, 7 * _DAY)} assert week == {"Old", "Ancient"}