CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceRun_ListActiveBookSummaryForUser @UserID int AS BEGIN SET NOCOUNT ON; ;WITH RunScopes AS ( SELECT r.UserID, r.ProjectID, r.BookID, LatestCreatedUtc = MAX(r.CreatedUtc), LatestUpdatedUtc = MAX(r.UpdatedUtc) FROM dbo.StoryIntelligenceRuns r INNER JOIN dbo.Projects p ON p.ProjectID = r.ProjectID AND p.IsArchived = 0 INNER JOIN dbo.Books b ON b.BookID = r.BookID AND b.IsArchived = 0 INNER JOIN dbo.ProjectUserAccess access ON access.ProjectID = r.ProjectID AND access.UserID = @UserID AND access.IsActive = 1 WHERE r.UserID = @UserID AND r.BookID IS NOT NULL AND r.Status IN (N'Pending', N'Running') GROUP BY r.UserID, r.ProjectID, r.BookID ), PipelineScopes AS ( SELECT @UserID AS UserID, p.ProjectID, p.BookID, LatestCreatedUtc = MAX(COALESCE(lastRun.CreatedUtc, p.CreatedUtc)), LatestUpdatedUtc = MAX(CASE WHEN lastRun.UpdatedUtc IS NOT NULL AND lastRun.UpdatedUtc > p.UpdatedUtc THEN lastRun.UpdatedUtc ELSE p.UpdatedUtc END) FROM dbo.StoryIntelligenceBookPipelines p INNER JOIN dbo.Projects project ON project.ProjectID = p.ProjectID AND project.IsArchived = 0 INNER JOIN dbo.Books book ON book.BookID = p.BookID AND book.IsArchived = 0 INNER JOIN dbo.ProjectUserAccess access ON access.ProjectID = p.ProjectID AND access.UserID = @UserID AND access.IsActive = 1 LEFT JOIN dbo.StoryIntelligenceRuns lastRun ON lastRun.StoryIntelligenceRunID = p.LastRunID WHERE p.Status = N'InProgress' GROUP BY p.ProjectID, p.BookID ), ActiveScopes AS ( SELECT scopes.UserID, scopes.ProjectID, scopes.BookID, LatestCreatedUtc = MAX(scopes.LatestCreatedUtc), LatestUpdatedUtc = MAX(scopes.LatestUpdatedUtc) FROM ( SELECT UserID, ProjectID, BookID, LatestCreatedUtc, LatestUpdatedUtc FROM RunScopes UNION ALL SELECT UserID, ProjectID, BookID, LatestCreatedUtc, LatestUpdatedUtc FROM PipelineScopes ) scopes GROUP BY scopes.UserID, scopes.ProjectID, scopes.BookID ), ScopedRuns AS ( SELECT r.* FROM dbo.StoryIntelligenceRuns r INNER JOIN ActiveScopes scope ON scope.UserID = r.UserID AND ISNULL(scope.ProjectID, -1) = ISNULL(r.ProjectID, -1) AND ISNULL(scope.BookID, -1) = ISNULL(r.BookID, -1) AND r.CreatedUtc >= DATEADD(minute, -30, scope.LatestCreatedUtc) WHERE r.Status IN (N'Pending', N'Running', N'Completed', N'CompletedWithWarnings', N'Failed', N'Cancelled') ), CanonicalSceneScope AS ( SELECT DISTINCT scope.UserID, scope.ProjectID, scope.BookID, s.SceneID FROM ActiveScopes scope INNER JOIN dbo.Chapters c ON c.BookID = scope.BookID AND c.IsArchived = 0 INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0 ), RunSummary AS ( SELECT scope.UserID, scope.ProjectID, scope.BookID, COUNT(r.StoryIntelligenceRunID) AS RunCount, SUM(CASE WHEN r.Status = N'Pending' THEN 1 ELSE 0 END) AS PendingRunCount, SUM(CASE WHEN r.Status = N'Running' THEN 1 ELSE 0 END) AS RunningRunCount, SUM(CASE WHEN r.Status IN (N'Completed', N'CompletedWithWarnings') THEN 1 ELSE 0 END) AS CompletedRunCount, SUM(CASE WHEN r.Status IN (N'Failed', N'Cancelled') THEN 1 ELSE 0 END) AS FailedRunCount, SUM(ISNULL(r.TotalDetectedScenes, 0)) AS TotalDetectedScenes, SUM(ISNULL(r.CompletedScenes, 0)) AS CompletedScenes, SUM(ISNULL(r.FailedScenes, 0)) AS FailedScenes, SUM(CASE WHEN r.Status IN (N'Pending', N'Running') THEN CASE WHEN ISNULL(r.TotalDetectedScenes, 0) - ISNULL(r.CompletedScenes, 0) - ISNULL(r.FailedScenes, 0) > 0 THEN ISNULL(r.TotalDetectedScenes, 0) - ISNULL(r.CompletedScenes, 0) - ISNULL(r.FailedScenes, 0) ELSE 0 END ELSE 0 END) AS ActiveScenes, SUM(ISNULL(r.TotalDurationMs, 0)) AS TotalDurationMs, MIN(COALESCE(r.CreatedUtc, scope.LatestCreatedUtc)) AS FirstCreatedUtc, MAX(CASE WHEN r.UpdatedUtc IS NOT NULL AND r.UpdatedUtc > scope.LatestUpdatedUtc THEN r.UpdatedUtc ELSE scope.LatestUpdatedUtc END) AS UpdatedUtc FROM ActiveScopes scope LEFT JOIN ScopedRuns r ON r.UserID = scope.UserID AND ISNULL(r.ProjectID, -1) = ISNULL(scope.ProjectID, -1) AND ISNULL(r.BookID, -1) = ISNULL(scope.BookID, -1) GROUP BY scope.UserID, scope.ProjectID, scope.BookID ), ExpectedSceneSummary AS ( SELECT scope.UserID, scope.ProjectID, scope.BookID, COUNT(scope.SceneID) AS TotalExpectedScenes FROM CanonicalSceneScope scope GROUP BY scope.UserID, scope.ProjectID, scope.BookID ) SELECT summary.UserID, summary.ProjectID, p.ProjectName AS ProjectTitle, summary.BookID, b.BookTitle, b.Subtitle AS BookSubtitle, summary.RunCount, summary.PendingRunCount, summary.RunningRunCount, summary.CompletedRunCount, summary.FailedRunCount, COALESCE(expected.TotalExpectedScenes, NULLIF(summary.TotalDetectedScenes, 0), 0) AS TotalExpectedScenes, summary.TotalDetectedScenes, summary.CompletedScenes, summary.FailedScenes, summary.ActiveScenes, CASE WHEN COALESCE(expected.TotalExpectedScenes, NULLIF(summary.TotalDetectedScenes, 0), 0) <= 0 THEN 0 ELSE CONVERT(int, ROUND(summary.CompletedScenes * 100.0 / COALESCE(expected.TotalExpectedScenes, NULLIF(summary.TotalDetectedScenes, 0), 1), 0)) END AS ProgressPercent, CASE WHEN pipeline.Status = N'InProgress' AND summary.RunningRunCount = 0 AND summary.PendingRunCount = 0 THEN N'Finalising' WHEN pipeline.Status = N'InProgress' AND COALESCE(expected.TotalExpectedScenes, NULLIF(summary.TotalDetectedScenes, 0), 0) > 0 AND summary.CompletedScenes + summary.FailedScenes >= COALESCE(expected.TotalExpectedScenes, NULLIF(summary.TotalDetectedScenes, 0), 0) THEN N'Finalising' WHEN summary.RunningRunCount > 0 THEN N'Running' WHEN summary.PendingRunCount > 0 THEN N'Pending' WHEN summary.FailedRunCount > 0 THEN N'Failed' WHEN summary.FailedScenes > 0 THEN N'CompletedWithWarnings' ELSE N'Completed' END AS Status, summary.TotalDurationMs, summary.FirstCreatedUtc, summary.UpdatedUtc FROM RunSummary summary LEFT JOIN ExpectedSceneSummary expected ON expected.UserID = summary.UserID AND ISNULL(expected.ProjectID, -1) = ISNULL(summary.ProjectID, -1) AND ISNULL(expected.BookID, -1) = ISNULL(summary.BookID, -1) LEFT JOIN dbo.StoryIntelligenceBookPipelines pipeline ON pipeline.BookID = summary.BookID AND pipeline.ProjectID = summary.ProjectID LEFT JOIN dbo.Projects p ON p.ProjectID = summary.ProjectID LEFT JOIN dbo.Books b ON b.BookID = summary.BookID WHERE summary.PendingRunCount > 0 OR summary.RunningRunCount > 0 OR pipeline.Status = N'InProgress' ORDER BY summary.UpdatedUtc DESC, summary.FirstCreatedUtc; END; GO