diff --git a/src/Web/autopilot-monitor-web/app/sla/page.tsx b/src/Web/autopilot-monitor-web/app/sla/page.tsx index fb432dc4..9deaa26b 100644 --- a/src/Web/autopilot-monitor-web/app/sla/page.tsx +++ b/src/Web/autopilot-monitor-web/app/sla/page.tsx @@ -101,12 +101,17 @@ export default function SlaPage() { const [metrics, setMetrics] = useState(null); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); const [months, setMonths] = useState(3); - const fetchMetrics = useCallback(async () => { + const fetchMetrics = useCallback(async (showRefreshing = false) => { if (!tenantId) return; try { - setLoading(true); + if (showRefreshing) { + setRefreshing(true); + } else { + setLoading(true); + } const response = await authenticatedFetch( api.metrics.sla(tenantId, months), getAccessToken @@ -118,11 +123,15 @@ export default function SlaPage() { const data = await response.json(); setMetrics(data); } catch (err) { - if (err instanceof TokenExpiredError) return; - console.error('Error loading SLA metrics:', err); - addNotification('error', 'Error', 'Failed to load SLA metrics', 'sla-fetch-error'); + if (err instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', err.message, 'session-expired-error'); + } else { + console.error('Error loading SLA metrics:', err); + addNotification('error', 'Error', 'Failed to load SLA metrics', 'sla-fetch-error'); + } } finally { setLoading(false); + setRefreshing(false); } }, [tenantId, months, getAccessToken, addNotification]); @@ -134,53 +143,74 @@ export default function SlaPage() { (metrics.targetSuccessRate != null || metrics.targetMaxDurationMinutes != null || metrics.targetAppInstallSuccessRate != null); + if (loading && !metrics) { + return ( +
+
+
+

Loading SLA metrics...

+
+
+ ); + } + return ( -
-
- {/* Header */} -
-
-

SLA Compliance

-

- Monitor enrollment performance against your SLA targets -

-
-
- - +
+
+
+
+
+

SLA Compliance

+

+ Monitor enrollment performance against your SLA targets + {metrics && ( + <> + {" ยท "}Computed at {new Date(metrics.computedAt).toLocaleString()} in {metrics.computeDurationMs}ms + {metrics.fromCache && ( + + From Cache + + )} + + )} +

+
+
+ + +
+
- {loading && !metrics && ( -
-
-
- )} - - {!loading && !hasTargets && ( -
-

No SLA Targets Configured

-

+

+ {!hasTargets && ( +
+

No SLA Targets Configured

+

Set up SLA targets to track enrollment success rate and duration compliance.

Configure SLA Targets @@ -190,9 +220,9 @@ export default function SlaPage() { {metrics && hasTargets && ( <> {/* Gauge Cards */} -
+
{metrics.targetSuccessRate != null && ( -
+
+
+
+
{/* Summary Stats */} -
- - - - +
+

Current Month

+
+
+
Total Sessions
+
{metrics.currentMonth.totalCompleted}
+
+
+
Succeeded
+
{metrics.currentMonth.succeeded}
+
+
+
Failed
+
{metrics.currentMonth.failed}
+
+
+
Duration Violations
+
{metrics.currentMonth.durationViolationCount}
+
+
{/* Trend Chart */} {metrics.monthlyTrend.length > 1 && ( -
-

Monthly Trend

- >} - xKey="month" - series={[ - { dataKey: "successRate", label: "Success Rate (%)", color: chartColors.primary }, - ...(metrics.targetAppInstallSuccessRate != null - ? [{ dataKey: "appInstallSuccessRate", label: "App Install Rate (%)", color: chartColors.success }] - : []), - ]} - height={300} - /> +
+

Monthly Trend

+
+ >} + xKey="month" + series={[ + { dataKey: "successRate", label: "Success Rate (%)", color: chartColors.primary }, + ...(metrics.targetAppInstallSuccessRate != null + ? [{ dataKey: "appInstallSuccessRate", label: "App Install Rate (%)", color: chartColors.success }] + : []), + ]} + height={300} + /> +
)} {/* Top Failing Apps */} {metrics.appInstallSla && metrics.appInstallSla.topFailingApps.length > 0 && ( -
-

Top Failing Apps

-
+
+

Top Failing Apps

+
- - - - - - + + + + + + {metrics.appInstallSla.topFailingApps.map((app) => ( - - - - - + + + + + ))} @@ -288,55 +335,55 @@ export default function SlaPage() { {/* Violators Table */} {metrics.violators.length > 0 && ( -
-

+
+

SLA Violators ({metrics.violators.length})

-
+

App NameFailuresTotalSuccess Rate
App NameFailuresTotalSuccess Rate
{app.appName}{app.failCount}{app.totalCount}{app.successRate.toFixed(1)}%
{app.appName}{app.failCount}{app.totalCount}{app.successRate.toFixed(1)}%
- - - - - - - - - + + + + + + + + + {metrics.violators.map((v) => ( - - + - - + - - - - @@ -346,17 +393,10 @@ export default function SlaPage() { )} - - {/* Cache / Timing info */} -
- Computed at {new Date(metrics.computedAt).toLocaleTimeString()} - {metrics.fromCache && " (cached)"} - {" "}({metrics.computeDurationMs}ms) -
)} - + ); } @@ -375,7 +415,7 @@ function OverallStatus({ metrics }: { metrics: SlaMetricsResponse }) { if (checks.length === 0) { return ( - No targets configured + No targets configured ); } @@ -384,7 +424,7 @@ function OverallStatus({ metrics }: { metrics: SlaMetricsResponse }) {
{allMet ? "\u2705" : "\u26a0\ufe0f"}
- + {allMet ? "Compliant" : "Breached"} @@ -393,12 +433,3 @@ function OverallStatus({ metrics }: { metrics: SlaMetricsResponse }) { ); } - -function StatCard({ label, value, color = "text-white" }: { label: string; value: number; color?: string }) { - return ( -
-
{value}
-
{label}
-
- ); -} diff --git a/src/Web/autopilot-monitor-web/components/charts/SlaGauge.tsx b/src/Web/autopilot-monitor-web/components/charts/SlaGauge.tsx index 2a80f393..b811cfee 100644 --- a/src/Web/autopilot-monitor-web/components/charts/SlaGauge.tsx +++ b/src/Web/autopilot-monitor-web/components/charts/SlaGauge.tsx @@ -1,36 +1,33 @@ "use client"; +import { useEffect, useState } from "react"; import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; -import { chartColors } from "./chartTheme"; interface SlaGaugeProps { - /** Current value (e.g. 94.5 for success rate, 32 for duration in minutes) */ value: number; - /** Target value (e.g. 95.0 for success rate, 60 for max duration) */ target: number; - /** Label shown below the value */ label: string; - /** Unit shown after the value (e.g. "%" or "min") */ unit: string; - /** If true, lower is better (e.g. duration) */ invert?: boolean; } -/** - * Semi-circular gauge displaying current value vs target with color zones. - * Green = at/above target, Yellow = within 5% margin, Red = >5% below target. - * For inverted metrics (duration), lower is better. - */ export function SlaGauge({ value, target, label, unit, invert = false }: SlaGaugeProps) { - // Determine compliance status + const [isDark, setIsDark] = useState(false); + + useEffect(() => { + const check = () => setIsDark(document.documentElement.classList.contains("dark")); + check(); + const observer = new MutationObserver(check); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] }); + return () => observer.disconnect(); + }, []); + const getStatus = (): "met" | "warning" | "breached" => { if (invert) { - // Lower is better (e.g. duration) if (value <= target) return "met"; if (value <= target * 1.05) return "warning"; return "breached"; } else { - // Higher is better (e.g. success rate) if (value >= target) return "met"; if (value >= target * 0.95) return "warning"; return "breached"; @@ -39,17 +36,17 @@ export function SlaGauge({ value, target, label, unit, invert = false }: SlaGaug const status = getStatus(); const statusColor = - status === "met" ? chartColors.success : - status === "warning" ? chartColors.warning : - chartColors.danger; + status === "met" ? "#16a34a" : + status === "warning" ? "#d97706" : + "#dc2626"; const statusLabel = status === "met" ? "Compliant" : status === "warning" ? "At Risk" : "Breached"; - // PieChart data for semi-circle gauge - // Fill portion vs empty portion (mapped to 180 degrees) + const emptyColor = isDark ? "#374151" : "#e5e7eb"; // gray-700 / gray-200 + const maxVal = invert ? target * 2 : 100; const fillPercent = Math.min(Math.max(value / maxVal, 0), 1); @@ -75,18 +72,18 @@ export function SlaGauge({ value, target, label, unit, invert = false }: SlaGaug stroke="none" > - +
- + {value.toFixed(1)}{unit}
- {label} - + {label} + {statusLabel} (target: {target}{unit})
DeviceSerialStartedDurationStatusViolationFailure Reason
DeviceSerialStartedDurationStatusViolationFailure Reason
+
{v.deviceName || "Unknown"} {v.serialNumber || "-"} + {v.serialNumber || "-"} {new Date(v.startedAt).toLocaleDateString()} + {v.durationSeconds ? formatDuration(v.durationSeconds) : "-"} - + + {statusLabels[v.status] ?? "Unknown"} + {v.violationType} + {v.failureReason || "-"}