milestone 30: overview as a dashboard, explicit health contract, period aggregations
Gates / frontend (push) Successful in 1m32s
Gates / test (push) Successful in 1m54s
Gates / package (push) Successful in 5m28s
Gates / container (push) Successful in 14s
Gates / test-aarch64 (push) Failing after 3h10m0s
CI / gates (push) Failing after 3h11m55s
Gates / frontend (push) Successful in 1m32s
Gates / test (push) Successful in 1m54s
Gates / package (push) Successful in 5m28s
Gates / container (push) Successful in 14s
Gates / test-aarch64 (push) Failing after 3h10m0s
CI / gates (push) Failing after 3h11m55s
This commit is contained in:
+10
-1
@@ -13,15 +13,24 @@ Query provenance: every logged query becomes exactly explainable — what the po
|
||||
- **Every logged query has a detail page.** A row in Activity now links to `/activity/queries/{id}`, which explains that one query in the order it was decided: the request, the group it was matched under, the policy verdict with the rule that produced it and the blocklist source that rule came from, any CNAME uncloaking or safe-search rewrite, the route the answer took — blocked, local, forward zone, upstream or cache — and what the client got back, RCODE and duration included. `GET /api/queries/{id}` serves the same object; an id that retention has already deleted is a 404. The live view carries the same provenance for the queries it streams, so a query is explainable as it happens as well as afterwards.
|
||||
- **Query Log, Live and Lookup are one Activity page.** `/activity` is the single surface for what nxdns answered: History reads the stored log, Live reads the stream, and both show the same seven columns — Time, Domain, Client, Type, Result, Route, Duration. The mode and every filter live in the URL, so an investigation is one link that shows the recipient exactly what you were looking at, and an absolute time range stays that range instead of drifting as the day goes on. A new **Result** column says what the client actually got — `Blocked`, `NOERROR`, `SERVFAIL` and the rest — with the **Route** column beside it saying how the answer was produced, which is the pair the old Status column could not show: a blocked name is answered with NOERROR, and reading only the code made a block look like a success. Both unhappy cases are marked by weight and shape as well as colour. Switching between History and Live keeps your filters, and leaving Live closes the stream instead of holding a viewer slot open. A live row that the log has not written yet opens its own provenance in place — no invented row id — and the open detail stays put while the 500-row buffer scrolls past underneath it. Domain testing moves to `/activity/test` as **Current policy simulation**, worded so it can never be misread as an account of a query that already happened.
|
||||
- **Diagnostics can be scoped to an absolute window.** `/diagnostics?since=…&until=…` now validates and applies both bounds to the active and resolved lists, and the page states the window it is showing with a way to clear it. A query's detail page links here with the five minutes either side of that query, which is where the underlying failure text for a SERVFAIL lives.
|
||||
- **The query log and the dashboard say how far back the history goes.** `GET /api/queries`, `/api/stats` and `/api/stats/timeseries` each carry a `coverage` object: `available_since`, the first second the file can answer for, and `complete`, whether the window you asked for begins inside it. A period that starts before the query log does now says so instead of charting the missing part as zero — which is what a recreate, a retention pass or a fresh install would otherwise look like.
|
||||
- **The query log and the stats endpoints say how far back the history goes.** `GET /api/queries` and all five `/api/stats*` endpoints each carry a `coverage` object: `available_since`, the first second the file can answer for, and `complete`, whether the window you asked for begins inside it. A period that starts before the query log does now says so instead of charting the missing part as zero — which is what a recreate, a retention pass or a fresh install would otherwise look like.
|
||||
- **Three new period breakdowns: `GET /api/stats/types`, `/api/stats/routes` and `/api/stats/clients`.** They take the same `period` parameter as `/api/stats` and report over the same UTC-aligned window, so every panel of one page describes the same span. `types` counts queries per DNS type, with the queries that recorded no type kept as their own row instead of dropped — the numeric type only, since naming types is the admin's job and a second table in the server would drift out of agreement with it. `routes` counts queries by how they were answered, grouping upstream rows by the answering resolver and forward-zone rows by the zone, with blocked, cache, local and rejected answers carrying no source. `clients` returns one bucketed series per client, aligned exactly like `/api/stats/timeseries` so the two charts share an x-axis: the eight busiest clients in the window are named and everything else sums into an `other` series, which is always present and always the same length as the named ones.
|
||||
- **Each of those responses is read atomically.** Every window-bounded read — the five stats endpoints and `GET /api/queries` — now takes its rows and its coverage watermark inside one SQLite read transaction. A retention pass that runs mid-response can no longer hand back rows from before the prune tagged with an `available_since` from after it, and the clients breakdown ranks and buckets from one database state rather than two. The transaction is a deferred read, so it never blocks the query logger or retention.
|
||||
|
||||
### Removed
|
||||
|
||||
- **The upstream-history subsystem and `GET /api/upstream/health` are gone.** nxdns recorded every upstream exchange into per-minute aggregates in `querylog.db` so the dashboard could show each upstream's counts, success rate and last failure over the selected period. The Overview replacement drops that table, which left a writer whose only reader was its own failure signal, so the whole subsystem goes: the accumulator and its flush task, the `upstream_targets` and `upstream_minute` tables, the `/api/upstream/health` endpoint, the retention pass over the minute rows, and the four `nxdns_upstream_history_*` and one `nxdns_retention_upstream_rows_pruned_total` Prometheus metrics. What replaces it: `/api/health` says how many upstreams are available of how many enabled, `/metrics` keeps the live per-upstream `nxdns_upstream_up` and `nxdns_upstream_success_rate` series, and a failing upstream is a Diagnostics episode (`upstream.exchange`) with its own error text and duration. Ranged per-upstream counts are not replaced. Existing `upstream_history.write` diagnostics entries stay readable; nothing writes new ones, and any that were still open when you upgrade are closed at the first start.
|
||||
- **`/queries`, `/queries/{id}`, `/live` and `/lookup` are gone, and bookmarks to them break.** There is no redirect and no alias: the paths simply stop resolving, and the app shows its not-found page. Everything those pages did is on `/activity`, `/activity/queries/{id}` and `/activity/test`. Three navigation entries collapse into one, "Activity". The API is untouched — `/api/queries`, `/api/queries/{id}`, `/api/queries/live` and `/api/lookup` all answer exactly as before.
|
||||
- **The Status column, and the block reason on every row.** The reason a query was blocked was repeated on each of a hundred rows and pushed the answer the client saw off the table. Result and Route replace it; the exact rule, the blocklist source and the historical group stay one click away on the query's detail page, which is the only place they were ever readable.
|
||||
|
||||
### Changed
|
||||
|
||||
- **The Dashboard is now Overview, and it takes Pi-hole's layout.** `/` redirects to `/overview`, and the page answers one question — what the resolver did over a period you choose — instead of laying out six widgets. The 1h/24h/7d/30d period is URL state (`/overview?period=1h`), so the view you are reading is a link you can send. Top to bottom: four neutral stat tiles — queries, blocked with its share, distinct clients, average response time — each linking into the rows behind its number; the query-volume timeline split blocked, cached and other; a new per-client chart on the same axis, the busiest clients named — by their registered or reverse-DNS name where they have one, exactly as the query tables name them — and the rest summed as "other"; and two donuts, query types and how queries were answered, with each upstream and forward zone named separately. Colours follow the identity of a client, a type or a route rather than its rank, so one client overtaking another between refreshes does not repaint the page. Each donut is drawn as decoration with a visible legend beside it and a table a screen reader reads instead of the graphic. All five panels describe one window — matched on the period, both bounds and the coverage watermark together — so a refresh that straddles a bucket boundary, or a retention pass mid-page, can never put a headline count above a chart of a different span. Panels load, fail and retry on their own: a failing donut leaves the charts standing. A period with nothing in it says "No queries in this period." rather than drawing an empty frame.
|
||||
- **The five health conditions moved to Diagnostics, and the nav item says when to look.** Protection, Upstreams, Query history, Diagnostics and Storage are now a compact strip at the top of `/diagnostics`, above the episodes that explain them, instead of a status list on the landing page. Each states its state in words and an icon as well as colour. A healthy condition is quiet; a degraded one is highlighted and links to what can fix it: protection to Blocklists, no reachable upstream to Upstreams, and a losing or failed query log or a low or critical disk to this same page filtered to the component that failed, with any time window cleared so the filter cannot hide the episodes it points at. Dropped rows are reported with the time of the newest drop, so a loss stays visible after the box recovers. When a health poll fails, the conditions on screen are labelled as the last reading that arrived rather than passing for the current state, and a Retry sits beside them. The Diagnostics navigation item carries a badge with the number of open episodes; it shows a plain "!" when the rollup is degraded with nothing open, and also when the last health poll failed, because an unknown must not look like good news. The badge is absent only when health answered and there was nothing to report.
|
||||
- **Pause moved to the sidebar, and the header indicator is gone.** The header carries nothing but the menu button and Log out. Pause and Resume sit at the foot of the navigation sidebar, above the version label, in both the desktop rail and the phone drawer — one global runtime action in the one place that belongs to the resolver rather than to whichever page you are on. The control still appears beside the detail of a query that was blocked, which is the other place the action answers what you are looking at. The control says what it is doing as well as what it offers: Pause while filtering is on, and while it is off, "Paused until 14:05" — or plain "Paused" when the pause has no end — above the Resume button, on every page. "Resume" on its own would name an action without naming the state it ends, and with the header indicator gone no other page could tell you filtering was off. Both controls read the same `protection` condition, so they cannot disagree, and a pause or resume is reflected immediately rather than at the next poll. Nothing offers to pause while protection is unavailable, since pausing a resolver with no filter snapshot changes nothing.
|
||||
- **`GET /api/health` changed shape completely.** The body is now `status` plus five condition objects — `protection`, `upstreams`, `query_history`, `diagnostics` and `disk` — and `status` is `degraded` when, and only when, one of them is in a degrading state: protection `unavailable`, upstreams `unavailable`, query history `losing` or `failed`, diagnostics `unavailable`, or disk `low` or `critical`. Nothing can degrade the rollup without appearing in the response any more; the old hidden upstream-history contribution was the reason for the rewrite. A paused protection is reported and does not degrade, because it is a choice you made rather than a fault. `queries_dropped` and `writer_failed` fold into `query_history`, which also carries `last_drop_s`, the time of the newest dropped row; `refreshes_gated` and `snapshot_generation` leave the body and stay in `/metrics`, as do the disk `db_bytes`, `log_bytes` and `sample_failures` fields. The disk monitor's `warn` state is reported as `low`, because `warn` reads as a log level rather than as a quantity of disk. This is a breaking change to a documented endpoint, taken pre-v0.1 rather than carried.
|
||||
- **Upgrading resets your query history a second time.** Dropping the `upstream_targets` and `upstream_minute` tables changes the `querylog.db` schema fingerprint, and that file is never migrated, so the first start after this release sets the old one aside as `querylog.db.schema-changed-<unix seconds>` and creates a fresh one — exactly as the provenance change above does, and in the same start. `config.db` is untouched.
|
||||
- **`GET /api/stats` no longer reports `cached`.** The standalone cache card is gone from Overview, so the totals field behind it has no consumer. Cache hits stay visible in the query-volume timeline's blocked/cached/other split, in `GET /api/stats/timeseries`, and in `nxdns_cache_hits_total`.
|
||||
- **`GET /api/queries` rows changed shape.** Each row gains `qclass`, `rcode`, `policy_action`, `policy_reason` and `route_kind`, and `block_reason` is gone: the reason a query was blocked is now one of a closed set of values rather than a formatted string. No table column shows it — the reason is read on the query's detail page, and by an API client from `policy_reason` on the row. `blocked`, `cache_hit`, `upstream` and every other existing field are unchanged.
|
||||
- **Upgrading resets your query history.** The `query_log` table gains the provenance columns below, and `querylog.db` is never migrated (it holds expendable log rows, so a schema change replaces the file instead of upgrading it). On the first start after the upgrade the old file is set aside as `querylog.db.schema-changed-<unix seconds>` and a fresh one is created. Nothing else is touched: `config.db` keeps your configuration and your diagnostics history. The recreate files a resolved `query_log.recreated` diagnostics entry naming the file that was kept and the timestamp the new history begins at, and a new `querylog_meta` table records that coverage start, so the dashboard can say "history is available from ..." instead of charting an empty range as zero. The set-aside file is a working SQLite database and can be deleted once you have decided you do not want it.
|
||||
- **`logging.query_log_buffer_max` now accepts 1 to 37449, down from 1 to 1000000.** The queued entry carries every new provenance field by value and is about four times as wide as before — 1792 bytes against 432 — so the meaningful bound is bytes rather than entries. The ceiling is computed at compile time from the width of the entry so that the queue's worst case stays within 64 MiB, and it moves whenever that width does. The default of 10000 is unchanged and costs about 17 MiB. A configuration above the new ceiling is rejected at startup with the ceiling in the message.
|
||||
|
||||
Reference in New Issue
Block a user