fix(reporter): an empty report says why; the first report's window ends now
Closes #407
Problem
The first checkup of a ClickHouse Managed Postgres database (UAT 2026-10-01) had 15 of 25 reports with data: {} and nothing saying why: F004, F005, H001, H002, H004, K001, K003–K008, M001–M003, N001. A reader cannot tell "nothing found" from "nothing collected" from "broken".
What we found on a real ClickHouse Postgres service:
postgres_ai_monhaspg_monitorandpg_read_all_statsthere and sees every user's rows inpg_stat_statements. The grants were not the cause.- The
pg_stat_statementsmetric records a query only after 3 calls and 1 s of total execution time (config/pgwatch-prometheus/metrics.yml). A new or idle database has almost none. - The reporter floors the window's end to the hour, and the first run comes soon after the stack starts, so the window ended before the box's first sample. That emptied K001, K003–K008, M001–M003 and N001 (UAT 2026-10-02).
- D004's
user: "unknown"comes from the metric, which has no user label. This MR leaves it as it is.
Change
Empty reports say why
format_report_data gives every node result without data available and a one-line reason.
available: true("nothing found") needs evidence: the generator passescollected=Truewhen the check's source metric had rows for the node and the report dropped them all. H001, H002 and H004 pass it (their rows can all be system-schema indexes).- Everything else empty is
available: false:- K/M: the collection threshold.
- N001: an idle database, or no collection yet.
- A007: not collected (initdb always writes non-default settings).
- F004/F005/H001/H002/H004 without rows: "none, not collected yet, or the query failed". These metrics have a row only for what they find, so no row cannot be read as an all-clear.
- Any other check: a generic line.
- A node result from a per-node report keeps its annotation when nodes are combined.
- A report with data is unchanged.
datais not touched.
First report window
config/scripts/postgres-reports.sh passes --use-current-time on its first cycle. Later cycles keep the hour-aligned window; USE_CURRENT_TIME=true still applies to every cycle. Noted in docker-compose.yml next to the reporter overrides.
Contract 1.1.0
The schemas accept available and reason at node level (optional). CONTRACT_VERSION is 1.1.0 in reporter/postgres_reports.py and cli/lib/checkup.ts. cli/README.md and cli/CHANGELOG.md say the fields are full-mode only (express does not emit them) and add a node-result field table.
Tests
tests/reporter/test_empty_reports.py: exact(available, reason)for all 30 schema check IDs, with and without evidence; unknown ID; non-dict node value; combined nodes; Prometheus unreachable through the six generators; system-schema-only rows; the first-report window (first sample 12:05, run 12:30) through K001.tests/reporter/test_postgres_reports_script.py: first cycle--use-current-time, later ones not;USE_CURRENT_TIME=trueon every cycle.- The 16 mutants from review r1/r2 are killed.
pytest tests/reporter: 615 passed, 14 skipped.
Evidence
Prometheus unreachable (the review's repro), before and after:
The real reporter CLI against a stub Prometheus for a fresh box (one database, no settings, no bloat or index rows, one H002 row for a pg_catalog index):
What we did not check
- The markdown rendering of these reports on the platform. The platform reads only
results[host]["data"], soreasonis not shown there yet. - Found, not fixed here: K001's hourly output (
other_calls_hourly) does not validate againstK001.schema.json, onmaintoo.

