From 4d5d6345b1dd98fc41044fb94941490596dcbddb Mon Sep 17 00:00:00 2001 From: Clara Vanacker Date: Tue, 30 Jun 2026 22:43:01 +0200 Subject: [PATCH] feat(web): long-range trend chart (min/max band + average) on the item page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a long-range chart backed by the hourly trend rollups: a min/max band with the average line, so an item's long-term shape stays visible after raw samples are pruned by retention. Sits alongside the raw-history chart on the item page. - charts.py: a pure `band_chart` helper → SVG geometry (avg polyline + a band polygon from the max edge then the min edge), with axis ticks; flat/too-few-points handled. - Item page renders it from up to ~30 days of trends (numeric items only). Tests: band geometry (line + band point counts, value range) and the item-page render. Full suite 221 passed; ruff + mypy(strict) green. --- README.md | 5 ++ app/api/routes/web/charts.py | 76 +++++++++++++++++++++++++++++++ app/api/routes/web/hosts.py | 11 ++++- docs/assets/longrange-chart.png | Bin 0 -> 49864 bytes docs/roadmap.md | 4 +- static/style.css | 1 + templates/hosts/item_detail.html | 21 +++++++++ tests/test_charts.py | 12 +++++ tests/test_trends.py | 22 +++++++++ 9 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 docs/assets/longrange-chart.png diff --git a/README.md b/README.md index b8c02d9..398b5e1 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,11 @@ library) over an item's history: ![Server-rendered item history chart](docs/assets/item-history-chart.png) +**Long-range trends** — an average line with a min/max band over hourly rollups, so +long-term shape survives raw-sample retention: + +![Long-range trend chart with min/max band](docs/assets/longrange-chart.png) + **Hosts overview** — every host's health at a glance (item count, ongoing problems and worst severity), problem hosts first: diff --git a/app/api/routes/web/charts.py b/app/api/routes/web/charts.py index 62e7f3d..7bc848b 100644 --- a/app/api/routes/web/charts.py +++ b/app/api/routes/web/charts.py @@ -101,3 +101,79 @@ def _y(v: float) -> float: ] view.empty = False return view + + +@dataclass(slots=True) +class BandChartView: + """An average line with a min/max band — for long-range trend rollups.""" + + width: int + height: int + empty: bool = True + line: str = "" # avg polyline + band: str = "" # polygon: max edge L→R then min edge R→L + y_ticks: list[tuple[float, str]] = field(default_factory=list) + x_ticks: list[tuple[float, str]] = field(default_factory=list) + plot_left: float = 0.0 + plot_right: float = 0.0 + plot_top: float = 0.0 + plot_bottom: float = 0.0 + + +def band_chart( + series: list[tuple[datetime, float, float, float]], + *, + width: int = 720, + height: int = 200, + pad_left: int = 48, + pad_right: int = 12, + pad_top: int = 12, + pad_bottom: int = 24, +) -> BandChartView: + """Build a min/avg/max band chart from chronological (bucket, min, avg, max).""" + view = BandChartView(width=width, height=height) + if len(series) < 2: + return view + + left, right = float(pad_left), float(width - pad_right) + top, bottom = float(pad_top), float(height - pad_bottom) + view.plot_left, view.plot_right, view.plot_top, view.plot_bottom = left, right, top, bottom + + times = [t for t, _, _, _ in series] + mins = [mn for _, mn, _, _ in series] + avgs = [av for _, _, av, _ in series] + maxs = [mx for _, _, _, mx in series] + t0, t1 = times[0], times[-1] + span = (t1 - t0).total_seconds() + lo, hi = min(mins), max(maxs) + if hi == lo: + lo, hi = lo - 1.0, hi + 1.0 + vrange = hi - lo + last = len(series) - 1 + + def _x(i: int, t: datetime) -> float: + frac = ((t - t0).total_seconds() / span) if span > 0 else (i / last) + return left + (right - left) * frac + + def _y(v: float) -> float: + return top + (bottom - top) * (1 - (v - lo) / vrange) + + xs = [_x(i, t) for i, t in enumerate(times)] + view.line = " ".join(f"{xs[i]:.1f},{_y(avgs[i]):.1f}" for i in range(len(series))) + top_edge = [f"{xs[i]:.1f},{_y(maxs[i]):.1f}" for i in range(len(series))] + bottom_edge = [f"{xs[i]:.1f},{_y(mins[i]):.1f}" for i in reversed(range(len(series)))] + view.band = " ".join(top_edge + bottom_edge) + + view.y_ticks = [ + (_y(lo), _fmt_value(lo)), + (_y((lo + hi) / 2), _fmt_value((lo + hi) / 2)), + (_y(hi), _fmt_value(hi)), + ] + mid = series[len(series) // 2] + view.x_ticks = [ + (left, _fmt_time(t0, span)), + (_x(len(series) // 2, mid[0]), _fmt_time(mid[0], span)), + (right, _fmt_time(t1, span)), + ] + view.empty = False + return view diff --git a/app/api/routes/web/hosts.py b/app/api/routes/web/hosts.py index 1d43f52..c5fbb80 100644 --- a/app/api/routes/web/hosts.py +++ b/app/api/routes/web/hosts.py @@ -8,7 +8,7 @@ from app.api.deps.db import DBSession from app.api.routes.web._shared import login_redirect, resolve_current_user, templates -from app.api.routes.web.charts import ChartView, line_chart +from app.api.routes.web.charts import BandChartView, ChartView, band_chart, line_chart from app.core.models.host import ItemSource, ItemValueType from app.core.models.trigger import Severity, TriggerAggregation, TriggerOperator, TriggerState from app.core.models.user import User @@ -353,6 +353,12 @@ async def _item_detail_context( recent = list(await item_svc.list_values(item_id, limit=300)) # newest first series = [(v.collected_at, v.value_num) for v in reversed(recent) if v.value_num is not None] chart = line_chart(series) if item.value_type.is_numeric else ChartView(width=720, height=240) + + trends = list(await item_svc.list_trends(item_id, limit=720)) # newest first (~30 days) + trend_series = [(t.bucket, t.value_min, t.value_avg, t.value_max) for t in reversed(trends)] + trend_chart = ( + band_chart(trend_series) if item.value_type.is_numeric else BandChartView(720, 200) + ) return { "current_user": user, "active_nav": "hosts", @@ -360,8 +366,9 @@ async def _item_detail_context( "item": item, "triggers": list(await TriggerService(session).list_for_item(item_id)), "values": recent[:25], - "trends": list(await item_svc.list_trends(item_id, limit=24)), + "trends": trends[:24], "chart": chart, + "trend_chart": trend_chart, "trigger_operators": [{"value": o.value, "label": s} for o, s in _OPERATOR_SYMBOLS.items()], "trigger_aggregations": [a.value for a in TriggerAggregation], "severities": [s.value for s in Severity], diff --git a/docs/assets/longrange-chart.png b/docs/assets/longrange-chart.png new file mode 100644 index 0000000000000000000000000000000000000000..13cbcfe78f6057439797a6299979b71128334b42 GIT binary patch literal 49864 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�sF^s5KrC-?>K0n*fl=0@ex-I0S zqM+YzJud2eI6YX{8B$L7_&OH;HBm7Jfy}F)+%a-FHLxK$ImbWza9Nuzz^yE=>$){! z6d#=^E$ayK)elTcm985c1%vY$=e$iH)dFeYF}&8OHqwA_&vy9F9UVE;d)Ir-e_l=s zco>xRbsy_EDHJ#0LAgaxwp?*l(Df+(NYtsNz>X)aU&BmZ^{3bK&1qvCxsJ%EkMtzA zfSP|d_~_o)>+4rQcjg8bAp5dWZHPdV%SR*c_|fV-xt#;=X}CKb*cl>lHx|xz5{m{U zBl8TkOUDZ`UrXk^h#a2V=!+ zH*VbAR*;d>La2;b)u0B|Yh;aE{hz-Q^hPesG(?H>-w6@^tJS;p2$NvNkOj$Vk!D?b zy8nJsGGxg3dL@Rcq|Tg)Ig{LX36iy=(lY;B&AyX7xgixS*KJ(GhA*0S<9s;7@M z!=%A+~3>;0QMA z2}#gbb0-f1Ht_ImMbW~u)w^zXX&y>Pd*=J=%isdi!KT}FaVP04m`>E{gJ)32ZYM%2{I zr)T_;*rq!F`bq5KYKk|J=mvr`6j#%-=_;az09k?=-$08iN?u9oXM1+f51;Xt`@+33 z_d4CZ5QMK5mHMMS7S26f#$*086-V&)xd+LRTtu%_X8DS}`x+MHmPEFFO}#2><{9 literal 0 HcmV?d00001 diff --git a/docs/roadmap.md b/docs/roadmap.md index 98ad7c1..33511ff 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -122,7 +122,9 @@ GhostMonitor's reason to exist over a plain Zabbix clone is privacy (ghost-suite making it POST a structured remediation intent (command + problem context) to a **webhook** channel for an external runbook — the server never runs commands itself (webhook-only, validated on create and at fire time). -- *(next)* long-range charts backed by trends. +- ✅ **Long-range trend chart**: a server-rendered min/max band + average line over an + item's hourly trend rollups on the item page — long-term shape that survives + raw-sample retention. Pure, unit-tested geometry helper (`band_chart`). ## Phase 5 — Discovery & scale *(in progress)* diff --git a/static/style.css b/static/style.css index a52105a..050adb7 100644 --- a/static/style.css +++ b/static/style.css @@ -639,6 +639,7 @@ footer a:hover { color: var(--brand-primary); } .chart { display: block; width: 100%; height: auto; color: var(--accent); } .chart-line { stroke: currentColor; stroke-width: 1.5; } .chart-area { fill: currentColor; opacity: 0.08; } +.chart-band { fill: currentColor; opacity: 0.12; } .chart-grid { stroke: currentColor; stroke-width: 0.5; opacity: 0.25; } .chart-label { fill: currentColor; opacity: 0.6; font-size: 11px; } diff --git a/templates/hosts/item_detail.html b/templates/hosts/item_detail.html index 4edcec2..c96a89f 100644 --- a/templates/hosts/item_detail.html +++ b/templates/hosts/item_detail.html @@ -117,6 +117,27 @@

History

{% endif %} + {% if trend_chart and not trend_chart.empty %} +
+

Trends (long-range)

+

+ Hourly average with the min/max band — survives raw-sample retention. +

+ + {% for y, label in trend_chart.y_ticks %} + + {{ label }} + {% endfor %} + {% for x, label in trend_chart.x_ticks %} + {{ label }} + {% endfor %} + + + +
+ {% endif %} + {% if trends %}

Hourly trends {{ trends|length }}

diff --git a/tests/test_charts.py b/tests/test_charts.py index c3a8212..110ddf3 100644 --- a/tests/test_charts.py +++ b/tests/test_charts.py @@ -65,3 +65,15 @@ async def test_item_detail_page_renders_chart( assert "

History

" in page.text assert 'class="chart"' in page.text assert "chart-line" in page.text + + +def test_band_chart_empty_and_geometry() -> None: + from app.api.routes.web.charts import band_chart + + assert band_chart([]).empty + series = [(BASE + timedelta(hours=i), float(i), float(i) + 1, float(i) + 2) for i in range(4)] + cv = band_chart(series) + assert not cv.empty + assert len(cv.line.split()) == 4 # avg polyline + assert len(cv.band.split()) == 8 # max edge (4) + min edge (4) + assert [label for _, label in cv.y_ticks] == ["0", "2.50", "5"] # band spans min(0)..max(5) diff --git a/tests/test_trends.py b/tests/test_trends.py index 6dea2e2..89bf931 100644 --- a/tests/test_trends.py +++ b/tests/test_trends.py @@ -146,3 +146,25 @@ async def test_item_detail_page_shows_trends( page = await web_client.get(f"/hosts/{item.host_id}/items/{item.id}") assert page.status_code == 200 assert "Hourly trends" in page.text + + +async def test_item_detail_shows_long_range_trend_chart( + web_client: httpx.AsyncClient, session: Any, user: Any +) -> None: + item = await _item(session, user.id) + for i in range(5): + session.add( + MetricTrend( + item_id=item.id, + bucket=NOW.replace(hour=8 + i, minute=0), + value_min=float(10 + i), + value_avg=float(20 + i), + value_max=float(30 + i), + sample_count=3, + ) + ) + await session.commit() + page = await web_client.get(f"/hosts/{item.host_id}/items/{item.id}") + assert page.status_code == 200 + assert "Trends (long-range)" in page.text + assert "chart-band" in page.text