|
|
|
|
@ -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):
|
|
|
|
|
|