diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/HealthCheckServiceAgentBinariesTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/HealthCheckServiceAgentBinariesTests.cs
new file mode 100644
index 00000000..c25da4ba
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/HealthCheckServiceAgentBinariesTests.cs
@@ -0,0 +1,107 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+using AutopilotMonitor.Functions.Services;
+using AutopilotMonitor.Functions.Tests.GraphResolution;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace AutopilotMonitor.Functions.Tests;
+
+///
+/// Service-level tests for the Agent Binaries HealthCheck card. The check probes the
+/// canonical download alias (download.autopilotmonitor.com) AND the legacy blob keepalive
+/// account: alias failure is unhealthy (new installs break), legacy failure is only a
+/// warning (existing customers break until they migrate to the alias).
+///
+public class HealthCheckServiceAgentBinariesTests
+{
+ private const string AliasHost = "download.autopilotmonitor.com";
+ private const string LegacyHost = "autopilotmonitor.blob.core.windows.net";
+
+ [Fact]
+ public async Task Check_AllEndpointsAvailable_ReportsHealthyWithoutUrlDetails()
+ {
+ var handler = new StubHttpMessageHandler()
+ .When(AliasHost, HttpStatusCode.OK, "")
+ .When(LegacyHost, HttpStatusCode.OK, "");
+
+ var check = await BuildService(handler).CheckAgentBinariesAsync(includeEndpointUrls: false);
+
+ Assert.Equal("Agent Binaries", check.Name);
+ Assert.Equal("healthy", check.Status);
+ Assert.Contains("download alias", check.Message);
+ Assert.Null(check.Details);
+ // Both hosts must actually have been probed (ZIP + PS1 each).
+ Assert.Equal(2, handler.Requests.FindAll(u => u.Contains(AliasHost)).Count);
+ Assert.Equal(2, handler.Requests.FindAll(u => u.Contains(LegacyHost)).Count);
+ }
+
+ [Fact]
+ public async Task Check_WithEndpointUrls_SurfacesAliasAndLegacyUrlsInDetails()
+ {
+ var handler = new StubHttpMessageHandler()
+ .When(AliasHost, HttpStatusCode.OK, "")
+ .When(LegacyHost, HttpStatusCode.OK, "");
+
+ var check = await BuildService(handler).CheckAgentBinariesAsync(includeEndpointUrls: true);
+
+ Assert.NotNull(check.Details);
+ Assert.Equal("https://download.autopilotmonitor.com/agent/AutopilotMonitor-Agent.zip", check.Details!["Agent ZIP"]);
+ Assert.Equal("https://download.autopilotmonitor.com/agent/Install-AutopilotMonitor.ps1", check.Details["Bootstrap script"]);
+ Assert.Equal("https://autopilotmonitor.blob.core.windows.net/agent", check.Details["Legacy blob (keepalive)"]);
+ }
+
+ [Fact]
+ public async Task Check_LegacyBlobMissing_ReportsWarningNamingTheLegacyEndpoint()
+ {
+ var handler = new StubHttpMessageHandler()
+ .When(AliasHost, HttpStatusCode.OK, "")
+ .When(LegacyHost, HttpStatusCode.NotFound, "");
+
+ var check = await BuildService(handler).CheckAgentBinariesAsync(includeEndpointUrls: false);
+
+ Assert.Equal("warning", check.Status);
+ Assert.Contains("legacy", check.Message, System.StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("404", check.Message);
+ }
+
+ [Fact]
+ public async Task Check_AliasMissing_ReportsUnhealthy()
+ {
+ var handler = new StubHttpMessageHandler()
+ .When(AliasHost, HttpStatusCode.NotFound, "")
+ .When(LegacyHost, HttpStatusCode.OK, "");
+
+ var check = await BuildService(handler).CheckAgentBinariesAsync(includeEndpointUrls: false);
+
+ Assert.Equal("unhealthy", check.Status);
+ Assert.Contains("download alias", check.Message);
+ }
+
+ [Fact]
+ public async Task Check_EndpointsUnreachable_ReportsUnhealthy()
+ {
+ // Un-scripted URLs make the stub throw — models a connection failure.
+ var check = await BuildService(new StubHttpMessageHandler()).CheckAgentBinariesAsync(includeEndpointUrls: false);
+
+ Assert.Equal("unhealthy", check.Status);
+ Assert.Contains("unreachable", check.Message);
+ }
+
+ private static HealthCheckService BuildService(StubHttpMessageHandler handler)
+ {
+ var config = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary())
+ .Build();
+
+ return new HealthCheckService(
+ NullLogger.Instance,
+ adminConfigService: null!,
+ httpClientFactory: new StubHttpClientFactory(handler),
+ metricsReader: null!,
+ poisonQueueProbe: null!,
+ configuration: config);
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/HealthCheckServiceMcpServerTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/HealthCheckServiceMcpServerTests.cs
index 38467893..41577c7d 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/HealthCheckServiceMcpServerTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/HealthCheckServiceMcpServerTests.cs
@@ -53,6 +53,29 @@ public class HealthCheckServiceMcpServerTests
Assert.DoesNotContain("http", check.Message, System.StringComparison.OrdinalIgnoreCase);
}
+ [Fact]
+ public async Task Check_WithEndpointUrl_SurfacesServerUrlAlongsideVersion()
+ {
+ var svc = BuildService(HttpStatusCode.OK, "{\"status\":\"healthy\",\"version\":\"1.4.0\"}");
+
+ var check = await svc.CheckMcpServerAsync(includeEndpointUrl: true);
+
+ // The Version detail must merge into — not replace — the Server URL detail.
+ Assert.Equal("https://mcp.example.test", check.Details!["Server URL"]);
+ Assert.Equal("1.4.0", check.Details["Version"]);
+ }
+
+ [Fact]
+ public async Task Check_WithEndpointUrl_SurfacesServerUrlEvenWhenUnhealthy()
+ {
+ var svc = BuildService(HttpStatusCode.ServiceUnavailable, "");
+
+ var check = await svc.CheckMcpServerAsync(includeEndpointUrl: true);
+
+ Assert.Equal("unhealthy", check.Status);
+ Assert.Equal("https://mcp.example.test", check.Details!["Server URL"]);
+ }
+
[Fact]
public async Task Check_TimeoutOrConnectionFailure_ReportsWarningForColdStart()
{
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs
index 9a086d71..093e1142 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs
@@ -102,17 +102,19 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
- // Perform comprehensive health checks
- var healthCheckResult = await _healthCheckService.PerformAllChecksAsync();
-
- // SignalR Quota check exposes the subscription/resource-group path of the
- // backing SignalR resource, and Poison Queues exposes internal async-worker
- // queue topology — restrict both to Global Admins. Non-GA callers
- // (Tenant Admins, Operators) get the rest of the report unchanged.
+ // SignalR Quota exposes the subscription/resource-group path of the backing
+ // SignalR resource, Poison Queues exposes internal async-worker queue topology,
+ // and endpoint URLs in the check details expose infrastructure hostnames —
+ // restrict all of that to Global Admins. Non-GA callers (Tenant Admins,
+ // Operators) get the rest of the report unchanged.
// The route is registered as AuthenticatedUser, so PolicyEnforcementMiddleware
// does NOT compute IsGlobalAdmin; resolve it directly via GlobalAdminService.
var requestCtx = context.GetRequestContext();
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(requestCtx.UserPrincipalName);
+
+ // Perform comprehensive health checks (endpoint URLs only for Global Admins)
+ var healthCheckResult = await _healthCheckService.PerformAllChecksAsync(includeEndpointUrls: isGlobalAdmin);
+
var visibleChecks = isGlobalAdmin
? healthCheckResult.Checks
: healthCheckResult.Checks
@@ -146,10 +148,14 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
///
[Function("McpHealthCheck")]
public async Task GetMcpHealthCheck(
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health/mcp")] HttpRequestData req)
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health/mcp")] HttpRequestData req,
+ FunctionContext context)
{
// Authentication enforced by PolicyEnforcementMiddleware (AuthenticatedUser).
- var check = await _healthCheckService.CheckMcpServerAsync();
+ // The MCP server URL is infrastructure topology — include it only for Global Admins.
+ var requestCtx = context.GetRequestContext();
+ var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(requestCtx.UserPrincipalName);
+ var check = await _healthCheckService.CheckMcpServerAsync(includeEndpointUrl: isGlobalAdmin);
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/HealthCheckService.cs b/src/Backend/AutopilotMonitor.Functions/Services/HealthCheckService.cs
index 5633fe3e..8aa9f6dc 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/HealthCheckService.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/HealthCheckService.cs
@@ -34,9 +34,12 @@ public class HealthCheckService
}
///
- /// Performs all health checks and returns the results
+ /// Performs all health checks and returns the results.
+ /// adds the probed/backing endpoint URLs to the
+ /// per-check details — infrastructure topology, so callers must only enable it for
+ /// Global Admins.
///
- public async Task PerformAllChecksAsync()
+ public async Task PerformAllChecksAsync(bool includeEndpointUrls = false)
{
var result = new HealthCheckResult
{
@@ -50,9 +53,9 @@ public class HealthCheckService
// one slow check. It is exposed via its own endpoint (GET health/mcp) that the
// frontend fetches independently and folds into the card grid incrementally.
var checks = await Task.WhenAll(
- CheckStorageBackendAsync(),
- CheckProcessingBackendAsync(),
- CheckAgentBinariesAsync(),
+ CheckStorageBackendAsync(includeEndpointUrls),
+ CheckProcessingBackendAsync(includeEndpointUrls),
+ CheckAgentBinariesAsync(includeEndpointUrls),
CheckSignalRQuotaAsync(),
CheckPoisonQueuesAsync()
);
@@ -73,7 +76,7 @@ public class HealthCheckService
///
/// Checks Azure Table Storage connectivity by querying the admin configuration table
///
- private async Task CheckStorageBackendAsync()
+ private async Task CheckStorageBackendAsync(bool includeEndpointUrls)
{
var check = new HealthCheck
{
@@ -97,29 +100,62 @@ public class HealthCheckService
_logger.LogError(ex, "Storage backend health check failed");
}
+ if (includeEndpointUrls)
+ {
+ // Same resolution order as TableStorageService: account name (Managed Identity)
+ // is the production path, connection string is the local-dev fallback.
+ var accountName = _configuration["AzureStorageAccountName"];
+ check.Details = new Dictionary
+ {
+ ["Table endpoint"] = string.IsNullOrWhiteSpace(accountName)
+ ? "connection string (local dev)"
+ : $"https://{accountName}.table.core.windows.net"
+ };
+ }
+
return check;
}
///
/// Checks that the Azure Functions host is running
///
- private static Task CheckProcessingBackendAsync()
+ private static Task CheckProcessingBackendAsync(bool includeEndpointUrls)
{
var uptime = DateTime.UtcNow - System.Diagnostics.Process.GetCurrentProcess().StartTime.ToUniversalTime();
- return Task.FromResult(new HealthCheck
+ var check = new HealthCheck
{
Name = "Processing Backend",
Description = "Application host process",
Status = "healthy",
Message = $"Host running (uptime: {(int)uptime.TotalMinutes}m {uptime.Seconds}s)"
- });
+ };
+
+ if (includeEndpointUrls)
+ {
+ // WEBSITE_HOSTNAME is stamped by App Service and reflects the host actually
+ // serving this process — no hardcoded URL to go stale after a migration.
+ var hostname = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME");
+ if (!string.IsNullOrWhiteSpace(hostname))
+ {
+ check.Details = new Dictionary
+ {
+ ["API endpoint"] = $"https://{hostname}"
+ };
+ }
+ }
+
+ return Task.FromResult(check);
}
///
- /// Checks that the agent binaries (ZIP) and bootstrap script (PS1) are reachable in blob storage
+ /// Checks that the agent binaries (ZIP) and bootstrap script (PS1) are reachable on the
+ /// canonical download alias (Front Door → current blob origin) AND on the legacy blob
+ /// keepalive account that already-deployed customer bootstrap scripts still use.
+ /// Alias failure is unhealthy (new installs break); legacy failure is a warning
+ /// (existing customers break until they migrate to the alias).
///
- private async Task CheckAgentBinariesAsync()
+ internal async Task CheckAgentBinariesAsync(bool includeEndpointUrls)
{
var check = new HealthCheck
{
@@ -127,8 +163,20 @@ public class HealthCheckService
Description = "Agent download package availability"
};
- var zipUrl = $"{Constants.AgentBlobBaseUrl}/{Constants.AgentZipFileName}";
- var ps1Url = $"{Constants.AgentBlobBaseUrl}/Install-AutopilotMonitor.ps1";
+ var aliasZipUrl = $"{Constants.AgentDownloadBaseUrl}/{Constants.AgentZipFileName}";
+ var aliasPs1Url = $"{Constants.AgentDownloadBaseUrl}/{Constants.BootstrapScriptName}";
+ var legacyZipUrl = $"{Constants.AgentBlobBaseUrl}/{Constants.AgentZipFileName}";
+ var legacyPs1Url = $"{Constants.AgentBlobBaseUrl}/{Constants.BootstrapScriptName}";
+
+ if (includeEndpointUrls)
+ {
+ check.Details = new Dictionary
+ {
+ ["Agent ZIP"] = aliasZipUrl,
+ ["Bootstrap script"] = aliasPs1Url,
+ ["Legacy blob (keepalive)"] = Constants.AgentBlobBaseUrl
+ };
+ }
try
{
@@ -138,40 +186,48 @@ public class HealthCheckService
var sw = System.Diagnostics.Stopwatch.StartNew();
// HEAD requests to check availability without downloading
- var zipRequest = new HttpRequestMessage(HttpMethod.Head, zipUrl);
- var ps1Request = new HttpRequestMessage(HttpMethod.Head, ps1Url);
-
var results = await Task.WhenAll(
- client.SendAsync(zipRequest),
- client.SendAsync(ps1Request)
+ client.SendAsync(new HttpRequestMessage(HttpMethod.Head, aliasZipUrl)),
+ client.SendAsync(new HttpRequestMessage(HttpMethod.Head, aliasPs1Url)),
+ client.SendAsync(new HttpRequestMessage(HttpMethod.Head, legacyZipUrl)),
+ client.SendAsync(new HttpRequestMessage(HttpMethod.Head, legacyPs1Url))
);
sw.Stop();
- var zipResponse = results[0];
- var ps1Response = results[1];
+ var aliasZipOk = results[0].IsSuccessStatusCode;
+ var aliasPs1Ok = results[1].IsSuccessStatusCode;
+ var legacyZipOk = results[2].IsSuccessStatusCode;
+ var legacyPs1Ok = results[3].IsSuccessStatusCode;
- var zipOk = zipResponse.IsSuccessStatusCode;
- var ps1Ok = ps1Response.IsSuccessStatusCode;
+ var issues = new List();
+ if (!aliasZipOk) issues.Add($"Agent ZIP (download alias): {(int)results[0].StatusCode}");
+ if (!aliasPs1Ok) issues.Add($"Bootstrap script (download alias): {(int)results[1].StatusCode}");
+ if (!legacyZipOk) issues.Add($"Agent ZIP (legacy blob): {(int)results[2].StatusCode}");
+ if (!legacyPs1Ok) issues.Add($"Bootstrap script (legacy blob): {(int)results[3].StatusCode}");
- if (zipOk && ps1Ok)
+ if (issues.Count == 0)
{
check.Status = "healthy";
- check.Message = $"Agent package and bootstrap script available ({sw.ElapsedMilliseconds}ms)";
+ check.Message = $"Agent package and bootstrap script available via download alias + legacy blob ({sw.ElapsedMilliseconds}ms)";
+ }
+ else if (aliasZipOk && aliasPs1Ok)
+ {
+ // Alias intact, legacy keepalive degraded — existing customer bootstrap
+ // scripts still download from the legacy account until they migrate.
+ check.Status = "warning";
+ check.Message = $"Download alias OK, but legacy keepalive degraded — {string.Join(", ", issues)}";
}
else
{
check.Status = "unhealthy";
- var issues = new List();
- if (!zipOk) issues.Add($"Agent ZIP: {(int)zipResponse.StatusCode}");
- if (!ps1Ok) issues.Add($"Bootstrap script: {(int)ps1Response.StatusCode}");
check.Message = $"Missing: {string.Join(", ", issues)}";
}
}
catch (Exception ex)
{
check.Status = "unhealthy";
- check.Message = $"Blob storage unreachable: {ex.Message}";
+ check.Message = $"Agent download endpoints unreachable: {ex.Message}";
_logger.LogError(ex, "Agent binaries health check failed");
}
@@ -182,7 +238,8 @@ public class HealthCheckService
/// Probes the MCP server's public /health endpoint (which sits in front of the
/// /mcp access guard, so no auth is required) and surfaces reachability + the
/// deployed MCP build version. Visible to all authenticated users, so the server URL is
- /// deliberately NOT included in the message or details.
+ /// only included in the details when is set —
+ /// callers must only enable it for Global Admins.
///
/// The Container App runs with minReplicas=0 and scales to zero on idle. Azure Container
/// Apps holds an inbound request while it activates a cold replica, so this probe doubles
@@ -193,7 +250,7 @@ public class HealthCheckService
/// the blocking all-checks batch, so the dashboard never waits on the wake.
///
///
- internal async Task CheckMcpServerAsync()
+ internal async Task CheckMcpServerAsync(bool includeEndpointUrl = false)
{
var check = new HealthCheck
{
@@ -203,6 +260,11 @@ public class HealthCheckService
var baseUrl = (_configuration["McpServerUrl"] ?? Constants.McpServerBaseUrl).TrimEnd('/');
var healthUrl = $"{baseUrl}/health";
+
+ if (includeEndpointUrl)
+ {
+ check.Details = new Dictionary { ["Server URL"] = baseUrl };
+ }
var timeoutSeconds = ParsePositiveInt("McpServerHealthTimeoutSeconds", 30);
// A cold start that takes longer than this is treated as a successful wake rather
@@ -233,7 +295,8 @@ public class HealthCheckService
: $"MCP server reachable ({sw.ElapsedMilliseconds}ms)";
if (!string.IsNullOrWhiteSpace(version))
{
- check.Details = new Dictionary { ["Version"] = version };
+ check.Details ??= new Dictionary();
+ check.Details["Version"] = version;
}
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException or HttpRequestException)
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.Aggregation.cs b/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.Aggregation.cs
index b3a92113..4444bc67 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.Aggregation.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.Aggregation.cs
@@ -554,52 +554,53 @@ namespace AutopilotMonitor.Functions.Services
}
///
- /// Verifies that agent binaries and bootstrap script are available on blob storage.
+ /// Verifies that agent binaries and bootstrap script are available on the canonical
+ /// download alias (Front Door → current blob origin) AND on the legacy blob keepalive
+ /// account that already-deployed customer bootstrap scripts still use.
/// Records an OpsEvent for each missing item so Global Admins see it in the dashboard.
///
private async Task CheckAgentBlobStorageAsync()
{
- _logger.LogInformation("Checking agent blob storage availability...");
+ _logger.LogInformation("Checking agent download endpoint availability...");
- var zipUrl = $"{AutopilotMonitor.Shared.Constants.AgentBlobBaseUrl}/{AutopilotMonitor.Shared.Constants.AgentZipFileName}";
- var ps1Url = $"{AutopilotMonitor.Shared.Constants.AgentBlobBaseUrl}/Install-AutopilotMonitor.ps1";
+ var probes = new (string Label, string Url)[]
+ {
+ ($"Agent ZIP ({AutopilotMonitor.Shared.Constants.AgentZipFileName}, download alias)",
+ $"{AutopilotMonitor.Shared.Constants.AgentDownloadBaseUrl}/{AutopilotMonitor.Shared.Constants.AgentZipFileName}"),
+ ($"Bootstrap script ({AutopilotMonitor.Shared.Constants.BootstrapScriptName}, download alias)",
+ $"{AutopilotMonitor.Shared.Constants.AgentDownloadBaseUrl}/{AutopilotMonitor.Shared.Constants.BootstrapScriptName}"),
+ ($"Agent ZIP ({AutopilotMonitor.Shared.Constants.AgentZipFileName}, legacy blob keepalive)",
+ $"{AutopilotMonitor.Shared.Constants.AgentBlobBaseUrl}/{AutopilotMonitor.Shared.Constants.AgentZipFileName}"),
+ ($"Bootstrap script ({AutopilotMonitor.Shared.Constants.BootstrapScriptName}, legacy blob keepalive)",
+ $"{AutopilotMonitor.Shared.Constants.AgentBlobBaseUrl}/{AutopilotMonitor.Shared.Constants.BootstrapScriptName}")
+ };
try
{
var client = _httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(15);
- var zipRequest = new HttpRequestMessage(HttpMethod.Head, zipUrl);
- var ps1Request = new HttpRequestMessage(HttpMethod.Head, ps1Url);
-
var results = await Task.WhenAll(
- client.SendAsync(zipRequest),
- client.SendAsync(ps1Request)
+ probes.Select(p => client.SendAsync(new HttpRequestMessage(HttpMethod.Head, p.Url)))
);
- var zipResponse = results[0];
- var ps1Response = results[1];
-
- if (!zipResponse.IsSuccessStatusCode)
+ var allOk = true;
+ for (var i = 0; i < probes.Length; i++)
{
- _logger.LogError("Agent ZIP not available on blob storage: HTTP {StatusCode}", (int)zipResponse.StatusCode);
- await _opsEventService.RecordBlobStorageMissingAsync("Agent ZIP (AutopilotMonitor-Agent.zip)", (int)zipResponse.StatusCode);
+ if (results[i].IsSuccessStatusCode) continue;
+ allOk = false;
+ _logger.LogError("{Item} not available: HTTP {StatusCode}", probes[i].Label, (int)results[i].StatusCode);
+ await _opsEventService.RecordBlobStorageMissingAsync(probes[i].Label, (int)results[i].StatusCode);
}
- if (!ps1Response.IsSuccessStatusCode)
+ if (allOk)
{
- _logger.LogError("Bootstrap script not available on blob storage: HTTP {StatusCode}", (int)ps1Response.StatusCode);
- await _opsEventService.RecordBlobStorageMissingAsync("Bootstrap script (Install-AutopilotMonitor.ps1)", (int)ps1Response.StatusCode);
- }
-
- if (zipResponse.IsSuccessStatusCode && ps1Response.IsSuccessStatusCode)
- {
- _logger.LogInformation("Agent blob storage check passed: all binaries available");
+ _logger.LogInformation("Agent download endpoint check passed: all binaries available on alias + legacy blob");
}
}
catch (Exception ex)
{
- _logger.LogError(ex, "Agent blob storage check failed — storage unreachable");
+ _logger.LogError(ex, "Agent download endpoint check failed — endpoints unreachable");
await _opsEventService.RecordBlobStorageUnreachableAsync(ex.Message);
}
}
diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs
index 84d37fef..a55c5d8f 100644
--- a/src/Shared/AutopilotMonitor.Shared/Constants.cs
+++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs
@@ -73,19 +73,32 @@ namespace AutopilotMonitor.Shared
public const string ApiBaseUrl = "https://autopilotmonitor-api-eu.azurewebsites.net";
///
- /// Public base URL of the MCP server Container App (no trailing slash, no /mcp suffix).
- /// Used by the backend health check to probe {McpServerBaseUrl}/health. The
- /// Container App runs with minReplicas=0, so the first probe after idle may incur a
- /// cold start. Overridable via the McpServerUrl app setting (e.g. for dev).
+ /// Public base URL of the MCP server (no trailing slash, no /mcp suffix). Stable custom
+ /// domain in front of the Container App — survives region/environment moves, unlike the
+ /// generated *.azurecontainerapps.io FQDN. Used by the backend health check to probe
+ /// {McpServerBaseUrl}/health. The Container App runs with minReplicas=0, so the
+ /// first probe after idle may incur a cold start. Overridable via the
+ /// McpServerUrl app setting (e.g. for dev).
///
- public const string McpServerBaseUrl = "https://autopilotmonitor-mcp.kindwave-58b4b547.westeurope.azurecontainerapps.io";
+ public const string McpServerBaseUrl = "https://mcp.autopilotmonitor.com";
// -----------------------------------------------------------------------
// Agent self-update
// -----------------------------------------------------------------------
///
- /// Base URL for agent binaries in Azure Blob Storage (public read)
+ /// Canonical public base URL for agent binaries: Front Door alias in front of the
+ /// current blob origin (autopilotmonitoreu, container "agent"). Storage-account moves
+ /// happen behind this name without touching customers. New bootstrap scripts and all
+ /// availability probes use this URL.
+ ///
+ public const string AgentDownloadBaseUrl = "https://download.autopilotmonitor.com/agent";
+
+ ///
+ /// LEGACY base URL for agent binaries: the pre-migration storage account, kept alive
+ /// (dual-push on every release) because bootstrap scripts already deployed in customer
+ /// Intune tenants still download from it. Remove only after the customer migration to
+ /// is complete and the legacy account is torn down.
///
public const string AgentBlobBaseUrl = "https://autopilotmonitor.blob.core.windows.net/agent";