feat(creator): overhaul Creator flow, editor duplication, and staging setup #83

Merged
mjasin merged 15 commits from feature/stage3-book-versioning into develop 2026-06-15 17:15:43 +00:00
Collaborator

This pull request completely overhauls the Creator editor flow, resolves the editor duplication race condition, aligns layout/styling themes in light and dark mode, and adds Docker staging setups.

Key Changes

  1. Creator Flow Polish: Redesigned the editor canvas to prevent double scrolling by delegating overflow to the editor canvas layer, updated styles to a premium aesthetic.
  2. Race Condition Prevention: Resolved Crepe editor duplication when loading or switching chapters by tracking state via shared window maps (window.editorCache, window.editorStates) and checking _lastInitializedEditorId synchronously in Blazor.
  3. Theme Synchronization: Integrated explicit theme initialization (ThemeService.InitializeAsync()) and anchored CSS isolation selectors to correctly sync with Light (Soft Sepia) and Deep Dark theme preferences.
  4. Staging Automation: Created staging docker configurations with --nexus-only flag to allow iterative development without resetting PG/Neo4j database containers.
This pull request completely overhauls the Creator editor flow, resolves the editor duplication race condition, aligns layout/styling themes in light and dark mode, and adds Docker staging setups. ### Key Changes 1. **Creator Flow Polish**: Redesigned the editor canvas to prevent double scrolling by delegating overflow to the editor canvas layer, updated styles to a premium aesthetic. 2. **Race Condition Prevention**: Resolved Crepe editor duplication when loading or switching chapters by tracking state via shared window maps (`window.editorCache`, `window.editorStates`) and checking `_lastInitializedEditorId` synchronously in Blazor. 3. **Theme Synchronization**: Integrated explicit theme initialization (`ThemeService.InitializeAsync()`) and anchored CSS isolation selectors to correctly sync with Light (Soft Sepia) and Deep Dark theme preferences. 4. **Staging Automation**: Created staging docker configurations with `--nexus-only` flag to allow iterative development without resetting PG/Neo4j database containers.
Antigravity added 14 commits 2026-06-15 17:07:33 +00:00
- Relocate dashboard routing to /creator and editor workspace to /creator/edit/{BookId}
- Implement CreateBookCommand and handler with transactional default chapter seeding
- Implement PublishBookVersionCommand and GetCreatorDashboardDataQuery
- Build CreatorDashboard modal and UI components with customized dark input styles
- Add run-stage.sh script to automate staging environment setup, database migrations, and health checks
- Update developer workflow rules in GEMINI.md
- Implementedfullscreen wrapper layout to eliminate browser scrollbar bugs
- Styled matte finish sidebar with active chapter indicators
- Standardized header alignment with clean monospace ID badge
- Integrated footer panel for state save indicators and premium action triggers
Antigravity reviewed 2026-06-15 17:10:31 +00:00
Antigravity left a comment
Author
Collaborator

I have reviewed the changes in PR #83. Since this is my own PR, I cannot submit a formal 'Request Changes' request, but I have left inline comments pointing out key issues including a concurrency race condition in the editor's debounce timer and some violations of the project's strict Result Pattern architecture.

I have reviewed the changes in PR #83. Since this is my own PR, I cannot submit a formal 'Request Changes' request, but I have left inline comments pointing out key issues including a concurrency race condition in the editor's debounce timer and some violations of the project's strict Result Pattern architecture.
@@ -0,0 +38,4 @@
if (book == null)
{
throw new BookNotFoundException(request.BookId);
Author
Collaborator

🟡 Design/Architecture: Violation of Result Pattern

Throwing BookNotFoundException inside the handler violates the project's strict architecture rule: Result Pattern: Zero exceptions for flow control. All handlers return Result<T> via FluentResult.

Suggested Fix:
Instead of throwing, return Result.Fail(...) containing an error message or a dedicated error class:

if (book == null)
{
    return Result.Fail(new Error($"Book with ID '{request.BookId}' was not found."));
}

Remember to update the MapPost endpoint in Program.cs and the tests as well.

🟡 Design/Architecture: Violation of Result Pattern Throwing `BookNotFoundException` inside the handler violates the project's strict architecture rule: `Result Pattern: Zero exceptions for flow control. All handlers return Result<T> via FluentResult.` **Suggested Fix:** Instead of throwing, return `Result.Fail(...)` containing an error message or a dedicated error class: ```csharp if (book == null) { return Result.Fail(new Error($"Book with ID '{request.BookId}' was not found.")); } ``` Remember to update the MapPost endpoint in `Program.cs` and the tests as well.
mjasin marked this conversation as resolved
@@ -0,0 +41,4 @@
if (!bookExists)
{
throw new BookNotFoundException(request.BookId);
Author
Collaborator

🟡 Design/Architecture: Violation of Result Pattern

Throwing BookNotFoundException inside the query handler violates the project's Result Pattern rule.

Suggested Fix:
Instead of throwing, return Result.Fail(...):

if (!bookExists)
{
    return Result.Fail<List<CreatorBookRevisionDto>>(new Error($"Book with ID '{request.BookId}' was not found."));
}
🟡 Design/Architecture: Violation of Result Pattern Throwing `BookNotFoundException` inside the query handler violates the project's Result Pattern rule. **Suggested Fix:** Instead of throwing, return `Result.Fail(...)`: ```csharp if (!bookExists) { return Result.Fail<List<CreatorBookRevisionDto>>(new Error($"Book with ID '{request.BookId}' was not found.")); } ```
mjasin marked this conversation as resolved
@@ -86,0 +374,4 @@
}
// Start 5-second idle debounce timer
_ = Task.Run(async () =>
Author
Collaborator

🔴 Blocking: Race Condition in Debounce Timer

The background task reads _debounceCts.Token from the shared component state after the task starts executing on the thread pool. If another keystroke happens in the meantime, it will read the new token.

Suggested Fix:
Capture the token synchronously on the UI thread before calling Task.Run and use the captured token directly inside the task:

CancellationToken token;
lock (_timerLock)
{
    token = _debounceCts.Token;
}

// Start 5-second idle debounce timer
_ = Task.Run(async () =>
{
    try
    {
        await Task.Delay(5000, token);
        await TriggerAutosaveAsync(currentMarkdown, token);
    }
    catch (TaskCanceledException) { }
    catch (Exception ex)
    {
        Console.WriteLine($"[MarkdownEditor] Debounce timer exception: {ex.Message}");
    }
});
🔴 Blocking: Race Condition in Debounce Timer The background task reads `_debounceCts.Token` from the shared component state *after* the task starts executing on the thread pool. If another keystroke happens in the meantime, it will read the *new* token. **Suggested Fix:** Capture the token synchronously on the UI thread before calling `Task.Run` and use the captured token directly inside the task: ```csharp CancellationToken token; lock (_timerLock) { token = _debounceCts.Token; } // Start 5-second idle debounce timer _ = Task.Run(async () => { try { await Task.Delay(5000, token); await TriggerAutosaveAsync(currentMarkdown, token); } catch (TaskCanceledException) { } catch (Exception ex) { Console.WriteLine($"[MarkdownEditor] Debounce timer exception: {ex.Message}"); } }); ```
mjasin marked this conversation as resolved
@@ -86,0 +399,4 @@
});
}
private async Task TriggerAutosaveAsync(string markdown, CancellationToken token)
Author
Collaborator

🟢 Minor/Suggestion: Missing component disposal check

TriggerAutosaveAsync runs on a background thread and calls await InvokeAsync(StateHasChanged). If the component has been disposed before or during the save operation, calling StateHasChanged may result in runtime warnings/exceptions.

Suggested Fix:
Define a _disposed boolean flag, set it to true in DisposeAsync, and check it before updating state:

private bool _disposed;

// In DisposeAsync:
_disposed = true;

// In TriggerAutosaveAsync:
if (_disposed) return;
_status = SaveStatus.Saving;
await InvokeAsync(StateHasChanged);
🟢 Minor/Suggestion: Missing component disposal check `TriggerAutosaveAsync` runs on a background thread and calls `await InvokeAsync(StateHasChanged)`. If the component has been disposed before or during the save operation, calling `StateHasChanged` may result in runtime warnings/exceptions. **Suggested Fix:** Define a `_disposed` boolean flag, set it to `true` in `DisposeAsync`, and check it before updating state: ```csharp private bool _disposed; // In DisposeAsync: _disposed = true; // In TriggerAutosaveAsync: if (_disposed) return; _status = SaveStatus.Saving; await InvokeAsync(StateHasChanged); ```
mjasin marked this conversation as resolved
@@ -0,0 +212,4 @@
// Act & Assert
var action = () => handler.Handle(command, CancellationToken.None);
await action.Should().ThrowAsync<BookNotFoundException>();
Author
Collaborator

🟡 Design/Architecture: Update test for Result Pattern alignment

Once the handler is refactored to return Result.Fail instead of throwing BookNotFoundException, update this test to assert failure on the returned result rather than expecting an exception.

Suggested Fix:

var result = await handler.Handle(command, CancellationToken.None);
result.IsSuccess.Should().BeFalse();
🟡 Design/Architecture: Update test for Result Pattern alignment Once the handler is refactored to return `Result.Fail` instead of throwing `BookNotFoundException`, update this test to assert failure on the returned result rather than expecting an exception. **Suggested Fix:** ```csharp var result = await handler.Handle(command, CancellationToken.None); result.IsSuccess.Should().BeFalse(); ```
mjasin marked this conversation as resolved
@@ -0,0 +263,4 @@
// Act & Assert
var queryMismatchedTenant = new GetBookRevisionsQuery(bookId, userId, "different-tenant");
var actionTenant = () => handler.Handle(queryMismatchedTenant, CancellationToken.None);
await actionTenant.Should().ThrowAsync<BookNotFoundException>();
Author
Collaborator

🟡 Design/Architecture: Update test for Result Pattern alignment

Update this test to assert failure on the returned result rather than expecting an exception once the query handler is refactored.

Suggested Fix:

var result = await handler.Handle(queryMismatchedTenant, CancellationToken.None);
result.IsSuccess.Should().BeFalse();
🟡 Design/Architecture: Update test for Result Pattern alignment Update this test to assert failure on the returned result rather than expecting an exception once the query handler is refactored. **Suggested Fix:** ```csharp var result = await handler.Handle(queryMismatchedTenant, CancellationToken.None); result.IsSuccess.Should().BeFalse(); ```
mjasin marked this conversation as resolved
mjasin added 1 commit 2026-06-15 17:14:27 +00:00
Author
Collaborator

I have resolved all 6 inline comments in the latest push:

  1. Debounce Timer Race: Fixed by capturing the CancellationToken synchronously under lock on the UI thread before starting Task.Run.
  2. Disposed state check: Added _disposed check in TriggerAutosaveAsync and before updating state after async calls, setting it in DisposeAsync.
  3. Result Pattern Violations: Replaced all BookNotFoundException throws in PublishBookVersionCommandHandler and GetBookRevisionsQueryHandler with Result.Fail.
  4. Web API Endpoint alignment: Updated Program.cs to return Results.NotFound(errorMsg) when a Result fail contains 'was not found' instead of catching the exception.
  5. Unit Tests: Updated the unit tests in PublishBookVersionTests.cs and CreatorDashboardTests.cs to assert result failure rather than expecting thrown exceptions. Verified all 54 tests pass successfully.
I have resolved all 6 inline comments in the latest push: 1. **Debounce Timer Race**: Fixed by capturing the `CancellationToken` synchronously under lock on the UI thread before starting `Task.Run`. 2. **Disposed state check**: Added `_disposed` check in `TriggerAutosaveAsync` and before updating state after async calls, setting it in `DisposeAsync`. 3. **Result Pattern Violations**: Replaced all `BookNotFoundException` throws in `PublishBookVersionCommandHandler` and `GetBookRevisionsQueryHandler` with `Result.Fail`. 4. **Web API Endpoint alignment**: Updated `Program.cs` to return `Results.NotFound(errorMsg)` when a Result fail contains `'was not found'` instead of catching the exception. 5. **Unit Tests**: Updated the unit tests in `PublishBookVersionTests.cs` and `CreatorDashboardTests.cs` to assert result failure rather than expecting thrown exceptions. Verified all 54 tests pass successfully.
mjasin merged commit c94e8f0acb into develop 2026-06-15 17:15:43 +00:00
mjasin deleted branch feature/stage3-book-versioning 2026-06-15 17:15:43 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mjasin/Nexus.Reader#83