From 66e28b7f0ec2179fbc52d4fbdc396e134cdfa309 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Mon, 31 Aug 2026 16:13:16 +0000 Subject: [PATCH] Add dev hook for basic location review rebuild --- PlotLine/Controllers/AdminController.cs | 115 ++++++++++++++++++ ...CoreImportBasicLocationReviewRepository.cs | 10 ++ .../CoreImportBasicLocationService.cs | 13 ++ ...ase27I_CoreImportBasicLocationDevReset.sql | 17 +++ PlotLine/ViewModels/OnboardingViewModels.cs | 3 + .../Views/Onboarding/ScanLocations.cshtml | 4 +- 6 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 PlotLine/Sql/182_Phase27I_CoreImportBasicLocationDevReset.sql diff --git a/PlotLine/Controllers/AdminController.cs b/PlotLine/Controllers/AdminController.cs index dd1eb4c..cd18b65 100644 --- a/PlotLine/Controllers/AdminController.cs +++ b/PlotLine/Controllers/AdminController.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using PlotLine.Data; using PlotLine.Models; using PlotLine.Services; using PlotLine.ViewModels; @@ -23,8 +25,17 @@ public sealed class AdminController( IStoryIntelligenceImportCommitService storyIntelligenceImportCommit, INameIntelligenceService nameIntelligence, IIllustrationLibraryService illustrationLibrary, + IProjectRepository projects, + IBookRepository books, + IOnboardingBuildRepository onboardingBuilds, + ICoreImportBasicLocationService coreImportBasicLocations, + IWebHostEnvironment environment, ILogger logger) : Controller { + private static readonly Guid DiscoveryBasicLocationReviewPreviewId = Guid.Parse("50760000-3068-4000-8000-000000005076"); + private const int DiscoveryProjectId = 3068; + private const int DiscoveryBookId = 5076; + [HttpGet("")] [HttpGet("index")] public async Task Index() @@ -41,6 +52,110 @@ public sealed class AdminController( return View(); } + [HttpGet("development/core-basic-location-review/discovery")] + public async Task DevelopmentDiscoveryBasicLocationReview(bool reset = true) + { + if (!environment.IsDevelopment()) + { + return NotFound(); + } + + if (currentUser.UserId is not int userId) + { + return Forbid(); + } + + var project = await projects.GetAsync(DiscoveryProjectId); + var book = await books.GetAsync(DiscoveryBookId); + if (project is null || book is null || book.ProjectID != DiscoveryProjectId) + { + return NotFound(); + } + + var sceneSources = await onboardingBuilds.ListSceneManuscriptSourcesByBookAsync(DiscoveryBookId); + if (sceneSources.Count == 0) + { + TempData["AdminError"] = "Book 5076 has no canonical scene source text to prepare."; + return RedirectToAction(nameof(Index)); + } + + if (reset) + { + await coreImportBasicLocations.ResetAndPrepareAsync(userId, DiscoveryBasicLocationReviewPreviewId, DiscoveryProjectId, DiscoveryBookId, null, sceneSources); + } + else + { + await coreImportBasicLocations.EnsurePreparedAsync(userId, DiscoveryBasicLocationReviewPreviewId, DiscoveryProjectId, DiscoveryBookId, null, sceneSources); + } + + var model = await coreImportBasicLocations.GetReviewAsync( + userId, + DiscoveryBasicLocationReviewPreviewId, + "Discovery retained Book 5076", + project.ProjectName, + book.BookDisplayTitle); + if (model is null) + { + return NotFound(); + } + + return View("~/Views/Onboarding/ScanLocations.cshtml", new CoreImportBasicLocationReviewViewModel + { + PreviewID = model.PreviewID, + ReviewID = model.ReviewID, + Status = model.Status, + DocumentTitle = model.DocumentTitle, + SelectedProjectName = model.SelectedProjectName, + SelectedBookTitle = model.SelectedBookTitle, + FormController = "Admin", + FormAction = nameof(SaveDevelopmentDiscoveryBasicLocationReview), + ContinueButtonText = "Save selected locations", + RawObservationCount = model.RawObservationCount, + CandidateCount = model.CandidateCount, + Candidates = model.Candidates, + ExistingLocationOptions = model.ExistingLocationOptions + }); + } + + [HttpPost("development/core-basic-location-review/discovery")] + [RequestFormLimits(ValueCountLimit = 20000, KeyLengthLimit = 4096, ValueLengthLimit = 1024 * 1024)] + [ValidateAntiForgeryToken] + public async Task SaveDevelopmentDiscoveryBasicLocationReview(CoreImportBasicLocationReviewForm form, string intent = "save") + { + if (!environment.IsDevelopment()) + { + return NotFound(); + } + + if (currentUser.UserId is not int userId) + { + return Forbid(); + } + + if (form.PreviewID != DiscoveryBasicLocationReviewPreviewId) + { + return BadRequest(); + } + + if (string.Equals(intent, "skip", StringComparison.OrdinalIgnoreCase)) + { + await coreImportBasicLocations.SkipAsync(userId, form.PreviewID); + TempData["AdminMessage"] = "Development basic location review skipped for Book 5076."; + return RedirectToAction(nameof(Index)); + } + + await coreImportBasicLocations.SaveDecisionsAsync(userId, form); + if (string.Equals(intent, "continue", StringComparison.OrdinalIgnoreCase)) + { + await coreImportBasicLocations.CompleteAsync(userId, form.PreviewID); + TempData["AdminMessage"] = "Development basic location review materialised selected locations for Book 5076."; + return RedirectToAction(nameof(Index)); + } + + TempData["OnboardingReviewMessage"] = "Development location review choices saved."; + return RedirectToAction(nameof(DevelopmentDiscoveryBasicLocationReview), new { reset = false }); + } + [HttpPost("name-intelligence/validate")] [ValidateAntiForgeryToken] public async Task ValidateNameIntelligence(CancellationToken cancellationToken) diff --git a/PlotLine/Data/CoreImportBasicLocationReviewRepository.cs b/PlotLine/Data/CoreImportBasicLocationReviewRepository.cs index a4ddc17..0e78824 100644 --- a/PlotLine/Data/CoreImportBasicLocationReviewRepository.cs +++ b/PlotLine/Data/CoreImportBasicLocationReviewRepository.cs @@ -7,6 +7,7 @@ namespace PlotLine.Data; public interface ICoreImportBasicLocationReviewRepository { Task GetByPreviewAsync(int userId, Guid previewId); + Task ResetByPreviewAsync(int userId, Guid previewId); Task CreateAsync(int userId, Guid previewId, int projectId, int bookId, int? buildId); Task SaveCandidateAsync(int reviewId, CoreImportBasicLocationCandidate candidate); Task DeleteEvidenceAsync(int candidateId); @@ -30,6 +31,15 @@ public sealed class CoreImportBasicLocationReviewRepository(ISqlConnectionFactor commandType: CommandType.StoredProcedure); } + public async Task ResetByPreviewAsync(int userId, Guid previewId) + { + using var connection = connectionFactory.CreateConnection(); + await connection.ExecuteAsync( + "dbo.CoreImportBasicLocationReview_ResetByPreview", + new { UserID = userId, PreviewID = previewId }, + commandType: CommandType.StoredProcedure); + } + public async Task CreateAsync(int userId, Guid previewId, int projectId, int bookId, int? buildId) { using var connection = connectionFactory.CreateConnection(); diff --git a/PlotLine/Services/CoreImportBasicLocationService.cs b/PlotLine/Services/CoreImportBasicLocationService.cs index 6e8c37e..650586a 100644 --- a/PlotLine/Services/CoreImportBasicLocationService.cs +++ b/PlotLine/Services/CoreImportBasicLocationService.cs @@ -8,6 +8,7 @@ namespace PlotLine.Services; public interface ICoreImportBasicLocationService { Task EnsurePreparedAsync(int userId, Guid previewId, int projectId, int bookId, int? buildId, IReadOnlyList sceneSources); + Task ResetAndPrepareAsync(int userId, Guid previewId, int projectId, int bookId, int? buildId, IReadOnlyList sceneSources); Task GetReviewAsync(int userId, Guid previewId, string documentTitle, string projectName, string bookTitle); Task IsCompleteAsync(int userId, Guid previewId); Task SaveDecisionsAsync(int userId, CoreImportBasicLocationReviewForm form); @@ -57,6 +58,18 @@ public sealed class CoreImportBasicLocationService( return await reviews.GetByPreviewAsync(userId, previewId); } + public async Task ResetAndPrepareAsync( + int userId, + Guid previewId, + int projectId, + int bookId, + int? buildId, + IReadOnlyList sceneSources) + { + await reviews.ResetByPreviewAsync(userId, previewId); + return await EnsurePreparedAsync(userId, previewId, projectId, bookId, buildId, sceneSources); + } + public async Task GetReviewAsync(int userId, Guid previewId, string documentTitle, string projectName, string bookTitle) { var review = await reviews.GetByPreviewAsync(userId, previewId); diff --git a/PlotLine/Sql/182_Phase27I_CoreImportBasicLocationDevReset.sql b/PlotLine/Sql/182_Phase27I_CoreImportBasicLocationDevReset.sql new file mode 100644 index 0000000..9b9c3d1 --- /dev/null +++ b/PlotLine/Sql/182_Phase27I_CoreImportBasicLocationDevReset.sql @@ -0,0 +1,17 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER PROCEDURE dbo.CoreImportBasicLocationReview_ResetByPreview + @UserID int, + @PreviewID uniqueidentifier +AS +BEGIN + SET NOCOUNT ON; + + DELETE dbo.CoreImportBasicLocationReviews + WHERE UserID = @UserID + AND PreviewID = @PreviewID; +END; +GO diff --git a/PlotLine/ViewModels/OnboardingViewModels.cs b/PlotLine/ViewModels/OnboardingViewModels.cs index a9fa9e3..044e41d 100644 --- a/PlotLine/ViewModels/OnboardingViewModels.cs +++ b/PlotLine/ViewModels/OnboardingViewModels.cs @@ -956,6 +956,9 @@ public sealed class CoreImportBasicLocationReviewViewModel public string DocumentTitle { get; init; } = "Word manuscript"; public string SelectedProjectName { get; init; } = string.Empty; public string SelectedBookTitle { get; init; } = string.Empty; + public string FormController { get; init; } = "Onboarding"; + public string FormAction { get; init; } = "SaveBasicLocations"; + public string ContinueButtonText { get; init; } = "Save locations and review characters"; public int RawObservationCount { get; init; } public int CandidateCount { get; init; } public IReadOnlyList Candidates { get; init; } = []; diff --git a/PlotLine/Views/Onboarding/ScanLocations.cshtml b/PlotLine/Views/Onboarding/ScanLocations.cshtml index 589fb67..262f823 100644 --- a/PlotLine/Views/Onboarding/ScanLocations.cshtml +++ b/PlotLine/Views/Onboarding/ScanLocations.cshtml @@ -31,7 +31,7 @@ -
+ @Html.AntiForgeryToken() @@ -122,7 +122,7 @@
- +