From 66733209ec17a4415b616b5f7aecdc7308d5874e Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Fri, 20 Mar 2026 00:31:48 +0100 Subject: [PATCH] added agent emit event explicit flag for immediate upload --- .gitignore | 3 ++ .../EspAndHelloTracker.HelloTracking.cs | 12 ++++--- .../EspAndHelloTracker.ShellCoreTracking.cs | 3 +- .../MonitoringService.CollectorLifecycle.cs | 6 ++-- .../Core/MonitoringService.EventUpload.cs | 33 +++++++++---------- .../Monitoring/Core/MonitoringService.cs | 18 ++++++---- .../EnrollmentTracker.CompletionLogic.cs | 28 +++++++++++----- .../Tracking/EnrollmentTracker.Diagnostics.cs | 16 +++++++-- .../EnrollmentTracker.EventHandlers.cs | 18 ++++++---- .../Monitoring/Tracking/EnrollmentTracker.cs | 1 + .../AutopilotMonitor.Agent/buildcounter.txt | 2 +- .../Models/Enrollment/EnrollmentEvent.cs | 8 +++++ 12 files changed, 99 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index 24511172..0936b4e4 100644 --- a/.gitignore +++ b/.gitignore @@ -107,8 +107,11 @@ tmpclaude-*-cwd .claude/commands/session-debug.md .claude/commands/table-query.md .claude/commands/ux-insights.md +.claude/commands/diag-agent-log.md +.claude/commands/diag-ime-log.md .claude/scripts/query-appinsights.sh .claude/scripts/query-table.sh +.claude/scripts/fetch-diag-zip.sh # Deployment scripts (local only) scripts/Deployment/build_and_upload_debug_agent_build.ps1 diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.HelloTracking.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.HelloTracking.cs index 3aa4a4e2..88ef3e31 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.HelloTracking.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.HelloTracking.cs @@ -263,7 +263,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors Source = "EspAndHelloTracker", Phase = EnrollmentPhase.Unknown, Message = message, - Data = data + Data = data, + ImmediateUpload = shouldTriggerHelloCompleted || eventType == "hello_provisioning_failed" }); _logger.Info($"Hello event detected: {eventType} (EventID {eventId}{(isBackfill ? ", backfill" : "")})"); @@ -340,7 +341,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors { "espExitDetected", _espExitDetected }, { "helloPolicyEnabled", true }, { "action", "extended_wait" } - } + }, + ImmediateUpload = true }); // Arm the long completion timer so we don't wait forever @@ -372,7 +374,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors { "espExitDetected", _espExitDetected }, { "helloPolicyEnabled", false }, { "action", "enrollment_complete" } - } + }, + ImmediateUpload = true }); // Trigger HelloCompleted so enrollment can proceed @@ -486,7 +489,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors { { "timeoutSeconds", HelloCompletionTimeoutSeconds }, { "helloWizardStarted", _helloWizardStarted } - } + }, + ImmediateUpload = true }); try diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ShellCoreTracking.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ShellCoreTracking.cs index 7bddd9ec..d75c10fb 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ShellCoreTracking.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ShellCoreTracking.cs @@ -214,7 +214,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors Source = "EspAndHelloTracker", Phase = EnrollmentPhase.Unknown, // Let EnrollmentTracker decide phase Message = message, - Data = eventData + Data = eventData, + ImmediateUpload = true }); _logger.Info($"Shell-Core event detected: {eventType} (EventID {eventId})"); diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs index 50437f3e..d62eb2a2 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs @@ -210,7 +210,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core { "failureType", "agent_timeout" }, { "failureSource", "max_lifetime_timer" }, { "agentUptimeMinutes", Math.Round(uptimeMinutes, 1) } - } + }, + ImmediateUpload = true }); } @@ -235,7 +236,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core Severity = EventSeverity.Info, Source = "DesktopArrivalDetector", Phase = EnrollmentPhase.Unknown, - Message = "User desktop detected (explorer.exe under real user)" + Message = "User desktop detected (explorer.exe under real user)", + ImmediateUpload = true }); // Notify EnrollmentTracker — it decides whether this triggers completion diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs index f60429dc..6fa5134e 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs @@ -27,7 +27,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core { evt.Sequence = Interlocked.Increment(ref _eventSequence); _spool.Add(evt); - _logger.Info($"Event emitted: {evt.EventType} - {evt.Message}"); + if (evt.Severity <= EventSeverity.Debug) + _logger.Verbose($"Event emitted: {evt.EventType} - {evt.Message}"); + else + _logger.Info($"Event emitted: {evt.EventType} - {evt.Message}"); // Track real enrollment activity for idle timeout if (!string.IsNullOrEmpty(evt.EventType) && !IsPeriodicEvent(evt.EventType)) @@ -59,13 +62,16 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core _maxLifetimeTimer?.Change(Timeout.Infinite, Timeout.Infinite); } - // Check if this is a phase transition - bool isPhaseTransition = false; + // Track phase transitions for logging and gather rule notifications if (evt.Phase != _lastPhase) { - _logger.Info($"Phase transition: {_lastPhase?.ToString() ?? "null"} -> {evt.Phase}"); + // Unknown is an intermediate/reset state — log at Debug to reduce noise. + // Real phase boundaries (DeviceSetup, AccountSetup, etc.) stay at Info. + if (evt.Phase == EnrollmentPhase.Unknown) + _logger.Debug($"Phase transition: {_lastPhase?.ToString() ?? "null"} -> {evt.Phase}"); + else + _logger.Info($"Phase transition: {_lastPhase?.ToString() ?? "null"} -> {evt.Phase}"); _lastPhase = evt.Phase; - isPhaseTransition = true; // Notify gather rule executor of phase change try { _gatherRuleExecutor?.OnPhaseChanged(evt.Phase); } @@ -139,17 +145,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core return; // Don't continue with normal event processing } - // Immediate upload for: - // 1. Critical events (errors) - for troubleshooting - // 2. Phase transitions (start/end) - for real-time phase tracking in UI - // 3. Events with "phase" in EventType - explicit phase-related events - // 4. App download/install events - for real-time download progress UI updates - var isAppEvent = evt.EventType?.StartsWith("app_", StringComparison.OrdinalIgnoreCase) == true; - - if (evt.Severity >= EventSeverity.Error || - isPhaseTransition || - evt.EventType?.Contains("phase", StringComparison.OrdinalIgnoreCase) == true || - isAppEvent) + // Immediate upload when explicitly requested by the emitter, or as a safety net for + // unhandled errors. All other events batch via the debounce timer. + if (evt.ImmediateUpload || evt.Severity >= EventSeverity.Error) { _logger.Info($"Triggering immediate upload for {evt.EventType} (bypassing debounce)"); Task.Run(() => UploadEventsAsync()); @@ -614,7 +612,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core Data = new Dictionary { { "rebootDelaySeconds", _configuration.RebootDelaySeconds } - } + }, + ImmediateUpload = true }); // Final upload to ensure the reboot event is sent diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs index d15ed36f..08aa5aea 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs @@ -210,7 +210,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core Source = "Agent", Phase = EnrollmentPhase.Start, Message = "Autopilot Monitor Agent started", - Data = startupData + Data = startupData, + ImmediateUpload = true }); // WhiteGlove Part 2 detection: if the previous boot completed pre-provisioning, @@ -226,7 +227,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core Severity = EventSeverity.Info, Source = "Agent", Phase = EnrollmentPhase.Start, - Message = "WhiteGlove Part 2 — user enrollment started after pre-provisioning" + Message = "WhiteGlove Part 2 — user enrollment started after pre-provisioning", + ImmediateUpload = true }); _sessionPersistence.ClearWhiteGloveComplete(); _sessionPersistence.ResetSessionCreatedAt(); @@ -270,7 +272,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core Source = "Network", Phase = EnrollmentPhase.Unknown, Message = $"Device location: {attempt.Location.City}, {attempt.Location.Region}, {attempt.Location.Country} (via {attempt.Location.Source})", - Data = attempt.Location.ToDictionary() + Data = attempt.Location.ToDictionary(), + ImmediateUpload = true }); } else @@ -441,7 +444,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core ["unrestrictedMode"] = true, ["source"] = "initial_config", ["configVersion"] = config.ConfigVersion - } + }, + ImmediateUpload = true }); } } @@ -464,7 +468,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core ["previousValue"] = false, ["source"] = "config_refresh", ["configVersion"] = config.ConfigVersion - } + }, + ImmediateUpload = true }); } else @@ -483,7 +488,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core ["previousValue"] = true, ["source"] = "config_refresh", ["configVersion"] = config.ConfigVersion - } + }, + ImmediateUpload = true }); } } diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs index e82707f7..c59dc78c 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs @@ -106,7 +106,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { "triggeredBy", "self_deploying_provisioning_complete" }, { "autopilotMode", _autopilotMode }, { "skipUserStatusPage", _skipUserStatusPage } - } + }, + ImmediateUpload = true }); try { CollectDeviceInfoAtFinalizingSetup("self_deploying_provisioning_complete"); } @@ -137,7 +138,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking Severity = EventSeverity.Info, Source = "EnrollmentTracker", Phase = EnrollmentPhase.Complete, - Message = "WhiteGlove (Pre-Provisioning) completed \u2014 device entering pending state" + Message = "WhiteGlove (Pre-Provisioning) completed \u2014 device entering pending state", + ImmediateUpload = true }); // WICHTIG: Kein WriteEnrollmentCompleteMarker()! @@ -287,7 +289,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { "failureSource", failureSource }, { "signalsSeen", _stateData.SignalsSeen }, { "agentUptimeSeconds", (DateTime.UtcNow - _agentStartTimeUtc).TotalSeconds } - } + }, + ImmediateUpload = true }); // Write final status dump before cleanup @@ -341,7 +344,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { "triggeredBy", reason }, { "previousPhase", _lastEspPhase ?? "unknown" }, { "desktopArrivedBackup", _desktopArrived && !string.Equals(_lastEspPhase, "AccountSetup", StringComparison.OrdinalIgnoreCase) } - } + }, + ImmediateUpload = true }); try { CollectDeviceInfoAtFinalizingSetup(reason); } @@ -380,7 +384,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { "triggeredBy", "device_only_esp_registry" }, { "previousPhase", _lastEspPhase ?? "unknown" }, { "skipUserStatusPage", true } - } + }, + ImmediateUpload = true }); try { CollectDeviceInfoAtFinalizingSetup("device_only_esp_registry"); } @@ -437,7 +442,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { "espPhase", "FinalizingSetup" }, { "autoDetected", true }, { "triggeredBy", reason } - } + }, + ImmediateUpload = true }); try { CollectDeviceInfoAtFinalizingSetup(reason); } @@ -603,7 +609,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { "signalsSeen", _stateData.SignalsSeen }, { "signalTimestamps", signalTimestamps }, { "agentUptimeSeconds", (DateTime.UtcNow - _agentStartTimeUtc).TotalSeconds } - } + }, + ImmediateUpload = true }); // Write final status dump and enrollment complete marker @@ -636,6 +643,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking Source = "EnrollmentTracker", Phase = EnrollmentPhase.Unknown, Message = $"Completion source '{source}' evaluated — {result}", + ImmediateUpload = true, Data = new Dictionary { { "source", source }, @@ -714,7 +722,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { "correctedBy", "desktop_arrival" }, { "previousPhase", previousPhase }, { "skipUserStatusPage", true } - } + }, + ImmediateUpload = true }); try { CollectDeviceInfoAtFinalizingSetup("desktop_arrived_skip_user"); } @@ -741,7 +750,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { "autoDetected", true }, { "correctedBy", "desktop_arrival" }, { "previousPhase", previousPhase } - } + }, + ImmediateUpload = true }); } } diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs index 0fb89660..2f7fc48a 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs @@ -110,17 +110,26 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking _lastEmittedSummaryHash = hash; + // Elevate to Info only when completed/error counts change — mere downloading/installing + // state changes (same completed count) are Verbose to reduce log noise. + var compactHash = $"{states.CountCompleted}_{states.ErrorCount}"; + var severity = (compactHash != _lastEmittedSummaryCompactHash) + ? (states.HasError ? EventSeverity.Warning : EventSeverity.Info) + : EventSeverity.Debug; + _lastEmittedSummaryCompactHash = compactHash; + _emitEvent(new EnrollmentEvent { SessionId = _sessionId, TenantId = _tenantId, EventType = "app_tracking_summary", - Severity = states.HasError ? EventSeverity.Warning : EventSeverity.Info, + Severity = severity, Source = "EnrollmentTracker", Phase = EnrollmentPhase.Unknown, Message = $"App tracking: {states.CountCompleted}/{states.CountAll} completed" + (states.HasError ? $" ({states.ErrorCount} errors)" : ""), - Data = states.GetSummaryData() + Data = states.GetSummaryData(), + ImmediateUpload = (severity != EventSeverity.Debug) }); } @@ -144,7 +153,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking Phase = EnrollmentPhase.Unknown, Message = $"App tracking: {states.CountCompleted}/{states.CountAll} completed" + (states.HasError ? $" ({states.ErrorCount} errors)" : ""), - Data = states.GetSummaryData() + Data = states.GetSummaryData(), + ImmediateUpload = true }); } diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs index b0c753ec..3aa572c8 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs @@ -103,7 +103,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking Source = "ImeLogTracker", Phase = enrollmentPhase, Message = $"ESP phase: {phase}", - Data = new Dictionary { { "espPhase", phase } } + Data = new Dictionary { { "espPhase", phase } }, + ImmediateUpload = true }); // Re-collect enrollment-dependent device info (AAD join, profile, ESP config) @@ -170,7 +171,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking Source = "EnrollmentTracker", Phase = EnrollmentPhase.AppsDevice, Message = "ESP phase: AppsDevice (auto-detected from app activity)", - Data = new Dictionary { { "espPhase", "AppsDevice" }, { "autoDetected", true } } + Data = new Dictionary { { "espPhase", "AppsDevice" }, { "autoDetected", true } }, + ImmediateUpload = true }); } else if (string.Equals(_lastEspPhase, "AccountSetup", StringComparison.OrdinalIgnoreCase)) @@ -187,7 +189,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking Source = "EnrollmentTracker", Phase = EnrollmentPhase.AppsUser, Message = "ESP phase: AppsUser (auto-detected from app activity)", - Data = new Dictionary { { "espPhase", "AppsUser" }, { "autoDetected", true } } + Data = new Dictionary { { "espPhase", "AppsUser" }, { "autoDetected", true } }, + ImmediateUpload = true }); } else if (string.Equals(_lastEspPhase, "FinalizingSetup", StringComparison.OrdinalIgnoreCase)) @@ -204,7 +207,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking Source = "EnrollmentTracker", Phase = EnrollmentPhase.AppsUser, Message = "ESP phase: AppsUser (auto-detected from app activity)", - Data = new Dictionary { { "espPhase", "AppsUser" }, { "autoDetected", true } } + Data = new Dictionary { { "espPhase", "AppsUser" }, { "autoDetected", true } }, + ImmediateUpload = true }); } } @@ -320,7 +324,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking Source = "ImeLogTracker", Phase = phase, Message = message, - Data = app.ToEventData() + Data = app.ToEventData(), + ImmediateUpload = true }); // Emit summary immediately if state-breakdown counters changed (instant UI updates) @@ -530,7 +535,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking Severity = EventSeverity.Info, Source = "EnrollmentTracker", Phase = EnrollmentPhase.FinalizingSetup, - Message = "User apps completed - waiting for Windows Hello provisioning to finish" + Message = "User apps completed - waiting for Windows Hello provisioning to finish", + ImmediateUpload = true }); // Set flag so we know we're waiting diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs index 135443fe..060f7a5c 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs @@ -40,6 +40,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking private bool _enrollmentStartDeviceInfoCollected = false; // Re-collect enrollment-dependent info once at first ESP phase private bool _finalDeviceInfoCollected = false; // Ensure final device info is emitted only once private string _lastEmittedSummaryHash; // Track last emitted state-breakdown to avoid redundant summary events + private string _lastEmittedSummaryCompactHash; // Track completed+error counts only — changes here elevate severity to Info // ESP failure handling (Phase 1) private Timer _espFailureTimer; diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 4d0e90cb..9a26b94d 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -512 +518 diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Enrollment/EnrollmentEvent.cs b/src/Shared/AutopilotMonitor.Shared/Models/Enrollment/EnrollmentEvent.cs index ec950e77..c881541e 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Enrollment/EnrollmentEvent.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Enrollment/EnrollmentEvent.cs @@ -79,6 +79,14 @@ namespace AutopilotMonitor.Shared.Models [JsonPropertyName("phaseName")] public string PhaseName => GetPhaseName(Phase); + /// + /// When true, the event bypasses the debounce timer and triggers an immediate upload. + /// Set by the emitter when real-time delivery is required (phase changes, app installs, + /// terminal events). Not serialized — upload transport only. + /// + [JsonIgnore] + public bool ImmediateUpload { get; set; } + private static string GetPhaseName(EnrollmentPhase phase) { return phase switch