From 2d5f87140f054783d9aabbb8ae61f7cae86c1767 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Sat, 18 Jul 2026 16:38:15 +0200 Subject: [PATCH] chore: retarget SignalR quota watcher to Standard S1 limits + CI build hygiene - SignalR quota watcher/health card: defaults 20/20k (Free tier) -> 1000/1M (Standard S1 x1 unit); settings renamed SignalRFreeTier* -> SignalRConnectionLimit/ SignalRDailyMessageLimit (no Azure overrides were set); all operator-facing texts tier-neutral (connections 429 hard, message overage is billed not throttled) - web: drop obsolete edge runtime from opengraph/twitter/apple-icon image routes (next/og runs on Node since 14; images now statically generated at build) - web: update caniuse-lite browser db (silences 6-months-old Browserslist warning) Co-Authored-By: Claude Fable 5 --- .../MaintenanceServiceSignalRQuotaTests.cs | 2 +- .../Admin/SignalRQuotaTimerFunction.cs | 9 +++--- .../Services/HealthCheckService.cs | 12 ++++---- .../MaintenanceService.SignalRQuota.cs | 30 ++++++++++--------- .../Services/OpsEventService.cs | 8 ++--- .../local.settings.json.template | 6 ++-- .../autopilot-monitor-web/app/apple-icon.tsx | 2 -- .../app/opengraph-image.tsx | 2 -- .../app/twitter-image.tsx | 2 -- .../autopilot-monitor-web/package-lock.json | 17 ++++++----- 10 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/MaintenanceServiceSignalRQuotaTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/MaintenanceServiceSignalRQuotaTests.cs index d9bb97ec..186e6c89 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/MaintenanceServiceSignalRQuotaTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/MaintenanceServiceSignalRQuotaTests.cs @@ -3,7 +3,7 @@ using AutopilotMonitor.Functions.Services; namespace AutopilotMonitor.Functions.Tests; /// -/// Tier-boundary + percent-math tests for the SignalR free-tier quota watcher. +/// Tier-boundary + percent-math tests for the SignalR quota watcher. /// Mirrors the cert-expiry tests' approach: pure-function coverage of the /// boundary risks (off-by-one, floor vs ceil, zero-division). Full-service /// smoke tests would require constructing 16 dependencies; the percent + diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/SignalRQuotaTimerFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/SignalRQuotaTimerFunction.cs index ee880337..a7278438 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/SignalRQuotaTimerFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/SignalRQuotaTimerFunction.cs @@ -7,10 +7,11 @@ using Microsoft.Extensions.Logging; namespace AutopilotMonitor.Functions.Functions.Admin { /// - /// Timer-triggered watcher for the Azure SignalR Free-tier limits (20 - /// concurrent connections / 20,000 messages per UTC day). Runs every full - /// hour and emits OpsEvents when the configured thresholds are crossed so - /// operators get a Telegram ping before clients start getting 429'd. + /// Timer-triggered watcher for the Azure SignalR plan limits (Standard S1 + /// x1 unit: 1,000 concurrent connections / 1M messages per UTC day). Runs + /// every full hour and emits OpsEvents when the configured thresholds are + /// crossed so operators get a Telegram ping before clients start getting + /// 429'd or message overage is billed. /// /// Separate from the 2h MaintenanceFunction: the SignalR limits can be /// breached by a brief connection spike, so the cadence is tighter. Cost diff --git a/src/Backend/AutopilotMonitor.Functions/Services/HealthCheckService.cs b/src/Backend/AutopilotMonitor.Functions/Services/HealthCheckService.cs index 67ad5e3e..5633fe3e 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/HealthCheckService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/HealthCheckService.cs @@ -290,7 +290,7 @@ public class HealthCheckService var check = new HealthCheck { Name = "SignalR Quota", - Description = "Free-tier connection + daily message usage" + Description = "Connection + daily message usage vs. plan limits" }; var resourceId = _configuration["SignalRResourceId"]; @@ -302,9 +302,9 @@ public class HealthCheckService } var connectionLimit = ParsePositiveInt( - "SignalRFreeTierConnectionLimit", MaintenanceService.DefaultSignalRConnectionLimit); + "SignalRConnectionLimit", MaintenanceService.DefaultSignalRConnectionLimit); var messageLimit = ParsePositiveLong( - "SignalRFreeTierDailyMessageLimit", MaintenanceService.DefaultSignalRDailyMessageLimit); + "SignalRDailyMessageLimit", MaintenanceService.DefaultSignalRDailyMessageLimit); var nowUtc = DateTimeOffset.UtcNow; var startOfDayUtc = new DateTimeOffset(nowUtc.UtcDateTime.Date, TimeSpan.Zero); @@ -372,9 +372,9 @@ public class HealthCheckService check.Message = worstTier switch { - MaintenanceService.SignalRQuotaTier.Critical => "Free-tier limit nearly saturated — scale to Standard before clients get 429'd", - MaintenanceService.SignalRQuotaTier.Warning => "Approaching free-tier limit — consider scaling to Standard", - _ => "Within free-tier limits" + MaintenanceService.SignalRQuotaTier.Critical => "Plan limit nearly saturated — add SignalR units before clients get 429'd", + MaintenanceService.SignalRQuotaTier.Warning => "Approaching plan limit — consider adding SignalR units", + _ => "Within plan limits" }; check.Details = details; return check; diff --git a/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.SignalRQuota.cs b/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.SignalRQuota.cs index 0e48f4e0..9075cb23 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.SignalRQuota.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.SignalRQuota.cs @@ -9,11 +9,13 @@ using Microsoft.Extensions.Logging; namespace AutopilotMonitor.Functions.Services { /// - /// SignalR Free-tier quota watcher. Reads ConnectionCount (max over the last - /// hour) and MessageCount (total since UTC midnight) from Azure Monitor and - /// emits tiered OpsEvents when either approaches the documented Free-tier - /// caps - 20 concurrent connections and 20,000 messages per day. The - /// operator can then scale to Standard before clients get 429'd. + /// SignalR quota watcher. Reads ConnectionCount (max over the last hour) + /// and MessageCount (total since UTC midnight) from Azure Monitor and + /// emits tiered OpsEvents when either approaches the plan caps - on + /// Standard S1 that is 1,000 concurrent connections per unit (hard limit, + /// excess connections are rejected) and 1M messages per unit per day + /// (included quota; overage is billed, not throttled). The operator can + /// then add units before clients get 429'd or overage costs accumulate. /// /// Globally scoped (TenantId = null) - SignalR is one shared resource for /// the whole Function App, not a per-tenant concern. @@ -25,12 +27,12 @@ namespace AutopilotMonitor.Functions.Services internal const int SignalRQuotaCriticalPercent = 95; internal const int SignalRQuotaWarningPercent = 80; - // Default Free-tier caps as documented by Microsoft. Overridable via - // app settings so an operator who has scaled to a paid tier can re-target - // the watcher without a code change (or disable it by setting the cap - // very high). - internal const int DefaultSignalRConnectionLimit = 20; - internal const long DefaultSignalRDailyMessageLimit = 20_000; + // Default caps for Standard S1 with one unit, as documented by Microsoft. + // Overridable via app settings so an operator who adds units or changes + // tier can re-target the watcher without a code change (or disable it by + // setting the cap very high). + internal const int DefaultSignalRConnectionLimit = 1_000; + internal const long DefaultSignalRDailyMessageLimit = 1_000_000; // Dedup prefix - all SignalR EventTypes start with this, so the seen-index // can be built with a single Security-category read. @@ -59,7 +61,7 @@ namespace AutopilotMonitor.Functions.Services /// /// Reads SignalR ConnectionCount + MessageCount metrics from Azure Monitor - /// and emits Warning/Error OpsEvents when the Free-tier limits are + /// and emits Warning/Error OpsEvents when the plan limits are /// approached. Dedup: one event per EventType per UTC day. Fail-soft - /// missing config or Azure Monitor errors yield a single log line, never /// a thrown exception. @@ -75,9 +77,9 @@ namespace AutopilotMonitor.Functions.Services } var connectionLimit = ParseIntSetting( - "SignalRFreeTierConnectionLimit", DefaultSignalRConnectionLimit); + "SignalRConnectionLimit", DefaultSignalRConnectionLimit); var messageLimit = ParseLongSetting( - "SignalRFreeTierDailyMessageLimit", DefaultSignalRDailyMessageLimit); + "SignalRDailyMessageLimit", DefaultSignalRDailyMessageLimit); var seenToday = await BuildSignalRQuotaSeenIndexAsync(); diff --git a/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs b/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs index e16e7aa4..2fa77b9b 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs @@ -310,25 +310,25 @@ namespace AutopilotMonitor.Functions.Services public Task RecordSignalRConnectionsHighAsync(int observed, int limit, int percent, string resourceId) => WriteAsync(OpsEventCategory.Security, "SignalRConnectionsHigh", OpsEventSeverity.Warning, - $"SignalR concurrent connections at {percent}% of free-tier limit ({observed}/{limit}) - watch for 429s; consider scaling to Standard before saturation", + $"SignalR concurrent connections at {percent}% of plan limit ({observed}/{limit}) - watch for 429s; consider adding units before saturation", null, "System.Monitoring", new { metric = "ConnectionCount", aggregation = "Maximum", windowMinutes = 60, observed, limit, percent, resourceId }); public Task RecordSignalRConnectionsCriticalAsync(int observed, int limit, int percent, string resourceId) => WriteAsync(OpsEventCategory.Security, "SignalRConnectionsCritical", OpsEventSeverity.Error, - $"CRITICAL: SignalR concurrent connections at {percent}% of free-tier limit ({observed}/{limit}) - new client connections will be 429'd at 100%; scale to Standard now", + $"CRITICAL: SignalR concurrent connections at {percent}% of plan limit ({observed}/{limit}) - new client connections will be 429'd at 100%; add units now", null, "System.Monitoring", new { metric = "ConnectionCount", aggregation = "Maximum", windowMinutes = 60, observed, limit, percent, resourceId }); public Task RecordSignalRMessagesHighAsync(long observed, long limit, int percent, string resourceId) => WriteAsync(OpsEventCategory.Security, "SignalRMessagesHigh", OpsEventSeverity.Warning, - $"SignalR daily message count at {percent}% of free-tier limit ({observed}/{limit}) - quota resets at 00:00 UTC; consider scaling if pattern persists", + $"SignalR daily message count at {percent}% of included plan quota ({observed}/{limit}) - resets at 00:00 UTC; overage is billed per extra million messages", null, "System.Monitoring", new { metric = "MessageCount", aggregation = "Total", windowDay = "UTC", observed, limit, percent, resourceId }); public Task RecordSignalRMessagesCriticalAsync(long observed, long limit, int percent, string resourceId) => WriteAsync(OpsEventCategory.Security, "SignalRMessagesCritical", OpsEventSeverity.Error, - $"CRITICAL: SignalR daily message count at {percent}% of free-tier limit ({observed}/{limit}) - hub will throttle at 100% until 00:00 UTC reset; scale to Standard now", + $"CRITICAL: SignalR daily message count at {percent}% of included plan quota ({observed}/{limit}) - overage beyond 100% is billed per extra million messages; review traffic or add units", null, "System.Monitoring", new { metric = "MessageCount", aggregation = "Total", windowDay = "UTC", observed, limit, percent, resourceId }); diff --git a/src/Backend/AutopilotMonitor.Functions/local.settings.json.template b/src/Backend/AutopilotMonitor.Functions/local.settings.json.template index e9d2c895..e3505da7 100644 --- a/src/Backend/AutopilotMonitor.Functions/local.settings.json.template +++ b/src/Backend/AutopilotMonitor.Functions/local.settings.json.template @@ -6,9 +6,9 @@ "AzureTableStorageConnectionString": "UseDevelopmentStorage=true", "AzureBlobStorageConnectionString": "UseDevelopmentStorage=true", - "// SignalR free-tier quota watcher": "Optional. Set to the full Azure resource id of the SignalR Service to enable hourly OpsEvents when the 20-connections / 20k-messages-per-day free-tier limits are approached. Function App's managed identity needs 'Monitoring Reader' on this resource. Leave unset to skip the watcher.", + "// SignalR quota watcher": "Optional. Set to the full Azure resource id of the SignalR Service to enable hourly OpsEvents when the plan limits are approached (defaults: Standard S1 x1 unit = 1000 connections / 1M messages per day; override the limits when adding units or changing tier). Function App's managed identity needs 'Monitoring Reader' on this resource. Leave unset to skip the watcher.", "SignalRResourceId": "", - "SignalRFreeTierConnectionLimit": "20", - "SignalRFreeTierDailyMessageLimit": "20000" + "SignalRConnectionLimit": "1000", + "SignalRDailyMessageLimit": "1000000" } } diff --git a/src/Web/autopilot-monitor-web/app/apple-icon.tsx b/src/Web/autopilot-monitor-web/app/apple-icon.tsx index 4bc16fd8..27aeac5b 100644 --- a/src/Web/autopilot-monitor-web/app/apple-icon.tsx +++ b/src/Web/autopilot-monitor-web/app/apple-icon.tsx @@ -1,7 +1,5 @@ import { ImageResponse } from "next/og"; -export const runtime = "edge"; - export const size = { width: 180, height: 180 }; export const contentType = "image/png"; diff --git a/src/Web/autopilot-monitor-web/app/opengraph-image.tsx b/src/Web/autopilot-monitor-web/app/opengraph-image.tsx index a74a0feb..060fb562 100644 --- a/src/Web/autopilot-monitor-web/app/opengraph-image.tsx +++ b/src/Web/autopilot-monitor-web/app/opengraph-image.tsx @@ -1,7 +1,5 @@ import { ImageResponse } from "next/og"; -export const runtime = "edge"; - export const alt = "AutopilotMonitor – Real-Time Windows Enrollment Monitoring"; export const size = { width: 1200, height: 630 }; export const contentType = "image/png"; diff --git a/src/Web/autopilot-monitor-web/app/twitter-image.tsx b/src/Web/autopilot-monitor-web/app/twitter-image.tsx index ecffdd06..76d56364 100644 --- a/src/Web/autopilot-monitor-web/app/twitter-image.tsx +++ b/src/Web/autopilot-monitor-web/app/twitter-image.tsx @@ -1,3 +1 @@ export { default, alt, size, contentType } from "./opengraph-image"; - -export const runtime = "edge"; diff --git a/src/Web/autopilot-monitor-web/package-lock.json b/src/Web/autopilot-monitor-web/package-lock.json index 3dc75897..2194eab9 100644 --- a/src/Web/autopilot-monitor-web/package-lock.json +++ b/src/Web/autopilot-monitor-web/package-lock.json @@ -2851,13 +2851,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/binary-extensions": { @@ -3002,9 +3005,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "funding": [ { "type": "opencollective",