using Microsoft.AspNetCore.DataProtection; namespace GuestOps.Web; public sealed class WorkerHeartbeat { [MongoDB.Bson.Serialization.Attributes.BsonId] public string Id {get;set;}="worker"; public DateTime At {get;set;}=DateTime.UtcNow; } public static class BackupProbe { const string Value="GuestOps backup key verification v1"; public static string Create(IDataProtectionProvider protection)=>protection.CreateProtector("GuestOps.BackupProbe.v1").Protect(Value); public static bool Verify(IDataProtectionProvider protection,string value)=>protection.CreateProtector("GuestOps.BackupProbe.v1").Unprotect(value)==Value; } public static class Operations { public static void Map(WebApplication app,RouteGroupBuilder api,bool preview) { app.MapGet("/health/ready",async(IStore store,HttpContext c)=> { c.Response.Headers.CacheControl="no-store"; try{await store.Ping();return Results.Ok(new{status="ready"});}catch{return Results.Json(new{status="unavailable"},statusCode:503);} }); api.MapGet("/operations",async(HttpContext c,IStore store)=> { var id=Session.Hotel(c);var boxes=await store.List(id);var messages=await store.List(id);var seen=await store.WorkerLastSeen(); return Results.Ok(new{checkedAt=DateTime.UtcNow,preview,database="Reachable",worker=new{lastSeenAt=seen,state=preview?"Preview":seen==null?"NotSeen":seenx.Status=="Connected"),attention=boxes.Count(x=>x.Status!="Connected"||x.SyncError.Length>0)},replies=new{sampleSize=messages.Count,sampleLimit=500,pending=messages.Count(x=>x.Delivery?.State is "Pending" or "Sending"),uncertain=messages.Count(x=>x.Delivery?.State=="NeedsReview"),rejected=messages.Count(x=>x.Delivery?.State=="Rejected")}}); }).RequireAuthorization("Owner"); } }