refactor: reorganize metrics/stats API routes and fix session-reports 404 handling

- Rename app-metrics → metrics/app (tenant + galactic)
- Rename usage-metrics → metrics/usage (tenant)
- Rename platform/usage-metrics → galactic/metrics/usage
- Rename platform-stats → stats/platform (anon whitelist updated)
- SessionReportService: gracefully handle missing table (404) with empty list
- SessionReportsSection: treat 404 as no reports yet (suppress error)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-03-01 16:47:55 +01:00
parent f092d601db
commit a2de05fe33
12 changed files with 43 additions and 32 deletions
@@ -20,7 +20,7 @@ namespace AutopilotMonitor.Functions.Functions
[Function("GetAppMetrics")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "app-metrics")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "metrics/app")] HttpRequestData req)
{
try
{
@@ -25,7 +25,7 @@ namespace AutopilotMonitor.Functions.Functions
[Function("GetGalacticAppMetrics")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/app-metrics")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/metrics/app")] HttpRequestData req)
{
try
{
@@ -23,7 +23,7 @@ namespace AutopilotMonitor.Functions.Functions
[Function("GetPlatformStats")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "platform-stats")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "stats/platform")] HttpRequestData req)
{
try
{
@@ -27,12 +27,12 @@ namespace AutopilotMonitor.Functions.Functions
}
/// <summary>
/// GET /api/platform/usage-metrics - Compute and return platform usage metrics
/// GET /api/galactic/metrics/usage - Compute and return platform usage metrics
/// On-demand computation with 5-minute cache (Galactic Admin only)
/// </summary>
[Function("GetPlatformUsageMetrics")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "platform/usage-metrics")]
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/metrics/usage")]
HttpRequestData req)
{
_logger.LogInformation("Platform usage metrics requested");
@@ -24,12 +24,12 @@ namespace AutopilotMonitor.Functions.Functions
}
/// <summary>
/// GET /api/usage-metrics?tenantId={tenantId} - Compute and return tenant-specific usage metrics
/// GET /api/metrics/usage?tenantId={tenantId} - Compute and return tenant-specific usage metrics
/// On-demand computation with 5-minute cache (Tenant Admin)
/// </summary>
[Function("GetTenantUsageMetrics")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "usage-metrics")]
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "metrics/usage")]
HttpRequestData req)
{
_logger.LogInformation("Tenant usage metrics requested");
@@ -41,7 +41,7 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware
private static readonly string[] _anonymousRoutes =
{
"/api/health",
"/api/platform-stats",
"/api/stats/platform",
"/api/agent/register-session",
"/api/agent/ingest",
"/api/agent/config",
@@ -4,6 +4,7 @@ using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading.Tasks;
using Azure;
using Azure.Data.Tables;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
@@ -172,19 +173,27 @@ namespace AutopilotMonitor.Functions.Services
var tableClient = _storageService.GetTableServiceClient().GetTableClient(Constants.TableNames.SessionReports);
var results = new List<SessionReportMetadata>();
await foreach (var entity in tableClient.QueryAsync<TableEntity>(e => e.PartitionKey == "reports"))
try
{
results.Add(new SessionReportMetadata
await foreach (var entity in tableClient.QueryAsync<TableEntity>(e => e.PartitionKey == "reports"))
{
ReportId = entity.GetString("ReportId") ?? string.Empty,
TenantId = entity.GetString("TenantId") ?? string.Empty,
SessionId = entity.GetString("SessionId") ?? string.Empty,
Comment = entity.GetString("Comment") ?? string.Empty,
Email = entity.GetString("Email") ?? string.Empty,
BlobName = entity.GetString("BlobName") ?? string.Empty,
SubmittedBy = entity.GetString("SubmittedBy") ?? string.Empty,
SubmittedAt = entity.GetDateTimeOffset("SubmittedAt")?.UtcDateTime ?? DateTime.MinValue
});
results.Add(new SessionReportMetadata
{
ReportId = entity.GetString("ReportId") ?? string.Empty,
TenantId = entity.GetString("TenantId") ?? string.Empty,
SessionId = entity.GetString("SessionId") ?? string.Empty,
Comment = entity.GetString("Comment") ?? string.Empty,
Email = entity.GetString("Email") ?? string.Empty,
BlobName = entity.GetString("BlobName") ?? string.Empty,
SubmittedBy = entity.GetString("SubmittedBy") ?? string.Empty,
SubmittedAt = entity.GetDateTimeOffset("SubmittedAt")?.UtcDateTime ?? DateTime.MinValue
});
}
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
// Table doesn't exist yet — no reports have been submitted
_logger.LogDebug("SessionReports table does not exist yet, returning empty list");
}
return results;
@@ -42,6 +42,11 @@ export function SessionReportsSection({
headers: { Authorization: `Bearer ${token}` }
});
if (res.status === 404) {
// Table/container doesn't exist yet — no reports submitted so far
setReports([]);
return;
}
if (!res.ok) throw new Error(`Failed to load reports: ${res.statusText}`);
const data = await res.json();
setReports(data.reports ?? []);
@@ -63,9 +68,6 @@ export function SessionReportsSection({
<div>
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold text-indigo-900 dark:text-indigo-100">Session Reports</h2>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300">
Private Preview
</span>
</div>
<p className="text-sm text-indigo-600 dark:text-indigo-300 mt-1">
Sessions reported by Tenant Admins for analysis
@@ -322,6 +322,12 @@ export default function AdminConfigurationPage() {
setSuccessMessage={setSuccessMessage}
/>
{/* Session Reports */}
<SessionReportsSection
getAccessToken={getAccessToken}
setError={setError}
/>
{/* Session Event Export */}
<SessionExportSection
tenants={tenants}
@@ -374,12 +380,6 @@ export default function AdminConfigurationPage() {
setError={setError}
setSuccessMessage={setSuccessMessage}
/>
{/* Session Reports (Private Preview) */}
<SessionReportsSection
getAccessToken={getAccessToken}
setError={setError}
/>
</div>
</main>
</div>
@@ -169,8 +169,8 @@ export default function FleetHealthPage() {
if (!token) return;
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
const endpoint = galacticAdminMode
? `${API_BASE_URL}/api/galactic/app-metrics?days=${days}`
: `${API_BASE_URL}/api/app-metrics?tenantId=${tenantId}&days=${days}`;
? `${API_BASE_URL}/api/galactic/metrics/app?days=${days}`
: `${API_BASE_URL}/api/metrics/app?tenantId=${tenantId}&days=${days}`;
const response = await fetch(
endpoint,
{ headers: { Authorization: `Bearer ${token}` } }
@@ -96,7 +96,7 @@ export default function PlatformUsageMetricsPage() {
throw new Error('Failed to get access token');
}
const response = await fetch(`${API_BASE_URL}/api/platform/usage-metrics`, {
const response = await fetch(`${API_BASE_URL}/api/galactic/metrics/usage`, {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -85,7 +85,7 @@ export default function UsageMetricsPage() {
return;
}
const response = await fetch(`${API_BASE_URL}/api/usage-metrics?tenantId=${tenantId}`, {
const response = await fetch(`${API_BASE_URL}/api/metrics/usage?tenantId=${tenantId}`, {
headers: {
'Authorization': `Bearer ${token}`
}