added boottime
functions:
fix completedAt
UI:
fix timeline missing events
This commit is contained in:
Oliver Kieselbach
2026-02-12 18:13:52 +01:00
parent 9f41300a13
commit 9ad36c458c
6 changed files with 76 additions and 21 deletions
@@ -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"
}
@@ -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<string, object>
{
@@ -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<string, object>();
// 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"
});
@@ -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}");
}
@@ -340,7 +340,7 @@ namespace AutopilotMonitor.Functions.Services
/// <summary>
/// Updates the session status and current phase
/// </summary>
public async Task<bool> UpdateSessionStatusAsync(string tenantId, string sessionId, SessionStatus status, EnrollmentPhase? currentPhase = null, string? failureReason = null)
public async Task<bool> 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
@@ -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<number, string> = { [-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<number, string> = { [-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<number, string> = { [-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<number, string> = { [-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() {
<h2 className="text-xl font-semibold text-gray-900 mb-6">Enrollment Progress</h2>
<PhaseTimeline
currentPhase={session.currentPhase}
completedPhases={session.status === 'Succeeded' ? [6] : []}
completedPhases={session.status === 'Succeeded' ? [7] : []}
events={events}
sessionStatus={session.status}
enrollmentType={session.enrollmentType}
@@ -1309,6 +1311,7 @@ function DeviceDetailsCard({ events }: { events: EnrollmentEvent[] }) {
};
const agentStarted = getEventData("agent_started");
const bootTime = getEventData("boot_time");
const networkAdapters = getEventData("network_adapters");
const dnsConfig = getEventData("dns_configuration");
const proxyConfig = getEventData("proxy_configuration");
@@ -1319,7 +1322,7 @@ function DeviceDetailsCard({ events }: { events: EnrollmentEvent[] }) {
const secureBootStatus = getEventData("secureboot_status");
// Check if we have any device detail events at all
const hasData = agentStarted || networkAdapters || dnsConfig || proxyConfig || autopilotProfile ||
const hasData = agentStarted || bootTime || networkAdapters || dnsConfig || proxyConfig || autopilotProfile ||
aadJoinStatus || imeVersion || bitLockerStatus || secureBootStatus;
if (!hasData) return null;
@@ -1342,10 +1345,11 @@ function DeviceDetailsCard({ events }: { events: EnrollmentEvent[] }) {
{expanded && (
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Boot Time */}
{agentStarted?.bootTime && (
{(bootTime?.bootTime || agentStarted?.bootTime) && (
<DetailSection title="System Boot">
<DetailRow label="Boot Time" value={new Date(agentStarted.bootTime).toLocaleString()} />
{agentStarted.agentVersion && <DetailRow label="Agent Version" value={agentStarted.agentVersion} />}
<DetailRow label="Boot Time" value={new Date(bootTime?.bootTime || agentStarted?.bootTime).toLocaleString()} />
{bootTime?.uptimeMinutes && <DetailRow label="Uptime" value={`${Math.floor(bootTime.uptimeMinutes / 60)}h ${bootTime.uptimeMinutes % 60}m`} />}
{agentStarted?.agentVersion && <DetailRow label="Agent Version" value={agentStarted.agentVersion} />}
</DetailSection>
)}