-
- );
-}
-
-function StatusPill({ status }: { status: string }) {
- const config = {
- InProgress: {
- bg: "bg-blue-500/20",
- text: "text-blue-400",
- label: "In Progress",
- dot: "bg-blue-400 animate-pulse",
- },
- Succeeded: {
- bg: "bg-green-500/20",
- text: "text-green-400",
- label: "Succeeded",
- dot: "bg-green-400",
- },
- Failed: {
- bg: "bg-red-500/20",
- text: "text-red-400",
- label: "Failed",
- dot: "bg-red-400",
- },
- };
-
- const c = config[status as keyof typeof config] || {
- bg: "bg-gray-500/20",
- text: "text-gray-400",
- label: status,
- dot: "bg-gray-400",
- };
-
- return (
-
- );
-}
diff --git a/src/Web/autopilot-monitor-web/app/progress/page.tsx b/src/Web/autopilot-monitor-web/app/progress/page.tsx
index d1e139eb..4245fa68 100644
--- a/src/Web/autopilot-monitor-web/app/progress/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/progress/page.tsx
@@ -6,6 +6,7 @@ import { useTenant } from "../../contexts/TenantContext";
import { useAuth } from "../../contexts/AuthContext";
import { useSignalR } from "../../contexts/SignalRContext";
import { ProtectedRoute } from "../../components/ProtectedRoute";
+import DownloadProgress from "../../components/DownloadProgress";
interface Session {
sessionId: string;
@@ -34,14 +35,13 @@ interface EnrollmentEvent {
}
const phaseSteps = [
- { id: 0, label: "Device registered", shortLabel: "Registration" },
- { id: 1, label: "Network configured", shortLabel: "Network" },
- { id: 2, label: "Identity verified", shortLabel: "Identity" },
- { id: 3, label: "Management enrolled", shortLabel: "Enrollment" },
- { id: 4, label: "Security policies applied", shortLabel: "Policies" },
- { id: 5, label: "Installing apps", shortLabel: "Apps" },
- { id: 6, label: "User setup", shortLabel: "User Setup" },
- { id: 7, label: "Finalizing setup", shortLabel: "Complete" },
+ { id: 0, label: "Setup start", shortLabel: "Start" },
+ { id: 1, label: "Device preparation", shortLabel: "Preparation" },
+ { id: 2, label: "Device setup", shortLabel: "Device" },
+ { id: 3, label: "Installing apps (device)", shortLabel: "Apps (D)" },
+ { id: 4, label: "Account setup", shortLabel: "Account" },
+ { id: 5, label: "Installing apps (user)", shortLabel: "Apps (U)" },
+ { id: 6, label: "Finalizing setup", shortLabel: "Complete" },
];
export default function ProgressPortalPage() {
@@ -58,7 +58,8 @@ export default function ProgressPortalPage() {
const { getAccessToken } = useAuth();
const { on, off, isConnected, joinGroup, leaveGroup } = useSignalR();
- const hasJoinedGroup = useRef(false);
+ const hasJoinedTenantGroup = useRef(false);
+ const hasJoinedSessionGroup = useRef(false);
const sessionRef = useRef
(null);
// Keep ref in sync
@@ -68,34 +69,95 @@ export default function ProgressPortalPage() {
// Join tenant group for real-time updates
useEffect(() => {
- if (isConnected && !hasJoinedGroup.current) {
+ if (isConnected && !hasJoinedTenantGroup.current) {
joinGroup(`tenant-${tenantId}`);
- hasJoinedGroup.current = true;
+ hasJoinedTenantGroup.current = true;
}
return () => {
- if (hasJoinedGroup.current) {
+ if (hasJoinedTenantGroup.current) {
leaveGroup(`tenant-${tenantId}`);
- hasJoinedGroup.current = false;
+ hasJoinedTenantGroup.current = false;
}
};
}, [isConnected, tenantId]);
+ // Join session-specific group when session is found
+ useEffect(() => {
+ if (!isConnected || !session) {
+ return;
+ }
+
+ const sessionGroup = `session-${session.tenantId}-${session.sessionId}`;
+ console.log('[Progress] Joining session group:', sessionGroup);
+ joinGroup(sessionGroup);
+ hasJoinedSessionGroup.current = true;
+
+ return () => {
+ console.log('[Progress] Leaving session group:', sessionGroup);
+ leaveGroup(sessionGroup);
+ hasJoinedSessionGroup.current = false;
+ };
+ }, [isConnected, session?.sessionId, session?.tenantId, joinGroup, leaveGroup]);
+
// Listen for real-time session updates
useEffect(() => {
- const handleNewEvents = (data: { session: Session }) => {
+ const handleNewEvents = (data: { session: Session; events?: EnrollmentEvent[] }) => {
+ console.log('[Progress] Received newevents:', data);
if (
data.session &&
sessionRef.current &&
data.session.sessionId === sessionRef.current.sessionId
) {
+ console.log('[Progress] Updating session from newevents');
setSession(data.session);
+
+ // Add new events to the events list
+ if (data.events && data.events.length > 0) {
+ console.log('[Progress] Adding', data.events.length, 'new events from newevents');
+ setEvents((prev) => {
+ const existingIds = new Set(prev.map((e) => e.eventId));
+ const newEvents = data.events!.filter((e) => !existingIds.has(e.eventId));
+ if (newEvents.length === 0) return prev;
+ return [...prev, ...newEvents].sort((a, b) => a.sequence - b.sequence);
+ });
+ }
}
};
+
+ const handleEventStream = (data: { session: Session; events?: EnrollmentEvent[] }) => {
+ console.log('[Progress] Received eventStream:', data);
+ if (
+ data.session &&
+ sessionRef.current &&
+ data.session.sessionId === sessionRef.current.sessionId
+ ) {
+ console.log('[Progress] Updating session from eventStream');
+ console.log('[Progress] Session currentPhase:', data.session.currentPhase, 'status:', data.session.status);
+ setSession(data.session);
+
+ // Add new events to the events list
+ if (data.events && data.events.length > 0) {
+ console.log('[Progress] Adding', data.events.length, 'new events from eventStream');
+ console.log('[Progress] Event types:', data.events.map(e => e.eventType).join(', '));
+ setEvents((prev) => {
+ const existingIds = new Set(prev.map((e) => e.eventId));
+ const newEvents = data.events!.filter((e) => !existingIds.has(e.eventId));
+ if (newEvents.length === 0) return prev;
+ console.log('[Progress] Actually adding', newEvents.length, 'new events');
+ console.log('[Progress] Total events after update:', prev.length + newEvents.length);
+ return [...prev, ...newEvents].sort((a, b) => a.sequence - b.sequence);
+ });
+ }
+ }
+ };
+
on("newevents", handleNewEvents);
on("newSession", handleNewEvents);
+ on("eventStream", handleEventStream);
return () => {
off("newevents", handleNewEvents);
off("newSession", handleNewEvents);
+ off("eventStream", handleEventStream);
};
}, [on, off]);
@@ -248,20 +310,20 @@ export default function ProgressPortalPage() {
((session.currentPhase === 99
? 3
: session.currentPhase) /
- 7) *
+ 6) *
100
)
- : Math.min(100, (session.currentPhase / 7) * 100)
+ : Math.min(100, (session.currentPhase / 6) * 100)
: 0;
const estimatedRemaining = session
? (() => {
if (session.status !== "InProgress") return null;
- const currentPhase = Math.min(session.currentPhase, 7);
+ const currentPhase = Math.min(session.currentPhase, 6);
if (currentPhase === 0) return null;
const elapsed = session.durationSeconds;
const rate = elapsed / currentPhase;
- const remaining = (7 - currentPhase) * rate;
+ const remaining = (6 - currentPhase) * rate;
return Math.round(remaining / 60);
})()
: null;
@@ -483,7 +545,7 @@ export default function ProgressPortalPage() {
? 3
: session.currentPhase;
const isCompleted =
- session.status === "Succeeded" ||
+ (session.status === "Succeeded" && step.id <= 6) ||
step.id < effectivePhase;
const isCurrent =
step.id === effectivePhase &&
@@ -555,14 +617,14 @@ export default function ProgressPortalPage() {
>
{step.label}
{isCurrent &&
- step.id === 5 &&
+ (step.id === 3 || step.id === 5) &&
appProgress &&
appProgress.total > 0 &&
appProgress.completed > 0 &&
` (${appProgress.completed}/${appProgress.total})`}
- {/* App install detail below the "Installing apps" step */}
- {isCurrent && step.id === 5 && (
+ {/* App install detail below the "Installing apps" steps */}
+ {isCurrent && (step.id === 3 || step.id === 5) && (
@@ -579,6 +641,20 @@ export default function ProgressPortalPage() {
})}
+ {/* Download Progress - Show during app installation phases */}
+ {session.status === "InProgress" &&
+ (session.currentPhase === 3 || session.currentPhase === 5) &&
+ events.filter(e => e.eventType === "download_progress").length > 0 && (
+
+ e.eventType === "download_progress").map(e => ({
+ timestamp: e.timestamp,
+ data: e.data
+ }))}
+ />
+
+ )}
+
{/* Estimated Time / Status */}
{session.status === "InProgress" && estimatedRemaining != null && estimatedRemaining > 0 && (
diff --git a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx
index 1613bc6c..a37a4142 100644
--- a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState, useRef } from "react";
+import { useEffect, useState, useRef, useMemo } from "react";
import { useParams, useRouter } from "next/navigation";
import { useSignalR } from "../../../contexts/SignalRContext";
import { useTenant } from "../../../contexts/TenantContext";
@@ -54,14 +54,14 @@ interface Session {
}
const phaseNames: Record = {
- 0: "PreFlight",
- 1: "Network",
- 2: "Identity",
- 3: "MDM Enrollment",
- 4: "ESP Device Setup",
- 5: "App Installation",
- 6: "ESP User Setup",
- 7: "Complete",
+ [-1]: "Unknown",
+ 0: "Start",
+ 1: "Device Preparation",
+ 2: "Device Setup",
+ 3: "Apps (Device)",
+ 4: "Account Setup",
+ 5: "Apps (User)",
+ 6: "Complete",
99: "Failed"
};
@@ -73,8 +73,7 @@ export default function SessionDetailPage() {
const [session, setSession] = useState(null);
const [sessionTenantId, setSessionTenantId] = useState(null);
const [loading, setLoading] = useState(true);
- const [expandedPhases, setExpandedPhases] = useState>(new Set());
- const [severityFilters, setSeverityFilters] = useState>(new Set(["Info", "Warning", "Error", "Critical", "Debug"]));
+ const [severityFilters, setSeverityFilters] = useState>(new Set(["Info", "Warning", "Error", "Critical"]));
const [showMarkFailedConfirm, setShowMarkFailedConfirm] = useState(false);
const [analysisResults, setAnalysisResults] = useState([]);
const [loadingAnalysis, setLoadingAnalysis] = useState(false);
@@ -105,7 +104,7 @@ export default function SessionDetailPage() {
// Use global contexts
const { on, off, isConnected, joinGroup, leaveGroup } = useSignalR();
const { tenantId } = useTenant();
- const { getAccessToken } = useAuth();
+ const { getAccessToken, user } = useAuth();
// Initial data fetch
useEffect(() => {
@@ -393,27 +392,63 @@ export default function SessionDetailPage() {
};
// Filter events by severity for the timeline
- const filteredEvents = events.filter(e => severityFilters.has(e.severity));
+ const filteredEvents = useMemo(() =>
+ events.filter(e => severityFilters.has(e.severity)),
+ [events, severityFilters]
+ );
- // Group filtered events by phase
- const eventsByPhase = filteredEvents.reduce((acc, event) => {
- const phaseName = event.phaseName || "Unknown";
- if (!acc[phaseName]) {
- acc[phaseName] = [];
+ // Group events by phase with memoization
+ const { eventsByPhase, orderedPhases } = useMemo(() => {
+ // Sort events by sequence (chronological order)
+ const sortedEvents = [...filteredEvents].sort((a, b) => a.sequence - b.sequence);
+
+ // Group events by phase, inserting "Unknown" events into their chronological position
+ const eventsByPhase = {} as Record;
+ let currentActivePhaseName = "Start"; // Default to Start phase
+
+ for (let i = 0; i < sortedEvents.length; i++) {
+ const event = sortedEvents[i];
+ let targetPhase = event.phaseName || "Unknown";
+
+ // If event has explicit phase (not Unknown), update current active phase
+ if (targetPhase !== "Unknown") {
+ currentActivePhaseName = targetPhase;
+ } else {
+ // Unknown events go into the current active phase
+ targetPhase = currentActivePhaseName;
+ }
+
+ if (!eventsByPhase[targetPhase]) {
+ eventsByPhase[targetPhase] = [];
+ }
+ eventsByPhase[targetPhase].push(event);
}
- acc[phaseName].push(event);
- return acc;
- }, {} as Record);
- const phaseOrder = ["PreFlight", "Network", "Identity", "MDM Enrollment", "ESP Device Setup", "App Installation", "ESP User Setup", "Complete", "Failed"];
- const orderedPhases = phaseOrder.filter(phase => eventsByPhase[phase]);
+ const phaseOrder = ["Start", "Device Preparation", "Device Setup", "Apps (Device)", "Account Setup", "Apps (User)", "Complete", "Failed"];
+ const orderedPhases = phaseOrder.filter(phase => eventsByPhase[phase] && eventsByPhase[phase].length > 0);
- // Initialize all phases as expanded on first load
+ return { eventsByPhase, orderedPhases };
+ }, [filteredEvents, events.length]);
+
+ const [expandedPhases, setExpandedPhases] = useState>(new Set());
+
+ // Auto-expand new phases as they appear (keeps existing expanded/collapsed state)
useEffect(() => {
- if (orderedPhases.length > 0 && expandedPhases.size === 0) {
- setExpandedPhases(new Set(orderedPhases));
- }
- }, [orderedPhases.length]);
+ setExpandedPhases(prev => {
+ // Add any new phases that aren't already tracked
+ const newExpanded = new Set(prev);
+ let hasChanges = false;
+
+ for (const phase of orderedPhases) {
+ if (!prev.has(phase)) {
+ newExpanded.add(phase);
+ hasChanges = true;
+ }
+ }
+
+ return hasChanges ? newExpanded : prev;
+ });
+ }, [orderedPhases]);
const expandAll = () => {
setExpandedPhases(new Set(orderedPhases));
@@ -424,13 +459,15 @@ export default function SessionDetailPage() {
};
const togglePhase = (phaseName: string) => {
- const newExpanded = new Set(expandedPhases);
- if (newExpanded.has(phaseName)) {
- newExpanded.delete(phaseName);
- } else {
- newExpanded.add(phaseName);
- }
- setExpandedPhases(newExpanded);
+ setExpandedPhases(prev => {
+ const newExpanded = new Set(prev);
+ if (newExpanded.has(phaseName)) {
+ newExpanded.delete(phaseName);
+ } else {
+ newExpanded.add(phaseName);
+ }
+ return newExpanded;
+ });
};
if (loading) {
@@ -459,15 +496,6 @@ export default function SessionDetailPage() {
-
{session?.status === 'Failed' && (
)}
+ {/* Device Details Card (from enrollment tracker events) */}
+
+
{/* Phase Timeline */}
{session && (
Enrollment Progress
@@ -611,9 +642,9 @@ export default function SessionDetailPage() {
events={events.filter(e => e.eventType === "performance_snapshot")}
/>
- {/* Download Progress (from download_progress events) */}
+ {/* Download Progress (from download_progress or app_tracking_summary events) */}
e.eventType === "download_progress")}
+ events={events.filter(e => e.eventType === "download_progress" || e.eventType === "app_tracking_summary")}
/>
{/* Timeline */}
@@ -681,6 +712,7 @@ export default function SessionDetailPage() {
events={eventsByPhase[phaseName]}
isExpanded={expandedPhases.has(phaseName)}
onToggle={() => togglePhase(phaseName)}
+ isGalacticAdmin={user?.isGalacticAdmin}
/>
))}
@@ -745,12 +777,14 @@ function PhaseSection({
phaseName,
events,
isExpanded,
- onToggle
+ onToggle,
+ isGalacticAdmin
}: {
phaseName: string;
events: EnrollmentEvent[];
isExpanded: boolean;
onToggle: () => void;
+ isGalacticAdmin?: boolean;
}) {
return (
@@ -767,7 +801,11 @@ function PhaseSection({
{isExpanded && (
{events.map((event, index) => (
-
+
))}
)}
@@ -775,8 +813,19 @@ function PhaseSection({
);
}
-function EventRow({ event }: { event: EnrollmentEvent }) {
+function EventRow({ event, isGalacticAdmin }: { event: EnrollmentEvent; isGalacticAdmin?: boolean }) {
const [showDetails, setShowDetails] = useState(false);
+ const [copied, setCopied] = useState(false);
+
+ const copyEventId = async () => {
+ try {
+ await navigator.clipboard.writeText(event.eventId);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error('Failed to copy EventID:', err);
+ }
+ };
return (
@@ -793,6 +842,15 @@ function EventRow({ event }: { event: EnrollmentEvent }) {
Source: {event.source}
Seq: {event.sequence}
+ {isGalacticAdmin && (
+
+ )}
{event.data && Object.keys(event.data).length > 0 && (
@@ -821,14 +879,13 @@ function PhaseTimeline({ currentPhase, completedPhases, events = [], sessionStat
sessionStatus?: string;
}) {
const phases = [
- { id: 0, name: "PreFlight", shortName: "Pre-Flight" },
- { id: 1, name: "Network", shortName: "Network" },
- { id: 2, name: "Identity", shortName: "Identity" },
- { id: 3, name: "MDM Enrollment", shortName: "MDM" },
- { id: 4, name: "ESP Device Setup", shortName: "Device" },
- { id: 5, name: "App Installation", shortName: "Apps" },
- { id: 6, name: "ESP User Setup", shortName: "User" },
- { id: 7, name: "Complete", shortName: "Complete" },
+ { id: 0, name: "Start", shortName: "Start" },
+ { id: 1, name: "Device Preparation", shortName: "Device Preparation" },
+ { id: 2, name: "Device Setup", shortName: "Device Setup" },
+ { id: 3, name: "Apps (Device)", shortName: "Apps (Device)" },
+ { id: 4, name: "Account Setup", shortName: "Account Setup" },
+ { id: 5, name: "Apps (User)", shortName: "Apps (User)" },
+ { id: 6, name: "Complete", shortName: "Complete" },
];
// Derive current activity for the active phase from events
@@ -850,7 +907,18 @@ function PhaseTimeline({ currentPhase, completedPhases, events = [], sessionStat
if (phaseEvents.length === 0) return null;
- // Check for esp_ui_state events to show app install progress
+ // Check for app_tracking_summary events (new strategic events)
+ const trackingSummary = phaseEvents.find(e => e.eventType === "app_tracking_summary");
+ if (trackingSummary?.data) {
+ const d = trackingSummary.data;
+ const completed = parseInt(d.appsCompleted ?? "0", 10);
+ const total = parseInt(d.totalApps ?? "0", 10);
+ if (total > 0) {
+ return `Installing apps (${completed}/${total})`;
+ }
+ }
+
+ // Check for esp_ui_state events (legacy) to show app install progress
const espState = phaseEvents.find(e => e.eventType === "esp_ui_state");
if (espState?.data) {
const d = espState.data;
@@ -865,7 +933,17 @@ function PhaseTimeline({ currentPhase, completedPhases, events = [], sessionStat
}
}
- // Check for download_progress to show active download
+ // Check for app install events (new strategic events)
+ const appInstallEvt = phaseEvents.find(e =>
+ e.eventType === "app_download_started" || e.eventType === "app_install_started"
+ );
+ if (appInstallEvt?.data) {
+ const appName = appInstallEvt.data.appName ?? appInstallEvt.data.appId ?? "app";
+ if (appInstallEvt.eventType === "app_download_started") return `Downloading ${appName}`;
+ return `Installing ${appName}`;
+ }
+
+ // Check for download_progress to show active download (legacy)
const downloadEvt = phaseEvents.find(e => e.eventType === "download_progress");
if (downloadEvt?.data) {
const d = downloadEvt.data;
@@ -932,6 +1010,10 @@ function PhaseTimeline({ currentPhase, completedPhases, events = [], sessionStat
})();
const getPhaseStatus = (phaseId: number) => {
+ // Agent starts at MDM phase (3) - Pre-Flight(0)/Network(1)/Identity(2) are inferred as completed
+ // since the machine reached MDM enrollment
+ if (phaseId >= 0 && phaseId <= 2) return 'completed'; // Pre-Flight(0), Network(1), Identity(2)
+
// If phase is completed, show as completed (green)
if (completedPhases.includes(phaseId)) return 'completed';
@@ -1164,6 +1246,246 @@ function AnalysisResultCard({ result }: { result: RuleResult }) {
);
}
+function DeviceDetailsCard({ events }: { events: EnrollmentEvent[] }) {
+ const [expanded, setExpanded] = useState(false);
+ const [showIpv6, setShowIpv6] = useState
>({});
+
+ // Extract device detail events
+ const getEventData = (eventType: string): Record | null => {
+ const evt = events.find(e => e.eventType === eventType);
+ return evt?.data ?? null;
+ };
+
+ // Helper to check if IP is IPv6
+ const isIpv6 = (ip: string): boolean => {
+ // IPv6 contains colons and has multiple segments
+ // IPv4 is like: 192.168.1.1
+ // IPv6 is like: fe80::1234:5678:abcd:ef01 or 2001:db8::1
+ if (!ip || typeof ip !== 'string') return false;
+
+ // If it contains a colon and doesn't look like IPv4, it's IPv6
+ // Simple check: IPv6 has at least 2 colons (even compressed form)
+ const colonCount = (ip.match(/:/g) || []).length;
+ return colonCount >= 2;
+ };
+
+ // Helper to split IPs into IPv4 and IPv6
+ const splitIpAddresses = (ipAddresses: string | string[]): { ipv4: string[]; ipv6: string[] } => {
+ // Handle both array and comma-separated string
+ let ips: string[];
+ if (Array.isArray(ipAddresses)) {
+ ips = ipAddresses;
+ } else if (typeof ipAddresses === 'string') {
+ // Split by comma if it's a comma-separated string
+ ips = ipAddresses.split(',').map(ip => ip.trim()).filter(ip => ip.length > 0);
+ } else {
+ ips = [];
+ }
+
+ const ipv4: string[] = [];
+ const ipv6: string[] = [];
+
+ for (const ip of ips) {
+ if (typeof ip === 'string' && ip.trim()) {
+ if (isIpv6(ip)) {
+ ipv6.push(ip);
+ } else {
+ ipv4.push(ip);
+ }
+ }
+ }
+
+ return { ipv4, ipv6 };
+ };
+
+ const agentStarted = getEventData("agent_started");
+ const networkAdapters = getEventData("network_adapters");
+ const dnsConfig = getEventData("dns_configuration");
+ const proxyConfig = getEventData("proxy_configuration");
+ const autopilotProfile = getEventData("autopilot_profile");
+ const aadJoinStatus = getEventData("aad_join_status");
+ const imeVersion = getEventData("ime_agent_version");
+ const bitLockerStatus = getEventData("bitlocker_status");
+ const secureBootStatus = getEventData("secureboot_status");
+
+ // Check if we have any device detail events at all
+ const hasData = agentStarted || networkAdapters || dnsConfig || proxyConfig || autopilotProfile ||
+ aadJoinStatus || imeVersion || bitLockerStatus || secureBootStatus;
+
+ if (!hasData) return null;
+
+ return (
+
+
+
+ {expanded && (
+
+ {/* Boot Time */}
+ {agentStarted?.bootTime && (
+
+
+ {agentStarted.agentVersion && }
+
+ )}
+
+ {/* Network */}
+ {networkAdapters && (
+
+ {networkAdapters.adapters ? (
+ (networkAdapters.adapters as any[]).map((adapter: any, i: number) => {
+ const { ipv4, ipv6 } = adapter.ipAddresses ? splitIpAddresses(adapter.ipAddresses) : { ipv4: [], ipv6: [] };
+ const hasIpv6 = ipv6.length > 0;
+ const isIpv6Shown = showIpv6[i] ?? false;
+
+ return (
+
+
{adapter.description || adapter.name || `Adapter ${i + 1}`}
+
+ {/* DHCP - first */}
+ {adapter.dhcpEnabled !== undefined &&
}
+
+ {/* MAC - second */}
+ {adapter.macAddress &&
}
+
+ {/* IPv4 - third */}
+ {ipv4.length > 0 &&
}
+
+ {/* IPv6 - fourth (collapsible) */}
+ {hasIpv6 && (
+
+
+ {isIpv6Shown && (
+
+ {ipv6.map((ip, idx) => (
+
{ip}
+ ))}
+
+ )}
+
+ )}
+
+ );
+ })
+ ) : (
+
+ )}
+
+ )}
+
+ {/* DNS */}
+ {dnsConfig && (
+
+ {dnsConfig.dnsEntries && Array.isArray(dnsConfig.dnsEntries) && dnsConfig.dnsEntries.length > 0 ? (
+ (dnsConfig.dnsEntries as any[]).map((entry: any, i: number) => (
+
+
+
+ ))
+ ) : (
+ No DNS data
+ )}
+
+ )}
+
+ {/* Proxy */}
+ {proxyConfig && (
+
+
+ {proxyConfig.proxyServer && }
+ {proxyConfig.autoConfigUrl && }
+ {proxyConfig.winHttpProxy && }
+
+ )}
+
+ {/* Autopilot Profile */}
+ {autopilotProfile && (
+
+ {autopilotProfile.tenantDomain && }
+ {autopilotProfile.deploymentProfileName && }
+ {autopilotProfile.cloudAssignedTenantId && }
+ {autopilotProfile.oobeConfig && }
+
+ )}
+
+ {/* Identity */}
+ {aadJoinStatus && (
+
+
+ {aadJoinStatus.tenantId && }
+
+ )}
+
+ {/* IME */}
+ {imeVersion && (
+
+
+
+ )}
+
+ {/* Security */}
+ {(bitLockerStatus || secureBootStatus) && (
+
+ {secureBootStatus && (
+
+ )}
+ {bitLockerStatus && (
+ <>
+
+ {bitLockerStatus.volumes && Array.isArray(bitLockerStatus.volumes) && bitLockerStatus.volumes.length > 0 && (
+
+ {(bitLockerStatus.volumes as any[]).map((vol: any, i: number) => (
+
+ {vol.driveLetter}: {vol.protectionStatus === "1" ? "Protected" : "Not Protected"}
+ {vol.encryptionMethod && ` (Method: ${vol.encryptionMethod})`}
+
+ ))}
+
+ )}
+ >
+ )}
+
+ )}
+
+ )}
+
+ );
+}
+
+function DetailSection({ title, children }: { title: string; children: React.ReactNode }) {
+ return (
+
+ );
+}
+
+function DetailRow({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
function StatusBadge({ status, failureReason }: { status: string; failureReason?: string }) {
const statusConfig = {
InProgress: { color: "bg-blue-100 text-blue-800", text: "In Progress" },
diff --git a/src/Web/autopilot-monitor-web/app/settings/page.tsx b/src/Web/autopilot-monitor-web/app/settings/page.tsx
index e3665a04..44394bd4 100644
--- a/src/Web/autopilot-monitor-web/app/settings/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/settings/page.tsx
@@ -20,11 +20,6 @@ interface TenantConfiguration {
// Agent collector settings
enablePerformanceCollector: boolean;
performanceCollectorIntervalSeconds: number;
- enableDownloadProgressCollector: boolean;
- downloadProgressCollectorIntervalSeconds: number;
- enableCertValidationCollector: boolean;
- enableEspUiStateCollector: boolean;
- espUiStateCollectorIntervalSeconds: number;
}
interface TenantAdmin {
@@ -63,11 +58,6 @@ export default function SettingsPage() {
// Collector settings state
const [enablePerformanceCollector, setEnablePerformanceCollector] = useState(false);
const [performanceCollectorInterval, setPerformanceCollectorInterval] = useState(60);
- const [enableDownloadProgressCollector, setEnableDownloadProgressCollector] = useState(false);
- const [downloadProgressCollectorInterval, setDownloadProgressCollectorInterval] = useState(15);
- const [enableCertValidationCollector, setEnableCertValidationCollector] = useState(false);
- const [enableEspUiStateCollector, setEnableEspUiStateCollector] = useState(false);
- const [espUiStateCollectorInterval, setEspUiStateCollectorInterval] = useState(30);
// Fetch configuration
@@ -106,11 +96,6 @@ export default function SettingsPage() {
setSessionTimeoutHours(data.sessionTimeoutHours ?? 5);
setEnablePerformanceCollector(data.enablePerformanceCollector ?? false);
setPerformanceCollectorInterval(data.performanceCollectorIntervalSeconds ?? 60);
- setEnableDownloadProgressCollector(data.enableDownloadProgressCollector ?? false);
- setDownloadProgressCollectorInterval(data.downloadProgressCollectorIntervalSeconds ?? 15);
- setEnableCertValidationCollector(data.enableCertValidationCollector ?? false);
- setEnableEspUiStateCollector(data.enableEspUiStateCollector ?? false);
- setEspUiStateCollectorInterval(data.espUiStateCollectorIntervalSeconds ?? 30);
} catch (err) {
console.error("Error fetching configuration:", err);
setError(err instanceof Error ? err.message : "Failed to load configuration");
@@ -176,11 +161,6 @@ export default function SettingsPage() {
sessionTimeoutHours,
enablePerformanceCollector,
performanceCollectorIntervalSeconds: performanceCollectorInterval,
- enableDownloadProgressCollector,
- downloadProgressCollectorIntervalSeconds: downloadProgressCollectorInterval,
- enableCertValidationCollector,
- enableEspUiStateCollector,
- espUiStateCollectorIntervalSeconds: espUiStateCollectorInterval,
};
const token = await getAccessToken();
@@ -225,11 +205,6 @@ export default function SettingsPage() {
setSessionTimeoutHours(config.sessionTimeoutHours ?? 5);
setEnablePerformanceCollector(config.enablePerformanceCollector ?? false);
setPerformanceCollectorInterval(config.performanceCollectorIntervalSeconds ?? 60);
- setEnableDownloadProgressCollector(config.enableDownloadProgressCollector ?? false);
- setDownloadProgressCollectorInterval(config.downloadProgressCollectorIntervalSeconds ?? 15);
- setEnableCertValidationCollector(config.enableCertValidationCollector ?? false);
- setEnableEspUiStateCollector(config.enableEspUiStateCollector ?? false);
- setEspUiStateCollectorInterval(config.espUiStateCollectorIntervalSeconds ?? 30);
setSuccessMessage(null);
setError(null);
};
@@ -770,92 +745,13 @@ export default function SettingsPage() {
- {/* Download Progress Collector */}
-
-
-
-
Download Progress Collector
-
High Traffic
-
-
Tracks IME content download progress and speed
- {enableDownloadProgressCollector && (
-
- Interval:
- setDownloadProgressCollectorInterval(parseInt(e.target.value) || 15)}
- className="w-20 px-2 py-1 border border-gray-300 rounded text-sm text-gray-900 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
- />
- seconds
-
- )}
-
-
-
-
- {/* Certificate Validation Collector */}
-
-
-
-
Certificate Validation Collector
-
Low Traffic
-
-
Validates enrollment endpoint certificates, detects SSL inspection
-
-
-
-
- {/* ESP UI State Collector */}
-
-
-
-
ESP UI State Collector
-
Medium Traffic
-
-
Tracks ESP blocking apps progress and status text
- {enableEspUiStateCollector && (
-
- Interval:
- setEspUiStateCollectorInterval(parseInt(e.target.value) || 30)}
- className="w-20 px-2 py-1 border border-gray-300 rounded text-sm text-gray-900 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
- />
- seconds
-
- )}
-
-
-
-
- Changes take effect on the next agent config refresh (default: every 5 minutes). Core collectors (EventLog, Registry, Phase Detector, Hello Detector) are always active.
+ Changes take effect on the next agent config refresh (default: every 5 minutes). Core collectors (Hello Detector) and enrollment tracking are always active.
diff --git a/src/Web/autopilot-monitor-web/components/DownloadProgress.tsx b/src/Web/autopilot-monitor-web/components/DownloadProgress.tsx
index c1211180..08bc2e88 100644
--- a/src/Web/autopilot-monitor-web/components/DownloadProgress.tsx
+++ b/src/Web/autopilot-monitor-web/components/DownloadProgress.tsx
@@ -18,6 +18,7 @@ interface DownloadItem {
downloadRateBps: number;
lastUpdated: string;
isComplete: boolean;
+ eventData?: Record;
}
function formatBytes(bytes: number): string {
@@ -55,25 +56,45 @@ export default function DownloadProgress({ events }: DownloadProgressProps) {
if (!d) continue;
const appName = d.app_name ?? d.appName ?? d.file_name ?? d.fileName ?? "Unknown App";
+
+ // Skip unknown apps
+ if (appName === "Unknown App") continue;
+
const bytesDownloaded = parseInt(d.bytes_downloaded ?? d.bytesDownloaded ?? "0", 10);
const bytesTotal = parseInt(d.bytes_total ?? d.bytesTotal ?? "0", 10);
const downloadRateBps = parseFloat(d.download_rate_bps ?? d.downloadRateBps ?? "0");
const status = d.status ?? "";
+ // Determine if complete: explicit status or bytes comparison
+ const isComplete = status === "completed" || status === "failed" || (bytesTotal > 0 && bytesDownloaded >= bytesTotal);
+
+ // Skip if bytesTotal is too small (< 1 KB) AND not explicitly completed/failed
+ if (bytesTotal > 0 && bytesTotal < 1024 && status !== "completed" && status !== "failed") {
+ continue;
+ }
+
+ // Skip if no download activity yet (0 bytes downloaded, not completed/failed)
+ // This makes downloads appear dynamically as they start
+ if (bytesDownloaded === 0 && bytesTotal === 0 && status !== "completed" && status !== "failed") {
+ continue;
+ }
+
downloadMap.set(appName, {
appName,
bytesDownloaded: isNaN(bytesDownloaded) ? 0 : bytesDownloaded,
bytesTotal: isNaN(bytesTotal) ? 0 : bytesTotal,
downloadRateBps: isNaN(downloadRateBps) ? 0 : downloadRateBps,
lastUpdated: evt.timestamp,
- isComplete: status === "completed" || (bytesTotal > 0 && bytesDownloaded >= bytesTotal),
+ isComplete,
+ eventData: d,
});
}
return Array.from(downloadMap.values()).sort((a, b) => {
// In-progress first, then completed
if (a.isComplete !== b.isComplete) return a.isComplete ? 1 : -1;
- return new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime();
+ // Chronological order: oldest first (first download on top, newest at bottom)
+ return new Date(a.lastUpdated).getTime() - new Date(b.lastUpdated).getTime();
});
}, [events]);
@@ -119,9 +140,18 @@ export default function DownloadProgress({ events }: DownloadProgressProps) {
: 0;
const bytesRemaining = dl.bytesTotal > 0 ? dl.bytesTotal - dl.bytesDownloaded : 0;
- return (
+ return ;
+ })}
+