fix(unifi): the Overview's history buckets on Postgres; devices name their state (#5207)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m24s
CI / unit (push) Successful in 1m35s
CI / publish (push) Successful in 1m29s

#5207 "UniFi Overview tab: func.strftime is SQLite-only": the WAN and
client sparklines bucketed with strftime('%s'), which Postgres lacks, so
/plugins/unifi/rows failed. Traefik's date_bin bucket moves to
core/time_range.time_bucket and both plugins use it. Devices list offline
and failing first, and a state other than online is named (Upgrading,
Adopting, Heartbeat missed...) instead of drawn as Down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 17:28:19 -04:00
co-authored by Claude Opus 5.5
parent 46a301652b
commit ea31deabf9
6 changed files with 145 additions and 49 deletions
+3 -12
View File
@@ -10,7 +10,7 @@ import time
from steward.auth.middleware import require_role
from steward.models.users import UserRole
from steward.core.time_range import parse_range, DEFAULT_RANGE, bucket_seconds, subsample
from steward.core.time_range import parse_range, DEFAULT_RANGE, bucket_seconds, subsample, time_bucket
from steward.core.formatting import cert_left, status_class
from steward.models.metrics import PluginMetricLatest
from .models import TraefikAccessSummary, TraefikCert, TraefikGlobalStat, TraefikMetric
@@ -430,15 +430,6 @@ async def widget_access_log():
summary=summary, widget_id=widget_id)
def _time_bucket(column, seconds: int):
"""The start of the `seconds`-wide window a timestamp falls in, as
host_agent/metrics_query.py buckets. Integer epoch / int rendered as
numeric division (no two scrapes shared a bucket) and overflowed int4
after 2038 (#5256)."""
return func.date_bin(func.make_interval(0, 0, 0, 0, 0, 0, int(seconds)), column,
func.to_timestamp(0)).label("bucket")
# A rows or widget request past this logs where its time went, phase by
# phase, so a slow query names itself (#5355: 8 s on every poll, warm).
SLOW_PAGE_SECONDS = 1.0
@@ -473,7 +464,7 @@ async def router_history(db, names: list[str], since, bucket_secs: int) -> dict[
history: dict[str, list] = {n: [] for n in names}
if not history:
return history
bucket_col = _time_bucket(TraefikMetric.scraped_at, bucket_secs)
bucket_col = time_bucket(TraefikMetric.scraped_at, bucket_secs)
result = await db.execute(
select(
TraefikMetric.router_name,
@@ -509,7 +500,7 @@ async def request_history(db, since, bucket_seconds: int) -> list[dict]:
.group_by(TraefikMetric.scraped_at)
.subquery()
)
bucket_col = _time_bucket(per_scrape.c.at, bucket_seconds)
bucket_col = time_bucket(per_scrape.c.at, bucket_seconds)
rows = (await db.execute(
select(
func.avg(per_scrape.c.req).label("req_rate"),