gui: add Delivered + Confirmed (acked) columns to consumers page (#119)

Surfaces ConsumerInfo.delivered.consumer_seq (total delivered) and
ConsumerInfo.ack_floor.consumer_seq (total acknowledged) as new columns
on the /consumers page between WAITING and CREATED.

Reuses the existing ConsumerInfo objects already fetched — no new NATS
calls. Both fields use safe getattr access and render '—' when None.
Legend updated to clarify these are consumer-level counters, not
end-to-end mesh delivery confirmation.

Bump version 0.14.8 → 0.14.9.

Co-authored-by: Ubuntu <zvx@cortex.echo6.co>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
malice 2026-06-28 22:32:50 -06:00 committed by GitHub
commit 9450696fe9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 50 additions and 3 deletions

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "central"
version = "0.14.8"
version = "0.14.9"
requires-python = ">=3.12,<3.13"
description = "Data hub spine — adapters, bus, archive."
readme = "README.md"

View file

@ -2224,6 +2224,8 @@ async def consumers_list(request: Request) -> HTMLResponse:
"num_ack_pending": ci.num_ack_pending,
"num_redelivered": ci.num_redelivered,
"num_waiting": ci.num_waiting,
"delivered": getattr(getattr(ci, "delivered", None), "consumer_seq", None),
"acked": getattr(getattr(ci, "ack_floor", None), "consumer_seq", None),
"created": ci.created,
"protected": ci.name.startswith("archive-"),
})

View file

@ -25,6 +25,8 @@ has no active subscriber — it is safe to delete if it is not a central-owned c
<th style="text-align: right; padding: 0.25rem 0.5rem;">Ack Pending</th>
<th style="text-align: right; padding: 0.25rem 0.5rem;">Redelivered</th>
<th style="text-align: right; padding: 0.25rem 0.5rem;">Waiting</th>
<th style="text-align: right; padding: 0.25rem 0.5rem;">Delivered</th>
<th style="text-align: right; padding: 0.25rem 0.5rem;">Confirmed</th>
<th style="text-align: left; padding: 0.25rem 0.5rem;">Created</th>
<th style="text-align: center; padding: 0.25rem 0.5rem;">Action</th>
</tr>
@ -37,6 +39,8 @@ has no active subscriber — it is safe to delete if it is not a central-owned c
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_ack_pending if c.num_ack_pending is not none else '—' }}</td>
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_redelivered if c.num_redelivered is not none else '—' }}</td>
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_waiting if c.num_waiting is not none else '—' }}</td>
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.delivered if c.delivered is not none else '—' }}</td>
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.acked if c.acked is not none else '—' }}</td>
<td style="padding: 0.25rem 0.5rem;">{{ c.created.isoformat() if c.created else '—' }}</td>
<td style="text-align: center; padding: 0.25rem 0.5rem;">
{% if c.protected %}
@ -64,7 +68,10 @@ has no active subscriber — it is safe to delete if it is not a central-owned c
<p class="muted" style="margin-top: 1rem; font-size: 0.9em;">
<strong>Legend:</strong> <em>Pending</em> = messages not yet delivered to this consumer;
<em>Ack Pending</em> = delivered but not yet acknowledged;
<em>Waiting</em> = active pull requests from a live subscriber.
<em>Waiting</em> = active pull requests from a live subscriber;
<em>Delivered</em> = total messages this consumer has received from the stream (lifetime counter);
<em>Confirmed</em> = total messages this consumer has acknowledged (processed) — these are consumer-level
counters (the subscriber got/acked it) and are NOT confirmation that the message reached a mesh device.
A consumer with high <em>Pending</em> and zero <em>Waiting</em> is abandoned — no subscriber
is pulling from it and messages are piling up.
Consumers marked <em>central-owned</em> (archive-*) are managed by central and cannot be deleted here.

View file

@ -2,6 +2,7 @@
import os
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@ -13,13 +14,16 @@ os.environ.setdefault("CENTRAL_NATS_URL", "nats://localhost:4222")
def _make_consumer_info(name: str, num_pending: int = 0, num_ack_pending: int = 0,
num_redelivered: int = 0, num_waiting: int = 0):
num_redelivered: int = 0, num_waiting: int = 0,
delivered_seq: int = 0, ack_floor_seq: int = 0):
ci = MagicMock()
ci.name = name
ci.num_pending = num_pending
ci.num_ack_pending = num_ack_pending
ci.num_redelivered = num_redelivered
ci.num_waiting = num_waiting
ci.delivered = SimpleNamespace(consumer_seq=delivered_seq)
ci.ack_floor = SimpleNamespace(consumer_seq=ack_floor_seq)
ci.created = datetime(2026, 5, 17, 12, 0, 0, tzinfo=timezone.utc)
return ci
@ -204,6 +208,8 @@ class TestConsumersListHtmlRender:
"num_ack_pending": 0,
"num_redelivered": 0,
"num_waiting": 0,
"delivered": 5000,
"acked": 4000,
"created": datetime(2026, 5, 17, 12, 0, 0, tzinfo=timezone.utc),
"protected": False,
},
@ -230,6 +236,8 @@ class TestConsumersListHtmlRender:
"num_ack_pending": 0,
"num_redelivered": 0,
"num_waiting": 1,
"delivered": 100,
"acked": 95,
"created": datetime(2026, 5, 17, 12, 0, 0, tzinfo=timezone.utc),
"protected": True,
},
@ -254,6 +262,8 @@ class TestConsumersListHtmlRender:
"num_ack_pending": None,
"num_redelivered": None,
"num_waiting": None,
"delivered": None,
"acked": None,
"created": None,
"protected": False,
},
@ -267,6 +277,34 @@ class TestConsumersListHtmlRender:
# The guarded fallback em dash is rendered instead
assert "" in html
def test_delivered_and_confirmed_columns_render(self):
streams = [
{
"stream": "CENTRAL_WX",
"error": None,
"consumers": [
{
"name": "meshai-wx",
"num_pending": 10,
"num_ack_pending": 2,
"num_redelivered": 0,
"num_waiting": 1,
"delivered": 7777,
"acked": 6543,
"created": datetime(2026, 5, 17, 12, 0, 0, tzinfo=timezone.utc),
"protected": False,
},
],
},
]
html = self._render(streams)
# Delivered and Confirmed column headers are present
assert "Delivered" in html
assert "Confirmed" in html
# The actual counter values appear in the rendered HTML
assert "7777" in html
assert "6543" in html
class TestConsumersDeleteArchiveGuard:
"""POST /consumers/{stream}/archive-*/delete must be refused."""