// ----------------------------------------------------------------------------- // TileEndpoints.cs // Sprint: SPRINT_20260125_002_Attestor_trust_automation // Task: PROXY-002 - Implement tile-proxy service // Description: Tile proxy API endpoints // ----------------------------------------------------------------------------- using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using StellaOps.Attestor.TileProxy.Services; using System.Text.Json; namespace StellaOps.Attestor.TileProxy.Endpoints; /// /// API endpoints for tile proxy service. /// public static class TileEndpoints { /// /// Maps all tile proxy endpoints. /// public static IEndpointRouteBuilder MapTileProxyEndpoints(this IEndpointRouteBuilder endpoints) { // Tile endpoints (passthrough) endpoints.MapGet("/tile/{level:int}/{index:long}", GetTile) .WithName("GetTile") .WithTags("Tiles") .Produces(StatusCodes.Status200OK, "application/octet-stream") .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status502BadGateway); endpoints.MapGet("/tile/{level:int}/{index:long}.p/{partialWidth:int}", GetPartialTile) .WithName("GetPartialTile") .WithTags("Tiles") .Produces(StatusCodes.Status200OK, "application/octet-stream") .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status502BadGateway); // Checkpoint endpoint endpoints.MapGet("/checkpoint", GetCheckpoint) .WithName("GetCheckpoint") .WithTags("Checkpoint") .Produces(StatusCodes.Status200OK, "text/plain") .Produces(StatusCodes.Status502BadGateway); // Admin endpoints var admin = endpoints.MapGroup("/_admin"); admin.MapGet("/cache/stats", GetCacheStats) .WithName("GetCacheStats") .WithTags("Admin") .Produces(StatusCodes.Status200OK); admin.MapGet("/metrics", GetMetrics) .WithName("GetMetrics") .WithTags("Admin") .Produces(StatusCodes.Status200OK); admin.MapPost("/cache/sync", TriggerSync) .WithName("TriggerSync") .WithTags("Admin") .Produces(StatusCodes.Status200OK); admin.MapDelete("/cache/prune", PruneCache) .WithName("PruneCache") .WithTags("Admin") .Produces(StatusCodes.Status200OK); admin.MapGet("/health", HealthCheck) .WithName("HealthCheck") .WithTags("Admin") .Produces(StatusCodes.Status200OK); admin.MapGet("/ready", ReadinessCheck) .WithName("ReadinessCheck") .WithTags("Admin") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status503ServiceUnavailable); return endpoints; } private static async Task GetTile( int level, long index, [FromServices] TileProxyService proxyService, CancellationToken cancellationToken) { var result = await proxyService.GetTileAsync(level, index, cancellationToken: cancellationToken); if (!result.Success) { return Results.Problem( detail: result.Error, statusCode: StatusCodes.Status502BadGateway); } if (result.Content == null) { return Results.NotFound(); } return Results.Bytes(result.Content, "application/octet-stream"); } private static async Task GetPartialTile( int level, long index, int partialWidth, [FromServices] TileProxyService proxyService, CancellationToken cancellationToken) { if (partialWidth <= 0 || partialWidth > 256) { return Results.BadRequest("Invalid partial width"); } var result = await proxyService.GetTileAsync(level, index, partialWidth, cancellationToken); if (!result.Success) { return Results.Problem( detail: result.Error, statusCode: StatusCodes.Status502BadGateway); } if (result.Content == null) { return Results.NotFound(); } return Results.Bytes(result.Content, "application/octet-stream"); } private static async Task GetCheckpoint( [FromServices] TileProxyService proxyService, CancellationToken cancellationToken) { var result = await proxyService.GetCheckpointAsync(cancellationToken); if (!result.Success) { return Results.Problem( detail: result.Error, statusCode: StatusCodes.Status502BadGateway); } return Results.Text(result.Content ?? "", "text/plain"); } private static async Task GetCacheStats( [FromServices] ContentAddressedTileStore tileStore, CancellationToken cancellationToken) { var stats = await tileStore.GetStatsAsync(cancellationToken); return Results.Ok(new CacheStatsResponse { TotalTiles = stats.TotalTiles, TotalBytes = stats.TotalBytes, TotalMb = Math.Round(stats.TotalBytes / (1024.0 * 1024.0), 2), PartialTiles = stats.PartialTiles, UsagePercent = Math.Round(stats.UsagePercent, 2), OldestTile = stats.OldestTile, NewestTile = stats.NewestTile }); } private static IResult GetMetrics( [FromServices] TileProxyService proxyService) { var metrics = proxyService.GetMetrics(); return Results.Ok(new MetricsResponse { CacheHits = metrics.CacheHits, CacheMisses = metrics.CacheMisses, HitRatePercent = Math.Round(metrics.HitRate, 2), UpstreamRequests = metrics.UpstreamRequests, UpstreamErrors = metrics.UpstreamErrors, InflightRequests = metrics.InflightRequests }); } private static IResult TriggerSync( [FromServices] IServiceProvider services, [FromServices] ILogger logger) { // TODO: Trigger background sync job logger.LogInformation("Manual sync triggered"); return Results.Ok(new SyncResponse { Message = "Sync job queued", QueuedAt = DateTimeOffset.UtcNow }); } private static async Task PruneCache( [FromServices] ContentAddressedTileStore tileStore, [FromQuery] long? targetSizeBytes, CancellationToken cancellationToken) { var prunedCount = await tileStore.PruneAsync(targetSizeBytes ?? 0, cancellationToken); return Results.Ok(new PruneResponse { TilesPruned = prunedCount, PrunedAt = DateTimeOffset.UtcNow }); } private static IResult HealthCheck() { return Results.Ok(new HealthResponse { Status = "healthy", Timestamp = DateTimeOffset.UtcNow }); } private static async Task ReadinessCheck( [FromServices] TileProxyService proxyService, CancellationToken cancellationToken) { // Check if we can reach upstream var checkpoint = await proxyService.GetCheckpointAsync(cancellationToken); if (checkpoint.Success) { return Results.Ok(new { ready = true, checkpoint = checkpoint.TreeSize }); } return Results.Json( new { ready = false, error = checkpoint.Error }, statusCode: StatusCodes.Status503ServiceUnavailable); } } // Response models public sealed record CacheStatsResponse { public int TotalTiles { get; init; } public long TotalBytes { get; init; } public double TotalMb { get; init; } public int PartialTiles { get; init; } public double UsagePercent { get; init; } public DateTimeOffset? OldestTile { get; init; } public DateTimeOffset? NewestTile { get; init; } } public sealed record MetricsResponse { public long CacheHits { get; init; } public long CacheMisses { get; init; } public double HitRatePercent { get; init; } public long UpstreamRequests { get; init; } public long UpstreamErrors { get; init; } public int InflightRequests { get; init; } } public sealed record SyncResponse { public string Message { get; init; } = string.Empty; public DateTimeOffset QueuedAt { get; init; } } public sealed record PruneResponse { public int TilesPruned { get; init; } public DateTimeOffset PrunedAt { get; init; } } public sealed record HealthResponse { public string Status { get; init; } = string.Empty; public DateTimeOffset Timestamp { get; init; } } // Logger class for endpoint logging file static class TileEndpoints { }