// -----------------------------------------------------------------------------
// 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
{
}