diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/OffboardingSessionEnumeratorTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/OffboardingSessionEnumeratorTests.cs index 7c945f85..e64ff35c 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/OffboardingSessionEnumeratorTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/OffboardingSessionEnumeratorTests.cs @@ -117,7 +117,6 @@ public class OffboardingSessionEnumeratorTests public Task> GetLegacyTimeoutFailedSessionsAsync(string tenantId, int maxResults) => throw new NotImplementedException(); public Task> GetSessionsLeanAsync(string tenantId) => throw new NotImplementedException(); public Task> GetAgentSilentSessionsAsync(string tenantId, DateTime silenceCutoff, DateTime hardCutoff) => throw new NotImplementedException(); - public Task> GetExcessiveDataSendersAsync(string tenantId, DateTime windowCutoff, int maxSessionWindowHours) => throw new NotImplementedException(); public Task> GetAllTenantIdsAsync() => throw new NotImplementedException(); public Task DeleteSessionEventsAsync(string tenantId, string sessionId) => throw new NotImplementedException(); public Task DeleteSessionRuleResultsAsync(string tenantId, string sessionId) => throw new NotImplementedException(); diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs index 47e4097c..d56e8c5b 100644 --- a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs @@ -590,8 +590,6 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage { "ExcessiveEventAutoActionMode", config.ExcessiveEventAutoActionMode ?? "Off" }, { "ExcessiveEventAutoActionThreshold", config.ExcessiveEventAutoActionThreshold }, { "ExcessiveEventAutoActionDurationHours", config.ExcessiveEventAutoActionDurationHours }, - { "MaxSessionWindowHours", config.MaxSessionWindowHours }, - { "MaintenanceBlockDurationHours", config.MaintenanceBlockDurationHours }, { "DiagnosticsGlobalLogPathsJson", config.DiagnosticsGlobalLogPathsJson }, { "ModernDeploymentHarmlessEventIdsJson", config.ModernDeploymentHarmlessEventIdsJson ?? string.Empty }, { "WhiteGloveSealingPatternIdsJson", config.WhiteGloveSealingPatternIdsJson ?? string.Empty }, @@ -657,8 +655,6 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage ExcessiveEventAutoActionMode = entity.GetString("ExcessiveEventAutoActionMode") ?? "Off", ExcessiveEventAutoActionThreshold = entity.GetInt32("ExcessiveEventAutoActionThreshold") ?? 2500, ExcessiveEventAutoActionDurationHours = entity.GetInt32("ExcessiveEventAutoActionDurationHours") ?? 24, - MaxSessionWindowHours = entity.GetInt32("MaxSessionWindowHours") ?? 24, - MaintenanceBlockDurationHours = entity.GetInt32("MaintenanceBlockDurationHours") ?? 12, DiagnosticsGlobalLogPathsJson = entity.GetString("DiagnosticsGlobalLogPathsJson"), ModernDeploymentHarmlessEventIdsJson = entity.GetString("ModernDeploymentHarmlessEventIdsJson"), WhiteGloveSealingPatternIdsJson = entity.GetString("WhiteGloveSealingPatternIdsJson"), diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMaintenanceRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMaintenanceRepository.cs index 45583de3..f38264fb 100644 --- a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMaintenanceRepository.cs +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMaintenanceRepository.cs @@ -69,9 +69,6 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage public Task> GetAgentSilentSessionsAsync(string tenantId, DateTime silenceCutoff, DateTime hardCutoff) => _storage.GetAgentSilentSessionsAsync(tenantId, silenceCutoff, hardCutoff); - public Task> GetExcessiveDataSendersAsync(string tenantId, DateTime windowCutoff, int maxSessionWindowHours) - => _storage.GetExcessiveDataSendersAsync(tenantId, windowCutoff, maxSessionWindowHours); - public Task> GetLegacyTimeoutFailedSessionsAsync(string tenantId, int maxResults) => _storage.GetLegacyTimeoutFailedSessionsAsync(tenantId, maxResults); diff --git a/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.cs b/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.cs index ff65cbd9..491c370c 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -134,7 +134,6 @@ namespace AutopilotMonitor.Functions.Services { await MarkStalledSessionsAsTimedOutAsync(); await DetectExcessiveEventSessionsAsync(); - await BlockExcessiveDataSendersAsync(); await AggregateMetricsWithCatchUpAsync(); // Plan §5 PR6 / §16 R14: session retention fanout extracted out of the 2h timer // into the dedicated 12h SessionDeletionMaintenanceFunction so cascade-lifecycle @@ -590,6 +589,14 @@ namespace AutopilotMonitor.Functions.Services /// mode mid-run never re-fires the warn. /// /// Both paths skip when their threshold is 0; the whole sweep no-ops when both are off. + /// + /// This is the ONLY automatic device block. A second, time-window detector used to run + /// beside it (AdminConfiguration.MaxSessionWindowHours) and blocked any session whose + /// first and last event lay more than N hours apart. It measured span, never volume, so + /// it flagged every enrollment that spanned a night — 23-event sessions included — and + /// was removed 2026-07-22. Event count is the quantity that actually costs anything; + /// if a span-based guard is ever reintroduced it has to gate on volume too. + /// /// private async Task DetectExcessiveEventSessionsAsync() { @@ -711,114 +718,6 @@ namespace AutopilotMonitor.Functions.Services } } - /// - /// Detects devices that are still actively sending data beyond the configured maximum session - /// window and blocks them automatically. Status-independent: uses LastEventAt (written on every - /// event batch) combined with StartedAt to identify sessions spanning too long a data window. - /// Controlled by AdminConfiguration.MaxSessionWindowHours (0 = disabled). - /// - private async Task BlockExcessiveDataSendersAsync() - { - _logger.LogInformation("Checking for excessive data senders..."); - var sw = Stopwatch.StartNew(); - - try - { - var adminConfig = await _adminConfigurationService.GetConfigurationAsync(); - if (adminConfig.MaxSessionWindowHours == 0) - { - _logger.LogInformation("Excessive data sender detection disabled (MaxSessionWindowHours = 0)"); - return 0; - } - - var windowCutoff = DateTime.UtcNow.AddHours(-adminConfig.MaxSessionWindowHours); - var blockDurationHours = adminConfig.MaintenanceBlockDurationHours; - var tenantIds = await _maintenanceRepo.GetAllTenantIdsAsync(); - int totalBlocked = 0; - - foreach (var tenantId in tenantIds) - { - try - { - var sessions = await _maintenanceRepo.GetExcessiveDataSendersAsync(tenantId, windowCutoff, adminConfig.MaxSessionWindowHours); - - if (sessions.Count == 0) - { - _logger.LogInformation($"Tenant {tenantId}: No excessive data sender sessions found (window: {adminConfig.MaxSessionWindowHours}h)"); - continue; - } - - int blockedCount = 0; - - foreach (var session in sessions) - { - if (string.IsNullOrEmpty(session.SerialNumber)) - continue; - - var (isBlocked, _, _, _) = await _blockedDeviceService.IsBlockedAsync(tenantId, session.SerialNumber); - if (isBlocked) - continue; - - var reason = $"Excessive data window: session active for >{adminConfig.MaxSessionWindowHours}h " + - $"(started {session.StartedAt:yyyy-MM-dd HH:mm:ss} UTC, " + - $"last event {session.LastEventAt:yyyy-MM-dd HH:mm:ss} UTC)"; - - await _blockedDeviceService.BlockDeviceAsync( - tenantId, - session.SerialNumber, - durationHours: blockDurationHours, - blockedByEmail: "System.Maintenance", - reason: reason, - blockedSessionId: session.SessionId); - - // One ops event per device, not per tenant: the alert is only actionable - // if it names the session and serial behind it (the detail modal's - // View session / Block / Kill shortcuts key off `sessionId`). - await _opsEventService.RecordExcessiveDataBlockedAsync( - tenantId, session.SerialNumber, session.SessionId, - adminConfig.MaxSessionWindowHours, blockDurationHours); - - blockedCount++; - } - - if (blockedCount > 0) - { - await _maintenanceRepo.LogAuditEntryAsync( - tenantId, - "ExcessiveDataBlock", - "Device", - $"{blockedCount} devices", - "System.Maintenance", - new Dictionary - { - { "DevicesBlocked", blockedCount.ToString() }, - { "WindowHours", adminConfig.MaxSessionWindowHours.ToString() }, - { "BlockDurationHours", blockDurationHours.ToString() } - }); - - _logger.LogInformation($"Tenant {tenantId}: Blocked {blockedCount} excessive data sender device(s) (window: {adminConfig.MaxSessionWindowHours}h, block: {blockDurationHours}h)"); - } - - totalBlocked += blockedCount; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Failed to check excessive data senders for tenant {tenantId}"); - } - } - - sw.Stop(); - _logger.LogInformation($"Excessive data sender check completed: {totalBlocked} devices blocked in {sw.ElapsedMilliseconds}ms"); - return totalBlocked; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to check for excessive data senders"); - return 0; - } - } - - /// /// Aggregates metrics for any missed days in the last 7 days, plus yesterday. /// Checks the UsageMetrics table for existing snapshots to avoid re-aggregation. diff --git a/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs b/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs index aa1143b7..987eb27b 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs @@ -255,19 +255,9 @@ namespace AutopilotMonitor.Functions.Services $"Device {serialNumber} blocked: {reason}", tenantId, blockedBy, new { serialNumber, reason }); - /// - /// Fired once per device that the time-window watchdog auto-blocks, i.e. a session still - /// uploading beyond AdminConfiguration.MaxSessionWindowHours. Per device rather than - /// per tenant so the Ops Events detail modal can deep-link View session / Block / Kill - /// against the device behind the alert — same payload contract as - /// . The aggregate count stays - /// in the maintenance audit entry, where a sum is the useful view. - /// - public Task RecordExcessiveDataBlockedAsync( - string tenantId, string serialNumber, string sessionId, int windowHours, int durationHours) - => WriteAsync(OpsEventCategory.Security, "ExcessiveDataBlocked", OpsEventSeverity.Warning, - $"Device {serialNumber} auto-blocked for excessive data (session active >{windowHours}h, {durationHours}h block)", - tenantId, "System.Maintenance", new { sessionId, serialNumber, windowHours, durationHours }); + // ExcessiveDataBlocked was removed 2026-07-22 together with the time-window detector that + // raised it — it blocked on session span alone and fired on ordinary overnight enrollments. + // Automatic blocks now come only from the event-count path below. public Task RecordVersionBlockedAsync(string pattern, string blockedBy) => WriteAsync(OpsEventCategory.Security, "VersionBlocked", OpsEventSeverity.Warning, diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Maintenance.cs b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Maintenance.cs index 255550cd..2a7b3331 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Maintenance.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Maintenance.cs @@ -1,4 +1,4 @@ -using Azure; +using Azure; using Azure.Data.Tables; using AutopilotMonitor.Functions.Helpers; using AutopilotMonitor.Functions.Pagination; @@ -345,8 +345,8 @@ namespace AutopilotMonitor.Functions.Services } } - // Suppress automated maintenance entries (SessionTimeout, ExcessiveDataBlock, - // DataRetentionCleanup, …) from the human-facing audit list. They are written + // Suppress automated maintenance entries (SessionTimeout, DataRetentionCleanup, …) + // from the human-facing audit list. They are written // by MaintenanceService for traceability but duplicate the OpsEvents stream // and are not actionable as user-attributable audits. Rows stay in storage // and remain reachable via raw table queries. @@ -792,73 +792,6 @@ namespace AutopilotMonitor.Functions.Services } } - /// - /// Gets sessions where the device has been actively sending data for longer than - /// . - /// Status-independent: detects excessive data senders regardless of session status. - /// Uses LastEventAt (written on every event batch) for the "still active" check. - /// Sessions without LastEventAt (predating this field) are not returned. - /// - /// The OData pre-filter narrows candidates to sessions that straddle the cutoff boundary, - /// then a post-filter verifies the actual session duration (LastEventAt − StartedAt) - /// exceeds the allowed window. This prevents false positives from short sessions that - /// merely happen to straddle the cutoff time. - /// - public async Task> GetExcessiveDataSendersAsync(string tenantId, DateTime windowCutoff, int maxSessionWindowHours) - { - SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); - - try - { - var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Sessions); - - // OData pre-filter: narrow to sessions that straddle the cutoff boundary. - // Status eq 'InProgress' → only block devices whose session is still actively - // sending data. Completed sessions (Succeeded/Failed) must NOT be re-blocked - // even if their LastEventAt is still within the window — they cannot continue - // to abuse data transfer. This also defends against ghost-blocks caused by - // devices with bad clocks: an agent that submits events with timestamps from - // weeks in the past pushes StartedAt back, making the session look long-lived; - // but once the session is Succeeded/Failed, no further data flows from it. - // IsPreProvisioned ne true → exclude WhiteGlove sessions: a pre-provisioned device - // that resumes after weeks in storage looks like an excessive sender (StartedAt old, - // LastEventAt recent) but is a legitimate resumption, not abuse. - var cutoffStr = windowCutoff.ToString("yyyy-MM-ddTHH:mm:ss"); - var filter = $"PartitionKey eq '{tenantId}' " + - $"and Status eq 'InProgress' " + - $"and LastEventAt gt datetime'{cutoffStr}Z' " + - $"and StartedAt lt datetime'{cutoffStr}Z' " + - $"and IsPreProvisioned ne true"; - - var query = tableClient.QueryAsync(filter: filter); - var maxDuration = TimeSpan.FromHours(maxSessionWindowHours); - - var sessions = new List(); - await foreach (var entity in query) - { - // Post-filter: verify actual session duration exceeds the window. - // OData cannot compute date differences, so we check in code. - var startedAt = entity.GetDateTimeOffset("StartedAt")?.UtcDateTime; - var lastEventAt = entity.GetDateTimeOffset("LastEventAt")?.UtcDateTime; - - if (startedAt.HasValue && lastEventAt.HasValue - && (lastEventAt.Value - startedAt.Value) < maxDuration) - { - continue; - } - - sessions.Add(MapToSessionSummary(entity)); - } - - return sessions; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Failed to get excessive data sender sessions for tenant {tenantId}"); - return new List(); - } - } - /// /// Gets all known tenant IDs from the TenantConfiguration table (one "config" row per /// tenant). This replaces an O(corpus) scan of the entire Sessions table — the maintenance diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IMaintenanceRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IMaintenanceRepository.cs index 5bdefd01..8eac1f32 100644 --- a/src/Shared/AutopilotMonitor.Shared/DataAccess/IMaintenanceRepository.cs +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IMaintenanceRepository.cs @@ -82,7 +82,6 @@ namespace AutopilotMonitor.Shared.DataAccess Task> GetGeoWindowSessionsAsync(DateTime startDate, DateTime endDate, string? tenantId = null); Task> GetStalledSessionsAsync(string tenantId, DateTime cutoffTime); Task> GetAgentSilentSessionsAsync(string tenantId, DateTime silenceCutoff, DateTime hardCutoff); - Task> GetExcessiveDataSendersAsync(string tenantId, DateTime windowCutoff, int maxSessionWindowHours); // --- Legacy Reclassification (misclassification audit 2026-07-16) --- /// diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs b/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs index 52e2e13a..038489aa 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs @@ -111,27 +111,15 @@ namespace AutopilotMonitor.Shared.Models public int ExcessiveEventAutoActionThreshold { get; set; } = 2500; /// - /// Duration in hours for the device block created by the auto-action path. Independent - /// of (which governs the time-window - /// ExcessiveDataBlocked path) so the operator can tune both autonomously. Default: 24. + /// Duration in hours for the device block created by the auto-action path. Default: 24. /// public int ExcessiveEventAutoActionDurationHours { get; set; } = 24; - // ===== MAINTENANCE AUTO-BLOCK SETTINGS ===== - - /// - /// Max active data window in hours for maintenance auto-block detection. - /// Sessions with LastEventAt within the last MaxSessionWindowHours AND StartedAt older - /// than MaxSessionWindowHours will have their device blocked by the nightly maintenance function. - /// 0 = disabled. Default: 24. - /// - public int MaxSessionWindowHours { get; set; } = 24; - - /// - /// Duration in hours for maintenance-triggered device blocks (excessive data senders). - /// Default: 12. - /// - public int MaintenanceBlockDurationHours { get; set; } = 12; + // MaxSessionWindowHours / MaintenanceBlockDurationHours were removed 2026-07-22 with the + // time-window auto-block. That detector blocked on session span alone, so every enrollment + // that spanned a night qualified — including 23-event sessions. The event-count settings + // above are now the only automatic block. Stored rows keep the two columns until their + // next write; nothing reads them. /// /// Retention period in days for operational events in the OpsEvents table. @@ -531,9 +519,7 @@ namespace AutopilotMonitor.Shared.Models GlobalAdminRateLimitRequestsPerMinute = 600, PlatformStatsBlobSasUrl = string.Empty, CollectorIdleTimeoutMinutes = 15, - DesktopDetectorNoCandidateTimeoutMinutes = 10, - MaxSessionWindowHours = 24, - MaintenanceBlockDurationHours = 12 + DesktopDetectorNoCandidateTimeoutMinutes = 10 }; } } diff --git a/src/Web/autopilot-monitor-web/app/admin/AdminConfigContext.tsx b/src/Web/autopilot-monitor-web/app/admin/AdminConfigContext.tsx index 9d863e5b..837df554 100644 --- a/src/Web/autopilot-monitor-web/app/admin/AdminConfigContext.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/AdminConfigContext.tsx @@ -34,10 +34,6 @@ interface AdminConfigContextValue { setCollectorIdleTimeoutMinutes: (value: number) => void; desktopDetectorNoCandidateTimeoutMinutes: number; setDesktopDetectorNoCandidateTimeoutMinutes: (value: number) => void; - maxSessionWindowHours: number; - setMaxSessionWindowHours: (value: number) => void; - maintenanceBlockDurationHours: number; - setMaintenanceBlockDurationHours: (value: number) => void; opsEventRetentionDays: number; setOpsEventRetentionDays: (value: number) => void; slaNotificationCooldownHours: number; @@ -138,8 +134,6 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode }) const [agentMigrateTenantOverridesJson, setAgentMigrateTenantOverridesJson] = useState(""); const [collectorIdleTimeoutMinutes, setCollectorIdleTimeoutMinutes] = useState(15); const [desktopDetectorNoCandidateTimeoutMinutes, setDesktopDetectorNoCandidateTimeoutMinutes] = useState(10); - const [maxSessionWindowHours, setMaxSessionWindowHours] = useState(24); - const [maintenanceBlockDurationHours, setMaintenanceBlockDurationHours] = useState(12); const [opsEventRetentionDays, setOpsEventRetentionDays] = useState(90); const [slaNotificationCooldownHours, setSlaNotificationCooldownHours] = useState(24); const [allowAgentDowngrade, setAllowAgentDowngrade] = useState(false); @@ -201,8 +195,6 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode }) setAgentMigrateTenantOverridesJson(data.agentMigrateTenantOverridesJson ?? ""); setCollectorIdleTimeoutMinutes(data.collectorIdleTimeoutMinutes ?? 15); setDesktopDetectorNoCandidateTimeoutMinutes(data.desktopDetectorNoCandidateTimeoutMinutes ?? 10); - setMaxSessionWindowHours(data.maxSessionWindowHours ?? 24); - setMaintenanceBlockDurationHours(data.maintenanceBlockDurationHours ?? 12); setOpsEventRetentionDays(data.opsEventRetentionDays ?? 90); setSlaNotificationCooldownHours(data.slaNotificationCooldownHours ?? 24); setAllowAgentDowngrade(data.allowAgentDowngrade ?? false); @@ -308,8 +300,6 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode }) agentMigrateTenantOverridesJson: agentMigrateTenantOverridesJson.trim(), collectorIdleTimeoutMinutes, desktopDetectorNoCandidateTimeoutMinutes, - maxSessionWindowHours, - maintenanceBlockDurationHours, opsEventRetentionDays, slaNotificationCooldownHours, allowAgentDowngrade, @@ -342,7 +332,7 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode }) } finally { setSavingConfig(false); } - }, [isGlobalAdmin, adminConfig, globalRateLimit, userRateLimit, globalAdminRateLimit, platformStatsBlobSasUrl, agentMigrateApiBaseUrl, agentMigrateTenantOverridesJson, collectorIdleTimeoutMinutes, desktopDetectorNoCandidateTimeoutMinutes, maxSessionWindowHours, maintenanceBlockDurationHours, opsEventRetentionDays, slaNotificationCooldownHours, allowAgentDowngrade, modernDeploymentHarmlessEventIds, enableIndexDualWrite, sessionDeletionKillSwitch, getAccessToken]); + }, [isGlobalAdmin, adminConfig, globalRateLimit, userRateLimit, globalAdminRateLimit, platformStatsBlobSasUrl, agentMigrateApiBaseUrl, agentMigrateTenantOverridesJson, collectorIdleTimeoutMinutes, desktopDetectorNoCandidateTimeoutMinutes, opsEventRetentionDays, slaNotificationCooldownHours, allowAgentDowngrade, modernDeploymentHarmlessEventIds, enableIndexDualWrite, sessionDeletionKillSwitch, getAccessToken]); // Reset admin config const handleResetAdminConfig = useCallback(() => { @@ -355,8 +345,6 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode }) setAgentMigrateTenantOverridesJson(adminConfig.agentMigrateTenantOverridesJson ?? ""); setCollectorIdleTimeoutMinutes(adminConfig.collectorIdleTimeoutMinutes ?? 15); setDesktopDetectorNoCandidateTimeoutMinutes(adminConfig.desktopDetectorNoCandidateTimeoutMinutes ?? 10); - setMaxSessionWindowHours(adminConfig.maxSessionWindowHours ?? 24); - setMaintenanceBlockDurationHours(adminConfig.maintenanceBlockDurationHours ?? 12); setOpsEventRetentionDays(adminConfig.opsEventRetentionDays ?? 90); setSlaNotificationCooldownHours(adminConfig.slaNotificationCooldownHours ?? 24); setAllowAgentDowngrade(adminConfig.allowAgentDowngrade ?? false); @@ -501,8 +489,6 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode }) agentMigrateTenantOverridesJson, setAgentMigrateTenantOverridesJson, collectorIdleTimeoutMinutes, setCollectorIdleTimeoutMinutes, desktopDetectorNoCandidateTimeoutMinutes, setDesktopDetectorNoCandidateTimeoutMinutes, - maxSessionWindowHours, setMaxSessionWindowHours, - maintenanceBlockDurationHours, setMaintenanceBlockDurationHours, opsEventRetentionDays, setOpsEventRetentionDays, slaNotificationCooldownHours, setSlaNotificationCooldownHours, allowAgentDowngrade, setAllowAgentDowngrade, diff --git a/src/Web/autopilot-monitor-web/app/admin/components/DeviceBlockSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/DeviceBlockSection.tsx index c3d35299..392449b5 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/DeviceBlockSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/DeviceBlockSection.tsx @@ -39,13 +39,6 @@ interface BlockedDevice { interface DeviceBlockSectionProps { tenants: TenantConfiguration[]; - maxSessionWindowHours: number; - setMaxSessionWindowHours: (value: number) => void; - maintenanceBlockDurationHours: number; - setMaintenanceBlockDurationHours: (value: number) => void; - savingConfig: boolean; - adminConfigExists: boolean; - onSaveAdminConfig: () => Promise; getAccessToken: () => Promise; setError: (error: string | null) => void; setSuccessMessage: (message: string | null) => void; @@ -53,13 +46,6 @@ interface DeviceBlockSectionProps { export function DeviceBlockSection({ tenants, - maxSessionWindowHours, - setMaxSessionWindowHours, - maintenanceBlockDurationHours, - setMaintenanceBlockDurationHours, - savingConfig, - adminConfigExists, - onSaveAdminConfig, getAccessToken, setError, setSuccessMessage, @@ -320,60 +306,6 @@ export function DeviceBlockSection({
- {/* Maintenance Auto-Block Settings */} -
-
- - - -

Maintenance Auto-Block Settings

-
-

- The nightly maintenance function automatically blocks devices that are still actively sending data beyond the configured window. Sessions with a LastEventAt timestamp within the window and a StartedAt older than the window are flagged – regardless of session status. -

-
-
- - setMaxSessionWindowHours(parseInt(e.target.value) || 0)} - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500" - /> -

0 = disabled

-
-
- - setMaintenanceBlockDurationHours(parseInt(e.target.value) || 12)} - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500" - /> -

Applied by the maintenance function

-
-
- -
- {/* Block / Kill a device form */}

Block or Kill a Device

diff --git a/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx index 0a0f4863..80a10d13 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx @@ -54,7 +54,7 @@ const OPS_EVENT_TYPES: Record = { ], Security: [ "DeviceBlocked", - "ExcessiveDataBlocked", + // ExcessiveDataBlocked removed 2026-07-22 with the time-window auto-block that raised it. // Critical-tier auto-action: emitted when maintenance auto-blocks/kills a device // after its session crosses `excessiveEventAutoActionThreshold`. Dual-register // per memory feedback_ops_event_types_dual_register so operators can wire a diff --git a/src/Web/autopilot-monitor-web/app/admin/security/sections/SectionDeviceBlock.tsx b/src/Web/autopilot-monitor-web/app/admin/security/sections/SectionDeviceBlock.tsx index 808feb0f..b931cdbc 100644 --- a/src/Web/autopilot-monitor-web/app/admin/security/sections/SectionDeviceBlock.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/security/sections/SectionDeviceBlock.tsx @@ -5,26 +5,13 @@ import { DeviceBlockSection } from "../../components/DeviceBlockSection"; import { AdminNotifications } from "../../AdminNotifications"; export function SectionDeviceBlock() { - const { - tenants, - maxSessionWindowHours, setMaxSessionWindowHours, - maintenanceBlockDurationHours, setMaintenanceBlockDurationHours, - savingConfig, adminConfig, - handleSaveAdminConfig, getAccessToken, setError, setSuccessMessage, - } = useAdminConfig(); + const { tenants, getAccessToken, setError, setSuccessMessage } = useAdminConfig(); return ( <> { }); }); -describe("ExcessiveDataBlocked payload reaches the detail modal's actions", () => { +describe("auto-block payload reaches the detail modal's actions", () => { it("carries a sessionId the modal can deep-link on", () => { - // Mirrors OpsEventService.RecordExcessiveDataBlockedAsync. This event used to be - // aggregated per tenant ({devicesBlocked, windowHours}), which left the modal with - // no device to act on — exactly the alert where an operator wants to act. + // Mirrors OpsEventService.RecordExcessiveSessionEventsAutoActionedAsync — the only + // automatic block left after the time-window detector was removed. const details = JSON.stringify({ sessionId: "806f61c3-1978-4e5c-8fd7-a571cb0fe6bc", serialNumber: "PF5YANFB", - windowHours: 12, - durationHours: 8, + eventCount: 2742, + threshold: 2500, + action: "Block", + durationHours: 24, }); expect(extractSessionId(details)).toBe("806f61c3-1978-4e5c-8fd7-a571cb0fe6bc"); }); - it("would have rendered no actions for the old aggregate payload", () => { + it("renders no actions for a payload without a session", () => { expect(extractSessionId(JSON.stringify({ devicesBlocked: 1, windowHours: 12 }))).toBeNull(); }); }); diff --git a/src/Web/autopilot-monitor-web/types/adminConfig.ts b/src/Web/autopilot-monitor-web/types/adminConfig.ts index ce5f5101..011eaf25 100644 --- a/src/Web/autopilot-monitor-web/types/adminConfig.ts +++ b/src/Web/autopilot-monitor-web/types/adminConfig.ts @@ -35,8 +35,6 @@ export interface AdminConfiguration { excessiveEventAutoActionThreshold?: number; /** Block duration in hours when the auto-action fires. */ excessiveEventAutoActionDurationHours?: number; - maxSessionWindowHours?: number; - maintenanceBlockDurationHours?: number; opsEventRetentionDays?: number; slaNotificationCooldownHours?: number; diagnosticsGlobalLogPathsJson?: string;