Phase 12F: Continuity Warning Management.

This commit is contained in:
Nick Beckley 2026-06-14 20:34:29 +01:00
parent 279c8f182c
commit 738b9f496d
8 changed files with 474 additions and 2 deletions

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
@ -25,4 +26,58 @@ public sealed class ContinuityExplorerController(IStoryStateService storyState)
{
return RedirectToAction(nameof(Index), filter);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AcknowledgeWarning(
string warningType,
int? sceneId,
int? characterId,
int? assetId,
string? notes,
[Bind(Prefix = "ReturnFilter")] ContinuityFilter returnFilter)
{
if (returnFilter.ProjectID == 0 || string.IsNullOrWhiteSpace(warningType))
{
return BadRequest("Warning identity is required.");
}
await storyState.AcknowledgeWarningAsync(new ContinuityWarningAcknowledgement
{
ProjectID = returnFilter.ProjectID,
WarningType = warningType,
SceneID = sceneId,
CharacterID = characterId,
AssetID = assetId,
Notes = notes
});
return RedirectToAction(nameof(Index), returnFilter);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RestoreWarning(
string warningType,
int? sceneId,
int? characterId,
int? assetId,
[Bind(Prefix = "ReturnFilter")] ContinuityFilter returnFilter)
{
if (returnFilter.ProjectID == 0 || string.IsNullOrWhiteSpace(warningType))
{
return BadRequest("Warning identity is required.");
}
await storyState.RestoreWarningAsync(new ContinuityWarningAcknowledgement
{
ProjectID = returnFilter.ProjectID,
WarningType = warningType,
SceneID = sceneId,
CharacterID = characterId,
AssetID = assetId
});
return RedirectToAction(nameof(Index), returnFilter);
}
}

View File

@ -223,6 +223,14 @@ public interface IWarningRepository
Task FinaliseSceneDependencyWarningsAsync(int projectId, int? bookId);
}
public interface IContinuityWarningAcknowledgementRepository
{
Task<IReadOnlyList<ContinuityWarningAcknowledgement>> ListByProjectAsync(int projectId);
Task<ContinuityWarningAcknowledgement?> GetAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId);
Task SaveAsync(ContinuityWarningAcknowledgement acknowledgement);
Task DeleteAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId);
}
public interface ISceneDependencyRepository
{
Task<SceneDependencyLookupData> GetLookupsAsync();
@ -606,6 +614,69 @@ public sealed class WarningRepository(ISqlConnectionFactory connectionFactory) :
}
}
public sealed class ContinuityWarningAcknowledgementRepository(ISqlConnectionFactory connectionFactory) : IContinuityWarningAcknowledgementRepository
{
public async Task<IReadOnlyList<ContinuityWarningAcknowledgement>> ListByProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ContinuityWarningAcknowledgement>(
"dbo.ContinuityWarningAcknowledgement_ListByProject",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<ContinuityWarningAcknowledgement?> GetAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<ContinuityWarningAcknowledgement>(
"dbo.ContinuityWarningAcknowledgement_Get",
new
{
ProjectID = projectId,
WarningType = warningType,
SceneID = sceneId,
CharacterID = characterId,
AssetID = assetId
},
commandType: CommandType.StoredProcedure);
}
public async Task SaveAsync(ContinuityWarningAcknowledgement acknowledgement)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.ContinuityWarningAcknowledgement_Save",
new
{
acknowledgement.ProjectID,
acknowledgement.WarningType,
acknowledgement.SceneID,
acknowledgement.CharacterID,
acknowledgement.AssetID,
acknowledgement.AcknowledgedByUserID,
acknowledgement.Notes
},
commandType: CommandType.StoredProcedure);
}
public async Task DeleteAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.ContinuityWarningAcknowledgement_Delete",
new
{
ProjectID = projectId,
WarningType = warningType,
SceneID = sceneId,
CharacterID = characterId,
AssetID = assetId
},
commandType: CommandType.StoredProcedure);
}
}
public sealed class AnalyticsData
{
public IReadOnlyList<AnalyticsMetricPoint> MetricPoints { get; init; } = [];

View File

@ -1087,6 +1087,19 @@ public sealed class ContinuityWarning
public DateTime LastDetectedDate { get; set; }
}
public sealed class ContinuityWarningAcknowledgement
{
public int ContinuityWarningAcknowledgementID { get; set; }
public int ProjectID { get; set; }
public string WarningType { get; set; } = string.Empty;
public int? SceneID { get; set; }
public int? CharacterID { get; set; }
public int? AssetID { get; set; }
public DateTime AcknowledgedDate { get; set; }
public int? AcknowledgedByUserID { get; set; }
public string? Notes { get; set; }
}
public sealed class WarningLookupData
{
public IReadOnlyList<WarningType> WarningTypes { get; init; } = [];

View File

@ -92,6 +92,7 @@ public class Program
builder.Services.AddScoped<ICharacterRepository, CharacterRepository>();
builder.Services.AddScoped<ILocationRepository, LocationRepository>();
builder.Services.AddScoped<IWarningRepository, WarningRepository>();
builder.Services.AddScoped<IContinuityWarningAcknowledgementRepository, ContinuityWarningAcknowledgementRepository>();
builder.Services.AddScoped<ISceneDependencyRepository, SceneDependencyRepository>();
builder.Services.AddScoped<IAnalyticsRepository, AnalyticsRepository>();
builder.Services.AddScoped<IStoryBibleRepository, StoryBibleRepository>();

View File

@ -119,6 +119,10 @@ public interface IStoryStateService
Task<List<AssetJourneyViewModel>> GetAssetJourneyAsync(int projectId, int assetId, ContinuityFilter filter);
Task<List<LocationActivityViewModel>> GetLocationActivityAsync(int projectId, int locationId, ContinuityFilter filter);
Task<List<ContinuityWarningViewModel>> GetContinuityWarningsAsync(int projectId, ContinuityFilter filter);
Task AcknowledgeWarningAsync(ContinuityWarningAcknowledgement acknowledgement);
Task RestoreWarningAsync(ContinuityWarningAcknowledgement acknowledgement);
Task<bool> IsAcknowledgedAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId);
Task<List<ContinuityWarningAcknowledgement>> GetAcknowledgementsAsync(int projectId);
}
public interface IPlotService
@ -2216,7 +2220,9 @@ public sealed class StoryStateService(
ITimelineRepository timelineRepository,
IAssetRepository assets,
ICharacterRepository characters,
ILocationRepository locations) : IStoryStateService
ILocationRepository locations,
IContinuityWarningAcknowledgementRepository warningAcknowledgements,
ICurrentUserService currentUser) : IStoryStateService
{
public async Task<StoryStateViewModel> GetStoryStateAsync(int sceneId)
{
@ -2550,7 +2556,11 @@ public sealed class StoryStateService(
warnings.AddRange(BuildUnknownAssetLocationWarnings(assetTimeline.Assets, assetTimeline.Events, allAssetLocations, custodyCharacters, enabledAssetIds, context));
}
var acknowledgements = await GetAcknowledgementsAsync(projectId);
ApplyAcknowledgements(warnings, acknowledgements);
return warnings
.Where(x => filter.ShowAcknowledgedWarnings || !x.IsAcknowledged)
.OrderBy(x => WarningSeveritySort(x.Severity))
.ThenBy(x => x.BookDisplayName)
.ThenBy(x => x.ChapterDisplayName)
@ -2560,6 +2570,35 @@ public sealed class StoryStateService(
.ToList();
}
public async Task AcknowledgeWarningAsync(ContinuityWarningAcknowledgement acknowledgement)
{
acknowledgement.WarningType = acknowledgement.WarningType.Trim();
acknowledgement.Notes = string.IsNullOrWhiteSpace(acknowledgement.Notes) ? null : acknowledgement.Notes.Trim();
acknowledgement.AcknowledgedByUserID = currentUser.UserId;
await warningAcknowledgements.SaveAsync(acknowledgement);
}
public Task RestoreWarningAsync(ContinuityWarningAcknowledgement acknowledgement)
{
return warningAcknowledgements.DeleteAsync(
acknowledgement.ProjectID,
acknowledgement.WarningType.Trim(),
acknowledgement.SceneID,
acknowledgement.CharacterID,
acknowledgement.AssetID);
}
public async Task<bool> IsAcknowledgedAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId)
{
var acknowledgement = await warningAcknowledgements.GetAsync(projectId, warningType, sceneId, characterId, assetId);
return acknowledgement is not null;
}
public async Task<List<ContinuityWarningAcknowledgement>> GetAcknowledgementsAsync(int projectId)
{
return (await warningAcknowledgements.ListByProjectAsync(projectId)).ToList();
}
private async Task<RangeContext> GetRangeContextAsync(int projectId, ContinuityFilter filter)
{
NormaliseFilter(filter);
@ -3102,6 +3141,7 @@ public sealed class StoryStateService(
var sceneContext = sceneId.HasValue ? context.SceneContexts.GetValueOrDefault(sceneId.Value) : null;
return new ContinuityWarningViewModel
{
WarningKey = BuildWarningKey(warningType, sceneId, characterId, assetId),
WarningType = warningType,
Severity = severity,
Message = message,
@ -3114,6 +3154,47 @@ public sealed class StoryStateService(
};
}
private static void ApplyAcknowledgements(
IReadOnlyList<ContinuityWarningViewModel> warnings,
IReadOnlyList<ContinuityWarningAcknowledgement> acknowledgements)
{
var acknowledgementsByKey = acknowledgements.ToDictionary(
x => BuildWarningKey(x.WarningType, x.SceneID, x.CharacterID, x.AssetID),
StringComparer.OrdinalIgnoreCase);
foreach (var warning in warnings)
{
if (!acknowledgementsByKey.TryGetValue(warning.WarningKey, out var acknowledgement))
{
continue;
}
warning.IsAcknowledged = true;
warning.AcknowledgementNotes = acknowledgement.Notes;
}
}
private static string BuildWarningKey(string warningType, int? sceneId, int? characterId, int? assetId)
{
var parts = new List<string> { warningType.Trim() };
if (sceneId.HasValue)
{
parts.Add($"Scene:{sceneId.Value}");
}
if (characterId.HasValue)
{
parts.Add($"Character:{characterId.Value}");
}
if (assetId.HasValue)
{
parts.Add($"Asset:{assetId.Value}");
}
return string.Join("|", parts);
}
private static int WarningSeveritySort(string severity)
{
return severity switch

View File

@ -0,0 +1,152 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.ContinuityWarningAcknowledgements', N'U') IS NULL
BEGIN
CREATE TABLE dbo.ContinuityWarningAcknowledgements
(
ContinuityWarningAcknowledgementID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ContinuityWarningAcknowledgements PRIMARY KEY,
ProjectID int NOT NULL,
WarningType nvarchar(100) NOT NULL,
SceneID int NULL,
CharacterID int NULL,
AssetID int NULL,
AcknowledgedDate datetime2 NOT NULL CONSTRAINT DF_ContinuityWarningAcknowledgements_AcknowledgedDate DEFAULT SYSUTCDATETIME(),
AcknowledgedByUserID int NULL,
Notes nvarchar(500) NULL,
SceneIDKey AS ISNULL(SceneID, 0) PERSISTED,
CharacterIDKey AS ISNULL(CharacterID, 0) PERSISTED,
AssetIDKey AS ISNULL(AssetID, 0) PERSISTED,
CONSTRAINT FK_ContinuityWarningAcknowledgements_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT FK_ContinuityWarningAcknowledgements_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_ContinuityWarningAcknowledgements_Characters FOREIGN KEY (CharacterID) REFERENCES dbo.Characters(CharacterID),
CONSTRAINT FK_ContinuityWarningAcknowledgements_Assets FOREIGN KEY (AssetID) REFERENCES dbo.StoryAssets(StoryAssetID),
CONSTRAINT FK_ContinuityWarningAcknowledgements_AppUser FOREIGN KEY (AcknowledgedByUserID) REFERENCES dbo.AppUser(UserID)
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_ContinuityWarningAcknowledgements_Identity' AND object_id = OBJECT_ID(N'dbo.ContinuityWarningAcknowledgements'))
BEGIN
CREATE UNIQUE INDEX UX_ContinuityWarningAcknowledgements_Identity
ON dbo.ContinuityWarningAcknowledgements(ProjectID, WarningType, SceneIDKey, CharacterIDKey, AssetIDKey);
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarningAcknowledgement_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ContinuityWarningAcknowledgementID,
ProjectID,
WarningType,
SceneID,
CharacterID,
AssetID,
AcknowledgedDate,
AcknowledgedByUserID,
Notes
FROM dbo.ContinuityWarningAcknowledgements
WHERE ProjectID = @ProjectID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarningAcknowledgement_Get
@ProjectID int,
@WarningType nvarchar(100),
@SceneID int = NULL,
@CharacterID int = NULL,
@AssetID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT ContinuityWarningAcknowledgementID,
ProjectID,
WarningType,
SceneID,
CharacterID,
AssetID,
AcknowledgedDate,
AcknowledgedByUserID,
Notes
FROM dbo.ContinuityWarningAcknowledgements
WHERE ProjectID = @ProjectID
AND WarningType = @WarningType
AND ISNULL(SceneID, 0) = ISNULL(@SceneID, 0)
AND ISNULL(CharacterID, 0) = ISNULL(@CharacterID, 0)
AND ISNULL(AssetID, 0) = ISNULL(@AssetID, 0);
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarningAcknowledgement_Save
@ProjectID int,
@WarningType nvarchar(100),
@SceneID int = NULL,
@CharacterID int = NULL,
@AssetID int = NULL,
@AcknowledgedByUserID int = NULL,
@Notes nvarchar(500) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @TrimmedWarningType nvarchar(100) = NULLIF(LTRIM(RTRIM(@WarningType)), N'');
DECLARE @TrimmedNotes nvarchar(500) = NULLIF(LTRIM(RTRIM(@Notes)), N'');
IF @ProjectID <= 0 OR @TrimmedWarningType IS NULL
RETURN;
IF EXISTS
(
SELECT 1
FROM dbo.ContinuityWarningAcknowledgements
WHERE ProjectID = @ProjectID
AND WarningType = @TrimmedWarningType
AND ISNULL(SceneID, 0) = ISNULL(@SceneID, 0)
AND ISNULL(CharacterID, 0) = ISNULL(@CharacterID, 0)
AND ISNULL(AssetID, 0) = ISNULL(@AssetID, 0)
)
BEGIN
UPDATE dbo.ContinuityWarningAcknowledgements
SET AcknowledgedDate = SYSUTCDATETIME(),
AcknowledgedByUserID = @AcknowledgedByUserID,
Notes = @TrimmedNotes
WHERE ProjectID = @ProjectID
AND WarningType = @TrimmedWarningType
AND ISNULL(SceneID, 0) = ISNULL(@SceneID, 0)
AND ISNULL(CharacterID, 0) = ISNULL(@CharacterID, 0)
AND ISNULL(AssetID, 0) = ISNULL(@AssetID, 0);
END
ELSE
BEGIN
INSERT dbo.ContinuityWarningAcknowledgements
(ProjectID, WarningType, SceneID, CharacterID, AssetID, AcknowledgedByUserID, Notes)
VALUES
(@ProjectID, @TrimmedWarningType, @SceneID, @CharacterID, @AssetID, @AcknowledgedByUserID, @TrimmedNotes);
END;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarningAcknowledgement_Delete
@ProjectID int,
@WarningType nvarchar(100),
@SceneID int = NULL,
@CharacterID int = NULL,
@AssetID int = NULL
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM dbo.ContinuityWarningAcknowledgements
WHERE ProjectID = @ProjectID
AND WarningType = @WarningType
AND ISNULL(SceneID, 0) = ISNULL(@SceneID, 0)
AND ISNULL(CharacterID, 0) = ISNULL(@CharacterID, 0)
AND ISNULL(AssetID, 0) = ISNULL(@AssetID, 0);
END;
GO

View File

@ -289,6 +289,7 @@ public sealed class ContinuityFilter
public int? EndSceneID { get; set; }
public string DisplayMode { get; set; } = "StoryState";
public int? SelectedSceneID { get; set; }
public bool ShowAcknowledgedWarnings { get; set; }
public bool IncludeCharacters => EntityTypes.Contains("Characters", StringComparer.OrdinalIgnoreCase);
public bool IncludeAssets => EntityTypes.Contains("Assets", StringComparer.OrdinalIgnoreCase);
@ -316,6 +317,7 @@ public sealed class ContinuityExplorerViewModel
public sealed class ContinuityWarningViewModel
{
public string WarningKey { get; set; } = string.Empty;
public string WarningType { get; set; } = string.Empty;
public string Severity { get; set; } = "Information";
public string Message { get; set; } = string.Empty;
@ -325,6 +327,8 @@ public sealed class ContinuityWarningViewModel
public string SceneDisplayName { get; set; } = string.Empty;
public string BookDisplayName { get; set; } = string.Empty;
public string ChapterDisplayName { get; set; } = string.Empty;
public bool IsAcknowledged { get; set; }
public string? AcknowledgementNotes { get; set; }
}
public sealed class CharacterJourneyViewModel

View File

@ -161,6 +161,10 @@
<div class="col-lg-8">
<label class="form-label" for="SelectedSceneID">Story State scene</label>
<select class="form-select form-select-sm" id="SelectedSceneID" name="SelectedSceneID" asp-items="Model.SceneOptions"></select>
<label class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="ShowAcknowledgedWarnings" value="true" checked="@Model.Filter.ShowAcknowledgedWarnings" />
<span class="form-check-label">Show acknowledged warnings</span>
</label>
</div>
</div>
@ -377,9 +381,11 @@ else if (Model.Filter.DisplayMode == "ContinuityWarnings")
<thead>
<tr>
<th>Severity</th>
<th>Status</th>
<th>Warning Type</th>
<th>Message</th>
<th>Scene</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -387,8 +393,24 @@ else if (Model.Filter.DisplayMode == "ContinuityWarnings")
{
<tr>
<td><span class="badge @WarningBadgeClass(warning.Severity)">@warning.Severity</span></td>
<td>
@if (warning.IsAcknowledged)
{
<span class="badge bg-secondary">Acknowledged</span>
}
else
{
<span class="badge bg-warning text-dark">Active</span>
}
</td>
<td>@warning.WarningType</td>
<td>@warning.Message</td>
<td>
<div>@warning.Message</div>
@if (warning.IsAcknowledged && !string.IsNullOrWhiteSpace(warning.AcknowledgementNotes))
{
<small class="text-muted">Note: @warning.AcknowledgementNotes</small>
}
</td>
<td>
@if (warning.SceneID.HasValue)
{
@ -399,6 +421,79 @@ else if (Model.Filter.DisplayMode == "ContinuityWarnings")
<span class="muted">Project-wide</span>
}
</td>
<td>
@if (warning.IsAcknowledged)
{
<form asp-action="RestoreWarning" method="post" class="d-inline">
<input type="hidden" name="WarningType" value="@warning.WarningType" />
<input type="hidden" name="SceneID" value="@warning.SceneID" />
<input type="hidden" name="CharacterID" value="@warning.CharacterID" />
<input type="hidden" name="AssetID" value="@warning.AssetID" />
<input type="hidden" name="ReturnFilter.ProjectID" value="@Model.Filter.ProjectID" />
@foreach (var entityType in Model.Filter.EntityTypes)
{
<input type="hidden" name="ReturnFilter.EntityTypes" value="@entityType" />
}
@foreach (var characterId in Model.Filter.CharacterIDs)
{
<input type="hidden" name="ReturnFilter.CharacterIDs" value="@characterId" />
}
@foreach (var assetId in Model.Filter.AssetIDs)
{
<input type="hidden" name="ReturnFilter.AssetIDs" value="@assetId" />
}
@foreach (var locationId in Model.Filter.LocationIDs)
{
<input type="hidden" name="ReturnFilter.LocationIDs" value="@locationId" />
}
<input type="hidden" name="ReturnFilter.SceneRangeMode" value="@Model.Filter.SceneRangeMode" />
<input type="hidden" name="ReturnFilter.BookID" value="@Model.Filter.BookID" />
<input type="hidden" name="ReturnFilter.ChapterID" value="@Model.Filter.ChapterID" />
<input type="hidden" name="ReturnFilter.StartSceneID" value="@Model.Filter.StartSceneID" />
<input type="hidden" name="ReturnFilter.EndSceneID" value="@Model.Filter.EndSceneID" />
<input type="hidden" name="ReturnFilter.DisplayMode" value="@Model.Filter.DisplayMode" />
<input type="hidden" name="ReturnFilter.SelectedSceneID" value="@Model.Filter.SelectedSceneID" />
<input type="hidden" name="ReturnFilter.ShowAcknowledgedWarnings" value="@Model.Filter.ShowAcknowledgedWarnings" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Restore</button>
</form>
}
else
{
<form asp-action="AcknowledgeWarning" method="post" class="d-flex gap-1 align-items-center">
<input type="hidden" name="WarningType" value="@warning.WarningType" />
<input type="hidden" name="SceneID" value="@warning.SceneID" />
<input type="hidden" name="CharacterID" value="@warning.CharacterID" />
<input type="hidden" name="AssetID" value="@warning.AssetID" />
<input type="hidden" name="ReturnFilter.ProjectID" value="@Model.Filter.ProjectID" />
@foreach (var entityType in Model.Filter.EntityTypes)
{
<input type="hidden" name="ReturnFilter.EntityTypes" value="@entityType" />
}
@foreach (var characterId in Model.Filter.CharacterIDs)
{
<input type="hidden" name="ReturnFilter.CharacterIDs" value="@characterId" />
}
@foreach (var assetId in Model.Filter.AssetIDs)
{
<input type="hidden" name="ReturnFilter.AssetIDs" value="@assetId" />
}
@foreach (var locationId in Model.Filter.LocationIDs)
{
<input type="hidden" name="ReturnFilter.LocationIDs" value="@locationId" />
}
<input type="hidden" name="ReturnFilter.SceneRangeMode" value="@Model.Filter.SceneRangeMode" />
<input type="hidden" name="ReturnFilter.BookID" value="@Model.Filter.BookID" />
<input type="hidden" name="ReturnFilter.ChapterID" value="@Model.Filter.ChapterID" />
<input type="hidden" name="ReturnFilter.StartSceneID" value="@Model.Filter.StartSceneID" />
<input type="hidden" name="ReturnFilter.EndSceneID" value="@Model.Filter.EndSceneID" />
<input type="hidden" name="ReturnFilter.DisplayMode" value="@Model.Filter.DisplayMode" />
<input type="hidden" name="ReturnFilter.SelectedSceneID" value="@Model.Filter.SelectedSceneID" />
<input type="hidden" name="ReturnFilter.ShowAcknowledgedWarnings" value="@Model.Filter.ShowAcknowledgedWarnings" />
<input class="form-control form-control-sm" type="text" name="Notes" maxlength="500" placeholder="Optional note" />
<button class="btn btn-outline-primary btn-sm" type="submit">Acknowledge</button>
</form>
}
</td>
</tr>
}
</tbody>