Bootstrap

- install only during enrollment
Agent
- added geolocation support
- HelloDetector
- Reboot on complete
- WMI Model parsing fix
...and general small additions here and there
This commit is contained in:
Oliver Kieselbach
2026-02-05 12:03:22 +01:00
parent 5690620058
commit db55809b49
9 changed files with 652 additions and 22 deletions
+47 -1
View File
@@ -38,7 +38,10 @@ param(
[string]$ApiBaseUrl = "http://localhost:7071",
[Parameter(Mandatory = $false)]
[string]$AgentDownloadUrl = "https://yourcdn.blob.core.windows.net/agent/AutopilotMonitor-Agent.zip"
[string]$AgentDownloadUrl = "https://yourcdn.blob.core.windows.net/agent/AutopilotMonitor-Agent.zip",
[Parameter(Mandatory = $false)]
[int]$MaxOsAgeHours = 5
)
# Configuration - Everything in ProgramData for easy cleanup
@@ -71,6 +74,49 @@ try {
Write-Log "===== Autopilot Monitor Bootstrap Started ====="
Write-Log "API Base URL: $ApiBaseUrl"
# ── Pre-flight: Skip installation on already-enrolled devices ──
# Check 1: OS install date must be within threshold (fresh device)
$osInstallDate = (Get-CimInstance Win32_OperatingSystem).InstallDate
$osAge = (Get-Date) - $osInstallDate
Write-Log "OS install date: $osInstallDate (age: $([math]::Round($osAge.TotalHours, 1))h)"
if ($osAge.TotalHours -gt $MaxOsAgeHours) {
Write-Log "SKIP: OS was installed $([math]::Round($osAge.TotalHours, 1))h ago (threshold: ${MaxOsAgeHours}h). Device is not freshly provisioned."
exit 0
}
# Check 2: MDM enrollment must not be completed yet
$enrollmentPath = "HKLM:\SOFTWARE\Microsoft\Enrollments"
$mdmEnrolled = $false
if (Test-Path $enrollmentPath) {
$enrollmentEntries = Get-ChildItem -Path $enrollmentPath -ErrorAction SilentlyContinue |
Where-Object { $_.PSChildName -match '^[0-9A-Fa-f\-]{36}$' }
foreach ($entry in $enrollmentEntries) {
$providerID = (Get-ItemProperty -Path $entry.PSPath -Name "ProviderID" -ErrorAction SilentlyContinue).ProviderID
if ($providerID) {
$mdmEnrolled = $true
Write-Log "Found MDM enrollment: ProviderID=$providerID (EnrollmentID=$($entry.PSChildName))"
break
}
}
}
if ($mdmEnrolled) {
# Double-check: Is enrollment status tracking showing completed?
$espPath = "HKLM:\SOFTWARE\Microsoft\Windows\Autopilot\EnrollmentStatusTracking\Device\Setup"
$hasCompleted = (Get-ItemProperty -Path $espPath -Name "HasProvisioningCompleted" -ErrorAction SilentlyContinue).HasProvisioningCompleted
if ($hasCompleted -eq 1) {
Write-Log "SKIP: Autopilot enrollment already completed (HasProvisioningCompleted=1). Device is already enrolled."
exit 0
}
}
Write-Log "Pre-flight checks passed - device is freshly provisioned and enrollment in progress"
# Create agent configuration (JSON - NO REGISTRY!)
Write-Log "Creating agent configuration..."
$agentConfig = @{
@@ -45,7 +45,7 @@ namespace AutopilotMonitor.Agent.Core.Configuration
/// <summary>
/// Whether to use client certificate authentication
/// </summary>
public bool UseClientCertAuth { get; set; } = false;
public bool UseClientCertAuth { get; set; } = true;
/// <summary>
/// Client certificate thumbprint (if using cert auth)
@@ -87,6 +87,16 @@ namespace AutopilotMonitor.Agent.Core.Configuration
/// </summary>
public string ScheduledTaskName { get; set; } = "AutopilotMonitor-Agent";
/// <summary>
/// Whether to reboot the device after enrollment completes
/// </summary>
public bool RebootOnComplete { get; set; } = false;
/// <summary>
/// Whether to enable geo-location detection (queries external IP services)
/// </summary>
public bool EnableGeoLocation { get; set; } = false;
/// <summary>
/// Validates the configuration
/// </summary>
@@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using AutopilotMonitor.Agent.Core.Logging;
using AutopilotMonitor.Shared.Models;
using Microsoft.Win32;
namespace AutopilotMonitor.Agent.Core.EventCollection
{
/// <summary>
/// Detects and tracks Windows Hello for Business provisioning during Autopilot enrollment.
/// Monitors the "Microsoft-Windows-User Device Registration/Admin" event log channel
/// and checks registry for WHfB policy configuration.
///
/// Key Event IDs:
/// 358 - Prerequisites passed, provisioning will be launched
/// 360 - Prerequisites failed, provisioning will NOT be launched
/// 362 - Provisioning blocked
/// 300 - NGC key registered successfully (Hello provisioned)
/// 301 - NGC key registration failed
/// </summary>
public class HelloDetector : IDisposable
{
private readonly AgentLogger _logger;
private readonly string _sessionId;
private readonly string _tenantId;
private readonly Action<EnrollmentEvent> _onEventCollected;
private System.Diagnostics.Eventing.Reader.EventLogWatcher _watcher;
private const string EventLogChannel = "Microsoft-Windows-User Device Registration/Admin";
// WHfB policy registry paths
private const string CspPolicyBasePath = @"SOFTWARE\Microsoft\Policies\PassportForWork";
private const string GpoPolicyPath = @"SOFTWARE\Policies\Microsoft\PassportForWork";
// Tracked event IDs
private const int EventId_NgcKeyRegistered = 300;
private const int EventId_NgcKeyRegistrationFailed = 301;
private const int EventId_ProvisioningWillLaunch = 358;
private const int EventId_ProvisioningWillNotLaunch = 360;
private const int EventId_ProvisioningBlocked = 362;
private const int EventId_PinStatus = 376;
private static readonly HashSet<int> TrackedEventIds = new HashSet<int>
{
EventId_NgcKeyRegistered,
EventId_NgcKeyRegistrationFailed,
EventId_ProvisioningWillLaunch,
EventId_ProvisioningWillNotLaunch,
EventId_ProvisioningBlocked,
EventId_PinStatus
};
public HelloDetector(string sessionId, string tenantId, Action<EnrollmentEvent> onEventCollected, AgentLogger logger)
{
_sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId));
_tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId));
_onEventCollected = onEventCollected ?? throw new ArgumentNullException(nameof(onEventCollected));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void Start()
{
_logger.Info("Starting Hello detector");
// Check if WHfB policy is configured
CheckHelloPolicy();
// Subscribe to User Device Registration event log
StartEventLogWatcher();
}
public void Stop()
{
_logger.Info("Stopping Hello detector");
if (_watcher != null)
{
try
{
_watcher.Enabled = false;
_watcher.EventRecordWritten -= OnEventRecordWritten;
_watcher.Dispose();
_watcher = null;
}
catch (Exception ex)
{
_logger.Error("Error stopping Hello detector watcher", ex);
}
}
}
private void CheckHelloPolicy()
{
try
{
var (isEnabled, source) = DetectHelloPolicy();
if (isEnabled.HasValue)
{
var status = isEnabled.Value ? "enabled" : "disabled";
_onEventCollected(new EnrollmentEvent
{
SessionId = _sessionId,
TenantId = _tenantId,
EventType = "hello_policy_detected",
Severity = EventSeverity.Info,
Source = "HelloDetector",
Phase = EnrollmentPhase.EspDeviceSetup,
Message = $"Windows Hello for Business policy detected: {status} (via {source})",
Data = new Dictionary<string, object>
{
{ "helloEnabled", isEnabled.Value },
{ "policySource", source }
}
});
_logger.Info($"WHfB policy: {status} (source: {source})");
}
else
{
_logger.Info("No WHfB policy found in registry - Hello may use tenant default");
}
}
catch (Exception ex)
{
_logger.Warning($"Error checking WHfB policy: {ex.Message}");
}
}
private (bool? isEnabled, string source) DetectHelloPolicy()
{
// 1. Check CSP/Intune-delivered policy (per-tenant paths)
try
{
using (var baseCspKey = Registry.LocalMachine.OpenSubKey(CspPolicyBasePath, false))
{
if (baseCspKey != null)
{
foreach (var tenantSubKey in baseCspKey.GetSubKeyNames())
{
using (var policiesKey = baseCspKey.OpenSubKey($@"{tenantSubKey}\Device\Policies", false))
{
if (policiesKey != null)
{
var value = policiesKey.GetValue("UsePassportForWork");
if (value != null)
{
return (Convert.ToInt32(value) == 1, "CSP/Intune");
}
}
}
}
}
}
}
catch { }
// 2. Check GPO-delivered policy
try
{
using (var gpoKey = Registry.LocalMachine.OpenSubKey(GpoPolicyPath, false))
{
if (gpoKey != null)
{
var value = gpoKey.GetValue("Enabled");
if (value != null)
{
return (Convert.ToInt32(value) == 1, "GPO");
}
}
}
}
catch { }
return (null, null);
}
private void StartEventLogWatcher()
{
try
{
// Watch for specific WHfB-related event IDs
var query = new EventLogQuery(
EventLogChannel,
PathType.LogName,
"*[System[TimeCreated[timediff(@SystemTime) <= 1000]]]"
);
_watcher = new System.Diagnostics.Eventing.Reader.EventLogWatcher(query);
_watcher.EventRecordWritten += OnEventRecordWritten;
_watcher.Enabled = true;
_logger.Info($"Started watching: {EventLogChannel}");
}
catch (EventLogNotFoundException)
{
_logger.Warning($"Event log not found: {EventLogChannel} (normal if not on a real device)");
}
catch (Exception ex)
{
_logger.Error($"Failed to start Hello event log watcher", ex);
}
}
private void OnEventRecordWritten(object sender, EventRecordWrittenEventArgs e)
{
if (e.EventRecord == null)
return;
try
{
var record = e.EventRecord;
var eventId = record.Id;
if (!TrackedEventIds.Contains(eventId))
return;
var description = record.FormatDescription() ?? $"Event ID {eventId}";
string eventType;
EventSeverity severity;
string message;
switch (eventId)
{
case EventId_ProvisioningWillLaunch: // 358
eventType = "hello_provisioning_started";
severity = EventSeverity.Info;
message = "Windows Hello for Business provisioning started - prerequisites passed";
break;
case EventId_NgcKeyRegistered: // 300
eventType = "hello_provisioning_completed";
severity = EventSeverity.Info;
message = "Windows Hello for Business provisioned successfully - NGC key registered";
break;
case EventId_NgcKeyRegistrationFailed: // 301
eventType = "hello_provisioning_failed";
severity = EventSeverity.Error;
message = "Windows Hello for Business provisioning failed - NGC key registration error";
break;
case EventId_ProvisioningWillNotLaunch: // 360
eventType = "hello_provisioning_skipped";
severity = EventSeverity.Warning;
message = "Windows Hello for Business provisioning skipped - prerequisites not met";
break;
case EventId_ProvisioningBlocked: // 362
eventType = "hello_provisioning_blocked";
severity = EventSeverity.Warning;
message = "Windows Hello for Business provisioning blocked";
break;
case EventId_PinStatus: // 376
eventType = "hello_pin_status";
severity = EventSeverity.Info;
message = "Windows Hello PIN status update";
break;
default:
return;
}
_onEventCollected(new EnrollmentEvent
{
SessionId = _sessionId,
TenantId = _tenantId,
Timestamp = record.TimeCreated ?? DateTime.UtcNow,
EventType = eventType,
Severity = severity,
Source = "HelloDetector",
Phase = EnrollmentPhase.EspDeviceSetup,
Message = message,
Data = new Dictionary<string, object>
{
{ "windowsEventId", eventId },
{ "providerName", record.ProviderName ?? "" },
{ "description", description }
}
});
_logger.Info($"Hello event detected: {eventType} (EventID {eventId})");
}
catch (Exception ex)
{
_logger.Error("Error processing Hello event record", ex);
}
}
public void Dispose()
{
Stop();
}
}
}
@@ -7,6 +7,7 @@ using AutopilotMonitor.Agent.Core.Api;
using AutopilotMonitor.Agent.Core.Configuration;
using AutopilotMonitor.Agent.Core.EventCollection;
using AutopilotMonitor.Agent.Core.Logging;
using AutopilotMonitor.Agent.Core.Network;
using AutopilotMonitor.Agent.Core.Storage;
using AutopilotMonitor.Shared.Models;
@@ -31,6 +32,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring
private EventLogWatcher _eventLogWatcher;
private RegistryMonitor _registryMonitor;
private PhaseDetector _phaseDetector;
private HelloDetector _helloDetector;
private AutopilotSimulator _simulator;
public MonitoringService(AgentConfiguration configuration, AgentLogger logger)
@@ -96,12 +98,44 @@ namespace AutopilotMonitor.Agent.Core.Monitoring
Message = "Autopilot Monitor Agent started"
});
// Collect and emit device geo-location (if enabled)
if (_configuration.EnableGeoLocation)
{
EmitGeoLocationEvent();
}
// Start event collectors
StartEventCollectors();
_logger.Info("Monitoring service started");
}
private void EmitGeoLocationEvent()
{
try
{
var location = GeoLocationService.GetLocationAsync(_logger).Result;
if (location != null)
{
EmitEvent(new EnrollmentEvent
{
SessionId = _configuration.SessionId,
TenantId = _configuration.TenantId,
EventType = "device_location",
Severity = EventSeverity.Info,
Source = "Network",
Phase = EnrollmentPhase.PreFlight,
Message = $"Device location: {location.City}, {location.Region}, {location.Country} (via {location.Source})",
Data = location.ToDictionary()
});
}
}
catch (Exception ex)
{
_logger.Warning($"Failed to collect geo-location: {ex.Message}");
}
}
/// <summary>
/// Starts all event collection components
/// </summary>
@@ -138,6 +172,15 @@ namespace AutopilotMonitor.Agent.Core.Monitoring
);
_phaseDetector.Start();
// Start Hello detector (WHfB provisioning tracking)
_helloDetector = new HelloDetector(
_configuration.SessionId,
_configuration.TenantId,
EmitEvent,
_logger
);
_helloDetector.Start();
// Start Autopilot Simulator if enabled
if (_configuration.EnableSimulator)
{
@@ -238,6 +281,21 @@ namespace AutopilotMonitor.Agent.Core.Monitoring
{
try
{
var manufacturer = GetManufacturer();
if (manufacturer.IndexOf("lenovo", StringComparison.OrdinalIgnoreCase) >= 0)
{
// Lenovo stores the friendly model name (e.g. "ThinkPad X1 Carbon Gen 9")
// in Win32_ComputerSystemProduct.Version instead of Win32_ComputerSystem.Model
// which only contains the internal type number (e.g. "20Y3S0FP00")
using (var searcher = new System.Management.ManagementObjectSearcher("SELECT Version FROM Win32_ComputerSystemProduct"))
{
foreach (var obj in searcher.Get())
{
return obj["Version"]?.ToString() ?? "Unknown";
}
}
}
using (var searcher = new System.Management.ManagementObjectSearcher("SELECT Model FROM Win32_ComputerSystem"))
{
foreach (var obj in searcher.Get())
@@ -312,6 +370,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring
_phaseDetector?.Stop();
_phaseDetector?.Dispose();
_helloDetector?.Stop();
_helloDetector?.Dispose();
_simulator?.Stop();
_simulator?.Dispose();
@@ -468,6 +529,18 @@ namespace AutopilotMonitor.Agent.Core.Monitoring
{
ExecuteCleanup();
}
else if (_configuration.RebootOnComplete)
{
_logger.Info("Reboot on complete enabled - initiating reboot");
var psi = new ProcessStartInfo
{
FileName = "shutdown.exe",
Arguments = "/r /t 10 /c \"Autopilot enrollment completed - rebooting\"",
UseShellExecute = false,
CreateNoWindow = true
};
Process.Start(psi);
}
_logger.Info("Self-destruct sequence initiated. Agent will now exit.");
@@ -488,7 +561,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring
{
try
{
_logger.Info("Executing FULL SELF-DESTRUCT (Scheduled Task + Files)");
_logger.Info($"Executing FULL SELF-DESTRUCT (Scheduled Task + Files{(_configuration.RebootOnComplete ? " + Reboot" : "")})");
var currentProcessId = Process.GetCurrentProcess().Id;
var agentBasePath = Path.GetFullPath(Path.Combine(
@@ -522,6 +595,11 @@ try {{
}}
Write-Host 'AutopilotMonitor Agent self-destruct complete'
{(_configuration.RebootOnComplete ? @"
# Reboot the device
Write-Host 'Initiating reboot...'
Restart-Computer -Force
" : "")}
";
// Write cleanup script to temp location (outside of agent folder)
@@ -554,7 +632,7 @@ Write-Host 'AutopilotMonitor Agent self-destruct complete'
{
try
{
_logger.Info("Executing CLEANUP (Files only, keeping Scheduled Task)");
_logger.Info($"Executing CLEANUP (Files only, keeping Scheduled Task{(_configuration.RebootOnComplete ? " + Reboot" : "")})");
var currentProcessId = Process.GetCurrentProcess().Id;
var agentBasePath = Path.GetFullPath(Path.Combine(
@@ -578,6 +656,11 @@ try {{
}}
Write-Host 'AutopilotMonitor Agent cleanup complete'
{(_configuration.RebootOnComplete ? @"
# Reboot the device
Write-Host 'Initiating reboot...'
Restart-Computer -Force
" : "")}
";
// Write cleanup script to temp location
@@ -612,6 +695,7 @@ Write-Host 'AutopilotMonitor Agent cleanup complete'
_eventLogWatcher?.Dispose();
_registryMonitor?.Dispose();
_phaseDetector?.Dispose();
_helloDetector?.Dispose();
_simulator?.Dispose();
}
}
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using AutopilotMonitor.Agent.Core.Logging;
using Newtonsoft.Json.Linq;
namespace AutopilotMonitor.Agent.Core.Network
{
public class GeoLocationResult
{
public string Country { get; set; }
public string Region { get; set; }
public string City { get; set; }
public string Loc { get; set; }
public string Timezone { get; set; }
public string Source { get; set; }
public Dictionary<string, object> ToDictionary()
{
return new Dictionary<string, object>
{
{ "country", Country },
{ "region", Region },
{ "city", City },
{ "loc", Loc },
{ "timezone", Timezone },
{ "source", Source }
};
}
}
public static class GeoLocationService
{
private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(5);
public static async Task<GeoLocationResult> GetLocationAsync(AgentLogger logger)
{
// Try ipinfo.io first
var result = await TryIpInfo(logger);
if (result != null) return result;
// Fallback to ifconfig.co
result = await TryIfConfigCo(logger);
if (result != null) return result;
logger?.Warning("GeoLocation: All providers failed, skipping location event");
return null;
}
private static async Task<GeoLocationResult> TryIpInfo(AgentLogger logger)
{
try
{
logger?.Info("GeoLocation: Querying ipinfo.io...");
using (var client = new HttpClient { Timeout = RequestTimeout })
{
client.DefaultRequestHeaders.Add("Accept", "application/json");
var response = await client.GetStringAsync("https://ipinfo.io/json");
var json = JObject.Parse(response);
var result = new GeoLocationResult
{
Country = json.Value<string>("country"),
Region = json.Value<string>("region"),
City = json.Value<string>("city"),
Loc = json.Value<string>("loc"),
Timezone = json.Value<string>("timezone"),
Source = "ipinfo"
};
logger?.Info($"GeoLocation: ipinfo.io returned {result.City}, {result.Region}, {result.Country}");
return result;
}
}
catch (Exception ex)
{
logger?.Warning($"GeoLocation: ipinfo.io failed: {ex.Message}");
return null;
}
}
private static async Task<GeoLocationResult> TryIfConfigCo(AgentLogger logger)
{
try
{
logger?.Info("GeoLocation: Falling back to ifconfig.co...");
using (var client = new HttpClient { Timeout = RequestTimeout })
{
client.DefaultRequestHeaders.Add("Accept", "application/json");
var response = await client.GetStringAsync("https://ifconfig.co/json");
var json = JObject.Parse(response);
var latitude = json.Value<string>("latitude") ?? "";
var longitude = json.Value<string>("longitude") ?? "";
var loc = !string.IsNullOrEmpty(latitude) && !string.IsNullOrEmpty(longitude)
? $"{latitude},{longitude}"
: "";
var result = new GeoLocationResult
{
Country = json.Value<string>("country_iso"),
Region = json.Value<string>("region_name"),
City = json.Value<string>("city"),
Loc = loc,
Timezone = json.Value<string>("time_zone"),
Source = "ifconfig.co"
};
logger?.Info($"GeoLocation: ifconfig.co returned {result.City}, {result.Region}, {result.Country}");
return result;
}
}
catch (Exception ex)
{
logger?.Warning($"GeoLocation: ifconfig.co failed: {ex.Message}");
return null;
}
}
}
}
+38 -4
View File
@@ -39,6 +39,7 @@ namespace AutopilotMonitor.Agent
logger.Info("Agent starting in console mode");
Console.WriteLine($"Session ID: {config.SessionId}");
Console.WriteLine($"Tenant ID: {config.TenantId}");
Console.WriteLine($"API URL: {config.ApiBaseUrl}");
Console.WriteLine($"Log Directory: {logDir}");
@@ -48,6 +49,16 @@ namespace AutopilotMonitor.Agent
Console.WriteLine($"Simulate Failure: {(config.SimulateFailure ? "YES" : "NO")}");
}
if (config.RebootOnComplete)
{
Console.WriteLine($"Reboot on Complete: ENABLED");
}
if (config.EnableGeoLocation)
{
Console.WriteLine($"GeoLocation Detection: ENABLED");
}
if (config.UseClientCertAuth)
{
Console.WriteLine($"Client Certificate Auth: ENABLED");
@@ -88,11 +99,14 @@ namespace AutopilotMonitor.Agent
// Parse command-line arguments
var enableSimulator = args?.Contains("--simulator") ?? false;
var simulateFailure = args?.Contains("--simulate-failure") ?? false;
var useAuth = args?.Contains("--use-auth") ?? false;
var noAuth = args?.Contains("--no-auth") ?? false;
var noCleanup = args?.Contains("--no-cleanup") ?? false;
var rebootOnComplete = args?.Contains("--reboot-on-complete") ?? false;
var enableGeoLocation = args?.Contains("--enable-geolocation") ?? false;
// Parse certificate thumbprint if provided
string certThumbprint = null;
string tenantIdOverride = null;
if (args != null)
{
var thumbprintIndex = Array.IndexOf(args, "--cert-thumbprint");
@@ -100,6 +114,12 @@ namespace AutopilotMonitor.Agent
{
certThumbprint = args[thumbprintIndex + 1];
}
var tenantIdIndex = Array.IndexOf(args, "--tenant-id");
if (tenantIdIndex >= 0 && tenantIdIndex + 1 < args.Length)
{
tenantIdOverride = args[tenantIdIndex + 1];
}
}
// Defaults (fallback values if no config file exists)
@@ -107,6 +127,9 @@ namespace AutopilotMonitor.Agent
int uploadIntervalSeconds = 30;
bool cleanupOnExit = true;
bool selfDestructOnComplete = true;
bool rebootOnCompleteConfig = false;
bool enableGeoLocationConfig = false;
bool useClientCertAuthConfig = true;
// Try to load configuration from JSON file (written by PowerShell bootstrap script)
var configFilePath = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Config\agent-config.json");
@@ -131,6 +154,15 @@ namespace AutopilotMonitor.Agent
if (configDict.ContainsKey("selfDestructOnComplete") && configDict["selfDestructOnComplete"] != null)
selfDestructOnComplete = Convert.ToBoolean(configDict["selfDestructOnComplete"]);
if (configDict.ContainsKey("rebootOnComplete") && configDict["rebootOnComplete"] != null)
rebootOnCompleteConfig = Convert.ToBoolean(configDict["rebootOnComplete"]);
if (configDict.ContainsKey("enableGeoLocation") && configDict["enableGeoLocation"] != null)
enableGeoLocationConfig = Convert.ToBoolean(configDict["enableGeoLocation"]);
if (configDict.ContainsKey("useClientCertAuth") && configDict["useClientCertAuth"] != null)
useClientCertAuthConfig = Convert.ToBoolean(configDict["useClientCertAuth"]);
Console.WriteLine($"Loaded configuration from {configFilePath}");
}
}
@@ -161,7 +193,7 @@ namespace AutopilotMonitor.Agent
ApiBaseUrl = apiBaseUrl,
SessionId = Guid.NewGuid().ToString(),
//TenantId = "deadbeef-dead-beef-dead-beefdeadbeef",
TenantId = "b54dc1af-5320-4f60-b5d4-821e0cf2a359",
TenantId = tenantIdOverride ?? "b54dc1af-5320-4f60-b5d4-821e0cf2a359",
SpoolDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Spool"),
LogDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Logs"),
UploadIntervalSeconds = uploadIntervalSeconds,
@@ -169,10 +201,12 @@ namespace AutopilotMonitor.Agent
EnableDebugLogging = true,
EnableSimulator = enableSimulator,
SimulateFailure = simulateFailure,
UseClientCertAuth = useAuth,
UseClientCertAuth = !noAuth && useClientCertAuthConfig,
ClientCertThumbprint = certThumbprint,
CleanupOnExit = cleanupOnExit,
SelfDestructOnComplete = selfDestructOnComplete
SelfDestructOnComplete = selfDestructOnComplete,
RebootOnComplete = rebootOnComplete || rebootOnCompleteConfig,
EnableGeoLocation = enableGeoLocation || enableGeoLocationConfig
};
}
}
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#3b82f6"/>
<rect x="6" y="18" width="5" height="8" rx="1.5" fill="white"/>
<rect x="13.5" y="12" width="5" height="14" rx="1.5" fill="white"/>
<rect x="21" y="6" width="5" height="20" rx="1.5" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

@@ -33,6 +33,7 @@ interface Session {
currentPhase: number;
eventCount: number;
durationSeconds: number;
failureReason?: string;
}
const phaseNames: Record<number, string> = {
@@ -356,7 +357,7 @@ export default function SessionDetailPage() {
<InfoItem label="Model" value={`${session.manufacturer} ${session.model}`} />
<InfoItem label="Serial Number" value={session.serialNumber} />
<InfoItem label="Session ID" value={session.sessionId} />
<InfoItem label="Status" value={<StatusBadge status={session.status} />} />
<InfoItem label="Status" value={<StatusBadge status={session.status} failureReason={session.failureReason} />} />
<InfoItem label="Events" value={session.eventCount.toString()} />
<InfoItem label="Duration" value={`${Math.round(session.durationSeconds / 60)} min`} />
</div>
@@ -640,7 +641,7 @@ function SeverityBadge({ severity }: { severity: string }) {
);
}
function StatusBadge({ status }: { status: string }) {
function StatusBadge({ status, failureReason }: { status: string; failureReason?: string }) {
const statusConfig = {
InProgress: { color: "bg-blue-100 text-blue-800", text: "In Progress" },
Succeeded: { color: "bg-green-100 text-green-800", text: "Succeeded" },
@@ -650,9 +651,19 @@ function StatusBadge({ status }: { status: string }) {
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.Unknown;
const isTimeout = status === "Failed" && failureReason && failureReason.toLowerCase().includes("timed out");
return (
<span className={`px-2 inline-block text-xs leading-5 font-semibold rounded-full ${config.color}`}>
<span
className={`px-2 inline-flex items-center gap-1 text-xs leading-5 font-semibold rounded-full ${config.color}`}
title={failureReason || undefined}
>
{config.text}
{isTimeout && (
<span title={failureReason} className="inline-flex items-center">
⏱️
</span>
)}
</span>
);
}
@@ -314,19 +314,36 @@ export default function Navbar() {
{/* User Dropdown */}
{showUserMenu && (
<div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 z-50">
<div className="px-4 py-3 border-b border-gray-200">
<p className="text-sm font-medium text-gray-900">{user?.displayName || 'User'}</p>
<p className="text-sm text-gray-600 truncate">{user?.upn}</p>
{user?.isGalacticAdmin && (
<span className="inline-block mt-2 px-2 py-1 text-xs font-semibold text-purple-800 bg-purple-100 rounded-full">
Galactic Admin
</span>
)}
<div className="absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-lg border border-gray-200 z-50">
<div className="px-4 py-3 border-b border-gray-200 flex items-start space-x-3">
<div className="w-10 h-10 rounded-full bg-blue-600 flex-shrink-0 flex items-center justify-center text-white font-semibold text-sm">
{(() => {
if (user?.displayName) {
const names = user.displayName.split(' ');
if (names.length >= 2) {
return `${names[0].charAt(0)}${names[names.length - 1].charAt(0)}`.toUpperCase();
}
return user.displayName.charAt(0).toUpperCase();
}
return user?.upn?.charAt(0).toUpperCase() || 'U';
})()}
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-gray-900">{user?.displayName || 'User'}</p>
<p className="text-sm text-gray-600 truncate">{user?.upn}</p>
{user?.isGalacticAdmin && (
<span className="inline-block mt-2 px-2 py-1 text-xs font-semibold text-purple-800 bg-purple-100 rounded-full">
Galactic Admin
</span>
)}
</div>
</div>
<div className="py-1">
<button onClick={() => { logout(); setShowUserMenu(false); }} className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Sign out
<button onClick={() => { logout(); setShowUserMenu(false); }} className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center space-x-2">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
<span>Sign out</span>
</button>
</div>
</div>