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 ----------------------------------------------------------- #