diff --git a/src/Web/autopilot-monitor-web/app/geographic-performance/page.tsx b/src/Web/autopilot-monitor-web/app/geographic-performance/page.tsx index e57631af..ef507a42 100644 --- a/src/Web/autopilot-monitor-web/app/geographic-performance/page.tsx +++ b/src/Web/autopilot-monitor-web/app/geographic-performance/page.tsx @@ -59,10 +59,14 @@ const formatBytes = (bytes: number) => { export default function GeographicPerformancePage() { // useSearchParams (the ?days= window) requires a Suspense boundary under the static export. + // ProtectedRoute stays outermost: the content's loading card waits for a tenant scope that a + // signed-out tab never gets, so rendered outside it the login redirect would never fire. return ( - }> - - + + }> + + + ); } @@ -168,429 +172,427 @@ function GeographicPerformanceContent() { } return ( - -
- -
-
-
-
-
- - - -

Geographic Performance

-
+
+ +
+
+
+
+
+ + + +

Geographic Performance

-
- - {/* Time Range Toggle */} - - {/* Group By Toggle */} - setGroupBy(v as GroupBy)} - /> - +
+
+ + {/* Time Range Toggle */} + + {/* Group By Toggle */} + setGroupBy(v as GroupBy)} + /> + +
+
+
+
+ +
+ {/* Geo Disabled Warning */} + {geoMetrics && !geoMetrics.geoLocationEnabled && !isAggregatedGlobalView && ( +
+ + + +
+
Geo-Location collection is disabled
+
+ New sessions will not collect location data. Enable geo-location in{" "} + Configuration{" "} + to start collecting geographic data for future enrollments.
-
+ )} -
- {/* Geo Disabled Warning */} - {geoMetrics && !geoMetrics.geoLocationEnabled && !isAggregatedGlobalView && ( -
- - - + {/* Coverage Info */} + {geoMetrics && ( +
+ {geoMetrics.locationsWithData} of {geoMetrics.totalSessions} sessions have location data + {geoMetrics.locationsWithData < geoMetrics.totalSessions && ( + (older sessions may lack geo data) + )} +
+ )} + + {/* Summary Cards */} + {geoMetrics && stats && ( +
+
+
Locations Tracked
+
{geoMetrics.locations.length}
+
{geoMetrics.locationsWithData} sessions with geo
+
+
+
Outliers Detected
+
0 ? "text-red-600" : "text-green-600"}`}> + {stats.outliers} +
+
>2 std deviations from mean
+
+
+
Fastest Location
+ +
+ {stats.fastest ? `${Math.round(stats.fastest.avgDurationMinutes)} min avg` : "No data"} +
+
+
+
Slowest Location
+ +
+ {stats.slowest ? `${Math.round(stats.slowest.avgDurationMinutes)} min avg` : "No data"} +
+
+
+
DO Peer Efficiency
+
30 ? "text-green-600" : + geoMetrics.globalAverages.avgDoPercentPeerCaching > 0 ? "text-yellow-600" : "text-gray-400" + }`}> + {geoMetrics.globalAverages.avgDoPercentPeerCaching > 0 + ? `${geoMetrics.globalAverages.avgDoPercentPeerCaching}%` + : "—"} +
+
+ {geoMetrics.globalAverages.totalDoBytesFromPeers > 0 + ? `${formatBytes(geoMetrics.globalAverages.totalDoBytesFromPeers)} from peers / ${formatBytes(geoMetrics.globalAverages.totalDoBytesFromPeers + geoMetrics.globalAverages.totalDoBytesFromHttp)} total` + : "No DO data yet"} +
+
+
+ )} + + {/* Map */} + {geoMetrics && geoMetrics.locations.length > 0 && ( +
+
+

Performance Map

+ +
+
+ +
+
+ +
+
+ )} + + {/* Global Averages Banner */} + {geoMetrics && ( +
+
Global Averages (Benchmark)
+
+ Computed over the sessions on this page, not over other customers — every vs Global figure compares against this line. +
+
+
+ Avg Duration:{" "} + {geoMetrics.globalAverages.avgDurationMinutes} min +
+
+ Median:{" "} + {geoMetrics.globalAverages.medianDurationMinutes} min +
-
Geo-Location collection is disabled
-
- New sessions will not collect location data. Enable geo-location in{" "} - Configuration{" "} - to start collecting geographic data for future enrollments. -
+ Avg Min/App:{" "} + {geoMetrics.globalAverages.avgMinutesPerApp} min
-
- )} - - {/* Coverage Info */} - {geoMetrics && ( -
- {geoMetrics.locationsWithData} of {geoMetrics.totalSessions} sessions have location data - {geoMetrics.locationsWithData < geoMetrics.totalSessions && ( - (older sessions may lack geo data) - )} -
- )} - - {/* Summary Cards */} - {geoMetrics && stats && ( -
-
-
Locations Tracked
-
{geoMetrics.locations.length}
-
{geoMetrics.locationsWithData} sessions with geo
+
+ Avg Throughput:{" "} + {formatThroughput(geoMetrics.globalAverages.avgThroughputBytesPerSec)}
-
-
Outliers Detected
-
0 ? "text-red-600" : "text-green-600"}`}> - {stats.outliers} -
-
>2 std deviations from mean
-
-
-
Fastest Location
- -
- {stats.fastest ? `${Math.round(stats.fastest.avgDurationMinutes)} min avg` : "No data"} -
-
-
-
Slowest Location
- -
- {stats.slowest ? `${Math.round(stats.slowest.avgDurationMinutes)} min avg` : "No data"} -
-
-
-
DO Peer Efficiency
-
30 ? "text-green-600" : - geoMetrics.globalAverages.avgDoPercentPeerCaching > 0 ? "text-yellow-600" : "text-gray-400" - }`}> +
+ DO P2P:{" "} + {geoMetrics.globalAverages.avgDoPercentPeerCaching > 0 - ? `${geoMetrics.globalAverages.avgDoPercentPeerCaching}%` - : "—"} -
-
- {geoMetrics.globalAverages.totalDoBytesFromPeers > 0 - ? `${formatBytes(geoMetrics.globalAverages.totalDoBytesFromPeers)} from peers / ${formatBytes(geoMetrics.globalAverages.totalDoBytesFromPeers + geoMetrics.globalAverages.totalDoBytesFromHttp)} total` - : "No DO data yet"} -
+ ? `${geoMetrics.globalAverages.avgDoPercentPeerCaching}% peers` + : "No data"} + +
+
+ API Latency:{" "} + + {geoMetrics.globalAverages.medianApiLatencyMs > 0 + ? `${Math.round(geoMetrics.globalAverages.medianApiLatencyMs)} ms` + : "No data"} +
- )} +
+ )} - {/* Map */} - {geoMetrics && geoMetrics.locations.length > 0 && ( -
-
-

Performance Map

- -
-
- -
-
- -
+ {/* Heatmap Table */} + {geoMetrics && ( +
+
+

Location Performance

- )} - - {/* Global Averages Banner */} - {geoMetrics && ( -
-
Global Averages (Benchmark)
-
- Computed over the sessions on this page, not over other customers — every vs Global figure compares against this line. + {sortedLocations.length === 0 ? ( +
+ No geographic data available for the selected time range.
-
-
- Avg Duration:{" "} - {geoMetrics.globalAverages.avgDurationMinutes} min -
-
- Median:{" "} - {geoMetrics.globalAverages.medianDurationMinutes} min -
-
- Avg Min/App:{" "} - {geoMetrics.globalAverages.avgMinutesPerApp} min -
-
- Avg Throughput:{" "} - {formatThroughput(geoMetrics.globalAverages.avgThroughputBytesPerSec)} -
-
- DO P2P:{" "} - - {geoMetrics.globalAverages.avgDoPercentPeerCaching > 0 - ? `${geoMetrics.globalAverages.avgDoPercentPeerCaching}% peers` - : "No data"} - -
-
- API Latency:{" "} - - {geoMetrics.globalAverages.medianApiLatencyMs > 0 - ? `${Math.round(geoMetrics.globalAverages.medianApiLatencyMs)} ms` - : "No data"} - -
-
-
- )} - - {/* Heatmap Table */} - {geoMetrics && ( -
-
-

Location Performance

-
- {sortedLocations.length === 0 ? ( -
- No geographic data available for the selected time range. -
- ) : ( -
- - - - - - - - - - - - - - - - - {sortedLocations.map((loc) => ( - { - // Carry the selected tenant into the drill-in so a cross-tenant caller (GA override - // or delegated/MSP) scopes the session list to it. Empty = GA all-tenants aggregate. - const tenantParam = selectedTenantId ? `&tenantId=${selectedTenantId}` : ""; - router.push(`/geographic-performance/sessions?locationKey=${encodeURIComponent(loc.locationKey)}&days=${days}&groupBy=${groupBy}${tenantParam}`); - }} - onMouseEnter={() => setSelectedLocation(loc.locationKey)} - > - {/* Outlier accent on the cell, not the row: the tbody's divide color overrides a row's border-color. */} - + + + + ))} + +
- Location - handleSort("sessionCount")} - > - Sessions - - Success - handleSort("avgDurationMinutes")} - title="Average enrollment duration of the succeeded sessions at this location in the selected range" - > - Avg Duration - - P95 - handleSort("appLoadScore")} - > - App-Load-Score - handleSort("avgThroughputBytesPerSec")} - > - Throughput - handleSort("medianApiLatencyMs")} - title="Agent→backend HTTP round-trip measured on the devices (median per location) — high values indicate network distance to the backend region" - > - API Latency - handleSort("avgDoPercentPeerCaching")} - > - P2P % - - vs Global -
-
- {loc.locationKey} - {loc.isOutlier && ( - - {loc.outlierDirection === "slow" ? "Slow" : "Fast"} - - )} + ) : ( +
+ + + + + + + + + + + + + + + + + {sortedLocations.map((loc) => ( + { + // Carry the selected tenant into the drill-in so a cross-tenant caller (GA override + // or delegated/MSP) scopes the session list to it. Empty = GA all-tenants aggregate. + const tenantParam = selectedTenantId ? `&tenantId=${selectedTenantId}` : ""; + router.push(`/geographic-performance/sessions?locationKey=${encodeURIComponent(loc.locationKey)}&days=${days}&groupBy=${groupBy}${tenantParam}`); + }} + onMouseEnter={() => setSelectedLocation(loc.locationKey)} + > + {/* Outlier accent on the cell, not the row: the tbody's divide color overrides a row's border-color. */} + - - - + + - - + + + + + - - - - - - ))} - -
+ Location + handleSort("sessionCount")} + > + Sessions + + Success + handleSort("avgDurationMinutes")} + title="Average enrollment duration of the succeeded sessions at this location in the selected range" + > + Avg Duration + + P95 + handleSort("appLoadScore")} + > + App-Load-Score + handleSort("avgThroughputBytesPerSec")} + > + Throughput + handleSort("medianApiLatencyMs")} + title="Agent→backend HTTP round-trip measured on the devices (median per location) — high values indicate network distance to the backend region" + > + API Latency + handleSort("avgDoPercentPeerCaching")} + > + P2P % + + vs Global +
+
+ {loc.locationKey} + {loc.isOutlier && ( + + {loc.outlierDirection === "slow" ? "Slow" : "Fast"} + + )} +
+ {loc.avgAppCount > 0 && ( +
+ ~{loc.avgAppCount} apps/session · {formatBytes(loc.totalDownloadBytes)} total
- {loc.avgAppCount > 0 && ( -
- ~{loc.avgAppCount} apps/session · {formatBytes(loc.totalDownloadBytes)} total -
- )} -
{loc.sessionCount} - {/* Rate is over finished enrollments (succeeded + failed) only; a - location where everything is still in flight has no rate yet. */} - {loc.succeeded + loc.failed > 0 ? ( - - {loc.successRate}% - - ) : ( - - — - - )} - + )} + {loc.sessionCount} + {/* Rate is over finished enrollments (succeeded + failed) only; a + location where everything is still in flight has no rate yet. */} + {loc.succeeded + loc.failed > 0 ? ( 0 ? undefined : "No succeeded enrollments yet"} + className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${MAP_COLOR_MODE_BY_ID.success.resolve(loc, geoMetrics.globalAverages).className}`} > - {loc.avgDurationMinutes > 0 ? `${Math.round(loc.avgDurationMinutes)} min` : "—"} + {loc.successRate}% - - {loc.p95DurationMinutes > 0 ? `${Math.round(loc.p95DurationMinutes)} min` : "—"} - - - {loc.appLoadScore > 0 ? Math.round(loc.appLoadScore) : "—"} + ) : ( + + — - {loc.minutesPerApp > 0 && ( - - ({loc.minutesPerApp.toFixed(1)} min/app) + )} + + 0 ? undefined : "No succeeded enrollments yet"} + > + {loc.avgDurationMinutes > 0 ? `${Math.round(loc.avgDurationMinutes)} min` : "—"} + + + {loc.p95DurationMinutes > 0 ? `${Math.round(loc.p95DurationMinutes)} min` : "—"} + + + {loc.appLoadScore > 0 ? Math.round(loc.appLoadScore) : "—"} + + {loc.minutesPerApp > 0 && ( + + ({loc.minutesPerApp.toFixed(1)} min/app) + + )} + + {formatThroughput(loc.avgThroughputBytesPerSec)} + + {loc.medianApiLatencyMs > 0 ? ( +
0 ? "+" : ""}${loc.apiLatencyVsGlobalPct.toFixed(0)}% vs global` : ""}`}> + + {Math.round(loc.medianApiLatencyMs)} ms - )} -
- {formatThroughput(loc.avgThroughputBytesPerSec)} - - {loc.medianApiLatencyMs > 0 ? ( -
0 ? "+" : ""}${loc.apiLatencyVsGlobalPct.toFixed(0)}% vs global` : ""}`}> - - {Math.round(loc.medianApiLatencyMs)} ms - - ({loc.apiLatencySessionCount}) -
- ) : ( - — - )} -
- {loc.doSessionCount > 0 ? ( -
0 - ? `LAN: ${formatBytes(loc.totalDoBytesFromLanPeers)} | Group: ${formatBytes(loc.totalDoBytesFromGroupPeers)} | Internet: ${formatBytes(loc.totalDoBytesFromInternetPeers)}` - : `${loc.doSessionCount} session(s) with DO data` - }> - - {loc.avgDoPercentPeerCaching > 0 ? `${loc.avgDoPercentPeerCaching}%` : "0%"} - - - ({loc.doSessionCount}) - -
- ) : ( - — - )} -
-
- {loc.avgDurationMinutes > 0 && loc.durationVsGlobalPct !== 0 ? ( - 0 - ? "bg-red-50 text-red-700" - : "bg-green-50 text-green-700" - }`} - > - {loc.durationVsGlobalPct > 0 ? "+" : ""} - {loc.durationVsGlobalPct.toFixed(0)}% - - ) : ( - — - )} - - - + ({loc.apiLatencySessionCount})
-
-
- )} - {sortedLocations.length > 0 && } -
- )} + ) : ( + — + )} +
+ {loc.doSessionCount > 0 ? ( +
0 + ? `LAN: ${formatBytes(loc.totalDoBytesFromLanPeers)} | Group: ${formatBytes(loc.totalDoBytesFromGroupPeers)} | Internet: ${formatBytes(loc.totalDoBytesFromInternetPeers)}` + : `${loc.doSessionCount} session(s) with DO data` + }> + + {loc.avgDoPercentPeerCaching > 0 ? `${loc.avgDoPercentPeerCaching}%` : "0%"} + + + ({loc.doSessionCount}) + +
+ ) : ( + — + )} +
+
+ {loc.avgDurationMinutes > 0 && loc.durationVsGlobalPct !== 0 ? ( + 0 + ? "bg-red-50 text-red-700" + : "bg-green-50 text-green-700" + }`} + > + {loc.durationVsGlobalPct > 0 ? "+" : ""} + {loc.durationVsGlobalPct.toFixed(0)}% + + ) : ( + — + )} + + + +
+
+
+ )} + {sortedLocations.length > 0 && } +
+ )} - {/* No data state */} - {!geoMetrics && !loading && ( -
- Failed to load geographic metrics. Please try again later. -
- )} -
-
-
+ {/* No data state */} + {!geoMetrics && !loading && ( +
+ Failed to load geographic metrics. Please try again later. +
+ )} + + ); } diff --git a/src/Web/autopilot-monitor-web/app/geographic-performance/sessions/page.tsx b/src/Web/autopilot-monitor-web/app/geographic-performance/sessions/page.tsx index 0250f4ed..5da17c72 100644 --- a/src/Web/autopilot-monitor-web/app/geographic-performance/sessions/page.tsx +++ b/src/Web/autopilot-monitor-web/app/geographic-performance/sessions/page.tsx @@ -57,14 +57,18 @@ function formatDate(dateStr: string): string { } export default function LocationSessionsPage() { + // ProtectedRoute stays outermost: the content's loading card must never render for a signed-out + // tab, or the login redirect would never fire. return ( - -
Loading...
- - }> - -
+ + +
Loading...
+ + }> + +
+
); } @@ -180,222 +184,220 @@ function LocationSessionsContent() { } return ( - -
- {/* Delegated ("MSP") admin: blue cross-tenant banner (GA gets the purple one below). */} - - {globalAdminMode && ( -
- - +
+ {/* Delegated ("MSP") admin: blue cross-tenant banner (GA gets the purple one below). */} + + {globalAdminMode && ( +
+ + + + Global Admin View + — aggregating data across all tenants +
+ )} +
+
+ +
+ + + - Global Admin View - — aggregating data across all tenants +
+

{locationKey}

+

+ {timeLabel} · grouped by {groupBy} +

+
+
+
+
+ +
+ {/* Summary Stats */} + {stats && ( +
+
+
Sessions
+
{stats.total}
+
+
+
API Latency
+
0 ? "text-gray-900" : "text-gray-400"}`}> + {stats.medianApiLatency > 0 ? `${stats.medianApiLatency} ms` : "—"} +
+
+ {stats.latencySessions > 0 ? `${stats.latencySessions} session(s) with data` : "requires latency-reporting agent"} +
+
+
+
Succeeded
+
{stats.succeeded}
+
+ {stats.total > 0 ? `${Math.round((stats.succeeded / stats.total) * 100)}%` : "—"} +
+
+
+
Failed
+
0 ? "text-red-600" : "text-gray-400"}`}> + {stats.failed} +
+
+ {stats.total > 0 && stats.failed > 0 ? `${Math.round((stats.failed / stats.total) * 100)}%` : "—"} +
+
+
+
Avg Duration
+
+ {stats.avgDuration > 0 ? `${stats.avgDuration} min` : "—"} +
+
succeeded sessions
+
+
+
DO Peers
+
= 50 ? "text-green-600" : + stats.avgDoPercent >= 10 ? "text-yellow-600" : + "text-gray-600" + }`}> + {stats.doSessions > 0 ? `${stats.avgDoPercent}%` : "—"} +
+
+ {stats.doSessions > 0 ? `${stats.doSessions} of ${stats.total} sessions` : "no DO telemetry"} +
+
)} -
-
- -
- - - - -
-

{locationKey}

-

- {timeLabel} · grouped by {groupBy} -

-
-
-
-
-
- {/* Summary Stats */} - {stats && ( -
-
-
Sessions
-
{stats.total}
-
-
-
API Latency
-
0 ? "text-gray-900" : "text-gray-400"}`}> - {stats.medianApiLatency > 0 ? `${stats.medianApiLatency} ms` : "—"} -
-
- {stats.latencySessions > 0 ? `${stats.latencySessions} session(s) with data` : "requires latency-reporting agent"} -
-
-
-
Succeeded
-
{stats.succeeded}
-
- {stats.total > 0 ? `${Math.round((stats.succeeded / stats.total) * 100)}%` : "—"} -
-
-
-
Failed
-
0 ? "text-red-600" : "text-gray-400"}`}> - {stats.failed} -
-
- {stats.total > 0 && stats.failed > 0 ? `${Math.round((stats.failed / stats.total) * 100)}%` : "—"} -
-
-
-
Avg Duration
-
- {stats.avgDuration > 0 ? `${stats.avgDuration} min` : "—"} -
-
succeeded sessions
-
-
-
DO Peers
-
= 50 ? "text-green-600" : - stats.avgDoPercent >= 10 ? "text-yellow-600" : - "text-gray-600" - }`}> - {stats.doSessions > 0 ? `${stats.avgDoPercent}%` : "—"} -
-
- {stats.doSessions > 0 ? `${stats.doSessions} of ${stats.total} sessions` : "no DO telemetry"} -
-
+ {/* Session Table */} +
+
+

+ Sessions ({data?.totalCount ?? 0}) +

+
+ {!data?.sessions?.length ? ( +
+ No sessions found for this location. +
+ ) : ( +
+ + + + + + + + + + + + + + + {data.sessions.map((session) => ( + router.push(sessionUrl(session.sessionId))} + className="hover:bg-gray-50 cursor-pointer transition-colors" + > + + + + + + + + + + ))} + +
+ Device Name + + Serial Number + + Model + + Status + + Duration + + API Latency + + DO Peers + + Started At +
+ {session.deviceName || "—"} + + {session.serialNumber || "—"} + + {session.model || "—"} + + + + {formatDuration(session.durationSeconds)} + + {(session.avgApiLatencyMs ?? 0) > 0 ? ( + + {Math.round(session.avgApiLatencyMs!)} ms + + ) : ( + — + )} + + {session.hasDoTelemetry ? ( +
+ = 50 + ? "bg-green-100 text-green-800" + : (session.doPercentPeerCaching ?? 0) >= 10 + ? "bg-yellow-100 text-yellow-800" + : "bg-gray-100 text-gray-600" + }`} + > + {(session.doPercentPeerCaching ?? 0).toFixed(1)}% + + + ({session.doAppCount}/{session.totalAppCount}) + +
+ ) : ( + 0 + ? `${session.totalAppCount} app(s) installed, no DO telemetry` + : "no app install data" + } + > + — + + )} +
+ {formatDate(session.startedAt)} +
)} - - {/* Session Table */} -
-
-

- Sessions ({data?.totalCount ?? 0}) -

-
- {!data?.sessions?.length ? ( -
- No sessions found for this location. -
- ) : ( -
- - - - - - - - - - - - - - - {data.sessions.map((session) => ( - router.push(sessionUrl(session.sessionId))} - className="hover:bg-gray-50 cursor-pointer transition-colors" - > - - - - - - - - - - ))} - -
- Device Name - - Serial Number - - Model - - Status - - Duration - - API Latency - - DO Peers - - Started At -
- {session.deviceName || "—"} - - {session.serialNumber || "—"} - - {session.model || "—"} - - - - {formatDuration(session.durationSeconds)} - - {(session.avgApiLatencyMs ?? 0) > 0 ? ( - - {Math.round(session.avgApiLatencyMs!)} ms - - ) : ( - — - )} - - {session.hasDoTelemetry ? ( -
- = 50 - ? "bg-green-100 text-green-800" - : (session.doPercentPeerCaching ?? 0) >= 10 - ? "bg-yellow-100 text-yellow-800" - : "bg-gray-100 text-gray-600" - }`} - > - {(session.doPercentPeerCaching ?? 0).toFixed(1)}% - - - ({session.doAppCount}/{session.totalAppCount}) - -
- ) : ( - 0 - ? `${session.totalAppCount} app(s) installed, no DO telemetry` - : "no app install data" - } - > - — - - )} -
- {formatDate(session.startedAt)} -
-
- )} -
-
-
- +
+ +
); }