Resolve story point POV and polish context display
This commit is contained in:
parent
10d5a657ad
commit
e06bcc57fe
@ -16,6 +16,7 @@ internal static class StoryPointTests
|
||||
public static void PeopleAndChronology()
|
||||
{
|
||||
var point = Snapshot();
|
||||
Check(point.PovName == "Ari", "POV must resolve the timeline ID from the canonical character batch even when Timeline omits its display name.");
|
||||
Check(point.Characters.Select(x => x.Appearance.CharacterID).SequenceEqual([1, 2]), "Only selected SceneCharacters belong in presence.");
|
||||
Check(point.Characters[0].PreviousAppearance?.SceneID == 900, "Previous appearance must cross books and exclude selected scene, regardless of IDs.");
|
||||
Check(point.Characters[1].PreviousAppearance is null, "First appearance has no previous source.");
|
||||
@ -125,7 +126,7 @@ internal static class StoryPointTests
|
||||
Books = [new Book { BookID = 2, BookNumber = 2, SortOrder = 2, BookTitle = "Voyage" }, new Book { BookID = 9, BookNumber = 1, SortOrder = 1, BookTitle = "Departure" }],
|
||||
Chapters = [new Chapter { ChapterID = 2, BookID = 2, ChapterNumber = 1, SortOrder = 1 }, new Chapter { ChapterID = 9, BookID = 9, ChapterNumber = 1, SortOrder = 1 }],
|
||||
Scenes = [new Scene { SceneID = 1, ChapterID = 2, SortOrder = 2, SceneNumber = 2, PrimaryLocationID = 5 },
|
||||
new Scene { SceneID = 20, ChapterID = 2, SortOrder = 1, SceneNumber = 1, PrimaryLocationID = 5, TimeModeName = "Exact Date", StartDateTime = new DateTime(2001, 2, 5) },
|
||||
new Scene { SceneID = 20, ChapterID = 2, POVCharacterID = 1, SortOrder = 1, SceneNumber = 1, PrimaryLocationID = 5, TimeModeName = "Exact Date", StartDateTime = new DateTime(2001, 2, 5) },
|
||||
new Scene { SceneID = 900, ChapterID = 9, SortOrder = 1, SceneNumber = 1, PrimaryLocationID = 5 }] },
|
||||
Characters = [new Character { CharacterID = 1, CharacterName = "Ari", BirthDate = new DateTime(1981, 1, 5), AgeAtSeriesStart = 19 }, new Character { CharacterID = 2, CharacterName = "Bo" }],
|
||||
Appearances = [new SceneCharacter { CharacterID = 1, CharacterName = "Ari", SceneID = 900, LocationID = 5, LocationPath = "Harbour" },
|
||||
|
||||
@ -72,7 +72,7 @@ public sealed class StoryPointContextService(IProjectAccessService access, ITime
|
||||
var sources = ordered.Take(maximum + 1).ToDictionary(x => x.SceneID, x =>
|
||||
{
|
||||
var ch = chaptersById[x.ChapterID];
|
||||
return new StoryPointSource(x.SceneID, $"{booksById[ch.BookID].BookDisplayTitle} · Ch {ch.ChapterNumber:g} · Scene {x.SceneNumber:g}");
|
||||
return new StoryPointSource(x.SceneID, $"{booksById[ch.BookID].BookDisplayTitle} · Ch {ch.ChapterNumber:0.##} · Scene {x.SceneNumber:0.##}");
|
||||
});
|
||||
var present = input.Appearances.Where(x => x.SceneID == sceneId).GroupBy(x => x.CharacterID)
|
||||
.Select(x => x.OrderByDescending(y => y.UpdatedDate).First()).OrderBy(x => x.CharacterName).ToList();
|
||||
@ -102,6 +102,7 @@ public sealed class StoryPointContextService(IProjectAccessService access, ITime
|
||||
return new StoryPointSnapshot
|
||||
{
|
||||
ProjectID = input.Timeline.Project.ProjectID, Book = book, Chapter = chapter, Scene = scene, Characters = people,
|
||||
PovName = scene.POVCharacterID.HasValue ? characterModels.GetValueOrDefault(scene.POVCharacterID.Value)?.CharacterName ?? scene.POVCharacterName : scene.POVCharacterName,
|
||||
Relationships = RelationshipMapService.BuildAtScene(input.Relationships, indexes, sceneId)
|
||||
.Where(x => presentIds.Contains(x.CharacterAID) || presentIds.Contains(x.CharacterBID)).ToList(),
|
||||
Assets = pointAssets, Location = location, PreviousLocationUse = lastLocationScene is null ? null : sources[lastLocationScene.SceneID],
|
||||
|
||||
@ -18,6 +18,7 @@ public sealed class StoryPointSnapshot
|
||||
public Book Book { get; init; } = new();
|
||||
public Chapter Chapter { get; init; } = new();
|
||||
public Scene Scene { get; init; } = new();
|
||||
public string? PovName { get; init; }
|
||||
public IReadOnlyList<StoryPointCharacter> Characters { get; init; } = [];
|
||||
public IReadOnlyList<RelationshipMapRelationshipViewModel> Relationships { get; init; } = [];
|
||||
public IReadOnlyList<AssetStoryStateViewModel> Assets { get; init; } = [];
|
||||
|
||||
@ -18,10 +18,10 @@
|
||||
@foreach (var book in Model.Books) { <option value="@book.BookID" selected="@(book.BookID == Model.Snapshot?.Book.BookID)">Book @book.BookNumber: @book.BookDisplayTitle</option> }
|
||||
</select></div>
|
||||
<div><label for="point-chapter" class="form-label">Chapter</label><select id="point-chapter" class="form-select" data-point-chapter>
|
||||
@foreach (var chapter in Model.Chapters) { <option value="@chapter.ChapterID" data-book="@chapter.BookID" selected="@(chapter.ChapterID == Model.Snapshot?.Chapter.ChapterID)">Ch @chapter.ChapterNumber.ToString("g"): @chapter.ChapterTitle</option> }
|
||||
@foreach (var chapter in Model.Chapters) { <option value="@chapter.ChapterID" data-book="@chapter.BookID" selected="@(chapter.ChapterID == Model.Snapshot?.Chapter.ChapterID)">Ch @chapter.ChapterNumber.ToString("0.##"): @chapter.ChapterTitle</option> }
|
||||
</select></div>
|
||||
<div><label for="point-scene" class="form-label">Scene</label><select id="point-scene" class="form-select" name="sceneId" data-point-scene>
|
||||
@foreach (var scene in Model.Scenes) { <option value="@scene.SceneID" data-chapter="@scene.ChapterID" selected="@(scene.SceneID == Model.Snapshot?.Scene.SceneID)">Scene @scene.SceneNumber.ToString("g"): @scene.SceneTitle</option> }
|
||||
@foreach (var scene in Model.Scenes) { <option value="@scene.SceneID" data-chapter="@scene.ChapterID" selected="@(scene.SceneID == Model.Snapshot?.Scene.SceneID)">Scene @scene.SceneNumber.ToString("0.##"): @scene.SceneTitle</option> }
|
||||
</select></div>
|
||||
<button class="btn btn-primary" type="submit">Show state</button>
|
||||
</form>
|
||||
|
||||
@ -10,7 +10,7 @@ else
|
||||
var exactTime = scene.TimeModeName is "Exact DateTime" or "Exact Date and Time";
|
||||
<article class="story-point" data-scene-id="@scene.SceneID">
|
||||
<header class="story-point-header">
|
||||
<p class="eyebrow">@Model.Book.BookDisplayTitle · Chapter @Model.Chapter.ChapterNumber.ToString("g") · Scene @scene.SceneNumber.ToString("g")</p>
|
||||
<p class="eyebrow">@Model.Book.BookDisplayTitle · Chapter @Model.Chapter.ChapterNumber.ToString("0.##") · Scene @scene.SceneNumber.ToString("0.##")</p>
|
||||
<h2>@scene.SceneTitle</h2>
|
||||
<p class="story-point-date">
|
||||
@if (scene.StartDateTime.HasValue)
|
||||
@ -21,7 +21,7 @@ else
|
||||
@if (!string.IsNullOrWhiteSpace(scene.RelativeTimeText)) { <span> · @scene.RelativeTimeText</span> }
|
||||
<small class="text-muted"> · @scene.TimeModeName</small>
|
||||
</p>
|
||||
<p>POV: <strong>@(scene.POVCharacterName ?? "Not recorded")</strong> · Location: <strong>@(Model.Location?.LocationPath ?? scene.PrimaryLocationName ?? "Not recorded")</strong></p>
|
||||
<p>POV: <strong>@(Model.PovName ?? "Not recorded")</strong> · Location: <strong>@(Model.Location?.LocationPath ?? scene.PrimaryLocationName ?? "Not recorded")</strong></p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-sm btn-outline-primary" asp-controller="Scenes" asp-action="Edit" asp-route-id="@scene.SceneID">Open Scene</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.ProjectID" asp-route-bookId="@Model.Book.BookID" asp-route-selectedSceneId="@scene.SceneID">Timeline</a>
|
||||
@ -33,7 +33,7 @@ else
|
||||
@if (Model.Characters.Count == 0) { <p class="text-muted">No characters attached to this scene.</p> }
|
||||
else { <div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Character</th><th>Age at this point</th><th>Known location</th><th>Previous appearance</th></tr></thead><tbody>
|
||||
@foreach (var person in Model.Characters) { <tr><td><a asp-controller="Characters" asp-action="Edit" asp-route-id="@person.Appearance.CharacterID">@person.Appearance.CharacterName</a>
|
||||
<small class="d-block text-muted">@person.Appearance.RoleInSceneTypeName @person.Appearance.PresenceTypeName</small></td>
|
||||
<small class="d-block text-muted">@string.Join(" · ", new[] { person.Appearance.RoleInSceneTypeName, person.Appearance.PresenceTypeName }.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase))</small></td>
|
||||
<td>@(person.Age.DisplayText ?? "Unknown") @if (!person.Age.HasExactAge && person.Age.DisplayText is not null) { <small class="d-block text-muted">Approximate</small> }</td>
|
||||
<td>@(person.State?.CurrentLocation ?? "Unknown")<small class="d-block text-muted">@(person.State?.State == "Inferred" ? "Last known" : "")</small></td>
|
||||
<td>@if (person.PreviousAppearance is { } previous) { <a asp-controller="Scenes" asp-action="Edit" asp-route-id="@previous.SceneID">@previous.Label</a> } else { <span class="text-muted">First recorded appearance</span> }</td></tr> }
|
||||
|
||||
@ -14,3 +14,7 @@
|
||||
.story-point .table { --bs-table-bg: transparent; font-size: .9rem; }
|
||||
.story-point .table th { color: var(--plotline-text-muted, var(--bs-secondary-color)); font-size: .8rem; font-weight: 600; }
|
||||
@media (max-width: 800px) { .story-point-selectors { grid-template-columns: 1fr; } .story-point-grid { grid-template-columns: 1fr; } }
|
||||
.story-point-header .btn-outline-primary,
|
||||
.story-point-header .btn-outline-secondary { background: transparent; color: var(--plotline-text); border-color: var(--plotline-border-soft); }
|
||||
.story-point-header .btn-outline-primary:hover,
|
||||
.story-point-header .btn-outline-secondary:hover { background: var(--plotline-surface); color: var(--plotline-text); border-color: var(--plotline-accent); }
|
||||
|
||||
@ -45,3 +45,9 @@ Read-only acceptance on project 4085 (The Alpha Flame), before publication:
|
||||
## Deliberate limits
|
||||
|
||||
Reader knowledge is deferred: a few relationship reader-awareness flags do not form a reliable general reader knowledge model. Word Companion automatic following is deferred; the service and partial are reusable for that integration. No AI briefing is included. Unanchored thread/relationship status and asset current-only fields cannot establish historical truth. Entity names and canonical authored facts are not versioned against later author edits; this feature reconstructs narrative event history, not database edit history. Undated chronology and incomplete locations/ages remain explicitly unknown.
|
||||
|
||||
Deployed acceptance (7 September 2026): authenticated Chromium checked both scenes, navigation, chronology and ages, early versus later thread visibility, Scene/Timeline/Calendar links (all HTTP 200), dependent chapter/scene selection, AJAX responses, browser Back, no-JavaScript GET submission, invalid scene/project rejection (404), and anonymous login redirect (302). Calendar and Timeline continued to load. No browser exceptions or server error entries were observed. Visual review prompted duplicate participation-label removal, compact scene numbering, and scoped dark-theme action-link contrast. A direct SQL header check confirmed POV IDs even though Timeline omits their names; the composition now resolves Beth and Maggie from the already-loaded canonical character batch.
|
||||
|
||||
The initial `/tmp` publish failed on tmpfs quota before installation. Publication succeeded from temporary staging on `/srv` using the same dotnet publish and physical release/backup/rsync/systemd process; production was untouched. The prior dev directory remains backed up. The first post-restart request briefly returned 502 during startup; subsequent home and authenticated checks returned 200. All 347 tests passed; the solution build and Release publish succeeded. The Word Companion README permission-description correction is included at the user's request.
|
||||
|
||||
Independent SQL evidence confirmed Beth's DOB as 7 May 1965, Rosie's as 28 December 1964, and Sophie's approximate series-start age as 21. At narrative index 201, threads 26 and 29 had already resolved (indexes 144 and 99); 33, 35 and 36 had not yet been introduced (233, 261 and 287). The displayed active set was exactly 27, 28, 30, 31, 32 and 34. At index 1, only thread 34 had been introduced.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user