using System.Net; using AutopilotMonitor.Functions.Helpers; using AutopilotMonitor.Functions.Services; using AutopilotMonitor.Shared.DataAccess; using AutopilotMonitor.Shared.Models; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; namespace AutopilotMonitor.Functions.Functions.Sessions { /// /// GET /api/sessions/{sessionId}/decision-graph — returns a pre-projected DAG of /// the session's reducer steps for the Inspector (Plan §M5 / §M6). Building the graph /// backend-side means the UI receives one structured shape instead of rebuilding it from /// the raw journal on every page open; it also keeps the "what counts as terminal" rule /// authoritatively next to 's copy of that list. /// public class GetSessionDecisionGraphFunction { /// Sane upper bound — a V2 session journal shouldn't produce more steps than this. internal const int MaxTransitionsToLoad = 5000; private readonly ILogger _logger; private readonly IDecisionTransitionRepository _transitionRepo; private readonly ISessionRepository _sessionRepo; public GetSessionDecisionGraphFunction( ILogger logger, IDecisionTransitionRepository transitionRepo, ISessionRepository sessionRepo) { _logger = logger; _transitionRepo = transitionRepo; _sessionRepo = sessionRepo; } [Function("GetSessionDecisionGraph")] public async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "sessions/{sessionId}/decision-graph")] HttpRequestData req, string sessionId) { if (string.IsNullOrWhiteSpace(sessionId)) { var badRequest = req.CreateResponse(HttpStatusCode.BadRequest); await badRequest.WriteAsJsonAsync(new { success = false, message = "SessionId is required" }); return badRequest; } var sessionPrefix = $"[Session: {sessionId.Substring(0, Math.Min(8, sessionId.Length))}]"; _logger.LogInformation("{Prefix} GetSessionDecisionGraph", sessionPrefix); try { // Global-scope (GA or read-only GlobalReader) callers resolve the session's owning // tenant upfront (point-read; same pattern as the other session-detail reads). The // resolved tenant also stamps the projection below. var requestCtx = req.GetRequestContext(); var effectiveTenantId = await requestCtx.ResolveSessionScopeAsync(_sessionRepo, sessionId); var transitions = await _transitionRepo.QueryBySessionAsync( effectiveTenantId, sessionId, MaxTransitionsToLoad); var projection = DecisionGraphBuilder.Build(effectiveTenantId, sessionId, transitions); return await req.OkAsync(new GetSessionDecisionGraphResponse { Success = true, Truncated = transitions.Count >= MaxTransitionsToLoad, Graph = projection, }); } catch (Exception ex) { return await req.InternalServerErrorAsync(_logger, ex, $"Get decision graph for session '{sessionId}'"); } } } }