Auto-commit local changes before build (2026-01-03 14:15:04) #21
@ -1 +1 @@
|
|||||||
v20260103-07-reports-advanced-reporting-foundation
|
v20260103-08-reports-stats-endpoint-fix
|
||||||
|
|||||||
@ -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"])
|
@main_bp.route("/api/reports/<int:report_id>", methods=["DELETE"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_reports_delete(report_id: int):
|
def api_reports_delete(report_id: int):
|
||||||
|
|||||||
@ -62,6 +62,14 @@
|
|||||||
### Fixed
|
### Fixed
|
||||||
- Ensured report deletion flow remains compatible with extended report definition handling.
|
- 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
|
## v0.1.15
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user