diff --git a/scripts/Bootstrap/Install-AutopilotMonitor.ps1 b/scripts/Bootstrap/Install-AutopilotMonitor.ps1 index 5d5dde17..88cd7f4e 100644 --- a/scripts/Bootstrap/Install-AutopilotMonitor.ps1 +++ b/scripts/Bootstrap/Install-AutopilotMonitor.ps1 @@ -132,8 +132,8 @@ try { $agentConfig = @{ apiBaseUrl = $ApiBaseUrl uploadIntervalSeconds = 30 - cleanupOnExit = $true - selfDestructOnComplete = $true + cleanupOnExit = $false + selfDestructOnComplete = $false keepLogFile = $true imeMatchLogPath = "C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log" } diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/HelloDetector.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/HelloDetector.cs index a44bf826..51f5fbe6 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/HelloDetector.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/HelloDetector.cs @@ -370,7 +370,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors EventType = eventType, Severity = severity, Source = "HelloDetector", - Phase = EnrollmentPhase.Unknown, + Phase = EnrollmentPhase.FinalizingSetup, Message = message, Data = new Dictionary { diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs index fd0dd2b4..9a99f912 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs @@ -161,6 +161,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { _logger.Info("EnrollmentTracker: collecting device info (at start)"); + CollectBootTime(); CollectNetworkAdapters(); CollectDnsConfiguration(); CollectProxyConfiguration(); @@ -184,6 +185,53 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking // Add more collectors here as needed } + private void CollectBootTime() + { + try + { + var data = new Dictionary(); + + // Query WMI for OS boot time + using (var searcher = new ManagementObjectSearcher("SELECT LastBootUpTime FROM Win32_OperatingSystem")) + { + foreach (ManagementObject obj in searcher.Get()) + { + var lastBootUpTimeStr = obj["LastBootUpTime"]?.ToString(); + if (!string.IsNullOrEmpty(lastBootUpTimeStr)) + { + // WMI time format: yyyyMMddHHmmss.ffffff+/-UUU + var bootTime = ManagementDateTimeConverter.ToDateTime(lastBootUpTimeStr); + data["bootTime"] = bootTime.ToString("o"); // ISO 8601 format + data["bootTimeUtc"] = bootTime.ToUniversalTime().ToString("o"); + + // Calculate uptime + var uptime = DateTime.Now - bootTime; + data["uptimeMinutes"] = (int)uptime.TotalMinutes; + data["uptimeHours"] = uptime.TotalHours; + + _logger.Debug($"Boot time: {bootTime:o}, Uptime: {uptime.TotalMinutes:F1} minutes"); + break; + } + } + } + + if (!data.ContainsKey("bootTime")) + { + data["note"] = "Boot time could not be determined"; + } + + EmitDeviceInfoEvent("boot_time", + data.ContainsKey("bootTime") + ? $"Last boot: {data["bootTime"]}" + : "Boot time unavailable", + data); + } + catch (Exception ex) + { + _logger.Warning($"EnrollmentTracker: failed to collect boot time: {ex.Message}"); + } + } + private void CollectNetworkAdapters() { try @@ -790,7 +838,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking EventType = "waiting_for_hello", Severity = EventSeverity.Info, Source = "EnrollmentTracker", - Phase = EnrollmentPhase.AccountSetup, + Phase = EnrollmentPhase.FinalizingSetup, Message = "User apps completed - waiting for Windows Hello provisioning to finish" }); diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs index 170c99bc..6c5989ea 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs @@ -152,18 +152,19 @@ namespace AutopilotMonitor.Functions.Functions // Update session status based on events if (completionEvent != null) { - // Enrollment completed successfully + // Enrollment completed successfully - use event timestamp for accurate duration await _storageService.UpdateSessionStatusAsync( request.TenantId, request.SessionId, SessionStatus.Succeeded, - completionEvent.Phase + completionEvent.Phase, + completedAt: completionEvent.Timestamp ); _logger.LogInformation($"{sessionPrefix} Status: Succeeded"); } else if (failureEvent != null) { - // Enrollment failed + // Enrollment failed - use event timestamp for accurate duration var failureReason = failureEvent.Data?.ContainsKey("errorCode") == true ? $"{failureEvent.Message} ({failureEvent.Data["errorCode"]})" : failureEvent.Message; @@ -173,7 +174,8 @@ namespace AutopilotMonitor.Functions.Functions request.SessionId, SessionStatus.Failed, failureEvent.Phase, - failureReason + failureReason, + completedAt: failureEvent.Timestamp ); _logger.LogWarning($"{sessionPrefix} Status: Failed - {failureReason}"); } diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs index 5fb45599..7acaecdb 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs @@ -340,7 +340,7 @@ namespace AutopilotMonitor.Functions.Services /// /// Updates the session status and current phase /// - public async Task UpdateSessionStatusAsync(string tenantId, string sessionId, SessionStatus status, EnrollmentPhase? currentPhase = null, string? failureReason = null) + public async Task UpdateSessionStatusAsync(string tenantId, string sessionId, SessionStatus status, EnrollmentPhase? currentPhase = null, string? failureReason = null, DateTime? completedAt = null) { SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); SecurityValidator.EnsureValidGuid(sessionId, nameof(sessionId)); @@ -370,7 +370,8 @@ namespace AutopilotMonitor.Functions.Services // Set completion time if succeeded or failed if (status == SessionStatus.Succeeded || status == SessionStatus.Failed) { - session["CompletedAt"] = DateTime.UtcNow; + // Use the provided completedAt timestamp (from event) if available, otherwise use current time + session["CompletedAt"] = completedAt ?? DateTime.UtcNow; } // Set failure reason if failed 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 6bf705f0..04ea3024 100644 --- a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx +++ b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx @@ -62,22 +62,24 @@ const V1_PHASES = [ { 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" }, + { id: 6, name: "Finalizing Setup", shortName: "Finalizing" }, + { id: 7, name: "Complete", shortName: "Complete" }, ]; const V2_PHASES = [ { id: 0, name: "Start", shortName: "Start" }, { id: 1, name: "Device Preparation", shortName: "Device Preparation" }, { id: 3, name: "App Installation", shortName: "Apps" }, - { id: 6, name: "Complete", shortName: "Complete" }, + { id: 6, name: "Finalizing Setup", shortName: "Finalizing" }, + { id: 7, name: "Complete", shortName: "Complete" }, ]; -const V1_PHASE_ORDER = ["Start", "Device Preparation", "Device Setup", "Apps (Device)", "Account Setup", "Apps (User)", "Complete", "Failed"]; -const V2_PHASE_ORDER = ["Start", "Device Preparation", "App Installation", "Complete", "Failed"]; +const V1_PHASE_ORDER = ["Start", "Device Preparation", "Device Setup", "Apps (Device)", "Account Setup", "Apps (User)", "Finalizing Setup", "Complete", "Failed"]; +const V2_PHASE_ORDER = ["Start", "Device Preparation", "App Installation", "Finalizing Setup", "Complete", "Failed"]; // Lookup by phase number — Phase 3 has different names per enrollment type -const V1_PHASE_NAMES: Record = { [-1]: "Unknown", 0: "Start", 1: "Device Preparation", 2: "Device Setup", 3: "Apps (Device)", 4: "Account Setup", 5: "Apps (User)", 6: "Complete", 99: "Failed" }; -const V2_PHASE_NAMES: Record = { [-1]: "Unknown", 0: "Start", 1: "Device Preparation", 2: "Device Setup", 3: "App Installation", 4: "Account Setup", 5: "Apps (User)", 6: "Complete", 99: "Failed" }; +const V1_PHASE_NAMES: Record = { [-1]: "Unknown", 0: "Start", 1: "Device Preparation", 2: "Device Setup", 3: "Apps (Device)", 4: "Account Setup", 5: "Apps (User)", 6: "Finalizing Setup", 7: "Complete", 99: "Failed" }; +const V2_PHASE_NAMES: Record = { [-1]: "Unknown", 0: "Start", 1: "Device Preparation", 2: "Device Setup", 3: "App Installation", 4: "Account Setup", 5: "Apps (User)", 6: "Finalizing Setup", 7: "Complete", 99: "Failed" }; export default function SessionDetailPage() { const params = useParams(); @@ -565,7 +567,7 @@ export default function SessionDetailPage() {

Enrollment Progress

{/* Boot Time */} - {agentStarted?.bootTime && ( + {(bootTime?.bootTime || agentStarted?.bootTime) && ( - - {agentStarted.agentVersion && } + + {bootTime?.uptimeMinutes && } + {agentStarted?.agentVersion && } )}