Compare commits

...

2 Commits

3 changed files with 9 additions and 112 deletions

View File

@ -1 +1 @@
v20260103-07-reports-advanced-reporting-foundation
v20260103-08-reports-stats-endpoint-fix

View File

@ -229,117 +229,6 @@ def api_reports_columns():
}
@main_bp.route("/api/reports/<int:report_id>/stats", methods=["GET"])
@login_required
def api_reports_stats(report_id: int):
"""Return lightweight KPI + chart datasets for a report.
Data is derived from report_object_snapshots, which is generated by
POST /api/reports/<id>/generate.
"""
err = _require_reporting_role()
if err is not None:
return err
ReportDefinition.query.get_or_404(report_id)
# KPI counts
# We treat missed runs as their own bucket, regardless of status string.
row = db.session.execute(
text(
"""
SELECT
COUNT(*)::INTEGER AS total_runs,
SUM(CASE WHEN missed = TRUE THEN 1 ELSE 0 END)::INTEGER AS missed_runs,
SUM(CASE WHEN missed = FALSE AND COALESCE(status,'') ILIKE 'success%' THEN 1 ELSE 0 END)::INTEGER AS success_runs,
SUM(CASE WHEN missed = FALSE AND COALESCE(status,'') ILIKE 'warning%' THEN 1 ELSE 0 END)::INTEGER AS warning_runs,
SUM(CASE WHEN missed = FALSE AND COALESCE(status,'') ILIKE 'fail%' THEN 1 ELSE 0 END)::INTEGER AS failed_runs,
SUM(CASE WHEN override_applied = TRUE THEN 1 ELSE 0 END)::INTEGER AS override_runs
FROM report_object_snapshots
WHERE report_id = :rid
"""
),
{"rid": report_id},
).fetchone()
total_runs = int(row.total_runs or 0) if row else 0
success_runs = int(row.success_runs or 0) if row else 0
warning_runs = int(row.warning_runs or 0) if row else 0
failed_runs = int(row.failed_runs or 0) if row else 0
missed_runs = int(row.missed_runs or 0) if row else 0
override_runs = int(row.override_runs or 0) if row else 0
success_rate = 0.0
if total_runs > 0:
# Consider overrides as success for success_rate.
success_rate = ((success_runs + override_runs) / float(total_runs)) * 100.0
# Trend datasets (per day)
trend_rows = db.session.execute(
text(
"""
SELECT
DATE_TRUNC('day', run_at) AS day,
COUNT(*)::INTEGER AS total,
SUM(CASE WHEN missed = TRUE THEN 1 ELSE 0 END)::INTEGER AS missed,
SUM(CASE WHEN missed = FALSE AND COALESCE(status,'') ILIKE 'success%' THEN 1 ELSE 0 END)::INTEGER AS success,
SUM(CASE WHEN missed = FALSE AND COALESCE(status,'') ILIKE 'warning%' THEN 1 ELSE 0 END)::INTEGER AS warning,
SUM(CASE WHEN missed = FALSE AND COALESCE(status,'') ILIKE 'fail%' THEN 1 ELSE 0 END)::INTEGER AS failed
FROM report_object_snapshots
WHERE report_id = :rid
AND run_at IS NOT NULL
GROUP BY DATE_TRUNC('day', run_at)
ORDER BY DATE_TRUNC('day', run_at) ASC
"""
),
{"rid": report_id},
).fetchall()
trend = []
for tr in trend_rows or []:
day = tr.day
day_iso = day.date().isoformat() if hasattr(day, "date") else str(day)
total = int(tr.total or 0)
succ = int(tr.success or 0)
fail = int(tr.failed or 0)
succ_rate = 0.0
if total > 0:
succ_rate = (succ / float(total)) * 100.0
trend.append(
{
"day": day_iso,
"total": total,
"success": succ,
"warning": int(tr.warning or 0),
"failed": fail,
"missed": int(tr.missed or 0),
"success_rate": succ_rate,
}
)
return {
"kpis": {
"total_runs": total_runs,
"success_runs": success_runs,
"warning_runs": warning_runs,
"failed_runs": failed_runs,
"missed_runs": missed_runs,
"override_runs": override_runs,
"success_rate": float(success_rate),
},
"charts": {
"status_distribution": {
"success": success_runs + override_runs,
"warning": warning_runs,
"failed": failed_runs,
"missed": missed_runs,
},
"trend": trend,
},
}
@main_bp.route("/api/reports/<int:report_id>", methods=["DELETE"])
@login_required
def api_reports_delete(report_id: int):

View File

@ -62,6 +62,14 @@
### Fixed
- Ensured report deletion flow remains compatible with extended report definition handling.
---
## v20260103-08-reports-stats-endpoint-fix
- Fixed application startup crash caused by duplicate registration of the `api_reports_stats` endpoint.
- Removed the redundant `/api/reports/<report_id>/stats` route definition to ensure the endpoint is registered only once.
- Restored proper Gunicorn boot sequence by resolving Flask endpoint name collision.
================================================================================================================================================
## v0.1.15