From 1c77d8fd8769018cb08cf0b385563488efb07b75 Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Sun, 2 Aug 2026 16:33:23 +0200 Subject: [PATCH] docs(status): note the window the age cannot cover, and read better inside it Review raised a case the age rendering cannot defend against, and it is worth writing down in both repos rather than leaving it to be rediscovered. The agent's MQTT will is fixed when its connection opens and the broker holds it, so a fault learned during that session never reaches it. If the agent is then killed, the broker publishes the older, healthier reading over the newer one and this page renders DISCONNECTED with a stale-healthy detail line. Nothing here can detect it -- retained MQTT v3.1.1 messages carry no publish timestamp -- so the note says where the recovery actually comes from: the agent restarts on a 5s systemd timer, seeds from its spool, which does have the fault, and republishes. Two review nits while here. `seen` is annotated `object` rather than `str | None`, because it comes out of json.loads on a payload this process did not produce and can be any JSON type; annotating it narrowly would move the lie into the signature. And under 10 seconds now reads "seen just now" instead of "seen 0s ago" -- that is the common case, since the page is usually looked at right after printing something. --- README.md | 10 ++++++++++ inventree_label_dispatch/status.py | 12 +++++++++++- tests/test_status.py | 6 ++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c5927ee..c9103ec 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ republishes what it last heard rather than going quiet, and the age of that read shown alongside it. ``` +CONNECTED fw 2.1.2 · 100% (4.17V) · media ok · Q223P4C31420105 · seen just now CONNECTED fw 2.1.2 · 100% (4.17V) · media ok · Q223P4C31420105 · seen 6h ago ``` @@ -61,6 +62,15 @@ rather than overruling it — a printer that had tape six hours ago is far likel still have tape than to have anything else — so the status mapping is unchanged. An agent too old to publish the field renders exactly as it did before. +One case the age cannot cover: the agent's MQTT will is fixed when its connection opens +and the broker holds it, so a printer fault learned *during* that session cannot reach +it. If the agent is then killed, the broker publishes the older, healthier reading over +the newer one, and this page renders it — correctly marked `DISCONNECTED`, but with a +stale-healthy detail line. It is self-correcting rather than sticky: the agent restarts +(`Restart=always`, `RestartSec=5`), seeds from its spool, which does have the fault, and +republishes. Nothing here can detect the window, since retained MQTT v3.1.1 messages +carry no publish timestamp. + ## Print outcomes `AWAIT_RESULT_S` makes the driver wait for the job's `JobResult` after dispatching, so diff --git a/inventree_label_dispatch/status.py b/inventree_label_dispatch/status.py index 8e3ac78..e3d6510 100644 --- a/inventree_label_dispatch/status.py +++ b/inventree_label_dispatch/status.py @@ -29,9 +29,14 @@ _GOOD_JOB_STATES = frozenset({"completed"}) -def _ago(seen, now: datetime) -> str: +def _ago(seen: object, now: datetime) -> str: """How long ago the device fields were true, as a short suffix. ``""`` if unknown. + ``seen`` is typed as ``object`` rather than ``str | None`` on purpose: it comes + straight out of ``json.loads`` on a payload this process did not produce, so it can + be any JSON type at all, and pretending otherwise would just move the lie into the + annotation. + Empty rather than a guess in three cases, all of which mean the same thing: nobody said. An agent older than the field simply omits it, and the page should look exactly as it did before rather than sprouting a fake age. @@ -52,6 +57,11 @@ def _ago(seen, now: datetime) -> str: # Clamped at zero: the agent's clock and this one are not the same clock, and a # small skew must read as "just now", never as a negative age. delta = max(0, int((now - stamp).total_seconds())) + # A reading taken while the operator was watching the print happen. "seen 0s ago" + # is technically right and reads like a machine; this is the common case, since the + # page is usually looked at right after printing something. + if delta < 10: + return "seen just now" if delta < 60: return f"seen {delta}s ago" if delta < 3600: diff --git a/tests/test_status.py b/tests/test_status.py index 7b84f83..66950b7 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -140,7 +140,8 @@ def _seen(delta: timedelta) -> dict: @pytest.mark.parametrize( ("delta", "expected"), [ - (timedelta(seconds=0), "seen 0s ago"), + (timedelta(seconds=0), "seen just now"), + (timedelta(seconds=9), "seen just now"), (timedelta(seconds=42), "seen 42s ago"), (timedelta(minutes=4), "seen 4m ago"), (timedelta(hours=6), "seen 6h ago"), @@ -188,7 +189,8 @@ def test_a_naive_timestamp_is_read_as_utc(): def test_a_clock_ahead_of_ours_reads_as_just_now_not_a_negative_age(): """The agent's clock and InvenTree's are not the same clock.""" _member, text = classify_status(_seen(timedelta(minutes=-5)), now=NOW) - assert "seen 0s ago" in text + assert "seen just now" in text + assert "-" not in text.split("seen")[-1] # -- job outcomes ----------------------------------------------------------- #