mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-10 11:54:37 +02:00
feat(events-subject): derive /events.json subject from inner payload
The events_json SELECT read payload->>'subject', but the CloudEvents
envelope has no top-level subject, so every JSON consumer saw
subject: null. The /events GUI already derives readable subjects via
per-adapter templates/_event_summaries/{adapter}.html (PR L-c).
This makes the JSON path produce the same plain-text subjects with no
duplicated logic: _derive_subject(event) renders the same partial the
table uses (falling back to _default.html) and html.unescapes the
autoescaped output so JSON consumers get plain text (e.g. ">=1 MeV"
rather than the escaped ">=1 MeV"). _fetch_events now sets subject
from it and drops the always-null SQL expression. The GUI Subject cell
is unchanged.
Adds TestEventsJsonSubject (parameterized over discover_adapters(), no
hardcoded list): non-null subject per adapter, equality with the rendered
partial, pinned human text for the deterministic adapters, swpc_alerts
truncation, and null fallbacks. Updates one TestEventRowDataAttributes
assertion that pinned the old SQL pass-through contract.
One route change plus tests; central-gui restart required.
Full suite: 629 passed, 1 skipped (central and unprivileged zvx).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d0375225b2
commit
578c9bc0fe
2 changed files with 140 additions and 6 deletions
|
|
@ -1,6 +1,7 @@
|
|||
"""Route handlers for Central GUI."""
|
||||
|
||||
import base64
|
||||
import html
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
|
@ -2727,6 +2728,24 @@ def _parse_events_params(params) -> tuple[dict | None, str | None]:
|
|||
}, None
|
||||
|
||||
|
||||
def _derive_subject(event: dict) -> str | None:
|
||||
"""Derive an event's plain-text subject for the JSON API.
|
||||
|
||||
Renders the same per-adapter ``_event_summaries/{adapter}.html`` partial
|
||||
the /events table uses (falling back to ``_default.html``), so the JSON
|
||||
subject carries the same human text as the GUI's Subject cell with no
|
||||
duplicated derivation logic. The partials are HTML-autoescaped for the
|
||||
table (e.g. ``>`` -> ``>``); we ``html.unescape`` so JSON consumers get
|
||||
plain text. Returns ``None`` when the partial yields no text -- an unknown
|
||||
adapter, or an event whose source fields don't support a subject (e.g. a
|
||||
wfigs row with neither county nor state).
|
||||
"""
|
||||
template = _get_templates().env.select_template(
|
||||
[f"_event_summaries/{event.get('adapter')}.html", "_event_summaries/_default.html"]
|
||||
)
|
||||
return html.unescape(template.render(event=event)).strip() or None
|
||||
|
||||
|
||||
async def _fetch_events(parsed_params: dict) -> EventsQueryResult:
|
||||
"""
|
||||
Fetch events from database using parsed parameters.
|
||||
|
|
@ -2794,7 +2813,6 @@ async def _fetch_events(parsed_params: dict) -> EventsQueryResult:
|
|||
received,
|
||||
adapter,
|
||||
category,
|
||||
payload->>'subject' as subject,
|
||||
ST_AsGeoJSON(geom) as geometry,
|
||||
payload as data,
|
||||
regions
|
||||
|
|
@ -2824,17 +2842,23 @@ async def _fetch_events(parsed_params: dict) -> EventsQueryResult:
|
|||
if row["geometry"]:
|
||||
geometry = json.loads(row["geometry"])
|
||||
|
||||
events.append({
|
||||
event = {
|
||||
"id": row["id"],
|
||||
"time": row["time"].isoformat(),
|
||||
"received": row["received"].isoformat(),
|
||||
"adapter": row["adapter"],
|
||||
"category": row["category"],
|
||||
"subject": row["subject"],
|
||||
"geometry": geometry,
|
||||
"data": dict(row["data"]) if row["data"] else {},
|
||||
"regions": list(row["regions"]) if row["regions"] else [],
|
||||
})
|
||||
}
|
||||
# Subject is derived from the inner adapter payload by rendering the
|
||||
# same _event_summaries partial the /events table uses, so the JSON
|
||||
# `subject` matches the GUI's Subject cell. (The CloudEvents envelope
|
||||
# has no top-level `subject`; the old `payload->>'subject'` was always
|
||||
# null for every consumer.)
|
||||
event["subject"] = _derive_subject(event)
|
||||
events.append(event)
|
||||
|
||||
# Build next_cursor if there are more results
|
||||
next_cursor = None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue