mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
Upload Diagnostics package implementation
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -143,6 +143,18 @@ namespace AutopilotMonitor.Agent.Core.Configuration
|
||||
/// </summary>
|
||||
public int AuthFailureTimeoutMinutes { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Azure Blob Storage Container SAS URL for diagnostics package upload.
|
||||
/// The agent uploads directly to the tenant's storage.
|
||||
/// </summary>
|
||||
public string DiagnosticsBlobSasUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When to upload diagnostics packages: "Off", "Always", "OnFailure".
|
||||
/// Default: "Off"
|
||||
/// </summary>
|
||||
public string DiagnosticsUploadMode { get; set; } = "Off";
|
||||
|
||||
/// <summary>
|
||||
/// Validates the configuration
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AutopilotMonitor.Agent.Core.Configuration;
|
||||
using AutopilotMonitor.Agent.Core.Logging;
|
||||
using AutopilotMonitor.Shared;
|
||||
|
||||
namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates and uploads a diagnostics ZIP package (agent logs + IME logs + session info)
|
||||
/// directly to the tenant's Azure Blob Storage container via SAS URL.
|
||||
/// </summary>
|
||||
public class DiagnosticsPackageService
|
||||
{
|
||||
private readonly AgentConfiguration _configuration;
|
||||
private readonly AgentLogger _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private static readonly string ImeLogFolder =
|
||||
Environment.ExpandEnvironmentVariables(@"%ProgramData%\Microsoft\IntuneManagementExtension\Logs");
|
||||
|
||||
private static readonly string AgentLogFolder =
|
||||
Environment.ExpandEnvironmentVariables(Constants.LogDirectory);
|
||||
|
||||
public DiagnosticsPackageService(AgentConfiguration configuration, AgentLogger logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
_httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a diagnostics ZIP and uploads it to the configured Blob Storage container.
|
||||
/// Returns true if the upload succeeded, false if skipped or failed.
|
||||
/// This method is non-fatal: all exceptions are caught and logged.
|
||||
/// </summary>
|
||||
public async Task<bool> CreateAndUploadAsync(bool enrollmentSucceeded)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if upload is needed based on configuration
|
||||
var mode = _configuration.DiagnosticsUploadMode ?? "Off";
|
||||
if (string.Equals(mode, "Off", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.Debug("Diagnostics upload disabled (mode=Off)");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_configuration.DiagnosticsBlobSasUrl))
|
||||
{
|
||||
_logger.Debug("Diagnostics upload skipped: no Blob Storage SAS URL configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.Equals(mode, "OnFailure", StringComparison.OrdinalIgnoreCase) && enrollmentSucceeded)
|
||||
{
|
||||
_logger.Info("Diagnostics upload skipped: enrollment succeeded and mode=OnFailure");
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.Info($"Creating diagnostics package (mode={mode}, enrollmentSucceeded={enrollmentSucceeded})...");
|
||||
|
||||
// Build ZIP in memory
|
||||
var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
|
||||
var zipFileName = $"AgentDiagnostics-{_configuration.SessionId}-{timestamp}.zip";
|
||||
|
||||
byte[] zipBytes;
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
||||
{
|
||||
// 1. sessioninfo.txt
|
||||
AddSessionInfo(archive, enrollmentSucceeded);
|
||||
|
||||
// 2. Agent logs
|
||||
AddLogFiles(archive, AgentLogFolder, "AgentLogs", "*.log");
|
||||
|
||||
// 3. IME logs
|
||||
AddLogFiles(archive, ImeLogFolder, "ImeLogs", "IntuneManagementExtension*.log");
|
||||
AddLogFiles(archive, ImeLogFolder, "ImeLogs", "AppWorkload*.log");
|
||||
}
|
||||
|
||||
zipBytes = ms.ToArray();
|
||||
}
|
||||
|
||||
_logger.Info($"Diagnostics package created: {zipFileName} ({zipBytes.Length / 1024} KB)");
|
||||
|
||||
// Upload to Blob Storage
|
||||
var uploaded = await UploadToBlobStorageAsync(zipFileName, zipBytes);
|
||||
if (uploaded)
|
||||
{
|
||||
_logger.Info($"Diagnostics package uploaded successfully: {zipFileName}");
|
||||
}
|
||||
|
||||
return uploaded;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Diagnostics package creation/upload failed (non-fatal): {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSessionInfo(ZipArchive archive, bool enrollmentSucceeded)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Session ID: {_configuration.SessionId}");
|
||||
sb.AppendLine($"Tenant ID: {_configuration.TenantId}");
|
||||
sb.AppendLine($"Device Name: {Environment.MachineName}");
|
||||
sb.AppendLine($"Timestamp: {DateTime.UtcNow:O}");
|
||||
sb.AppendLine($"Enrollment Result: {(enrollmentSucceeded ? "Succeeded" : "Failed")}");
|
||||
|
||||
// Hardware info via existing DeviceInfoProvider (WMI)
|
||||
sb.AppendLine($"Manufacturer: {DeviceInfoProvider.GetManufacturer()}");
|
||||
sb.AppendLine($"Model: {DeviceInfoProvider.GetModel()}");
|
||||
sb.AppendLine($"Serial Number: {DeviceInfoProvider.GetSerialNumber()}");
|
||||
|
||||
var entry = archive.CreateEntry("sessioninfo.txt");
|
||||
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
|
||||
writer.Write(sb.ToString());
|
||||
}
|
||||
|
||||
private void AddLogFiles(ZipArchive archive, string sourceFolder, string zipFolder, string searchPattern)
|
||||
{
|
||||
if (!Directory.Exists(sourceFolder))
|
||||
{
|
||||
_logger.Debug($"Log folder not found, skipping: {sourceFolder}");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var files = Directory.GetFiles(sourceFolder, searchPattern);
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileName = Path.GetFileName(file);
|
||||
var entryName = $"{zipFolder}/{fileName}";
|
||||
|
||||
// Read with FileShare.ReadWrite to avoid locking conflicts with active log writers
|
||||
byte[] content;
|
||||
using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
using (var reader = new MemoryStream())
|
||||
{
|
||||
fs.CopyTo(reader);
|
||||
content = reader.ToArray();
|
||||
}
|
||||
|
||||
var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
|
||||
using var entryStream = entry.Open();
|
||||
entryStream.Write(content, 0, content.Length);
|
||||
|
||||
_logger.Debug($"Added to diagnostics package: {entryName} ({content.Length / 1024} KB)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Failed to add log file to diagnostics package: {file} - {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Failed to enumerate log files in {sourceFolder}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads the ZIP bytes to the configured Azure Blob Storage container via PUT with SAS token.
|
||||
/// Container SAS URL format: https://account.blob.core.windows.net/container?sv=...&sig=...
|
||||
/// Blob URL is constructed by inserting the blob name before the query string.
|
||||
/// </summary>
|
||||
private async Task<bool> UploadToBlobStorageAsync(string blobName, byte[] data)
|
||||
{
|
||||
var containerSasUrl = _configuration.DiagnosticsBlobSasUrl;
|
||||
|
||||
// Build blob URL: split at '?' to insert blob name
|
||||
var questionMarkIndex = containerSasUrl.IndexOf('?');
|
||||
string blobUrl;
|
||||
if (questionMarkIndex >= 0)
|
||||
{
|
||||
var basePath = containerSasUrl.Substring(0, questionMarkIndex).TrimEnd('/');
|
||||
var queryString = containerSasUrl.Substring(questionMarkIndex);
|
||||
blobUrl = $"{basePath}/{blobName}{queryString}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// No query string (unlikely for SAS but handle gracefully)
|
||||
blobUrl = $"{containerSasUrl.TrimEnd('/')}/{blobName}";
|
||||
}
|
||||
|
||||
const int maxRetries = 3;
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Info($"Uploading diagnostics package (attempt {attempt}/{maxRetries}, {data.Length / 1024} KB)...");
|
||||
|
||||
using var content = new ByteArrayContent(data);
|
||||
content.Headers.Add("x-ms-blob-type", "BlockBlob");
|
||||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/zip");
|
||||
|
||||
var response = await _httpClient.PutAsync(blobUrl, content);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
_logger.Warning($"Blob upload attempt {attempt} failed: HTTP {(int)response.StatusCode} - {responseBody}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Blob upload attempt {attempt} failed: {ex.Message}");
|
||||
}
|
||||
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)); // 2s, 4s, 8s
|
||||
_logger.Info($"Retrying blob upload in {delay.TotalSeconds}s...");
|
||||
await Task.Delay(delay);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Warning($"Diagnostics package upload failed after {maxRetries} attempts");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
// Cleanup/self-destruct
|
||||
private readonly CleanupService _cleanupService;
|
||||
|
||||
// Diagnostics package upload
|
||||
private readonly DiagnosticsPackageService _diagnosticsService;
|
||||
|
||||
// Auth failure circuit breaker
|
||||
private int _consecutiveAuthFailures = 0;
|
||||
private DateTime? _firstAuthFailureTime = null;
|
||||
@@ -67,6 +70,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
_spool = new EventSpool(_configuration.SpoolDirectory);
|
||||
_apiClient = new BackendApiClient(_configuration.ApiBaseUrl, _configuration, _logger);
|
||||
_cleanupService = new CleanupService(_configuration, _logger);
|
||||
_diagnosticsService = new DiagnosticsPackageService(_configuration, _logger);
|
||||
|
||||
// Subscribe to spool events for batched upload when new events arrive
|
||||
_spool.EventsAvailable += OnEventsAvailable;
|
||||
@@ -251,6 +255,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
_configuration.RebootOnComplete = config.RebootOnComplete;
|
||||
_configuration.RebootDelaySeconds = config.RebootDelaySeconds;
|
||||
_configuration.MaxBatchSize = config.MaxBatchSize;
|
||||
_configuration.DiagnosticsBlobSasUrl = config.DiagnosticsBlobSasUrl;
|
||||
_configuration.DiagnosticsUploadMode = config.DiagnosticsUploadMode;
|
||||
|
||||
// Apply log level from remote config
|
||||
if (Enum.TryParse<Logging.AgentLogLevel>(config.LogLevel, ignoreCase: true, out var remoteLogLevel))
|
||||
@@ -529,6 +535,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
// Check for enrollment completion events
|
||||
if (evt.EventType == "enrollment_complete" || evt.EventType == "enrollment_failed")
|
||||
{
|
||||
var enrollmentSucceeded = evt.EventType == "enrollment_complete";
|
||||
_logger.Info($"Enrollment completion detected: {evt.EventType}");
|
||||
|
||||
// Delete session ID so a new session will be created on next enrollment
|
||||
@@ -559,9 +566,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
// Trigger self-destruct or reboot if configured
|
||||
if (_configuration.SelfDestructOnComplete || _configuration.RebootOnComplete)
|
||||
{
|
||||
Task.Run(() => HandleEnrollmentComplete());
|
||||
Task.Run(() => HandleEnrollmentComplete(enrollmentSucceeded));
|
||||
return; // Don't continue with normal event processing
|
||||
}
|
||||
|
||||
// No self-destruct/reboot — still upload diagnostics if configured
|
||||
Task.Run(() => UploadDiagnosticsWithTimelineEvents(enrollmentSucceeded));
|
||||
}
|
||||
|
||||
// Immediate upload for:
|
||||
@@ -724,10 +734,66 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads diagnostics package with timeline events (start + result).
|
||||
/// Emits events, uploads them, then performs the actual diagnostics upload.
|
||||
/// </summary>
|
||||
private async Task UploadDiagnosticsWithTimelineEvents(bool enrollmentSucceeded)
|
||||
{
|
||||
var mode = _configuration.DiagnosticsUploadMode ?? "Off";
|
||||
if (string.Equals(mode, "Off", StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
if (string.IsNullOrEmpty(_configuration.DiagnosticsBlobSasUrl))
|
||||
return;
|
||||
if (string.Equals(mode, "OnFailure", StringComparison.OrdinalIgnoreCase) && enrollmentSucceeded)
|
||||
return;
|
||||
|
||||
// Emit "collecting" event
|
||||
EmitEvent(new EnrollmentEvent
|
||||
{
|
||||
SessionId = _configuration.SessionId,
|
||||
TenantId = _configuration.TenantId,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
EventType = "diagnostics_collecting",
|
||||
Severity = EventSeverity.Info,
|
||||
Source = "MonitoringService",
|
||||
Phase = EnrollmentPhase.Complete,
|
||||
Message = $"Collecting diagnostics package (mode={mode})",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "uploadMode", mode }
|
||||
}
|
||||
});
|
||||
await UploadEventsAsync();
|
||||
|
||||
// Perform the actual ZIP + upload
|
||||
var success = await _diagnosticsService.CreateAndUploadAsync(enrollmentSucceeded);
|
||||
|
||||
// Emit result event
|
||||
EmitEvent(new EnrollmentEvent
|
||||
{
|
||||
SessionId = _configuration.SessionId,
|
||||
TenantId = _configuration.TenantId,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
EventType = success ? "diagnostics_uploaded" : "diagnostics_upload_failed",
|
||||
Severity = success ? EventSeverity.Info : EventSeverity.Warning,
|
||||
Source = "MonitoringService",
|
||||
Phase = EnrollmentPhase.Complete,
|
||||
Message = success
|
||||
? "Diagnostics package uploaded successfully"
|
||||
: "Diagnostics package upload failed",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "uploadMode", mode }
|
||||
}
|
||||
});
|
||||
await UploadEventsAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles enrollment completion and triggers self-destruct sequence
|
||||
/// </summary>
|
||||
private async Task HandleEnrollmentComplete()
|
||||
private async Task HandleEnrollmentComplete(bool enrollmentSucceeded)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -745,6 +811,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
// Give a moment for final upload to complete
|
||||
await Task.Delay(2000);
|
||||
|
||||
// Step 2.5: Upload diagnostics package (must complete before self-destruct/reboot)
|
||||
await UploadDiagnosticsWithTimelineEvents(enrollmentSucceeded);
|
||||
|
||||
// Step 3: Self-destruct (removes Scheduled Task + files) or reboot-only
|
||||
if (_configuration.SelfDestructOnComplete)
|
||||
{
|
||||
|
||||
@@ -1 +1 @@
|
||||
38
|
||||
42
|
||||
|
||||
@@ -102,6 +102,8 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
RebootOnComplete = tenantConfig.RebootOnComplete ?? false,
|
||||
RebootDelaySeconds = tenantConfig.RebootDelaySeconds ?? 10,
|
||||
MaxBatchSize = tenantConfig.MaxBatchSize ?? 100,
|
||||
DiagnosticsBlobSasUrl = tenantConfig.DiagnosticsBlobSasUrl,
|
||||
DiagnosticsUploadMode = tenantConfig.DiagnosticsUploadMode ?? "Off",
|
||||
Collectors = collectors,
|
||||
GatherRules = gatherRules,
|
||||
ImeLogPatterns = imeLogPatterns
|
||||
|
||||
@@ -186,6 +186,8 @@ namespace AutopilotMonitor.Functions.Services
|
||||
{ "EnableImeMatchLog", config.EnableImeMatchLog },
|
||||
{ "LogLevel", config.LogLevel },
|
||||
{ "MaxBatchSize", config.MaxBatchSize },
|
||||
{ "DiagnosticsBlobSasUrl", config.DiagnosticsBlobSasUrl },
|
||||
{ "DiagnosticsUploadMode", config.DiagnosticsUploadMode },
|
||||
{ "TeamsWebhookUrl", config.TeamsWebhookUrl },
|
||||
{ "TeamsNotifyOnSuccess", config.TeamsNotifyOnSuccess },
|
||||
{ "TeamsNotifyOnFailure", config.TeamsNotifyOnFailure }
|
||||
@@ -226,6 +228,8 @@ namespace AutopilotMonitor.Functions.Services
|
||||
EnableImeMatchLog = entity.GetBoolean("EnableImeMatchLog"),
|
||||
LogLevel = entity.GetString("LogLevel"),
|
||||
MaxBatchSize = entity.GetInt32("MaxBatchSize"),
|
||||
DiagnosticsBlobSasUrl = entity.GetString("DiagnosticsBlobSasUrl"),
|
||||
DiagnosticsUploadMode = entity.GetString("DiagnosticsUploadMode") ?? "Off",
|
||||
TeamsWebhookUrl = entity.GetString("TeamsWebhookUrl"),
|
||||
TeamsNotifyOnSuccess = entity.GetBoolean("TeamsNotifyOnSuccess") ?? true,
|
||||
TeamsNotifyOnFailure = entity.GetBoolean("TeamsNotifyOnFailure") ?? true
|
||||
|
||||
@@ -91,6 +91,18 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// Default: 100
|
||||
/// </summary>
|
||||
public int MaxBatchSize { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Azure Blob Storage Container SAS URL for diagnostics package upload.
|
||||
/// The agent uploads directly to the tenant's storage — data never passes through the backend.
|
||||
/// </summary>
|
||||
public string DiagnosticsBlobSasUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When to upload diagnostics packages: "Off", "Always", "OnFailure".
|
||||
/// Default: "Off"
|
||||
/// </summary>
|
||||
public string DiagnosticsUploadMode { get; set; } = "Off";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -203,6 +203,21 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// </summary>
|
||||
public int? MaxBatchSize { get; set; }
|
||||
|
||||
// ===== DIAGNOSTICS SETTINGS =====
|
||||
|
||||
/// <summary>
|
||||
/// Azure Blob Storage Container SAS URL for diagnostics package upload.
|
||||
/// Each tenant provides their own container — data stays in the customer's storage.
|
||||
/// If null or empty, diagnostics upload is disabled.
|
||||
/// </summary>
|
||||
public string DiagnosticsBlobSasUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When to upload diagnostics packages: "Off", "Always", "OnFailure".
|
||||
/// Default: "Off"
|
||||
/// </summary>
|
||||
public string DiagnosticsUploadMode { get; set; } = "Off";
|
||||
|
||||
// ===== TEAMS NOTIFICATIONS =====
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -33,6 +33,9 @@ interface TenantConfiguration {
|
||||
teamsWebhookUrl?: string;
|
||||
teamsNotifyOnSuccess?: boolean;
|
||||
teamsNotifyOnFailure?: boolean;
|
||||
// Diagnostics package
|
||||
diagnosticsBlobSasUrl?: string;
|
||||
diagnosticsUploadMode?: string;
|
||||
}
|
||||
|
||||
interface TenantAdmin {
|
||||
@@ -93,6 +96,10 @@ export default function SettingsPage() {
|
||||
const [teamsNotifyOnSuccess, setTeamsNotifyOnSuccess] = useState(true);
|
||||
const [teamsNotifyOnFailure, setTeamsNotifyOnFailure] = useState(true);
|
||||
|
||||
// Diagnostics package state
|
||||
const [diagnosticsBlobSasUrl, setDiagnosticsBlobSasUrl] = useState("");
|
||||
const [diagnosticsUploadMode, setDiagnosticsUploadMode] = useState("Off");
|
||||
|
||||
// Fetch configuration
|
||||
useEffect(() => {
|
||||
if (!tenantId) return;
|
||||
@@ -139,6 +146,8 @@ export default function SettingsPage() {
|
||||
setTeamsWebhookUrl(data.teamsWebhookUrl ?? "");
|
||||
setTeamsNotifyOnSuccess(data.teamsNotifyOnSuccess ?? true);
|
||||
setTeamsNotifyOnFailure(data.teamsNotifyOnFailure ?? true);
|
||||
setDiagnosticsBlobSasUrl(data.diagnosticsBlobSasUrl ?? "");
|
||||
setDiagnosticsUploadMode(data.diagnosticsUploadMode ?? "Off");
|
||||
} catch (err) {
|
||||
console.error("Error fetching configuration:", err);
|
||||
setError(err instanceof Error ? err.message : "Failed to load configuration");
|
||||
@@ -216,6 +225,8 @@ export default function SettingsPage() {
|
||||
teamsWebhookUrl: teamsWebhookUrl || undefined,
|
||||
teamsNotifyOnSuccess,
|
||||
teamsNotifyOnFailure,
|
||||
diagnosticsBlobSasUrl: diagnosticsBlobSasUrl || undefined,
|
||||
diagnosticsUploadMode,
|
||||
};
|
||||
|
||||
const token = await getAccessToken();
|
||||
@@ -386,6 +397,8 @@ export default function SettingsPage() {
|
||||
setTeamsWebhookUrl(config.teamsWebhookUrl ?? "");
|
||||
setTeamsNotifyOnSuccess(config.teamsNotifyOnSuccess ?? true);
|
||||
setTeamsNotifyOnFailure(config.teamsNotifyOnFailure ?? true);
|
||||
setDiagnosticsBlobSasUrl(config.diagnosticsBlobSasUrl ?? "");
|
||||
setDiagnosticsUploadMode(config.diagnosticsUploadMode ?? "Off");
|
||||
setSuccessMessage(null);
|
||||
setError(null);
|
||||
};
|
||||
@@ -1165,6 +1178,75 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diagnostics Package */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-amber-50 to-orange-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Diagnostics Package</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Upload agent and IME logs as a ZIP package to your Azure Blob Storage after enrollment.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
|
||||
{/* Info */}
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
<p className="text-sm text-amber-900">
|
||||
The agent uploads diagnostics (agent logs + IME logs) directly to <strong>your</strong> Blob Storage container. Data never passes through our backend.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Blob Storage SAS URL */}
|
||||
<div>
|
||||
<label className="block">
|
||||
<span className="text-gray-700 font-medium">Blob Storage Container SAS URL</span>
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
Create an Azure Blob Storage container and generate a Container-level SAS URL with Write and Create permissions.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="url"
|
||||
value={diagnosticsBlobSasUrl}
|
||||
onChange={(e) => setDiagnosticsBlobSasUrl(e.target.value)}
|
||||
placeholder="https://storageaccount.blob.core.windows.net/diagnostics?sv=...&sig=..."
|
||||
className="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-amber-500 transition-colors font-mono text-sm"
|
||||
/>
|
||||
{diagnosticsBlobSasUrl && diagnosticsUploadMode !== "Off" && (
|
||||
<span className="mt-1 inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 whitespace-nowrap">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Upload Mode */}
|
||||
<div className={`p-4 rounded-lg border transition-colors ${diagnosticsBlobSasUrl ? 'border-gray-200 hover:border-amber-200' : 'border-gray-100 opacity-50'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Upload Mode</p>
|
||||
<p className="text-sm text-gray-500">Choose when diagnostics packages are uploaded</p>
|
||||
</div>
|
||||
<select
|
||||
value={diagnosticsUploadMode}
|
||||
onChange={(e) => setDiagnosticsUploadMode(e.target.value)}
|
||||
disabled={!diagnosticsBlobSasUrl}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-amber-500 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
||||
>
|
||||
<option value="Off">Off</option>
|
||||
<option value="Always">Always</option>
|
||||
<option value="OnFailure">On Failure Only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Serial Number Validation */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
|
||||
Reference in New Issue
Block a user