- {CLASS_LABELS[cls.enrollmentClass] ?? cls.enrollmentClass}
+ {enrollmentClassLabel(cls.enrollmentClass)}
n={cls.cleanSessionCount} · median total ≈ {formatMinutes(stackTotal)}
@@ -198,7 +182,7 @@ export default function TimeAttributionSection({ data }: { data: TimeAttribution
|
{app.appName || app.appId}
|
-
{CLASS_LABELS[cls] ?? cls} |
+
{enrollmentClassLabel(cls)} |
{app.sessionCount} |
{formatMinutes(app.medianSeconds)} |
diff --git a/src/Web/autopilot-monitor-web/app/sessions/components/TimeAttributionLane.tsx b/src/Web/autopilot-monitor-web/app/sessions/components/TimeAttributionLane.tsx
index dfb8261a..89e53b31 100644
--- a/src/Web/autopilot-monitor-web/app/sessions/components/TimeAttributionLane.tsx
+++ b/src/Web/autopilot-monitor-web/app/sessions/components/TimeAttributionLane.tsx
@@ -3,6 +3,7 @@
import React from "react";
import type { SessionTimeBreakdown } from "@/lib/generated/wire-types.generated";
import { osUpdateDetail } from "./timeAttributionLogic";
+import { TIME_ATTRIBUTION_SEGMENTS } from "@/components/timeAttributionSegments";
/**
* F1 time attribution (insights spec §F1): renders the PRE-COMPUTED breakdown row of a
@@ -16,17 +17,6 @@ import { osUpdateDetail } from "./timeAttributionLogic";
/** The session time breakdown (wire shape). */
export type SessionTimeBreakdownDto = SessionTimeBreakdown;
-const SEGMENT_META: { key: string; label: string; color: string }[] = [
- { key: "device_prep", label: "Device preparation", color: "bg-slate-400" },
- { key: "esp_apps", label: "Apps (ESP)", color: "bg-blue-500" },
- { key: "os_update", label: "Windows Update", color: "bg-cyan-500" },
- { key: "awaiting_sign_in", label: "Waiting for sign-in", color: "bg-sky-200" },
- { key: "identity_hello", label: "Identity & Hello", color: "bg-violet-500" },
- { key: "user_esp", label: "User ESP", color: "bg-indigo-400" },
- { key: "desktop_handoff", label: "Desktop handoff", color: "bg-emerald-500" },
- { key: "unattributed", label: "Unattributed", color: "bg-gray-300" },
-];
-
const FLAG_LABELS: Record = {
ClockSkewDropped: "Clock-skewed data dropped",
PartialObservation: "Agent started late — early phases underobserved",
@@ -59,7 +49,7 @@ export default function TimeAttributionLane({ breakdown }: { breakdown: SessionT
// the Windows Update slice.
const updateDetail = osUpdateDetail(breakdown.osUpdates ?? []);
- const parts = SEGMENT_META
+ const parts = TIME_ATTRIBUTION_SEGMENTS
.map(meta => ({
...meta,
seconds: totals.get(meta.key) ?? 0,
diff --git a/src/Web/autopilot-monitor-web/app/usage-metrics/page.tsx b/src/Web/autopilot-monitor-web/app/usage-metrics/page.tsx
index f6b17b15..1d7b9934 100644
--- a/src/Web/autopilot-monitor-web/app/usage-metrics/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/usage-metrics/page.tsx
@@ -12,6 +12,14 @@ import { CardSkeleton } from "@/components/skeletons/PageSkeleton";
import { DocsLink } from "@/components/DocsLink";
import { DOCS_PATHS } from "@/lib/docsPaths";
import type { PlatformUsageMetrics } from "@/lib/generated/wire-types.generated";
+import {
+ AppScriptsBlock,
+ DeploymentTypeBreakdown,
+ HardwareBlock,
+ PerformanceBlock,
+ SessionOutcomeCards,
+ SessionRecencyCards,
+} from "@/components/UsageMetricsBlocks";
import { fetchJson } from "@/lib/apiClient";
import { useWindowDays } from "@/hooks/useWindowDays";
import { notifyApiError } from "@/contexts/NotificationContext";
@@ -96,15 +104,6 @@ function UsageMetricsContent() {
const selectedTenantName = tenants.find(t => t.tenantId === selectedTenantId)?.domainName;
- const formatDuration = (minutes: number) => {
- if (minutes < 60) {
- return `${minutes.toFixed(1)}m`;
- }
- const hours = Math.floor(minutes / 60);
- const mins = Math.round(minutes % 60);
- return `${hours}h ${mins}m`;
- };
-
const formatTimestamp = (timestamp: string) => {
const date = new Date(timestamp);
return date.toLocaleString();
@@ -195,108 +194,15 @@ function UsageMetricsContent() {
{metrics.sessions.total.toLocaleString()}
last {metrics.windowDays ?? 90} days
-
- Today
- {metrics.sessions.today.toLocaleString()}
-
-
- Last 7 Days
- {metrics.sessions.last7Days.toLocaleString()}
-
-
- Last 30 Days
- {metrics.sessions.last30Days.toLocaleString()}
-
+
-
-
- Succeeded
- {metrics.sessions.succeeded.toLocaleString()}
-
-
- Failed
- {metrics.sessions.failed.toLocaleString()}
-
-
- Incomplete
- {metrics.sessions.incomplete.toLocaleString()}
-
-
- In Progress
- {metrics.sessions.inProgress.toLocaleString()}
-
-
- Success Rate
- {metrics.sessions.successRate}%
-
-
+
- {/* Deployment Types */}
-
- Deployment Types
-
-
-
-
- User Driven
- {metrics.deploymentTypes.userDriven.toLocaleString()}
-
-
- {metrics.deploymentTypes.userDrivenPercentage}%
-
-
-
-
-
-
-
- Pre-Provisioned
- {metrics.deploymentTypes.whiteGlove.toLocaleString()}
-
-
- {metrics.deploymentTypes.whiteGlovePercentage}%
-
-
-
-
-
-
+
- {/* Apps & Scripts */}
- {metrics.appScripts && (
-
- Apps & Scripts
-
-
- Avg Apps / Session
- {metrics.appScripts.avgAppsPerSession}
- {metrics.appScripts.totalUniqueApps.toLocaleString()} unique apps total
-
-
- Avg Platform Scripts / Session
- {metrics.appScripts.avgPlatformScriptsPerSession}
- {metrics.appScripts.totalPlatformScripts.toLocaleString()} executions total
-
-
- Avg Remediation Scripts / Session
- {metrics.appScripts.avgRemediationScriptsPerSession}
- {metrics.appScripts.totalRemediationScripts.toLocaleString()} executions total
-
-
-
- )}
+ {metrics.appScripts && }
{/* User Statistics */}
@@ -355,91 +261,9 @@ function UsageMetricsContent() {
)}
- {/* Performance Statistics */}
-
- Performance
-
-
- Average Duration
- {formatDuration(metrics.performance.avgDurationMinutes)}
-
-
- Median Duration
- {formatDuration(metrics.performance.medianDurationMinutes)}
-
-
- P95 Duration
- {formatDuration(metrics.performance.p95DurationMinutes)}
-
-
- P99 Duration
- {formatDuration(metrics.performance.p99DurationMinutes)}
-
-
-
+
- {/* Hardware Statistics */}
-
- {/* Top Manufacturers */}
-
- Top Manufacturers
-
-
- {metrics.hardware.topManufacturers.length > 0 ? (
- metrics.hardware.topManufacturers.map((item, index) => (
-
-
- {item.name || 'Unknown'}
-
- {item.count.toLocaleString()}
- ({item.percentage}%)
-
-
-
-
- ))
- ) : (
- No data available
- )}
-
-
-
-
- {/* Top Models */}
-
- Top Models
-
-
- {metrics.hardware.topModels.length > 0 ? (
- metrics.hardware.topModels.map((item, index) => (
-
-
- {item.name || 'Unknown'}
-
- {item.count.toLocaleString()}
- ({item.percentage}%)
-
-
-
-
- ))
- ) : (
- No data available
- )}
-
-
-
-
+
>
)}
diff --git a/src/Web/autopilot-monitor-web/components/UsageMetricsBlocks.tsx b/src/Web/autopilot-monitor-web/components/UsageMetricsBlocks.tsx
new file mode 100644
index 00000000..fc2db9a9
--- /dev/null
+++ b/src/Web/autopilot-monitor-web/components/UsageMetricsBlocks.tsx
@@ -0,0 +1,231 @@
+import type { ReactNode } from "react";
+import type {
+ AppScriptMetrics,
+ DeploymentTypeMetrics,
+ HardwareCount,
+ HardwareMetrics,
+ PerformanceMetrics,
+ SessionMetrics,
+} from "@/lib/generated/wire-types.generated";
+import { ENROLLMENT_CLASS_LABELS } from "@/lib/enrollmentClass";
+
+// The blocks the tenant Usage Metrics page and the platform Usage Metrics section share.
+
+function StatCard({ label, title, valueClass, note, children }: {
+ label: string;
+ title?: string;
+ valueClass: string;
+ note?: string;
+ children: ReactNode;
+}) {
+ return (
+
+ {label}
+ {children}
+ {note != null && {note} }
+
+ );
+}
+
+/** Today / last 7 / last 30 days: three cards for the page's own session grid. */
+export function SessionRecencyCards({ sessions }: { sessions: SessionMetrics }) {
+ return (
+ <>
+ {sessions.today.toLocaleString()}
+ {sessions.last7Days.toLocaleString()}
+ {sessions.last30Days.toLocaleString()}
+ >
+ );
+}
+
+export function SessionOutcomeCards({ sessions }: { sessions: SessionMetrics }) {
+ return (
+
+ {sessions.succeeded.toLocaleString()}
+ {sessions.failed.toLocaleString()}
+
+ {sessions.incomplete.toLocaleString()}
+
+ {sessions.inProgress.toLocaleString()}
+ {sessions.successRate}%
+
+ );
+}
+
+function ShareRow({ label, count, percentage, barClass }: {
+ label: string;
+ count: number;
+ percentage: number;
+ barClass: string;
+}) {
+ return (
+
+
+ {label}
+
+ {count.toLocaleString()}
+ ({percentage}%)
+
+
+
+
+ );
+}
+
+interface ShareGroup {
+ title: string;
+ hint: string;
+ barClass: string;
+ rows: { label: string; count: number; percentage: number }[];
+}
+
+function deploymentGroups(t: DeploymentTypeMetrics): ShareGroup[] {
+ return [
+ {
+ title: "Enrollment Type",
+ hint: "Share of all sessions in the window.",
+ barClass: "bg-blue-600",
+ rows: [
+ { label: "Autopilot (v1)", count: t.autopilotV1, percentage: t.autopilotV1Percentage },
+ { label: ENROLLMENT_CLASS_LABELS.device_preparation, count: t.devicePreparation, percentage: t.devicePreparationPercentage },
+ ],
+ },
+ {
+ title: "Join Type",
+ hint: "Share of all sessions in the window.",
+ barClass: "bg-indigo-600",
+ rows: [
+ { label: "Entra joined", count: t.entraJoin, percentage: t.entraJoinPercentage },
+ { label: "Hybrid joined", count: t.hybridJoin, percentage: t.hybridJoinPercentage },
+ ],
+ },
+ {
+ title: "Autopilot Deployment Mode",
+ hint: "Autopilot (v1) sessions by mode, as a share of all sessions in the window.",
+ barClass: "bg-purple-600",
+ rows: [
+ { label: ENROLLMENT_CLASS_LABELS.user_driven, count: t.userDriven, percentage: t.userDrivenPercentage },
+ { label: ENROLLMENT_CLASS_LABELS.self_deploying, count: t.selfDeploying, percentage: t.selfDeployingPercentage },
+ { label: ENROLLMENT_CLASS_LABELS.whiteglove, count: t.whiteGlove, percentage: t.whiteGlovePercentage },
+ ],
+ },
+ ];
+}
+
+/** Enrollment type, join type and Autopilot deployment mode of the sessions in the window. */
+export function DeploymentTypeBreakdown({ deploymentTypes }: { deploymentTypes: DeploymentTypeMetrics }) {
+ return (
+
+ Deployment Types
+
+ {deploymentGroups(deploymentTypes).map((group) => (
+
+ {group.title}
+
+ {group.rows.map((row) => (
+
+ ))}
+
+
+ ))}
+
+
+ );
+}
+
+export function AppScriptsBlock({ appScripts }: { appScripts: AppScriptMetrics }) {
+ return (
+
+ Apps & Scripts
+
+
+ {appScripts.avgAppsPerSession}
+
+
+ {appScripts.avgPlatformScriptsPerSession}
+
+
+ {appScripts.avgRemediationScriptsPerSession}
+
+
+
+ );
+}
+
+function formatDuration(minutes: number): string {
+ if (minutes < 60) {
+ return `${minutes.toFixed(1)}m`;
+ }
+ const hours = Math.floor(minutes / 60);
+ const mins = Math.round(minutes % 60);
+ return `${hours}h ${mins}m`;
+}
+
+export function PerformanceBlock({ performance }: { performance: PerformanceMetrics }) {
+ return (
+
+ Performance
+
+ {formatDuration(performance.avgDurationMinutes)}
+ {formatDuration(performance.medianDurationMinutes)}
+ {formatDuration(performance.p95DurationMinutes)}
+ {formatDuration(performance.p99DurationMinutes)}
+
+
+ );
+}
+
+function TopHardwareList({ title, items, barClass }: { title: string; items: HardwareCount[]; barClass: string }) {
+ return (
+
+ {title}
+
+
+ {items.length > 0 ? (
+ items.map((item, index) => (
+
+ ))
+ ) : (
+ No data available
+ )}
+
+
+
+ );
+}
+
+export function HardwareBlock({ hardware }: { hardware: HardwareMetrics }) {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/Web/autopilot-monitor-web/components/timeAttributionSegments.ts b/src/Web/autopilot-monitor-web/components/timeAttributionSegments.ts
new file mode 100644
index 00000000..efea64b7
--- /dev/null
+++ b/src/Web/autopilot-monitor-web/components/timeAttributionSegments.ts
@@ -0,0 +1,11 @@
+/** Time-attribution segments in display order, with label and bar colour (session lane and fleet panel). */
+export const TIME_ATTRIBUTION_SEGMENTS: { key: string; label: string; color: string }[] = [
+ { key: "device_prep", label: "Device preparation", color: "bg-slate-400" },
+ { key: "esp_apps", label: "Apps (ESP)", color: "bg-blue-500" },
+ { key: "os_update", label: "Windows Update", color: "bg-cyan-500" },
+ { key: "awaiting_sign_in", label: "Waiting for sign-in", color: "bg-sky-200" },
+ { key: "identity_hello", label: "Identity & Hello", color: "bg-violet-500" },
+ { key: "user_esp", label: "User ESP", color: "bg-indigo-400" },
+ { key: "desktop_handoff", label: "Desktop handoff", color: "bg-emerald-500" },
+ { key: "unattributed", label: "Unattributed", color: "bg-gray-300" },
+];
diff --git a/src/Web/autopilot-monitor-web/lib/enrollmentClass.ts b/src/Web/autopilot-monitor-web/lib/enrollmentClass.ts
new file mode 100644
index 00000000..0c0b8015
--- /dev/null
+++ b/src/Web/autopilot-monitor-web/lib/enrollmentClass.ts
@@ -0,0 +1,15 @@
+/**
+ * Display names of the fleet enrollment classes (backend `TimeAttributionCalculator.GetEnrollmentClass`:
+ * pre-provisioning → device preparation → self-deploying → user-driven).
+ */
+export const ENROLLMENT_CLASS_LABELS = {
+ user_driven: "User-driven",
+ whiteglove: "Pre-provisioning (WhiteGlove)",
+ self_deploying: "Self-deploying",
+ device_preparation: "Device Preparation (v2)",
+} as const;
+
+/** The display name of an enrollment class key; an unknown key shows as itself. */
+export function enrollmentClassLabel(enrollmentClass: string): string {
+ return (ENROLLMENT_CLASS_LABELS as Record)[enrollmentClass] ?? enrollmentClass;
+}
diff --git a/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.json b/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.json
index f0d57999..37a0f370 100644
--- a/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.json
+++ b/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.json
@@ -7428,9 +7428,93 @@
},
"DeploymentTypeMetrics": {
"kind": "object",
+ "doc": "Sessions in the window in three groups, each summing to Total: enrollment type (AutopilotV1 + DevicePreparation), join type (EntraJoin + HybridJoin) and Autopilot deployment mode (UserDriven + SelfDeploying + WhiteGlove, summing to AutopilotV1). Every percentage is the share of all sessions in the window.",
"fields": [
+ {
+ "name": "autopilotV1",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ },
+ "doc": "Windows Autopilot sessions (classic, enrollment type v1)."
+ },
+ {
+ "name": "autopilotV1Percentage",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ }
+ },
+ {
+ "name": "devicePreparation",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ },
+ "doc": "Windows Autopilot device preparation sessions (enrollment type v2)."
+ },
+ {
+ "name": "devicePreparationPercentage",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ }
+ },
+ {
+ "name": "entraJoin",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ },
+ "doc": "Sessions whose Autopilot profile does not ask for a hybrid join."
+ },
+ {
+ "name": "entraJoinPercentage",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ }
+ },
+ {
+ "name": "hybridJoin",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ },
+ "doc": "Sessions whose Autopilot profile asks for a hybrid join."
+ },
+ {
+ "name": "hybridJoinPercentage",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ }
+ },
{
"name": "userDriven",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ },
+ "doc": "Autopilot v1 sessions that are neither pre-provisioned nor self-deploying."
+ },
+ {
+ "name": "userDrivenPercentage",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ }
+ },
+ {
+ "name": "selfDeploying",
+ "type": {
+ "kind": "primitive",
+ "name": "number"
+ },
+ "doc": "Autopilot v1 sessions with a self-deploying profile that are not pre-provisioned. Sessions whose agent predates the profile flag count as user-driven."
+ },
+ {
+ "name": "selfDeployingPercentage",
"type": {
"kind": "primitive",
"name": "number"
@@ -7441,14 +7525,8 @@
"type": {
"kind": "primitive",
"name": "number"
- }
- },
- {
- "name": "userDrivenPercentage",
- "type": {
- "kind": "primitive",
- "name": "number"
- }
+ },
+ "doc": "Pre-provisioned (White Glove) sessions."
},
{
"name": "whiteGlovePercentage",
@@ -16319,7 +16397,7 @@
"kind": "ref",
"name": "DeploymentTypeMetrics"
},
- "doc": "Deployment type metrics (User Driven vs White Glove)"
+ "doc": "Sessions in the window by enrollment type, join type and Autopilot deployment mode"
},
{
"name": "appScripts",
diff --git a/src/Web/autopilot-monitor-web/lib/generated/wire-types.generated.ts b/src/Web/autopilot-monitor-web/lib/generated/wire-types.generated.ts
index 9f2f9159..4cec9c75 100644
--- a/src/Web/autopilot-monitor-web/lib/generated/wire-types.generated.ts
+++ b/src/Web/autopilot-monitor-web/lib/generated/wire-types.generated.ts
@@ -1453,10 +1453,28 @@ export interface DeliveryLatencyMetrics {
clockSkewPercent: number;
}
+/** Sessions in the window in three groups, each summing to Total: enrollment type (AutopilotV1 + DevicePreparation), join type (EntraJoin + HybridJoin) and Autopilot deployment mode (UserDriven + SelfDeploying + WhiteGlove, summing to AutopilotV1). Every percentage is the share of all sessions in the window. */
export interface DeploymentTypeMetrics {
+ /** Windows Autopilot sessions (classic, enrollment type v1). */
+ autopilotV1: number;
+ autopilotV1Percentage: number;
+ /** Windows Autopilot device preparation sessions (enrollment type v2). */
+ devicePreparation: number;
+ devicePreparationPercentage: number;
+ /** Sessions whose Autopilot profile does not ask for a hybrid join. */
+ entraJoin: number;
+ entraJoinPercentage: number;
+ /** Sessions whose Autopilot profile asks for a hybrid join. */
+ hybridJoin: number;
+ hybridJoinPercentage: number;
+ /** Autopilot v1 sessions that are neither pre-provisioned nor self-deploying. */
userDriven: number;
- whiteGlove: number;
userDrivenPercentage: number;
+ /** Autopilot v1 sessions with a self-deploying profile that are not pre-provisioned. Sessions whose agent predates the profile flag count as user-driven. */
+ selfDeploying: number;
+ selfDeployingPercentage: number;
+ /** Pre-provisioned (White Glove) sessions. */
+ whiteGlove: number;
whiteGlovePercentage: number;
}
@@ -3463,7 +3481,7 @@ export interface PlatformUsageMetrics {
performance: PerformanceMetrics;
/** Hardware metrics */
hardware: HardwareMetrics;
- /** Deployment type metrics (User Driven vs White Glove) */
+ /** Sessions in the window by enrollment type, join type and Autopilot deployment mode */
deploymentTypes: DeploymentTypeMetrics;
/** App and script count metrics */
appScripts: AppScriptMetrics;
diff --git a/tools/quality/jscpd-baseline.json b/tools/quality/jscpd-baseline.json
index 456c6ae3..80e39232 100644
--- a/tools/quality/jscpd-baseline.json
+++ b/tools/quality/jscpd-baseline.json
@@ -4,7 +4,6 @@
"016e952b3d382f92": 1,
"04f18ccf5471e650": 1,
"09722ff0399d0079": 1,
- "0d52e627a698ed73": 1,
"1102405933d6d36d": 1,
"12a9c705cd1be309": 1,
"1321e7dfb58dc00e": 1,
@@ -18,9 +17,7 @@
"1d7bffc15ccf7436": 1,
"29bbb90f9b9e306a": 1,
"2a985532f2e52066": 1,
- "2eead0be45077bd5": 1,
"33829a769f2eacb9": 1,
- "34398099b2dfe86d": 1,
"376b94382a0f2fc3": 1,
"391740d0bd84d73b": 1,
"3a83e25903a9f78b": 1,
@@ -36,11 +33,9 @@
"629fe688894bc289": 1,
"64c0bc57fd5351c3": 1,
"654a9df3c383502e": 1,
- "6792a21ecd44b52c": 1,
"6a3941fb9f2fdf26": 1,
"7247997f500a7474": 1,
"757cad917cd84ca1": 1,
- "7eadf5a3615bfd95": 1,
"8084f14fe903eabb": 1,
"853f4f98ba80f168": 1,
"8be66cde251a0e1c": 1,
@@ -51,7 +46,6 @@
"94cba67a903b5d04": 1,
"953067f449c638aa": 1,
"99f2b03f158babad": 1,
- "9ac8c860f07eec4e": 1,
"9ef49d93827bfe4b": 1,
"9f468c94ae4a7636": 1,
"a3b1b3898c247667": 1,
@@ -64,7 +58,6 @@
"bf3345bef96687f1": 1,
"c20b0ee6fe8ea9bb": 1,
"c69b00e7d0d6e3dd": 1,
- "cb3abe4fe2f6babc": 1,
"cdcc39d2dad88d50": 1,
"d171e886de82fa87": 1,
"d2f7815d2bf8f83d": 1,
|