mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
geo report drill down has now DO usage stats as well
This commit is contained in:
@@ -1 +1 @@
|
||||
980
|
||||
982
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
using AutopilotMonitor.Functions.Functions.Metrics;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
|
||||
namespace AutopilotMonitor.Functions.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression guard: the geographic drilldown must surface per-session DO telemetry
|
||||
/// so operators can troubleshoot which devices contributed to a region's DO % badge.
|
||||
/// Formula must stay consistent with GetGeographicMetricsFunction (peers / total * 100,
|
||||
/// preferring DoTotalBytesDownloaded as denominator, falling back to peers+http).
|
||||
/// </summary>
|
||||
public class GeographicLocationSessionsDoTests
|
||||
{
|
||||
private static AppInstallSummary App(string sessionId, int downloadMode, long peers, long http, long total,
|
||||
long lan = 0, long group = 0, long internetPeers = 0)
|
||||
{
|
||||
return new AppInstallSummary
|
||||
{
|
||||
SessionId = sessionId,
|
||||
DoDownloadMode = downloadMode,
|
||||
DoBytesFromPeers = peers,
|
||||
DoBytesFromHttp = http,
|
||||
DoTotalBytesDownloaded = total,
|
||||
DoBytesFromLanPeers = lan,
|
||||
DoBytesFromGroupPeers = group,
|
||||
DoBytesFromInternetPeers = internetPeers,
|
||||
};
|
||||
}
|
||||
|
||||
private static SessionSummary Session(string id) => new SessionSummary { SessionId = id };
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_SessionWithDoAndNonDoApps_ComputesWeightedPercent()
|
||||
{
|
||||
var sessions = new List<SessionSummary> { Session("S1") };
|
||||
var apps = new List<AppInstallSummary>
|
||||
{
|
||||
App("S1", downloadMode: 0, peers: 50_000_000, http: 50_000_000, total: 100_000_000),
|
||||
App("S1", downloadMode: 1, peers: 0, http: 100_000_000, total: 100_000_000),
|
||||
App("S1", downloadMode: -1, peers: 0, http: 0, total: 0), // no DO telemetry
|
||||
};
|
||||
|
||||
var rows = GetGeographicLocationSessionsFunction.BuildRows(sessions, apps);
|
||||
|
||||
Assert.Single(rows);
|
||||
var row = rows[0];
|
||||
Assert.True(row.HasDoTelemetry);
|
||||
Assert.Equal(2, row.DoAppCount);
|
||||
Assert.Equal(3, row.TotalAppCount);
|
||||
// 50MB peers / 200MB total = 25%
|
||||
Assert.Equal(25.0, row.DoPercentPeerCaching);
|
||||
Assert.Equal(50_000_000, row.DoBytesFromPeers);
|
||||
Assert.Equal(150_000_000, row.DoBytesFromHttp);
|
||||
Assert.Equal(200_000_000, row.DoTotalBytesDownloaded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_SessionWithoutAppData_MarkedNoTelemetry()
|
||||
{
|
||||
var rows = GetGeographicLocationSessionsFunction.BuildRows(
|
||||
new List<SessionSummary> { Session("S2") },
|
||||
new List<AppInstallSummary>());
|
||||
|
||||
Assert.Single(rows);
|
||||
Assert.False(rows[0].HasDoTelemetry);
|
||||
Assert.Equal(0, rows[0].DoAppCount);
|
||||
Assert.Equal(0, rows[0].TotalAppCount);
|
||||
Assert.Equal(0, rows[0].DoPercentPeerCaching);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_FallsBackToPeersPlusHttp_WhenTotalBytesMissing()
|
||||
{
|
||||
// Legacy records may not populate DoTotalBytesDownloaded — must fall back
|
||||
// to peers + http so the denominator isn't zero and the % is still meaningful.
|
||||
var apps = new List<AppInstallSummary>
|
||||
{
|
||||
App("S3", downloadMode: 0, peers: 40_000_000, http: 60_000_000, total: 0),
|
||||
};
|
||||
var rows = GetGeographicLocationSessionsFunction.BuildRows(
|
||||
new List<SessionSummary> { Session("S3") }, apps);
|
||||
|
||||
Assert.True(rows[0].HasDoTelemetry);
|
||||
Assert.Equal(40.0, rows[0].DoPercentPeerCaching);
|
||||
Assert.Equal(100_000_000, rows[0].DoTotalBytesDownloaded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRows_MultipleSessions_AggregatesMatchRegionFormula()
|
||||
{
|
||||
// Invariant: sum(session peers) / sum(session total bytes) across the drilldown
|
||||
// must equal the region-level AvgDoPercentPeerCaching computed by
|
||||
// GetGeographicMetricsFunction from the same app summaries.
|
||||
var sessions = new List<SessionSummary> { Session("A"), Session("B"), Session("C") };
|
||||
var apps = new List<AppInstallSummary>
|
||||
{
|
||||
App("A", 0, peers: 30_000_000, http: 70_000_000, total: 100_000_000),
|
||||
App("B", 0, peers: 80_000_000, http: 20_000_000, total: 100_000_000),
|
||||
App("C", -1, 0, 0, 0),
|
||||
};
|
||||
|
||||
var rows = GetGeographicLocationSessionsFunction.BuildRows(sessions, apps);
|
||||
var withDo = rows.Where(r => r.HasDoTelemetry).ToList();
|
||||
var totalPeers = withDo.Sum(r => r.DoBytesFromPeers);
|
||||
var totalBytes = withDo.Sum(r => r.DoTotalBytesDownloaded);
|
||||
var weighted = totalBytes > 0 ? (double)totalPeers / totalBytes * 100 : 0;
|
||||
|
||||
Assert.Equal(2, withDo.Count);
|
||||
Assert.Equal(55.0, weighted, precision: 6);
|
||||
}
|
||||
}
|
||||
+27
-3
@@ -12,11 +12,16 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
{
|
||||
private readonly ILogger<GetGeographicLocationSessionsFunction> _logger;
|
||||
private readonly IMaintenanceRepository _maintenanceRepo;
|
||||
private readonly IMetricsRepository _metricsRepo;
|
||||
|
||||
public GetGeographicLocationSessionsFunction(ILogger<GetGeographicLocationSessionsFunction> logger, IMaintenanceRepository maintenanceRepo)
|
||||
public GetGeographicLocationSessionsFunction(
|
||||
ILogger<GetGeographicLocationSessionsFunction> logger,
|
||||
IMaintenanceRepository maintenanceRepo,
|
||||
IMetricsRepository metricsRepo)
|
||||
{
|
||||
_logger = logger;
|
||||
_maintenanceRepo = maintenanceRepo;
|
||||
_metricsRepo = metricsRepo;
|
||||
}
|
||||
|
||||
[Function("GetGeographicLocationSessions")]
|
||||
@@ -49,11 +54,13 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
|
||||
var cutoff = DateTime.UtcNow.AddDays(-days);
|
||||
var sessions = await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1), tenantId);
|
||||
|
||||
var filtered = FilterSessionsByLocation(sessions, locationKey, groupBy);
|
||||
|
||||
var appSummaries = await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantId);
|
||||
var rows = BuildRows(filtered, appSummaries);
|
||||
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new { success = true, sessions = filtered, totalCount = filtered.Count });
|
||||
await response.WriteAsJsonAsync(new { success = true, sessions = rows, totalCount = rows.Count });
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -73,5 +80,22 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
.OrderByDescending(s => s.StartedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
internal static List<LocationSessionRow> BuildRows(
|
||||
List<SessionSummary> sessions, List<AppInstallSummary> appSummaries)
|
||||
{
|
||||
var appsBySession = appSummaries
|
||||
.GroupBy(a => a.SessionId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var rows = new List<LocationSessionRow>(sessions.Count);
|
||||
foreach (var s in sessions)
|
||||
{
|
||||
var apps = appsBySession.TryGetValue(s.SessionId, out var list) ? list : new List<AppInstallSummary>();
|
||||
var agg = DoAggregator.Compute(apps);
|
||||
rows.Add(LocationSessionRow.From(s, agg, apps.Count));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-14
@@ -123,17 +123,10 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
.Where(s => appsBySession.ContainsKey(s.SessionId))
|
||||
.SelectMany(s => appsBySession[s.SessionId])
|
||||
.ToList();
|
||||
var doApps = allLocationApps.Where(a => a.DoDownloadMode >= 0).ToList();
|
||||
var doAgg = DoAggregator.Compute(allLocationApps);
|
||||
var doSessionCount = group
|
||||
.Where(s => appsBySession.ContainsKey(s.SessionId))
|
||||
.Count(s => appsBySession[s.SessionId].Any(a => a.DoDownloadMode >= 0));
|
||||
var totalDoPeers = doApps.Sum(a => a.DoBytesFromPeers);
|
||||
var totalDoHttp = doApps.Sum(a => a.DoBytesFromHttp);
|
||||
// Use DoTotalBytesDownloaded as ground truth denominator (accounts for all sources);
|
||||
// fall back to peers+http sum for old records without the field
|
||||
var totalDoDownloaded = doApps.Sum(a => a.DoTotalBytesDownloaded);
|
||||
var totalDoBytes = totalDoDownloaded > 0 ? totalDoDownloaded : (totalDoPeers + totalDoHttp);
|
||||
var avgDoPct = totalDoBytes > 0 ? (double)totalDoPeers / totalDoBytes * 100 : 0;
|
||||
|
||||
locations.Add(new LocationMetrics
|
||||
{
|
||||
@@ -155,12 +148,12 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
TotalDownloadBytes = totalBytes,
|
||||
// Delivery Optimization
|
||||
DoSessionCount = doSessionCount,
|
||||
AvgDoPercentPeerCaching = Math.Round(avgDoPct, 1),
|
||||
TotalDoBytesFromPeers = totalDoPeers,
|
||||
TotalDoBytesFromHttp = totalDoHttp,
|
||||
TotalDoBytesFromLanPeers = doApps.Sum(a => a.DoBytesFromLanPeers),
|
||||
TotalDoBytesFromGroupPeers = doApps.Sum(a => a.DoBytesFromGroupPeers),
|
||||
TotalDoBytesFromInternetPeers = doApps.Sum(a => a.DoBytesFromInternetPeers)
|
||||
AvgDoPercentPeerCaching = Math.Round(doAgg.PercentPeerCaching, 1),
|
||||
TotalDoBytesFromPeers = doAgg.BytesFromPeers,
|
||||
TotalDoBytesFromHttp = doAgg.BytesFromHttp,
|
||||
TotalDoBytesFromLanPeers = doAgg.BytesFromLanPeers,
|
||||
TotalDoBytesFromGroupPeers = doAgg.BytesFromGroupPeers,
|
||||
TotalDoBytesFromInternetPeers = doAgg.BytesFromInternetPeers
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -11,13 +11,16 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
{
|
||||
private readonly ILogger<GetGlobalGeographicLocationSessionsFunction> _logger;
|
||||
private readonly IMaintenanceRepository _maintenanceRepo;
|
||||
private readonly IMetricsRepository _metricsRepo;
|
||||
|
||||
public GetGlobalGeographicLocationSessionsFunction(
|
||||
ILogger<GetGlobalGeographicLocationSessionsFunction> logger,
|
||||
IMaintenanceRepository maintenanceRepo)
|
||||
IMaintenanceRepository maintenanceRepo,
|
||||
IMetricsRepository metricsRepo)
|
||||
{
|
||||
_logger = logger;
|
||||
_maintenanceRepo = maintenanceRepo;
|
||||
_metricsRepo = metricsRepo;
|
||||
}
|
||||
|
||||
[Function("GetGlobalGeographicLocationSessions")]
|
||||
@@ -50,11 +53,13 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
|
||||
var cutoff = DateTime.UtcNow.AddDays(-days);
|
||||
var sessions = await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1));
|
||||
|
||||
var filtered = GetGeographicLocationSessionsFunction.FilterSessionsByLocation(sessions, locationKey, groupBy);
|
||||
|
||||
var appSummaries = await _metricsRepo.GetAllAppInstallSummariesAsync();
|
||||
var rows = GetGeographicLocationSessionsFunction.BuildRows(filtered, appSummaries);
|
||||
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new { success = true, sessions = filtered, totalCount = filtered.Count });
|
||||
await response.WriteAsJsonAsync(new { success = true, sessions = rows, totalCount = rows.Count });
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -75,6 +75,86 @@ namespace AutopilotMonitor.Shared.Models
|
||||
public long TotalDoBytesFromInternetPeers { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Session row returned by the geographic drilldown endpoint. Extends <see cref="SessionSummary"/>
|
||||
/// with per-session Delivery Optimization aggregates so the user can troubleshoot DO usage
|
||||
/// without leaving the drilldown view.
|
||||
/// </summary>
|
||||
public class LocationSessionRow : SessionSummary
|
||||
{
|
||||
/// <summary>True if any app in this session has DO telemetry (DoDownloadMode >= 0).</summary>
|
||||
public bool HasDoTelemetry { get; set; }
|
||||
/// <summary>Number of apps in this session that have DO telemetry.</summary>
|
||||
public int DoAppCount { get; set; }
|
||||
/// <summary>Total number of app install summaries recorded for this session.</summary>
|
||||
public int TotalAppCount { get; set; }
|
||||
/// <summary>Weighted peer-caching percentage for this session (0-100).</summary>
|
||||
public double DoPercentPeerCaching { get; set; }
|
||||
public long DoBytesFromPeers { get; set; }
|
||||
public long DoBytesFromHttp { get; set; }
|
||||
public long DoTotalBytesDownloaded { get; set; }
|
||||
public long DoBytesFromLanPeers { get; set; }
|
||||
public long DoBytesFromGroupPeers { get; set; }
|
||||
public long DoBytesFromInternetPeers { get; set; }
|
||||
|
||||
public static LocationSessionRow From(SessionSummary s, DoAggregator.DoAggregate a, int totalAppCount)
|
||||
{
|
||||
return new LocationSessionRow
|
||||
{
|
||||
// SessionSummary fields
|
||||
SessionId = s.SessionId,
|
||||
TenantId = s.TenantId,
|
||||
SerialNumber = s.SerialNumber,
|
||||
DeviceName = s.DeviceName,
|
||||
Manufacturer = s.Manufacturer,
|
||||
Model = s.Model,
|
||||
StartedAt = s.StartedAt,
|
||||
CompletedAt = s.CompletedAt,
|
||||
CurrentPhase = s.CurrentPhase,
|
||||
CurrentPhaseDetail = s.CurrentPhaseDetail,
|
||||
Status = s.Status,
|
||||
FailureReason = s.FailureReason,
|
||||
FailureSource = s.FailureSource,
|
||||
EventCount = s.EventCount,
|
||||
DurationSeconds = s.DurationSeconds,
|
||||
EnrollmentType = s.EnrollmentType,
|
||||
DiagnosticsBlobName = s.DiagnosticsBlobName,
|
||||
LastEventAt = s.LastEventAt,
|
||||
IsPreProvisioned = s.IsPreProvisioned,
|
||||
ResumedAt = s.ResumedAt,
|
||||
StalledAt = s.StalledAt,
|
||||
IsHybridJoin = s.IsHybridJoin,
|
||||
OsName = s.OsName,
|
||||
OsBuild = s.OsBuild,
|
||||
OsDisplayVersion = s.OsDisplayVersion,
|
||||
OsEdition = s.OsEdition,
|
||||
OsLanguage = s.OsLanguage,
|
||||
IsUserDriven = s.IsUserDriven,
|
||||
AgentVersion = s.AgentVersion,
|
||||
ImeAgentVersion = s.ImeAgentVersion,
|
||||
GeoCountry = s.GeoCountry,
|
||||
GeoRegion = s.GeoRegion,
|
||||
GeoCity = s.GeoCity,
|
||||
GeoLoc = s.GeoLoc,
|
||||
PlatformScriptCount = s.PlatformScriptCount,
|
||||
RemediationScriptCount = s.RemediationScriptCount,
|
||||
PendingActionsJson = s.PendingActionsJson,
|
||||
PendingActionsQueuedAt = s.PendingActionsQueuedAt,
|
||||
// DO aggregate
|
||||
HasDoTelemetry = a.HasTelemetry,
|
||||
DoAppCount = a.DoAppCount,
|
||||
TotalAppCount = totalAppCount,
|
||||
DoPercentPeerCaching = System.Math.Round(a.PercentPeerCaching, 1),
|
||||
DoBytesFromPeers = a.BytesFromPeers,
|
||||
DoBytesFromHttp = a.BytesFromHttp,
|
||||
DoTotalBytesDownloaded = a.TotalBytesDownloaded,
|
||||
DoBytesFromLanPeers = a.BytesFromLanPeers,
|
||||
DoBytesFromGroupPeers = a.BytesFromGroupPeers,
|
||||
DoBytesFromInternetPeers = a.BytesFromInternetPeers,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Global average benchmarks for geographic comparison.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AutopilotMonitor.Shared.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes Delivery Optimization peer-caching aggregates from a collection of
|
||||
/// <see cref="AppInstallSummary"/> records. Single source of truth shared between
|
||||
/// the region-level aggregate (GetGeographicMetricsFunction) and the per-session
|
||||
/// aggregate surfaced by the drilldown endpoint.
|
||||
/// </summary>
|
||||
public static class DoAggregator
|
||||
{
|
||||
public class DoAggregate
|
||||
{
|
||||
public int DoAppCount { get; set; }
|
||||
public long BytesFromPeers { get; set; }
|
||||
public long BytesFromHttp { get; set; }
|
||||
public long TotalBytesDownloaded { get; set; }
|
||||
public long BytesFromLanPeers { get; set; }
|
||||
public long BytesFromGroupPeers { get; set; }
|
||||
public long BytesFromInternetPeers { get; set; }
|
||||
|
||||
public bool HasTelemetry => DoAppCount > 0;
|
||||
public double PercentPeerCaching => TotalBytesDownloaded > 0
|
||||
? (double)BytesFromPeers / TotalBytesDownloaded * 100
|
||||
: 0;
|
||||
}
|
||||
|
||||
public static DoAggregate Compute(IEnumerable<AppInstallSummary> apps)
|
||||
{
|
||||
var doApps = apps.Where(a => a.DoDownloadMode >= 0).ToList();
|
||||
var peers = doApps.Sum(a => a.DoBytesFromPeers);
|
||||
var http = doApps.Sum(a => a.DoBytesFromHttp);
|
||||
var downloaded = doApps.Sum(a => a.DoTotalBytesDownloaded);
|
||||
return new DoAggregate
|
||||
{
|
||||
DoAppCount = doApps.Count,
|
||||
BytesFromPeers = peers,
|
||||
BytesFromHttp = http,
|
||||
TotalBytesDownloaded = downloaded > 0 ? downloaded : (peers + http),
|
||||
BytesFromLanPeers = doApps.Sum(a => a.DoBytesFromLanPeers),
|
||||
BytesFromGroupPeers = doApps.Sum(a => a.DoBytesFromGroupPeers),
|
||||
BytesFromInternetPeers = doApps.Sum(a => a.DoBytesFromInternetPeers),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,26 @@ interface SessionSummary {
|
||||
failureReason: string | null;
|
||||
durationSeconds: number | null;
|
||||
enrollmentType: string;
|
||||
// Per-session Delivery Optimization aggregate (added by geographic drilldown endpoint)
|
||||
hasDoTelemetry?: boolean;
|
||||
doAppCount?: number;
|
||||
totalAppCount?: number;
|
||||
doPercentPeerCaching?: number;
|
||||
doBytesFromPeers?: number;
|
||||
doBytesFromHttp?: number;
|
||||
doTotalBytesDownloaded?: number;
|
||||
doBytesFromLanPeers?: number;
|
||||
doBytesFromGroupPeers?: number;
|
||||
doBytesFromInternetPeers?: number;
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (!bytes || bytes <= 0) return "—";
|
||||
if (bytes >= 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
||||
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(0)} MB`;
|
||||
return `${(bytes / 1024).toFixed(0)} KB`;
|
||||
};
|
||||
|
||||
interface LocationSessionsResponse {
|
||||
success: boolean;
|
||||
sessions: SessionSummary[];
|
||||
@@ -139,6 +157,14 @@ function LocationSessionsContent() {
|
||||
.map((s) => s.durationSeconds!);
|
||||
return durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length / 60) : 0;
|
||||
})(),
|
||||
// Weighted peer-caching % across sessions with DO telemetry (matches region aggregate formula)
|
||||
doSessions: data.sessions.filter((s) => s.hasDoTelemetry).length,
|
||||
avgDoPercent: (() => {
|
||||
const doSessions = data.sessions.filter((s) => s.hasDoTelemetry);
|
||||
const totalPeers = doSessions.reduce((a, s) => a + (s.doBytesFromPeers ?? 0), 0);
|
||||
const totalBytes = doSessions.reduce((a, s) => a + (s.doTotalBytesDownloaded ?? 0), 0);
|
||||
return totalBytes > 0 ? Math.round((totalPeers / totalBytes) * 1000) / 10 : 0;
|
||||
})(),
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -191,7 +217,7 @@ function LocationSessionsContent() {
|
||||
<main className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
{/* Summary Stats */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-sm font-medium text-gray-500">Sessions</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
|
||||
@@ -219,6 +245,20 @@ function LocationSessionsContent() {
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">succeeded sessions</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-sm font-medium text-gray-500">DO Peers</div>
|
||||
<div className={`text-2xl font-bold ${
|
||||
stats.doSessions === 0 ? "text-gray-400" :
|
||||
stats.avgDoPercent >= 50 ? "text-green-600" :
|
||||
stats.avgDoPercent >= 10 ? "text-yellow-600" :
|
||||
"text-gray-600"
|
||||
}`}>
|
||||
{stats.doSessions > 0 ? `${stats.avgDoPercent}%` : "—"}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{stats.doSessions > 0 ? `${stats.doSessions} of ${stats.total} sessions` : "no DO telemetry"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -253,6 +293,9 @@ function LocationSessionsContent() {
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Duration
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
DO Peers
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Started At
|
||||
</th>
|
||||
@@ -280,6 +323,42 @@ function LocationSessionsContent() {
|
||||
<td className="px-4 py-3 text-sm text-gray-600">
|
||||
{formatDuration(session.durationSeconds)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
{session.hasDoTelemetry ? (
|
||||
<div
|
||||
title={
|
||||
`Peers: ${formatBytes(session.doBytesFromPeers ?? 0)} | HTTP: ${formatBytes(session.doBytesFromHttp ?? 0)} | Total: ${formatBytes(session.doTotalBytesDownloaded ?? 0)}\n` +
|
||||
`LAN: ${formatBytes(session.doBytesFromLanPeers ?? 0)} | Group: ${formatBytes(session.doBytesFromGroupPeers ?? 0)} | Internet: ${formatBytes(session.doBytesFromInternetPeers ?? 0)}`
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
|
||||
(session.doPercentPeerCaching ?? 0) >= 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)}%
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 ml-1">
|
||||
({session.doAppCount}/{session.totalAppCount})
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
className="text-gray-300"
|
||||
title={
|
||||
(session.totalAppCount ?? 0) > 0
|
||||
? `${session.totalAppCount} app(s) installed, no DO telemetry`
|
||||
: "no app install data"
|
||||
}
|
||||
>
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-500">
|
||||
{formatDate(session.startedAt)}
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user