refactor: remove crash workaround scaffolding
This commit is contained in:
18
README.md
18
README.md
@@ -59,7 +59,7 @@ Frontend:
|
|||||||
Current repo note:
|
Current repo note:
|
||||||
|
|
||||||
- `POSTMORTEM.md` documents why the previous authenticated workspace architecture was fragile under Firefox plus RoboForm.
|
- `POSTMORTEM.md` documents why the previous authenticated workspace architecture was fragile under Firefox plus RoboForm.
|
||||||
- `TASKS.md` records the route-first rewrite that addressed that architecture.
|
- `TASKS.md` records the route-first rewrite and the final Blazor configuration change that resolved the Firefox plus RoboForm crash.
|
||||||
|
|
||||||
## Runtime and Persistence
|
## Runtime and Persistence
|
||||||
|
|
||||||
@@ -115,6 +115,16 @@ Authenticated interactivity is route-local instead of global:
|
|||||||
- Blazor startup is manual with `Blazor.start({ ssr: { disableDomPreservation: true } })` so the app can disable enhanced SSR DOM preservation during interactive attach
|
- Blazor startup is manual with `Blazor.start({ ssr: { disableDomPreservation: true } })` so the app can disable enhanced SSR DOM preservation during interactive attach
|
||||||
- Header route changes now use full document navigation so moving between authenticated routes remounts the target per-page interactive root instead of trying to reuse the previous page circuit
|
- Header route changes now use full document navigation so moving between authenticated routes remounts the target per-page interactive root instead of trying to reuse the previous page circuit
|
||||||
|
|
||||||
|
Firefox plus RoboForm resolution:
|
||||||
|
|
||||||
|
- the route-first rewrite reduced the authenticated surface area, but it was not the final fix
|
||||||
|
- the crash stopped only after the app stopped using global Blazor interactivity
|
||||||
|
- the working combination is:
|
||||||
|
- per-page `InteractiveServerRenderMode(prerender: false)` on `/play`, `/campaigns`, and `/admin`
|
||||||
|
- manual `Blazor.start({ ssr: { disableDomPreservation: true } })`
|
||||||
|
- full document navigation between authenticated routes with `forceLoad: true`
|
||||||
|
- earlier phased first-render shells and heavy diagnostics were investigative steps and have been removed
|
||||||
|
|
||||||
Interactive bootstrap is now route-local:
|
Interactive bootstrap is now route-local:
|
||||||
|
|
||||||
- `WorkspaceRouteView.razor` performs the first-render JS-dependent session initialization for the authenticated route that mounted
|
- `WorkspaceRouteView.razor` performs the first-render JS-dependent session initialization for the authenticated route that mounted
|
||||||
@@ -201,10 +211,8 @@ SQLite migration rule:
|
|||||||
- Static assets are linked through Blazor's `@Assets[...]` pipeline for fingerprinted cache-busting URLs.
|
- Static assets are linked through Blazor's `@Assets[...]` pipeline for fingerprinted cache-busting URLs.
|
||||||
- Workspace reads are resolved through API requests in `WorkspaceQueryService`; browser interop stays focused on auth forms, session storage, SSE wiring, and small DOM helpers.
|
- Workspace reads are resolved through API requests in `WorkspaceQueryService`; browser interop stays focused on auth forms, session storage, SSE wiring, and small DOM helpers.
|
||||||
- Interactive authenticated startup begins in `WorkspaceRouteView.razor` after first render because `RpgRollerApiClient` still depends on JS interop-backed `fetch`.
|
- Interactive authenticated startup begins in `WorkspaceRouteView.razor` after first render because `RpgRollerApiClient` still depends on JS interop-backed `fetch`.
|
||||||
- Workspace startup diagnostics now log route initialization, route-content render phases, and browser-side workspace mutation snapshots to help isolate the remaining Firefox startup crash documented in `POSTMORTEM.md`.
|
- Authenticated routes avoid global `Routes @rendermode` because upstream issue `dotnet/aspnetcore#58824` reports Firefox-specific failures with global interactivity and explicitly calls out per-page mode as the safer path.
|
||||||
- Pre-Blazor diagnostics also watch the static `#rr-interactive-host` container before `_framework/blazor.web.js` connects, so extension-driven DOM mutations can be compared against the first failing interactive batch.
|
- Authenticated route changes use full document navigations so each route remounts its own per-page interactive root.
|
||||||
- Authenticated routes now avoid global `Routes @rendermode` because upstream issue `dotnet/aspnetcore#58824` reports Firefox-specific failures with global interactivity and explicitly calls out per-page mode as the safer path.
|
|
||||||
- Authenticated routes now mount through phased interactive batches: minimal shell first, then a simple header placeholder, then route skeletons, and only then the real header and control-heavy route content after route initialization succeeds.
|
|
||||||
- Live workspace refresh compares separate roster, per-character sheet, and log versions so unrelated changes do not trigger full reloads.
|
- Live workspace refresh compares separate roster, per-character sheet, and log versions so unrelated changes do not trigger full reloads.
|
||||||
- Campaign log data is loaded in bounded slices: campaign summaries, one selected roster, one selected character sheet, and a 25-row incremental log window from `/api/campaigns/{campaignId}/log/page`.
|
- Campaign log data is loaded in bounded slices: campaign summaries, one selected roster, one selected character sheet, and a 25-row incremental log window from `/api/campaigns/{campaignId}/log/page`.
|
||||||
- Log rows return compact summary data first and lazy-load full detail from `/api/rolls/{rollId}` when expanded.
|
- Log rows return compact summary data first and lazy-load full detail from `/api/rolls/{rollId}` when expanded.
|
||||||
|
|||||||
@@ -25,16 +25,11 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div id="rr-interactive-host" data-request-path="@RequestPath">
|
<Routes/>
|
||||||
<Routes/>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
<script src="js/rpgroller-api.js"></script>
|
<script src="js/rpgroller-api.js"></script>
|
||||||
@if (UseInteractiveApp)
|
@if (UseInteractiveApp)
|
||||||
{
|
{
|
||||||
<script>
|
|
||||||
window.rpgRollerApi.bootstrapPreBlazorDiagnostics("@RequestPath");
|
|
||||||
</script>
|
|
||||||
<script src="_framework/blazor.web.js" autostart="false"></script>
|
<script src="_framework/blazor.web.js" autostart="false"></script>
|
||||||
<script>
|
<script>
|
||||||
Blazor.start({
|
Blazor.start({
|
||||||
@@ -67,8 +62,6 @@ else
|
|||||||
|
|
||||||
private bool AuthStatusIsError => string.Equals(ReadAuthQueryValue("kind"), "error", StringComparison.OrdinalIgnoreCase);
|
private bool AuthStatusIsError => string.Equals(ReadAuthQueryValue("kind"), "error", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private string RequestPath => HttpContext?.Request.Path.Value ?? "/";
|
|
||||||
|
|
||||||
private string BaseHref
|
private string BaseHref
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -1,86 +1,70 @@
|
|||||||
@using Microsoft.AspNetCore.Components
|
@using Microsoft.AspNetCore.Components
|
||||||
|
|
||||||
@if (!Workspace.ShowLiveContent)
|
<main class="management-screen">
|
||||||
{
|
@if (Workspace.State.IsCurrentUserAdmin)
|
||||||
<main class="management-screen">
|
{
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h2>Admin</h2>
|
<h2>Database</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="skeleton-stack">
|
<p class="muted">Download the current SQLite file for backup or offline inspection.</p>
|
||||||
<div class="skeleton-line"></div>
|
<div class="management-actions">
|
||||||
<div class="skeleton-line short"></div>
|
<a class="action-link" href="@Workspace.AdminDatabaseDownloadUrl" download>Download SQLite database</a>
|
||||||
<div class="skeleton-line"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
}
|
||||||
}
|
<section class="card">
|
||||||
else
|
<div class="section-head">
|
||||||
{
|
<h2>User Management</h2>
|
||||||
<main class="management-screen">
|
</div>
|
||||||
@if (Workspace.State.IsCurrentUserAdmin)
|
@if (IsAdminDataLoading)
|
||||||
{
|
{
|
||||||
<section class="card">
|
<p class="empty">Loading users...</p>
|
||||||
<div class="section-head">
|
|
||||||
<h2>Database</h2>
|
|
||||||
</div>
|
|
||||||
<p class="muted">Download the current SQLite file for backup or offline inspection.</p>
|
|
||||||
<div class="management-actions">
|
|
||||||
<a class="action-link" href="@Workspace.AdminDatabaseDownloadUrl" download>Download SQLite database</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
}
|
||||||
<section class="card">
|
else if (!Workspace.State.IsCurrentUserAdmin)
|
||||||
<div class="section-head">
|
{
|
||||||
<h2>User Management</h2>
|
<p class="empty">Admin role is required to manage users.</p>
|
||||||
</div>
|
}
|
||||||
@if (IsAdminDataLoading)
|
else if (Workspace.State.AdminUsers.Count == 0)
|
||||||
{
|
{
|
||||||
<p class="empty">Loading users...</p>
|
<p class="empty">No users found.</p>
|
||||||
}
|
}
|
||||||
else if (!Workspace.State.IsCurrentUserAdmin)
|
else
|
||||||
{
|
{
|
||||||
<p class="empty">Admin role is required to manage users.</p>
|
<ul class="management-list">
|
||||||
}
|
@foreach (var user in Workspace.State.AdminUsers)
|
||||||
else if (Workspace.State.AdminUsers.Count == 0)
|
{
|
||||||
{
|
<li>
|
||||||
<p class="empty">No users found.</p>
|
<div>
|
||||||
}
|
<strong>@user.Username</strong>
|
||||||
else
|
<p class="muted">@user.DisplayName</p>
|
||||||
{
|
<p class="muted">Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))</p>
|
||||||
<ul class="management-list">
|
</div>
|
||||||
@foreach (var user in Workspace.State.AdminUsers)
|
<div class="skill-chip-actions">
|
||||||
{
|
<button type="button"
|
||||||
<li>
|
class="chip-button"
|
||||||
<div>
|
disabled="@(Workspace.State.IsMutating || user.Id == Workspace.State.User?.Id)"
|
||||||
<strong>@user.Username</strong>
|
@onclick="() => Workspace.Admin.ToggleAdminRoleAsync(user)">
|
||||||
<p class="muted">@user.DisplayName</p>
|
<span aria-hidden="true" class="emoji">🛡️</span>
|
||||||
<p class="muted">Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))</p>
|
<span class="sr-only">Toggle admin role for @user.Username</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="skill-chip-actions">
|
<button type="button"
|
||||||
<button type="button"
|
class="chip-button"
|
||||||
class="chip-button"
|
disabled="@(Workspace.State.IsMutating || user.Id == Workspace.State.User?.Id)"
|
||||||
disabled="@(Workspace.State.IsMutating || user.Id == Workspace.State.User?.Id)"
|
@onclick="() => Workspace.Admin.DeleteUserAsync(user)">
|
||||||
@onclick="() => Workspace.Admin.ToggleAdminRoleAsync(user)">
|
<span aria-hidden="true" class="emoji">🗑️</span>
|
||||||
<span aria-hidden="true" class="emoji">🛡️</span>
|
<span class="sr-only">Delete user @user.Username</span>
|
||||||
<span class="sr-only">Toggle admin role for @user.Username</span>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
<button type="button"
|
</li>
|
||||||
class="chip-button"
|
}
|
||||||
disabled="@(Workspace.State.IsMutating || user.Id == Workspace.State.User?.Id)"
|
</ul>
|
||||||
@onclick="() => Workspace.Admin.DeleteUserAsync(user)">
|
}
|
||||||
<span aria-hidden="true" class="emoji">🗑️</span>
|
</section>
|
||||||
<span class="sr-only">Delete user @user.Username</span>
|
</main>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
||||||
|
|
||||||
private bool IsAdminDataLoading => !Workspace.HasSessionInitialized || Workspace.State.IsAdminDataLoading;
|
private bool IsAdminDataLoading => !Workspace.HasSessionInitialized || Workspace.State.IsAdminDataLoading;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages;
|
|
||||||
|
|
||||||
[ExcludeFromCodeCoverage]
|
|
||||||
public partial class AdminWorkspaceContent
|
|
||||||
{
|
|
||||||
protected override void OnParametersSet()
|
|
||||||
{
|
|
||||||
Logger.LogInformation("AdminWorkspaceContent.OnParametersSet [{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeAdminSurface(Workspace));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
Logger.LogInformation("AdminWorkspaceContent.OnAfterRenderAsync firstRender={FirstRender} [{State}]",
|
|
||||||
firstRender, WorkspaceDiagnosticSummary.DescribeAdminSurface(Workspace));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Inject] private ILogger<AdminWorkspaceContent> Logger { get; set; } = null!;
|
|
||||||
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
|
||||||
}
|
|
||||||
@@ -1,44 +1,28 @@
|
|||||||
@using Microsoft.AspNetCore.Components
|
@using Microsoft.AspNetCore.Components
|
||||||
@using RpgRoller.Components.Pages.HomeControls
|
@using RpgRoller.Components.Pages.HomeControls
|
||||||
|
|
||||||
@if (!Workspace.ShowLiveContent)
|
<CampaignManagementPanel
|
||||||
{
|
Campaigns="Workspace.State.Campaigns"
|
||||||
<main class="management-screen">
|
SelectedCampaignId="Workspace.State.SelectedCampaignId"
|
||||||
<section class="card">
|
SelectedCampaign="Workspace.State.SelectedCampaign"
|
||||||
<div class="section-head">
|
Rulesets="Workspace.State.Rulesets"
|
||||||
<h2>Campaign Management</h2>
|
IsMutating="Workspace.State.IsMutating"
|
||||||
</div>
|
OwnerLabel="Workspace.State.OwnerLabel"
|
||||||
<div class="skeleton-stack">
|
CanEditCharacter="Workspace.Campaigns.CanEditCharacter"
|
||||||
<div class="skeleton-line"></div>
|
CanDeleteCharacter="Workspace.Campaigns.CanDeleteCharacter"
|
||||||
<div class="skeleton-line short"></div>
|
CanDeleteCampaign="Workspace.State.CanDeleteSelectedCampaign"
|
||||||
<div class="skeleton-line"></div>
|
CampaignSelectionChanged="OnCampaignSelectionChangedAsync"
|
||||||
</div>
|
CampaignCreated="Workspace.Campaigns.OnCampaignCreatedAsync"
|
||||||
</section>
|
DeleteCampaignRequested="Workspace.Campaigns.DeleteSelectedCampaignAsync"
|
||||||
</main>
|
CreateCharacterRequested="Workspace.Campaigns.OpenCreateCharacterModal"
|
||||||
}
|
EditCharacterRequested="Workspace.Campaigns.OpenEditCharacterModal"
|
||||||
else
|
DeleteCharacterRequested="Workspace.Campaigns.DeleteCharacterAsync"/>
|
||||||
{
|
|
||||||
<CampaignManagementPanel
|
|
||||||
Campaigns="Workspace.State.Campaigns"
|
|
||||||
SelectedCampaignId="Workspace.State.SelectedCampaignId"
|
|
||||||
SelectedCampaign="Workspace.State.SelectedCampaign"
|
|
||||||
Rulesets="Workspace.State.Rulesets"
|
|
||||||
IsMutating="Workspace.State.IsMutating"
|
|
||||||
OwnerLabel="Workspace.State.OwnerLabel"
|
|
||||||
CanEditCharacter="Workspace.Campaigns.CanEditCharacter"
|
|
||||||
CanDeleteCharacter="Workspace.Campaigns.CanDeleteCharacter"
|
|
||||||
CanDeleteCampaign="Workspace.State.CanDeleteSelectedCampaign"
|
|
||||||
CampaignSelectionChanged="OnCampaignSelectionChangedAsync"
|
|
||||||
CampaignCreated="Workspace.Campaigns.OnCampaignCreatedAsync"
|
|
||||||
DeleteCampaignRequested="Workspace.Campaigns.DeleteSelectedCampaignAsync"
|
|
||||||
CreateCharacterRequested="Workspace.Campaigns.OpenCreateCharacterModal"
|
|
||||||
EditCharacterRequested="Workspace.Campaigns.OpenEditCharacterModal"
|
|
||||||
DeleteCharacterRequested="Workspace.Campaigns.DeleteCharacterAsync"/>
|
|
||||||
|
|
||||||
<CharacterManagementModals Workspace="Workspace"/>
|
<CharacterManagementModals Workspace="Workspace"/>
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
||||||
|
|
||||||
private async Task OnCampaignSelectionChangedAsync(ChangeEventArgs args)
|
private async Task OnCampaignSelectionChangedAsync(ChangeEventArgs args)
|
||||||
{
|
{
|
||||||
await Workspace.Campaigns.OnCampaignSelectionChangedAsync(args);
|
await Workspace.Campaigns.OnCampaignSelectionChangedAsync(args);
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages;
|
|
||||||
|
|
||||||
[ExcludeFromCodeCoverage]
|
|
||||||
public partial class CampaignsWorkspaceContent
|
|
||||||
{
|
|
||||||
protected override void OnParametersSet()
|
|
||||||
{
|
|
||||||
Logger.LogInformation("CampaignsWorkspaceContent.OnParametersSet [{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeCampaignsSurface(Workspace));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
Logger.LogInformation("CampaignsWorkspaceContent.OnAfterRenderAsync firstRender={FirstRender} [{State}]",
|
|
||||||
firstRender, WorkspaceDiagnosticSummary.DescribeCampaignsSurface(Workspace));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Inject] private ILogger<CampaignsWorkspaceContent> Logger { get; set; } = null!;
|
|
||||||
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages.HomeControls;
|
namespace RpgRoller.Components.Pages.HomeControls;
|
||||||
@@ -8,30 +7,11 @@ namespace RpgRoller.Components.Pages.HomeControls;
|
|||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public partial class AppHeader
|
public partial class AppHeader
|
||||||
{
|
{
|
||||||
protected override void OnParametersSet()
|
|
||||||
{
|
|
||||||
Logger.LogInformation(
|
|
||||||
"AppHeader.OnParametersSet user={User} showCampaign={ShowCampaign} campaignName={CampaignName} showConnectionState={ShowConnectionState} connectionStateLabel={ConnectionStateLabel} menuItems={MenuItemCount} isMenuOpen={IsMenuOpen}",
|
|
||||||
User?.Username ?? "<null>", ShowCampaign, CampaignName ?? "<null>", ShowConnectionState,
|
|
||||||
ConnectionStateLabel,
|
|
||||||
MenuItems.Count, IsMenuOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
Logger.LogInformation(
|
|
||||||
"AppHeader.OnAfterRenderAsync firstRender={FirstRender} user={User} menuItems={MenuItemCount} isMenuOpen={IsMenuOpen}",
|
|
||||||
firstRender, User?.Username ?? "<null>", MenuItems.Count, IsMenuOpen);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task SelectMenuItemAsync(AppHeaderMenuItem item)
|
private Task SelectMenuItemAsync(AppHeaderMenuItem item)
|
||||||
{
|
{
|
||||||
return item.OnSelected?.Invoke() ?? Task.CompletedTask;
|
return item.OnSelected?.Invoke() ?? Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Inject] private ILogger<AppHeader> Logger { get; set; } = null!;
|
|
||||||
|
|
||||||
[Parameter] public string Title { get; set; } = "RpgRoller";
|
[Parameter] public string Title { get; set; } = "RpgRoller";
|
||||||
|
|
||||||
[Parameter] public UserSummary? User { get; set; }
|
[Parameter] public UserSummary? User { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
@@ -14,18 +13,10 @@ public partial class CampaignLogPanel
|
|||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
var currentLastRollId = CampaignLog.LastOrDefault()?.RollId;
|
var currentLastRollId = CampaignLog.LastOrDefault()?.RollId;
|
||||||
Logger.LogInformation(
|
|
||||||
"CampaignLogPanel.OnAfterRenderAsync start firstRender={FirstRender} loading={IsCampaignDataLoading} logCount={LogCount} selectedCharacter={SelectedCharacterId} expandedRollId={ExpandedRollId} lastRenderedLogCount={LastRenderedLogCount} lastRenderedLogRollId={LastRenderedLogRollId} currentLastRollId={CurrentLastRollId}",
|
|
||||||
firstRender, IsCampaignDataLoading, CampaignLog.Count, SelectedCharacterId, ExpandedRollId,
|
|
||||||
LastRenderedLogCount,
|
|
||||||
LastRenderedLogRollId, currentLastRollId);
|
|
||||||
if (IsCampaignDataLoading || CampaignLog.Count == 0)
|
if (IsCampaignDataLoading || CampaignLog.Count == 0)
|
||||||
{
|
{
|
||||||
LastRenderedLogCount = CampaignLog.Count;
|
LastRenderedLogCount = CampaignLog.Count;
|
||||||
LastRenderedLogRollId = currentLastRollId;
|
LastRenderedLogRollId = currentLastRollId;
|
||||||
Logger.LogInformation(
|
|
||||||
"CampaignLogPanel.OnAfterRenderAsync earlyExit loading={IsCampaignDataLoading} logCount={LogCount}",
|
|
||||||
IsCampaignDataLoading, CampaignLog.Count);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,27 +24,19 @@ public partial class CampaignLogPanel
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.LogInformation(
|
|
||||||
"CampaignLogPanel.OnAfterRenderAsync scrollingToBottom logCount={LogCount} currentLastRollId={CurrentLastRollId}",
|
|
||||||
CampaignLog.Count, currentLastRollId);
|
|
||||||
await JS.InvokeVoidAsync("rpgRollerApi.scrollElementToBottom", LogFeedRef);
|
await JS.InvokeVoidAsync("rpgRollerApi.scrollElementToBottom", LogFeedRef);
|
||||||
}
|
}
|
||||||
catch (JSDisconnectedException)
|
catch (JSDisconnectedException)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("CampaignLogPanel.OnAfterRenderAsync scroll skipped due to JS disconnect");
|
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex) when (ex.Message.Contains("statically rendered",
|
catch (InvalidOperationException ex) when (ex.Message.Contains("statically rendered",
|
||||||
StringComparison.OrdinalIgnoreCase))
|
StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "CampaignLogPanel.OnAfterRenderAsync scroll skipped during static render");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LastRenderedLogCount = CampaignLog.Count;
|
LastRenderedLogCount = CampaignLog.Count;
|
||||||
LastRenderedLogRollId = currentLastRollId;
|
LastRenderedLogRollId = currentLastRollId;
|
||||||
Logger.LogInformation(
|
|
||||||
"CampaignLogPanel.OnAfterRenderAsync end firstRender={FirstRender} logCount={LogCount} lastRenderedLogRollId={LastRenderedLogRollId}",
|
|
||||||
firstRender, CampaignLog.Count, LastRenderedLogRollId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SubmitCustomRollAsync()
|
private async Task SubmitCustomRollAsync()
|
||||||
@@ -154,7 +137,6 @@ public partial class CampaignLogPanel
|
|||||||
[Inject] private IJSRuntime JS { get; set; } = null!;
|
[Inject] private IJSRuntime JS { get; set; } = null!;
|
||||||
|
|
||||||
[Inject] private RpgRollerApiClient ApiClient { get; set; } = null!;
|
[Inject] private RpgRollerApiClient ApiClient { get; set; } = null!;
|
||||||
[Inject] private ILogger<CampaignLogPanel> Logger { get; set; } = null!;
|
|
||||||
|
|
||||||
private ElementReference LogPanelRef { get; set; }
|
private ElementReference LogPanelRef { get; set; }
|
||||||
private ElementReference LogFeedRef { get; set; }
|
private ElementReference LogFeedRef { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages.HomeControls;
|
namespace RpgRoller.Components.Pages.HomeControls;
|
||||||
@@ -8,23 +7,6 @@ namespace RpgRoller.Components.Pages.HomeControls;
|
|||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public partial class CharacterPanel
|
public partial class CharacterPanel
|
||||||
{
|
{
|
||||||
protected override void OnParametersSet()
|
|
||||||
{
|
|
||||||
Logger.LogInformation(
|
|
||||||
"CharacterPanel.OnParametersSet loading={IsCampaignDataLoading} selectedCampaign={SelectedCampaignId} selectedCharacter={SelectedCharacterId} campaignCharacters={CampaignCharacterCount} skills={SkillCount} skillGroups={SkillGroupCount} filter={SkillFilterText} createSkillModal={ShowCreateSkillModal} editSkillModal={ShowEditSkillModal} createGroupModal={ShowCreateSkillGroupModal} editGroupModal={ShowEditSkillGroupModal}",
|
|
||||||
IsCampaignDataLoading, SelectedCampaign?.Id, SelectedCharacterId, SelectedCampaign?.Characters.Length ?? 0,
|
|
||||||
SelectedCharacterSkills.Count, SelectedCharacterSkillGroups.Count, SkillFilterText, ShowCreateSkillModal,
|
|
||||||
ShowEditSkillModal, ShowCreateSkillGroupModal, ShowEditSkillGroupModal);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
Logger.LogInformation(
|
|
||||||
"CharacterPanel.OnAfterRenderAsync firstRender={FirstRender} loading={IsCampaignDataLoading} selectedCampaign={SelectedCampaignId} selectedCharacter={SelectedCharacterId} filter={SkillFilterText}",
|
|
||||||
firstRender, IsCampaignDataLoading, SelectedCampaign?.Id, SelectedCharacterId, SkillFilterText);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenCreateSkillModal(Guid? skillGroupId = null)
|
private void OpenCreateSkillModal(Guid? skillGroupId = null)
|
||||||
{
|
{
|
||||||
var selectedGroup = skillGroupId.HasValue
|
var selectedGroup = skillGroupId.HasValue
|
||||||
@@ -369,7 +351,6 @@ public partial class CharacterPanel
|
|||||||
private string SkillFilterText { get; set; } = string.Empty;
|
private string SkillFilterText { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Inject] private RpgRollerApiClient ApiClient { get; set; } = null!;
|
[Inject] private RpgRollerApiClient ApiClient { get; set; } = null!;
|
||||||
[Inject] private ILogger<CharacterPanel> Logger { get; set; } = null!;
|
|
||||||
|
|
||||||
[Parameter] public bool IsCampaignDataLoading { get; set; }
|
[Parameter] public bool IsCampaignDataLoading { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages.HomeControls;
|
namespace RpgRoller.Components.Pages.HomeControls;
|
||||||
|
|
||||||
@@ -10,9 +9,6 @@ public partial class RolemasterSkillRollModal
|
|||||||
{
|
{
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
Logger.LogInformation(
|
|
||||||
"RolemasterSkillRollModal.OnParametersSet visible={Visible} wasVisible={WasVisible} pendingFocus={PendingFocus} skillName={SkillName} isMutating={IsMutating} isSubmitting={IsSubmitting}",
|
|
||||||
Visible, WasVisible, PendingFocus, SkillName, IsMutating, IsSubmitting);
|
|
||||||
CurrentModifierText = ModifierText;
|
CurrentModifierText = ModifierText;
|
||||||
if (!Visible || WasVisible)
|
if (!Visible || WasVisible)
|
||||||
{
|
{
|
||||||
@@ -26,9 +22,6 @@ public partial class RolemasterSkillRollModal
|
|||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
Logger.LogInformation(
|
|
||||||
"RolemasterSkillRollModal.OnAfterRenderAsync firstRender={FirstRender} visible={Visible} pendingFocus={PendingFocus} skillName={SkillName}",
|
|
||||||
firstRender, Visible, PendingFocus, SkillName);
|
|
||||||
if (!Visible || !PendingFocus)
|
if (!Visible || !PendingFocus)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -67,7 +60,6 @@ public partial class RolemasterSkillRollModal
|
|||||||
private bool WasVisible { get; set; }
|
private bool WasVisible { get; set; }
|
||||||
private string CurrentModifierText { get; set; } = string.Empty;
|
private string CurrentModifierText { get; set; } = string.Empty;
|
||||||
private ElementReference ModifierInputElement { get; set; }
|
private ElementReference ModifierInputElement { get; set; }
|
||||||
[Inject] private ILogger<RolemasterSkillRollModal> Logger { get; set; } = null!;
|
|
||||||
|
|
||||||
[Parameter] public bool Visible { get; set; }
|
[Parameter] public bool Visible { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages.HomeControls;
|
namespace RpgRoller.Components.Pages.HomeControls;
|
||||||
@@ -10,9 +9,6 @@ public partial class SkillFormModal
|
|||||||
{
|
{
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
Logger.LogInformation(
|
|
||||||
"SkillFormModal.OnParametersSet visible={Visible} formVersion={FormVersion} appliedFormVersion={AppliedFormVersion} rulesetId={RulesetId} selectedCharacterId={SelectedCharacterId} editingSkillId={EditingSkillId} autoFocusName={AutoFocusName}",
|
|
||||||
Visible, FormVersion, AppliedFormVersion, RulesetId, SelectedCharacterId, EditingSkillId, AutoFocusName);
|
|
||||||
if (!Visible || FormVersion == AppliedFormVersion)
|
if (!Visible || FormVersion == AppliedFormVersion)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -32,9 +28,6 @@ public partial class SkillFormModal
|
|||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
Logger.LogInformation(
|
|
||||||
"SkillFormModal.OnAfterRenderAsync firstRender={FirstRender} visible={Visible} pendingNameFocus={PendingNameFocus} formVersion={FormVersion}",
|
|
||||||
firstRender, Visible, PendingNameFocus, FormVersion);
|
|
||||||
if (!Visible || !PendingNameFocus)
|
if (!Visible || !PendingNameFocus)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -170,7 +163,6 @@ public partial class SkillFormModal
|
|||||||
: "Enter the dice expression used for this skill.";
|
: "Enter the dice expression used for this skill.";
|
||||||
|
|
||||||
[Inject] private RpgRollerApiClient ApiClient { get; set; } = null!;
|
[Inject] private RpgRollerApiClient ApiClient { get; set; } = null!;
|
||||||
[Inject] private ILogger<SkillFormModal> Logger { get; set; } = null!;
|
|
||||||
|
|
||||||
private FormState<SkillFormModel> FormState { get; } = new();
|
private FormState<SkillFormModel> FormState { get; } = new();
|
||||||
private int AppliedFormVersion { get; set; } = -1;
|
private int AppliedFormVersion { get; set; } = -1;
|
||||||
|
|||||||
@@ -1,104 +1,77 @@
|
|||||||
@using Microsoft.AspNetCore.Components
|
@using Microsoft.AspNetCore.Components
|
||||||
@using RpgRoller.Components.Pages.HomeControls
|
@using RpgRoller.Components.Pages.HomeControls
|
||||||
|
|
||||||
@if (!Workspace.ShowLiveContent)
|
<main class="play-screen @(Workspace.State.MobilePanel == "log" ? "mobile-log" : "mobile-character")">
|
||||||
{
|
<CharacterPanel
|
||||||
<main class="play-screen mobile-character">
|
IsCampaignDataLoading="@IsCampaignDataLoading"
|
||||||
<section class="card character-panel">
|
SelectedCampaign="Workspace.State.PlaySelectedCampaign"
|
||||||
<div class="skeleton-stack">
|
SelectedCharacterId="Workspace.State.PlaySelectedCharacterId"
|
||||||
<div class="skeleton-line"></div>
|
SelectedCharacter="Workspace.State.PlaySelectedCharacter"
|
||||||
<div class="skeleton-line short"></div>
|
|
||||||
<div class="skeleton-line"></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<aside class="card log-panel">
|
|
||||||
<div class="section-head">
|
|
||||||
<h2>Campaign Log</h2>
|
|
||||||
</div>
|
|
||||||
<div class="skeleton-stack">
|
|
||||||
<div class="skeleton-line"></div>
|
|
||||||
<div class="skeleton-line short"></div>
|
|
||||||
<div class="skeleton-line"></div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</main>
|
|
||||||
<div class="mobile-bottom-nav" aria-hidden="true">
|
|
||||||
<span class="switch active">Character</span>
|
|
||||||
<span class="switch">Log</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<main class="play-screen @(Workspace.State.MobilePanel == "log" ? "mobile-log" : "mobile-character")">
|
|
||||||
<CharacterPanel
|
|
||||||
IsCampaignDataLoading="@IsCampaignDataLoading"
|
|
||||||
SelectedCampaign="Workspace.State.PlaySelectedCampaign"
|
|
||||||
SelectedCharacterId="Workspace.State.PlaySelectedCharacterId"
|
|
||||||
SelectedCharacter="Workspace.State.PlaySelectedCharacter"
|
|
||||||
IsMutating="Workspace.State.IsMutating"
|
|
||||||
SelectedCharacterSkills="Workspace.State.PlaySelectedCharacterSkills"
|
|
||||||
SelectedCharacterSkillGroups="Workspace.State.PlaySelectedCharacterSkillGroups"
|
|
||||||
SelectedCampaignRulesetId="@(Workspace.State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
|
|
||||||
RollVisibility="Workspace.State.RollVisibility"
|
|
||||||
RollVisibilityChanged="Workspace.Session.OnRollVisibilityChangedAsync"
|
|
||||||
OwnerLabel="Workspace.State.OwnerLabel"
|
|
||||||
SkillDefinitionLabel="Workspace.State.SkillDefinitionLabel"
|
|
||||||
CanEditCharacter="Workspace.Campaigns.CanEditCharacter"
|
|
||||||
CanEditSkill="Workspace.Play.CanEditSkill"
|
|
||||||
CharacterSelected="Workspace.Play.SelectCharacterAsync"
|
|
||||||
EditCharacterRequested="Workspace.Campaigns.OpenEditCharacterModal"
|
|
||||||
SkillCreated="Workspace.Play.OnSkillCreatedAsync"
|
|
||||||
SkillUpdated="Workspace.Play.OnSkillUpdatedAsync"
|
|
||||||
SkillGroupCreated="Workspace.Play.OnSkillGroupCreatedAsync"
|
|
||||||
SkillGroupUpdated="Workspace.Play.OnSkillGroupUpdatedAsync"
|
|
||||||
SkillDeleted="Workspace.Play.OnSkillDeletedAsync"
|
|
||||||
SkillGroupDeleted="Workspace.Play.OnSkillGroupDeletedAsync"
|
|
||||||
ErrorOccurred="Workspace.Play.OnCharacterPanelErrorAsync"
|
|
||||||
RollRequested="Workspace.Play.RollSkillAsync"/>
|
|
||||||
|
|
||||||
<CampaignLogPanel
|
|
||||||
IsCampaignDataLoading="@IsCampaignDataLoading"
|
|
||||||
CampaignLog="Workspace.State.PlayVisibleCampaignLog"
|
|
||||||
ExpandedRollId="Workspace.State.ExpandedCampaignLogRollId"
|
|
||||||
FreshRollId="Workspace.State.FreshCampaignLogRollId"
|
|
||||||
SelectedCharacterId="Workspace.State.PlaySelectedCharacterId"
|
|
||||||
SelectedCharacterName="@(Workspace.State.PlaySelectedCharacter?.Name)"
|
|
||||||
SelectedCampaignRulesetId="@(Workspace.State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
|
|
||||||
RollVisibility="Workspace.State.RollVisibility"
|
|
||||||
IsMutating="Workspace.State.IsMutating"
|
|
||||||
ToggleRollDetailRequested="Workspace.Play.ToggleRollDetailAsync"
|
|
||||||
ResolveRollDetail="Workspace.Play.ResolveRollDetail"
|
|
||||||
IsRollDetailLoading="Workspace.Play.IsRollDetailLoading"
|
|
||||||
GetRollDetailError="Workspace.Play.GetRollDetailError"
|
|
||||||
CustomRollCreated="Workspace.Play.OnCustomRollCreatedAsync"
|
|
||||||
ErrorOccurred="Workspace.Play.OnCampaignLogPanelErrorAsync"/>
|
|
||||||
</main>
|
|
||||||
<nav class="mobile-bottom-nav" aria-label="Play panel selector">
|
|
||||||
<button type="button" class="switch @(Workspace.State.MobilePanel == "character" ? "active" : string.Empty)"
|
|
||||||
@onclick='() => Workspace.Scope.SetMobilePanelAsync("character")'>
|
|
||||||
Character
|
|
||||||
</button>
|
|
||||||
<button type="button" class="switch @(Workspace.State.MobilePanel == "log" ? "active" : string.Empty)"
|
|
||||||
@onclick='() => Workspace.Scope.SetMobilePanelAsync("log")'>
|
|
||||||
Log
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<CharacterManagementModals Workspace="Workspace"/>
|
|
||||||
|
|
||||||
<RolemasterSkillRollModal
|
|
||||||
Visible="Workspace.State.ShowRolemasterSkillRollModal"
|
|
||||||
SkillName="@(Workspace.State.PendingRolemasterSkillRoll?.Name ?? string.Empty)"
|
|
||||||
Expression="@(Workspace.State.PendingRolemasterSkillRoll?.DiceRollDefinition ?? string.Empty)"
|
|
||||||
ModifierText="@Workspace.State.PendingRolemasterSituationalModifier"
|
|
||||||
ModifierTextChanged="@(text => Workspace.State.PendingRolemasterSituationalModifier = text)"
|
|
||||||
ErrorMessage="@Workspace.State.PendingRolemasterSkillRollError"
|
|
||||||
IsMutating="Workspace.State.IsMutating"
|
IsMutating="Workspace.State.IsMutating"
|
||||||
IsSubmitting="Workspace.State.IsSubmittingRolemasterSkillRoll"
|
SelectedCharacterSkills="Workspace.State.PlaySelectedCharacterSkills"
|
||||||
ConfirmRequested="Workspace.Play.SubmitRolemasterSkillRollAsync"
|
SelectedCharacterSkillGroups="Workspace.State.PlaySelectedCharacterSkillGroups"
|
||||||
CancelRequested="Workspace.Play.CancelRolemasterSkillRollAsync"/>
|
SelectedCampaignRulesetId="@(Workspace.State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
|
||||||
}
|
RollVisibility="Workspace.State.RollVisibility"
|
||||||
|
RollVisibilityChanged="Workspace.Session.OnRollVisibilityChangedAsync"
|
||||||
|
OwnerLabel="Workspace.State.OwnerLabel"
|
||||||
|
SkillDefinitionLabel="Workspace.State.SkillDefinitionLabel"
|
||||||
|
CanEditCharacter="Workspace.Campaigns.CanEditCharacter"
|
||||||
|
CanEditSkill="Workspace.Play.CanEditSkill"
|
||||||
|
CharacterSelected="Workspace.Play.SelectCharacterAsync"
|
||||||
|
EditCharacterRequested="Workspace.Campaigns.OpenEditCharacterModal"
|
||||||
|
SkillCreated="Workspace.Play.OnSkillCreatedAsync"
|
||||||
|
SkillUpdated="Workspace.Play.OnSkillUpdatedAsync"
|
||||||
|
SkillGroupCreated="Workspace.Play.OnSkillGroupCreatedAsync"
|
||||||
|
SkillGroupUpdated="Workspace.Play.OnSkillGroupUpdatedAsync"
|
||||||
|
SkillDeleted="Workspace.Play.OnSkillDeletedAsync"
|
||||||
|
SkillGroupDeleted="Workspace.Play.OnSkillGroupDeletedAsync"
|
||||||
|
ErrorOccurred="Workspace.Play.OnCharacterPanelErrorAsync"
|
||||||
|
RollRequested="Workspace.Play.RollSkillAsync"/>
|
||||||
|
|
||||||
|
<CampaignLogPanel
|
||||||
|
IsCampaignDataLoading="@IsCampaignDataLoading"
|
||||||
|
CampaignLog="Workspace.State.PlayVisibleCampaignLog"
|
||||||
|
ExpandedRollId="Workspace.State.ExpandedCampaignLogRollId"
|
||||||
|
FreshRollId="Workspace.State.FreshCampaignLogRollId"
|
||||||
|
SelectedCharacterId="Workspace.State.PlaySelectedCharacterId"
|
||||||
|
SelectedCharacterName="@(Workspace.State.PlaySelectedCharacter?.Name)"
|
||||||
|
SelectedCampaignRulesetId="@(Workspace.State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
|
||||||
|
RollVisibility="Workspace.State.RollVisibility"
|
||||||
|
IsMutating="Workspace.State.IsMutating"
|
||||||
|
ToggleRollDetailRequested="Workspace.Play.ToggleRollDetailAsync"
|
||||||
|
ResolveRollDetail="Workspace.Play.ResolveRollDetail"
|
||||||
|
IsRollDetailLoading="Workspace.Play.IsRollDetailLoading"
|
||||||
|
GetRollDetailError="Workspace.Play.GetRollDetailError"
|
||||||
|
CustomRollCreated="Workspace.Play.OnCustomRollCreatedAsync"
|
||||||
|
ErrorOccurred="Workspace.Play.OnCampaignLogPanelErrorAsync"/>
|
||||||
|
</main>
|
||||||
|
<nav class="mobile-bottom-nav" aria-label="Play panel selector">
|
||||||
|
<button type="button" class="switch @(Workspace.State.MobilePanel == "character" ? "active" : string.Empty)"
|
||||||
|
@onclick='() => Workspace.Scope.SetMobilePanelAsync("character")'>
|
||||||
|
Character
|
||||||
|
</button>
|
||||||
|
<button type="button" class="switch @(Workspace.State.MobilePanel == "log" ? "active" : string.Empty)"
|
||||||
|
@onclick='() => Workspace.Scope.SetMobilePanelAsync("log")'>
|
||||||
|
Log
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<CharacterManagementModals Workspace="Workspace"/>
|
||||||
|
|
||||||
|
<RolemasterSkillRollModal
|
||||||
|
Visible="Workspace.State.ShowRolemasterSkillRollModal"
|
||||||
|
SkillName="@(Workspace.State.PendingRolemasterSkillRoll?.Name ?? string.Empty)"
|
||||||
|
Expression="@(Workspace.State.PendingRolemasterSkillRoll?.DiceRollDefinition ?? string.Empty)"
|
||||||
|
ModifierText="@Workspace.State.PendingRolemasterSituationalModifier"
|
||||||
|
ModifierTextChanged="@(text => Workspace.State.PendingRolemasterSituationalModifier = text)"
|
||||||
|
ErrorMessage="@Workspace.State.PendingRolemasterSkillRollError"
|
||||||
|
IsMutating="Workspace.State.IsMutating"
|
||||||
|
IsSubmitting="Workspace.State.IsSubmittingRolemasterSkillRoll"
|
||||||
|
ConfirmRequested="Workspace.Play.SubmitRolemasterSkillRollAsync"
|
||||||
|
CancelRequested="Workspace.Play.CancelRolemasterSkillRollAsync"/>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
||||||
|
|
||||||
private bool IsCampaignDataLoading => !Workspace.HasSessionInitialized || Workspace.State.IsCampaignDataLoading;
|
private bool IsCampaignDataLoading => !Workspace.HasSessionInitialized || Workspace.State.IsCampaignDataLoading;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages;
|
|
||||||
|
|
||||||
[ExcludeFromCodeCoverage]
|
|
||||||
public partial class PlayWorkspaceContent
|
|
||||||
{
|
|
||||||
protected override void OnParametersSet()
|
|
||||||
{
|
|
||||||
Logger.LogInformation("PlayWorkspaceContent.OnParametersSet [{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribePlaySurface(Workspace));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
Logger.LogInformation("PlayWorkspaceContent.OnAfterRenderAsync firstRender={FirstRender} [{State}]",
|
|
||||||
firstRender, WorkspaceDiagnosticSummary.DescribePlaySurface(Workspace));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Inject] private ILogger<PlayWorkspaceContent> Logger { get; set; } = null!;
|
|
||||||
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
@using RpgRoller.Components.Pages.HomeControls
|
@using RpgRoller.Components.Pages.HomeControls
|
||||||
<div class="@AppCssClass"
|
<div class="@AppCssClass">
|
||||||
data-workspace-root="true"
|
|
||||||
data-workspace-route="@Route"
|
|
||||||
data-workspace-phase="@PageContext.RenderPhase"
|
|
||||||
data-workspace-session-initialized="@HasSessionInitialized"
|
|
||||||
data-workspace-campaign-loading="@State.IsCampaignDataLoading"
|
|
||||||
data-workspace-admin-loading="@State.IsAdminDataLoading"
|
|
||||||
data-workspace-user="@(State.User?.Username ?? "loading")">
|
|
||||||
<p class="sr-only" aria-live="polite">@State.LiveAnnouncement</p>
|
<p class="sr-only" aria-live="polite">@State.LiveAnnouncement</p>
|
||||||
|
|
||||||
@if (PageContext.ShowLiveContent && State.HasHealthIssue)
|
@if (State.HasHealthIssue)
|
||||||
{
|
{
|
||||||
<section class="health-banner" role="alert">
|
<section class="health-banner" role="alert">
|
||||||
<div>
|
<div>
|
||||||
@@ -21,62 +14,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="workspace-shell">
|
<div class="workspace-shell">
|
||||||
@if (PageContext.ShowLiveContent)
|
<AppHeader
|
||||||
{
|
User="State.User"
|
||||||
<AppHeader
|
ShowCampaign="@ShowCampaignInHeader"
|
||||||
User="State.User"
|
CampaignName="@State.SelectedCampaignName"
|
||||||
ShowCampaign="@ShowCampaignInHeader"
|
ShowConnectionState="@ShowConnectionStateInHeader"
|
||||||
CampaignName="@State.SelectedCampaignName"
|
ConnectionStateLabel="@State.ConnectionStateLabel"
|
||||||
ShowConnectionState="@ShowConnectionStateInHeader"
|
ConnectionStateCssClass="@State.ConnectionStateCssClass"
|
||||||
ConnectionStateLabel="@State.ConnectionStateLabel"
|
IsMenuOpen="State.IsScreenMenuOpen"
|
||||||
ConnectionStateCssClass="@State.ConnectionStateCssClass"
|
MenuButtonId="workspace-screen-menu-button"
|
||||||
IsMenuOpen="State.IsScreenMenuOpen"
|
MenuId="workspace-screen-menu"
|
||||||
MenuButtonId="workspace-screen-menu-button"
|
MenuItems="HeaderMenuItems"
|
||||||
MenuId="workspace-screen-menu"
|
ToggleMenuRequested="ToggleScreenMenu"
|
||||||
MenuItems="HeaderMenuItems"
|
LogoutRequested="Session.LogoutAsync"/>
|
||||||
ToggleMenuRequested="ToggleScreenMenu"
|
|
||||||
LogoutRequested="Session.LogoutAsync"/>
|
|
||||||
|
|
||||||
@if (ChildContent is not null)
|
@if (ChildContent is not null)
|
||||||
{
|
|
||||||
@ChildContent(PageContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (PageContext.ShowHeaderPlaceholder)
|
|
||||||
{
|
{
|
||||||
<header class="workspace-header">
|
@ChildContent(PageContext)
|
||||||
<div class="header-row">
|
|
||||||
<h1>RpgRoller</h1>
|
|
||||||
<p class="header-identity">
|
|
||||||
<strong>Loading workspace...</strong>
|
|
||||||
</p>
|
|
||||||
@if (ShowCampaignInHeader)
|
|
||||||
{
|
|
||||||
<p class="header-campaign">Campaign: <strong>Loading...</strong></p>
|
|
||||||
}
|
|
||||||
@if (ShowConnectionStateInHeader)
|
|
||||||
{
|
|
||||||
<div class="header-connection-cell">
|
|
||||||
<p class="connection offline">Offline fallback</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
@if (PageContext.ShowRouteSkeleton && ChildContent is not null)
|
|
||||||
{
|
|
||||||
@ChildContent(PageContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<section class="card" aria-live="polite">
|
|
||||||
<p>Loading workspace...</p>
|
|
||||||
</section>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (PageContext.ShowLiveContent && State.Toasts.Count > 0)
|
@if (State.Toasts.Count > 0)
|
||||||
{
|
{
|
||||||
<div class="toast-stack" aria-live="polite" aria-atomic="false">
|
<div class="toast-stack" aria-live="polite" aria-atomic="false">
|
||||||
@foreach (var toast in State.Toasts)
|
@foreach (var toast in State.Toasts)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using RpgRoller.Components.Pages.HomeControls;
|
using RpgRoller.Components.Pages.HomeControls;
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
@@ -10,55 +9,25 @@ namespace RpgRoller.Components.Pages;
|
|||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public partial class Workspace : IAsyncDisposable
|
public partial class Workspace : IAsyncDisposable
|
||||||
{
|
{
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
Logger.LogInformation("Workspace.OnInitialized route={Route}", Route);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
Logger.LogInformation(
|
|
||||||
"Workspace.OnParametersSet route={Route} previousRoute={PreviousRoute} hasSessionInitialized={HasSessionInitialized} state=[{State}]",
|
|
||||||
Route, PreviousRoute, HasSessionInitialized, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
State.IsScreenMenuOpen = false;
|
State.IsScreenMenuOpen = false;
|
||||||
if (PreviousRoute.HasValue && PreviousRoute.Value != Route && HasSessionInitialized)
|
|
||||||
_ = InvokeAsync(HandleRouteChangedAsync);
|
|
||||||
|
|
||||||
PreviousRoute = Route;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
RenderCount += 1;
|
|
||||||
Logger.LogInformation(
|
|
||||||
"Workspace.OnAfterRenderAsync route={Route} renderCount={RenderCount} firstRender={FirstRender} renderPhase={RenderPhase} hasSessionInitialized={HasSessionInitialized} state=[{State}]",
|
|
||||||
Route, RenderCount, firstRender, RenderPhase, HasSessionInitialized,
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
if (RenderPhase < WorkspaceRenderPhase.RouteSkeleton)
|
|
||||||
return AdvanceRenderPhaseAsync("Workspace.OnAfterRenderAsync");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public Task OnStateEventReceived(CampaignStateSnapshot state)
|
public Task OnStateEventReceived(CampaignStateSnapshot state)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Workspace.OnStateEventReceived route={Route} snapshot=[{Snapshot}]",
|
|
||||||
Route, WorkspaceDiagnosticSummary.DescribeSnapshot(state));
|
|
||||||
return Live.OnStateEventReceivedAsync(state);
|
return Live.OnStateEventReceivedAsync(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public Task OnConnectionStateChanged(string state)
|
public Task OnConnectionStateChanged(string state)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Workspace.OnConnectionStateChanged route={Route} state={ConnectionState}", Route, state);
|
|
||||||
return Live.OnConnectionStateChangedAsync(state);
|
return Live.OnConnectionStateChangedAsync(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Workspace.DisposeAsync route={Route} state=[{State}]",
|
|
||||||
Route, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
await StopStateEventsAsync();
|
await StopStateEventsAsync();
|
||||||
DotNetRef?.Dispose();
|
DotNetRef?.Dispose();
|
||||||
}
|
}
|
||||||
@@ -85,15 +54,12 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
|
|
||||||
private async Task StartStateEventsCoreAsync(Guid campaignId)
|
private async Task StartStateEventsCoreAsync(Guid campaignId)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Workspace.StartStateEventsCoreAsync route={Route} campaignId={CampaignId}", Route,
|
|
||||||
campaignId);
|
|
||||||
DotNetRef ??= DotNetObjectReference.Create(this);
|
DotNetRef ??= DotNetObjectReference.Create(this);
|
||||||
await JS.InvokeVoidAsync("rpgRollerApi.startStateEvents", campaignId.ToString(), DotNetRef);
|
await JS.InvokeVoidAsync("rpgRollerApi.startStateEvents", campaignId.ToString(), DotNetRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StopStateEventsCoreAsync()
|
private async Task StopStateEventsCoreAsync()
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Workspace.StopStateEventsCoreAsync route={Route}", Route);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await JS.InvokeVoidAsync("rpgRollerApi.stopStateEvents");
|
await JS.InvokeVoidAsync("rpgRollerApi.stopStateEvents");
|
||||||
@@ -113,8 +79,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
|
|
||||||
private Task NavigateToRouteAsync(string route)
|
private Task NavigateToRouteAsync(string route)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Workspace.NavigateToRouteAsync fromRoute={Route} toRoute={TargetRoute} state=[{State}]",
|
|
||||||
Route, route, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
State.IsScreenMenuOpen = false;
|
State.IsScreenMenuOpen = false;
|
||||||
Navigation.NavigateTo(route, forceLoad: true);
|
Navigation.NavigateTo(route, forceLoad: true);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -125,27 +89,13 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
if (IsPlayRoute)
|
if (IsPlayRoute)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
Logger.LogWarning("Workspace.RedirectToPlayAsync fromRoute={Route} state=[{State}]",
|
|
||||||
Route, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
Navigation.NavigateTo("/play", forceLoad: true);
|
Navigation.NavigateTo("/play", forceLoad: true);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RequestRefreshAsync()
|
private Task RequestRefreshAsync()
|
||||||
{
|
{
|
||||||
return RequestRefreshAsync("Workspace");
|
return InvokeAsync(StateHasChanged);
|
||||||
}
|
|
||||||
|
|
||||||
private Task RequestRefreshAsync(string source)
|
|
||||||
{
|
|
||||||
Logger.LogInformation("Workspace.RequestRefreshAsync source={Source} route={Route} state=[{State}]",
|
|
||||||
source, Route, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
return InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
Logger.LogInformation("Workspace.StateHasChanged source={Source} route={Route} state=[{State}]",
|
|
||||||
source, Route, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
StateHasChanged();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task InitializeRouteAsync()
|
private Task InitializeRouteAsync()
|
||||||
@@ -153,56 +103,14 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return InitializationTask ??= InitializeRouteCoreAsync();
|
return InitializationTask ??= InitializeRouteCoreAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task EnsureLiveRenderPhaseAsync()
|
|
||||||
{
|
|
||||||
if (RenderPhase == WorkspaceRenderPhase.Live)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
return AdvanceRenderPhaseAsync("WorkspaceRouteView.EnsureLiveRenderPhaseAsync", WorkspaceRenderPhase.Live);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InitializeRouteCoreAsync()
|
private async Task InitializeRouteCoreAsync()
|
||||||
{
|
{
|
||||||
if (HasSessionInitialized)
|
if (HasSessionInitialized)
|
||||||
{
|
|
||||||
Logger.LogInformation(
|
|
||||||
"Workspace.InitializeRouteCoreAsync skipped route={Route} alreadyInitialized state=[{State}]",
|
|
||||||
Route, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogInformation("Workspace.InitializeRouteCoreAsync start route={Route} stateBefore=[{State}]",
|
|
||||||
Route, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
State.HasInteractiveRenderStarted = true;
|
State.HasInteractiveRenderStarted = true;
|
||||||
await Session.InitializeAsync();
|
await Session.InitializeAsync();
|
||||||
HasSessionInitialized = true;
|
HasSessionInitialized = true;
|
||||||
Logger.LogInformation("Workspace.InitializeRouteCoreAsync end route={Route} stateAfter=[{State}]",
|
|
||||||
Route, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
await RequestRefreshAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleRouteChangedAsync()
|
|
||||||
{
|
|
||||||
if (!HasSessionInitialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Logger.LogInformation(
|
|
||||||
"Workspace.HandleRouteChangedAsync start route={Route} previousRoute={PreviousRoute} stateBefore=[{State}]",
|
|
||||||
Route, PreviousRoute, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
if (IsAdminRoute)
|
|
||||||
{
|
|
||||||
await Live.SyncStateEventsAsync();
|
|
||||||
await EnsureAdminUsersLoadedAsync();
|
|
||||||
Logger.LogInformation("Workspace.HandleRouteChangedAsync admin end route={Route} stateAfter=[{State}]",
|
|
||||||
Route, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
await RequestRefreshAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Scope.RefreshCampaignScopeAsync();
|
|
||||||
await Live.SyncStateEventsAsync();
|
|
||||||
Logger.LogInformation("Workspace.HandleRouteChangedAsync end route={Route} stateAfter=[{State}]",
|
|
||||||
Route, WorkspaceDiagnosticSummary.DescribeState(State));
|
|
||||||
await RequestRefreshAsync();
|
await RequestRefreshAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,19 +119,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
|
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task AdvanceRenderPhaseAsync(string source, WorkspaceRenderPhase? targetPhase = null)
|
|
||||||
{
|
|
||||||
var nextPhase = targetPhase ?? (WorkspaceRenderPhase)((int)RenderPhase + 1);
|
|
||||||
if (nextPhase <= RenderPhase)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
Logger.LogInformation(
|
|
||||||
"Workspace.AdvanceRenderPhaseAsync source={Source} route={Route} from={FromPhase} to={ToPhase}",
|
|
||||||
source, Route, RenderPhase, nextPhase);
|
|
||||||
RenderPhase = nextPhase;
|
|
||||||
return RequestRefreshAsync(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Inject] private IJSRuntime JS { get; set; } = null!;
|
[Inject] private IJSRuntime JS { get; set; } = null!;
|
||||||
|
|
||||||
[Inject] private RpgRollerApiClient ApiClient { get; set; } = null!;
|
[Inject] private RpgRollerApiClient ApiClient { get; set; } = null!;
|
||||||
@@ -231,8 +126,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
[Inject] private WorkspaceQueryService WorkspaceQuery { get; set; } = null!;
|
[Inject] private WorkspaceQueryService WorkspaceQuery { get; set; } = null!;
|
||||||
|
|
||||||
[Inject] private NavigationManager Navigation { get; set; } = null!;
|
[Inject] private NavigationManager Navigation { get; set; } = null!;
|
||||||
[Inject] private ILogger<Workspace> Logger { get; set; } = null!;
|
|
||||||
[Inject] private ILoggerFactory LoggerFactory { get; set; } = null!;
|
|
||||||
|
|
||||||
[Parameter] public EventCallback<string?> LoggedOut { get; set; }
|
[Parameter] public EventCallback<string?> LoggedOut { get; set; }
|
||||||
[Parameter] public WorkspaceRoute Route { get; set; } = WorkspaceRoute.Play;
|
[Parameter] public WorkspaceRoute Route { get; set; } = WorkspaceRoute.Play;
|
||||||
@@ -248,25 +141,23 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private bool ShowConnectionStateInHeader => IsPlayRoute;
|
private bool ShowConnectionStateInHeader => IsPlayRoute;
|
||||||
|
|
||||||
private WorkspacePageContext PageContext => new(State, Play, Campaigns, Admin, Scope, Session,
|
private WorkspacePageContext PageContext => new(State, Play, Campaigns, Admin, Scope, Session,
|
||||||
InitializeRouteAsync, HasSessionInitialized, RequestRefreshAsync, EnsureLiveRenderPhaseAsync,
|
InitializeRouteAsync, HasSessionInitialized, RequestRefreshAsync, AdminDatabaseDownloadUrl, HeaderMenuItems,
|
||||||
AdminDatabaseDownloadUrl, HeaderMenuItems, RenderPhase, IsPlayRoute, IsCampaignsRoute, IsAdminRoute);
|
IsPlayRoute, IsCampaignsRoute, IsAdminRoute);
|
||||||
|
|
||||||
private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new(State, Feedback, JS, WorkspaceQuery,
|
private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new(State, Feedback, JS, WorkspaceQuery,
|
||||||
() => IsPlayRoute, Play.EnsureSelectedCharacterActiveAsync, Play.RefreshSelectedCharacterSheetAsync,
|
() => IsPlayRoute, Play.EnsureSelectedCharacterActiveAsync, Play.RefreshSelectedCharacterSheetAsync,
|
||||||
Play.RefreshCampaignLogAsync, Play.ResetCampaignLogDetailState, Play.ResetCampaignStateTracking,
|
Play.RefreshCampaignLogAsync, Play.ResetCampaignLogDetailState, Play.ResetCampaignStateTracking,
|
||||||
ClearAuthenticatedState,
|
ClearAuthenticatedState,
|
||||||
StopStateEventsAsync, message => LoggedOut.InvokeAsync(message),
|
StopStateEventsAsync, message => LoggedOut.InvokeAsync(message));
|
||||||
LoggerFactory.CreateLogger("Workspace.CampaignScope"));
|
|
||||||
|
|
||||||
private WorkspaceLiveStateController Live => m_Live ??= new(State, Feedback, () => IsPlayRoute, () => IsAdminRoute,
|
private WorkspaceLiveStateController Live => m_Live ??=
|
||||||
StartStateEventsCoreAsync,
|
new(State, Feedback, () => IsPlayRoute, () => IsAdminRoute, StartStateEventsCoreAsync,
|
||||||
StopStateEventsCoreAsync, Scope.RefreshCampaignRosterAsync, Play.RefreshSelectedCharacterSheetAsync,
|
StopStateEventsCoreAsync, Scope.RefreshCampaignRosterAsync, Play.RefreshSelectedCharacterSheetAsync,
|
||||||
Play.RefreshCampaignLogAsync, () => RequestRefreshAsync("WorkspaceLiveStateController"),
|
Play.RefreshCampaignLogAsync, RequestRefreshAsync);
|
||||||
LoggerFactory.CreateLogger("Workspace.LiveState"));
|
|
||||||
|
|
||||||
private WorkspacePlayCoordinator Play => m_Play ??= new(State, Feedback, () => IsPlayRoute, ApiClient,
|
private WorkspacePlayCoordinator Play => m_Play ??= new(State, Feedback, () => IsPlayRoute, ApiClient,
|
||||||
WorkspaceQuery,
|
WorkspaceQuery,
|
||||||
CanEditCharacter, () => RequestRefreshAsync("WorkspacePlayCoordinator"));
|
CanEditCharacter, RequestRefreshAsync);
|
||||||
|
|
||||||
private WorkspaceCampaignCoordinator Campaigns => m_Campaigns ??= new(State, Feedback, JS, ApiClient,
|
private WorkspaceCampaignCoordinator Campaigns => m_Campaigns ??= new(State, Feedback, JS, ApiClient,
|
||||||
Session.LoadKnownUsernamesAsync, Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync,
|
Session.LoadKnownUsernamesAsync, Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync,
|
||||||
@@ -275,15 +166,13 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private WorkspaceAdminCoordinator Admin => m_Admin ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery,
|
private WorkspaceAdminCoordinator Admin => m_Admin ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery,
|
||||||
ClearAuthenticatedState, StopStateEventsAsync, message => LoggedOut.InvokeAsync(message));
|
ClearAuthenticatedState, StopStateEventsAsync, message => LoggedOut.InvokeAsync(message));
|
||||||
|
|
||||||
private WorkspaceFeedbackService Feedback =>
|
private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, RequestRefreshAsync);
|
||||||
m_Feedback ??= new(State, () => RequestRefreshAsync("WorkspaceFeedbackService"));
|
|
||||||
|
|
||||||
private WorkspaceSessionCoordinator Session => m_Session ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery,
|
private WorkspaceSessionCoordinator Session => m_Session ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery,
|
||||||
() => IsAdminRoute, RedirectToPlayAsync,
|
() => IsAdminRoute, RedirectToPlayAsync,
|
||||||
Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync, Scope.RefreshCampaignScopeAsync,
|
Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync, Scope.RefreshCampaignScopeAsync,
|
||||||
Live.SyncStateEventsAsync, Live.StopStateEventsAsync, EnsureAdminUsersLoadedAsync,
|
Live.SyncStateEventsAsync, Live.StopStateEventsAsync, EnsureAdminUsersLoadedAsync,
|
||||||
Play.ResetCampaignLogDetailState, message => LoggedOut.InvokeAsync(message),
|
Play.ResetCampaignLogDetailState, message => LoggedOut.InvokeAsync(message));
|
||||||
LoggerFactory.CreateLogger("Workspace.Session"));
|
|
||||||
|
|
||||||
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
|
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
|
||||||
{
|
{
|
||||||
@@ -331,7 +220,4 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private WorkspaceCampaignScopeCoordinator? m_Scope;
|
private WorkspaceCampaignScopeCoordinator? m_Scope;
|
||||||
private WorkspaceSessionCoordinator? m_Session;
|
private WorkspaceSessionCoordinator? m_Session;
|
||||||
private Task? InitializationTask { get; set; }
|
private Task? InitializationTask { get; set; }
|
||||||
private WorkspaceRoute? PreviousRoute { get; set; }
|
|
||||||
private int RenderCount { get; set; }
|
|
||||||
private WorkspaceRenderPhase RenderPhase { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages;
|
namespace RpgRoller.Components.Pages;
|
||||||
@@ -18,13 +17,10 @@ public sealed class WorkspaceCampaignScopeCoordinator(
|
|||||||
Action resetCampaignStateTracking,
|
Action resetCampaignStateTracking,
|
||||||
Action clearAuthenticatedState,
|
Action clearAuthenticatedState,
|
||||||
Func<Task> stopStateEventsAsync,
|
Func<Task> stopStateEventsAsync,
|
||||||
Func<string?, Task> onLoggedOutAsync,
|
Func<string?, Task> onLoggedOutAsync)
|
||||||
ILogger logger)
|
|
||||||
{
|
{
|
||||||
public async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
|
public async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceCampaignScopeCoordinator.ReloadCampaignsAsync start preferredCampaignId={PreferredCampaignId}",
|
|
||||||
preferredCampaignId);
|
|
||||||
var campaigns = await workspaceQuery.GetCampaignsAsync();
|
var campaigns = await workspaceQuery.GetCampaignsAsync();
|
||||||
state.Campaigns = campaigns.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
state.Campaigns = campaigns.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
@@ -43,29 +39,21 @@ public sealed class WorkspaceCampaignScopeCoordinator(
|
|||||||
|
|
||||||
await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey,
|
await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey,
|
||||||
state.SelectedCampaignId?.ToString());
|
state.SelectedCampaignId?.ToString());
|
||||||
logger.LogInformation("WorkspaceCampaignScopeCoordinator.ReloadCampaignsAsync end selectedCampaignId={SelectedCampaignId} campaignCount={CampaignCount}",
|
|
||||||
state.SelectedCampaignId, state.Campaigns.Count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReloadCharacterCampaignOptionsAsync()
|
public async Task ReloadCharacterCampaignOptionsAsync()
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceCampaignScopeCoordinator.ReloadCharacterCampaignOptionsAsync start");
|
|
||||||
var campaignOptions = await workspaceQuery.GetCharacterCampaignOptionsAsync();
|
var campaignOptions = await workspaceQuery.GetCharacterCampaignOptionsAsync();
|
||||||
state.CharacterCampaignOptions =
|
state.CharacterCampaignOptions =
|
||||||
campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
logger.LogInformation("WorkspaceCampaignScopeCoordinator.ReloadCharacterCampaignOptionsAsync end optionCount={OptionCount}",
|
|
||||||
state.CharacterCampaignOptions.Count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshCampaignRosterAsync()
|
public async Task RefreshCampaignRosterAsync()
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceCampaignScopeCoordinator.RefreshCampaignRosterAsync start selectedCampaignId={SelectedCampaignId}",
|
|
||||||
state.SelectedCampaignId);
|
|
||||||
if (!state.SelectedCampaignId.HasValue)
|
if (!state.SelectedCampaignId.HasValue)
|
||||||
{
|
{
|
||||||
state.SelectedCampaign = null;
|
state.SelectedCampaign = null;
|
||||||
state.SelectedCharacterId = null;
|
state.SelectedCharacterId = null;
|
||||||
logger.LogInformation("WorkspaceCampaignScopeCoordinator.RefreshCampaignRosterAsync no selected campaign");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,15 +65,10 @@ public sealed class WorkspaceCampaignScopeCoordinator(
|
|||||||
state.SelectedCharacterId = state.PlaySelectedCharacterId;
|
state.SelectedCharacterId = state.PlaySelectedCharacterId;
|
||||||
|
|
||||||
await ensureSelectedCharacterActiveAsync();
|
await ensureSelectedCharacterActiveAsync();
|
||||||
logger.LogInformation(
|
|
||||||
"WorkspaceCampaignScopeCoordinator.RefreshCampaignRosterAsync end selectedCampaign={SelectedCampaignId} selectedCharacterId={SelectedCharacterId} rosterCharacters={CharacterCount}",
|
|
||||||
state.SelectedCampaign?.Id, state.SelectedCharacterId, state.SelectedCampaign?.Characters.Length ?? 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshCampaignScopeAsync()
|
public async Task RefreshCampaignScopeAsync()
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceCampaignScopeCoordinator.RefreshCampaignScopeAsync start isPlayRoute={IsPlayRoute} stateBefore=[{State}]",
|
|
||||||
isPlayRoute(), WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
if (!state.SelectedCampaignId.HasValue)
|
if (!state.SelectedCampaignId.HasValue)
|
||||||
{
|
{
|
||||||
state.SelectedCampaign = null;
|
state.SelectedCampaign = null;
|
||||||
@@ -97,7 +80,6 @@ public sealed class WorkspaceCampaignScopeCoordinator(
|
|||||||
state.CurrentCampaignState = null;
|
state.CurrentCampaignState = null;
|
||||||
state.CampaignLogCursor = null;
|
state.CampaignLogCursor = null;
|
||||||
resetCampaignLogDetailState();
|
resetCampaignLogDetailState();
|
||||||
logger.LogInformation("WorkspaceCampaignScopeCoordinator.RefreshCampaignScopeAsync cleared empty scope");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,28 +106,22 @@ public sealed class WorkspaceCampaignScopeCoordinator(
|
|||||||
}
|
}
|
||||||
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "WorkspaceCampaignScopeCoordinator.RefreshCampaignScopeAsync unauthorized");
|
|
||||||
clearAuthenticatedState();
|
clearAuthenticatedState();
|
||||||
await stopStateEventsAsync();
|
await stopStateEventsAsync();
|
||||||
await onLoggedOutAsync("Session expired. Please log in again.");
|
await onLoggedOutAsync("Session expired. Please log in again.");
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex)
|
catch (ApiRequestException ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "WorkspaceCampaignScopeCoordinator.RefreshCampaignScopeAsync failed");
|
|
||||||
feedback.SetStatus(ex.Message, true);
|
feedback.SetStatus(ex.Message, true);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
state.IsCampaignDataLoading = false;
|
state.IsCampaignDataLoading = false;
|
||||||
logger.LogInformation("WorkspaceCampaignScopeCoordinator.RefreshCampaignScopeAsync end stateAfter=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetMobilePanelAsync(string panel)
|
public async Task SetMobilePanelAsync(string panel)
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceCampaignScopeCoordinator.SetMobilePanelAsync old={OldPanel} new={NewPanel}",
|
|
||||||
state.MobilePanel, panel);
|
|
||||||
state.MobilePanel = string.Equals(panel, "log", StringComparison.OrdinalIgnoreCase) ? "log" : "character";
|
state.MobilePanel = string.Equals(panel, "log", StringComparison.OrdinalIgnoreCase) ? "log" : "character";
|
||||||
await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", MobilePanelSessionKey, state.MobilePanel);
|
await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", MobilePanelSessionKey, state.MobilePanel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
using RpgRoller.Contracts;
|
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages;
|
|
||||||
|
|
||||||
internal static class WorkspaceDiagnosticSummary
|
|
||||||
{
|
|
||||||
public static string DescribeState(WorkspaceState state)
|
|
||||||
{
|
|
||||||
var user = state.User?.Username ?? "<null>";
|
|
||||||
var selectedCampaign = state.SelectedCampaignId?.ToString() ?? "<null>";
|
|
||||||
var selectedCharacter = state.SelectedCharacterId?.ToString() ?? "<null>";
|
|
||||||
return
|
|
||||||
$"user={user}, selectedCampaign={selectedCampaign}, selectedCharacter={selectedCharacter}, campaigns={state.Campaigns.Count}, adminUsers={state.AdminUsers.Count}, skills={state.SelectedCharacterSkills.Count}, skillGroups={state.SelectedCharacterSkillGroups.Count}, logEntries={state.CampaignLog.Count}, isCampaignLoading={state.IsCampaignDataLoading}, isAdminLoading={state.IsAdminDataLoading}, connection={state.ConnectionState}, mobilePanel={state.MobilePanel}, hasHealthIssue={state.HasHealthIssue}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string DescribeSnapshot(CampaignStateSnapshot snapshot)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
$"campaignId={snapshot.CampaignId}, totalVersion={snapshot.TotalVersion}, rosterVersion={snapshot.RosterVersion}, logVersion={snapshot.LogVersion}, characterVersions={snapshot.CharacterVersions.Count}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string DescribePlaySurface(WorkspacePageContext workspace)
|
|
||||||
{
|
|
||||||
var playCampaign = workspace.State.PlaySelectedCampaign;
|
|
||||||
return
|
|
||||||
$"hasSessionInitialized={workspace.HasSessionInitialized}, selectedCampaign={workspace.State.SelectedCampaignId?.ToString() ?? "<null>"}, playCampaignCharacters={playCampaign?.Characters.Length ?? 0}, playSelectedCharacter={workspace.State.PlaySelectedCharacterId?.ToString() ?? "<null>"}, playSkills={workspace.State.PlaySelectedCharacterSkills.Count}, playSkillGroups={workspace.State.PlaySelectedCharacterSkillGroups.Count}, playLog={workspace.State.PlayVisibleCampaignLog.Count}, mobilePanel={workspace.State.MobilePanel}, isCampaignLoading={workspace.State.IsCampaignDataLoading}, showRolemasterModal={workspace.State.ShowRolemasterSkillRollModal}, showCreateCharacterModal={workspace.State.ShowCreateCharacterModal}, showEditCharacterModal={workspace.State.ShowEditCharacterModal}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string DescribeCampaignsSurface(WorkspacePageContext workspace)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
$"hasSessionInitialized={workspace.HasSessionInitialized}, selectedCampaign={workspace.State.SelectedCampaignId?.ToString() ?? "<null>"}, selectedCampaignName={workspace.State.SelectedCampaignName ?? "<null>"}, campaigns={workspace.State.Campaigns.Count}, rulesets={workspace.State.Rulesets.Count}, selectedRosterCharacters={workspace.State.SelectedCampaign?.Characters.Length ?? 0}, characterCampaignOptions={workspace.State.CharacterCampaignOptions.Count}, isCampaignLoading={workspace.State.IsCampaignDataLoading}, showCreateCharacterModal={workspace.State.ShowCreateCharacterModal}, showEditCharacterModal={workspace.State.ShowEditCharacterModal}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string DescribeAdminSurface(WorkspacePageContext workspace)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
$"hasSessionInitialized={workspace.HasSessionInitialized}, currentUser={(workspace.State.User?.Username ?? "<null>")}, isCurrentUserAdmin={workspace.State.IsCurrentUserAdmin}, adminUsers={workspace.State.AdminUsers.Count}, hasLoadedAdminUsers={workspace.State.HasLoadedAdminUsers}, isAdminLoading={workspace.State.IsAdminDataLoading}, isMutating={workspace.State.IsMutating}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages;
|
namespace RpgRoller.Components.Pages;
|
||||||
@@ -15,13 +14,10 @@ public sealed class WorkspaceLiveStateController(
|
|||||||
Func<Task> refreshCampaignRosterAsync,
|
Func<Task> refreshCampaignRosterAsync,
|
||||||
Func<Task> refreshSelectedCharacterSheetAsync,
|
Func<Task> refreshSelectedCharacterSheetAsync,
|
||||||
Func<Guid?, Task> refreshCampaignLogAsync,
|
Func<Guid?, Task> refreshCampaignLogAsync,
|
||||||
Func<Task> requestRefreshAsync,
|
Func<Task> requestRefreshAsync)
|
||||||
ILogger logger)
|
|
||||||
{
|
{
|
||||||
public async Task OnStateEventReceivedAsync(CampaignStateSnapshot state1)
|
public async Task OnStateEventReceivedAsync(CampaignStateSnapshot state1)
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceLiveStateController.OnStateEventReceivedAsync start snapshot=[{Snapshot}] stateBefore=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeSnapshot(state1), WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
if (state.StateRefreshInProgress)
|
if (state.StateRefreshInProgress)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -62,15 +58,12 @@ public sealed class WorkspaceLiveStateController(
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
state.StateRefreshInProgress = false;
|
state.StateRefreshInProgress = false;
|
||||||
logger.LogInformation("WorkspaceLiveStateController.OnStateEventReceivedAsync end stateAfter=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
await requestRefreshAsync();
|
await requestRefreshAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnConnectionStateChangedAsync(string state1)
|
public async Task OnConnectionStateChangedAsync(string state1)
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceLiveStateController.OnConnectionStateChangedAsync newState={ConnectionState}", state1);
|
|
||||||
state.ConnectionState = state1 switch
|
state.ConnectionState = state1 switch
|
||||||
{
|
{
|
||||||
"connected" => "connected",
|
"connected" => "connected",
|
||||||
@@ -89,20 +82,15 @@ public sealed class WorkspaceLiveStateController(
|
|||||||
|
|
||||||
public async Task SyncStateEventsAsync()
|
public async Task SyncStateEventsAsync()
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceLiveStateController.SyncStateEventsAsync start isPlayRoute={IsPlayRoute} isAdminRoute={IsAdminRoute} state=[{State}]",
|
|
||||||
isPlayRoute(), isAdminRoute(), WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
if (state.User is null || !state.SelectedCampaignId.HasValue || isAdminRoute() || !isPlayRoute())
|
if (state.User is null || !state.SelectedCampaignId.HasValue || isAdminRoute() || !isPlayRoute())
|
||||||
{
|
{
|
||||||
await StopStateEventsAsync();
|
await StopStateEventsAsync();
|
||||||
state.ConnectionState = "offline";
|
state.ConnectionState = "offline";
|
||||||
logger.LogInformation("WorkspaceLiveStateController.SyncStateEventsAsync disabled");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await startStateEventsAsync(state.SelectedCampaignId.Value);
|
await startStateEventsAsync(state.SelectedCampaignId.Value);
|
||||||
state.ConnectionState = "reconnecting";
|
state.ConnectionState = "reconnecting";
|
||||||
logger.LogInformation("WorkspaceLiveStateController.SyncStateEventsAsync started campaignId={CampaignId}",
|
|
||||||
state.SelectedCampaignId.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopStateEventsAsync()
|
public async Task StopStateEventsAsync()
|
||||||
@@ -110,7 +98,6 @@ public sealed class WorkspaceLiveStateController(
|
|||||||
if (!state.HasInteractiveRenderStarted)
|
if (!state.HasInteractiveRenderStarted)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
logger.LogInformation("WorkspaceLiveStateController.StopStateEventsAsync");
|
|
||||||
await stopStateEventsCoreAsync();
|
await stopStateEventsCoreAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,8 @@ public sealed class WorkspacePageContext(
|
|||||||
Func<Task> initializeRouteAsync,
|
Func<Task> initializeRouteAsync,
|
||||||
bool hasSessionInitialized,
|
bool hasSessionInitialized,
|
||||||
Func<Task> requestRefreshAsync,
|
Func<Task> requestRefreshAsync,
|
||||||
Func<Task> ensureLiveRenderPhaseAsync,
|
|
||||||
string adminDatabaseDownloadUrl,
|
string adminDatabaseDownloadUrl,
|
||||||
IReadOnlyList<AppHeaderMenuItem> headerMenuItems,
|
IReadOnlyList<AppHeaderMenuItem> headerMenuItems,
|
||||||
WorkspaceRenderPhase renderPhase,
|
|
||||||
bool isPlayRoute,
|
bool isPlayRoute,
|
||||||
bool isCampaignsRoute,
|
bool isCampaignsRoute,
|
||||||
bool isAdminRoute)
|
bool isAdminRoute)
|
||||||
@@ -29,14 +27,9 @@ public sealed class WorkspacePageContext(
|
|||||||
public Func<Task> InitializeRouteAsync { get; } = initializeRouteAsync;
|
public Func<Task> InitializeRouteAsync { get; } = initializeRouteAsync;
|
||||||
public bool HasSessionInitialized { get; } = hasSessionInitialized;
|
public bool HasSessionInitialized { get; } = hasSessionInitialized;
|
||||||
public Func<Task> RequestRefreshAsync { get; } = requestRefreshAsync;
|
public Func<Task> RequestRefreshAsync { get; } = requestRefreshAsync;
|
||||||
public Func<Task> EnsureLiveRenderPhaseAsync { get; } = ensureLiveRenderPhaseAsync;
|
|
||||||
public string AdminDatabaseDownloadUrl { get; } = adminDatabaseDownloadUrl;
|
public string AdminDatabaseDownloadUrl { get; } = adminDatabaseDownloadUrl;
|
||||||
public IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems { get; } = headerMenuItems;
|
public IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems { get; } = headerMenuItems;
|
||||||
public WorkspaceRenderPhase RenderPhase { get; } = renderPhase;
|
|
||||||
public bool IsPlayRoute { get; } = isPlayRoute;
|
public bool IsPlayRoute { get; } = isPlayRoute;
|
||||||
public bool IsCampaignsRoute { get; } = isCampaignsRoute;
|
public bool IsCampaignsRoute { get; } = isCampaignsRoute;
|
||||||
public bool IsAdminRoute { get; } = isAdminRoute;
|
public bool IsAdminRoute { get; } = isAdminRoute;
|
||||||
public bool ShowHeaderPlaceholder => RenderPhase >= WorkspaceRenderPhase.HeaderPlaceholder;
|
|
||||||
public bool ShowRouteSkeleton => RenderPhase >= WorkspaceRenderPhase.RouteSkeleton;
|
|
||||||
public bool ShowLiveContent => RenderPhase == WorkspaceRenderPhase.Live;
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace RpgRoller.Components.Pages;
|
|
||||||
|
|
||||||
public enum WorkspaceRenderPhase
|
|
||||||
{
|
|
||||||
Minimal = 0,
|
|
||||||
HeaderPlaceholder = 1,
|
|
||||||
RouteSkeleton = 2,
|
|
||||||
Live = 3
|
|
||||||
}
|
|
||||||
@@ -1,61 +1,14 @@
|
|||||||
@using Microsoft.AspNetCore.Components
|
|
||||||
@using Microsoft.JSInterop
|
|
||||||
|
|
||||||
@ChildContent(Workspace)
|
@ChildContent(Workspace)
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
Logger.LogInformation(
|
|
||||||
"WorkspaceRouteView.OnAfterRenderAsync routeFlags=play:{IsPlayRoute} campaigns:{IsCampaignsRoute} admin:{IsAdminRoute} firstRender={FirstRender} renderPhase={RenderPhase} hasSessionInitialized={HasSessionInitialized} state=[{State}]",
|
|
||||||
Workspace.IsPlayRoute, Workspace.IsCampaignsRoute, Workspace.IsAdminRoute, firstRender,
|
|
||||||
Workspace.RenderPhase, Workspace.HasSessionInitialized, WorkspaceDiagnosticSummary.DescribeState(Workspace.State));
|
|
||||||
await TryMarkWorkspacePhaseAsync(firstRender ? "after-first-render" : "after-render");
|
|
||||||
if (!firstRender)
|
if (!firstRender)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await TryInstallWorkspaceDiagnosticsAsync();
|
|
||||||
await Workspace.InitializeRouteAsync();
|
await Workspace.InitializeRouteAsync();
|
||||||
await Workspace.EnsureLiveRenderPhaseAsync();
|
|
||||||
Logger.LogInformation("WorkspaceRouteView.OnAfterRenderAsync initialized state=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(Workspace.State));
|
|
||||||
await TryMarkWorkspacePhaseAsync("after-initialize-route");
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Inject] private ILogger<WorkspaceRouteView> Logger { get; set; } = null!;
|
|
||||||
[Inject] private IJSRuntime JS { get; set; } = null!;
|
|
||||||
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
||||||
|
|
||||||
[Parameter, EditorRequired] public RenderFragment<WorkspacePageContext> ChildContent { get; set; } = null!;
|
[Parameter, EditorRequired] public RenderFragment<WorkspacePageContext> ChildContent { get; set; } = null!;
|
||||||
|
|
||||||
private async Task TryInstallWorkspaceDiagnosticsAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await JS.InvokeVoidAsync("rpgRollerApi.installWorkspaceDiagnostics", WorkspaceRouteName);
|
|
||||||
}
|
|
||||||
catch (JSDisconnectedException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex) when (ex.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TryMarkWorkspacePhaseAsync(string phase)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await JS.InvokeVoidAsync("rpgRollerApi.markWorkspacePhase", $"{WorkspaceRouteName}:{phase}");
|
|
||||||
}
|
|
||||||
catch (JSDisconnectedException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex) when (ex.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string WorkspaceRouteName => Workspace.IsPlayRoute ? "play" : Workspace.IsCampaignsRoute ? "campaigns" : "admin";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages;
|
namespace RpgRoller.Components.Pages;
|
||||||
@@ -19,13 +18,10 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
Func<Task> stopStateEventsAsync,
|
Func<Task> stopStateEventsAsync,
|
||||||
Func<Task> ensureAdminUsersLoadedAsync,
|
Func<Task> ensureAdminUsersLoadedAsync,
|
||||||
Action resetCampaignLogDetailState,
|
Action resetCampaignLogDetailState,
|
||||||
Func<string?, Task> onLoggedOutAsync,
|
Func<string?, Task> onLoggedOutAsync)
|
||||||
ILogger logger)
|
|
||||||
{
|
{
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.InitializeAsync start isAdminRoute={IsAdminRoute} state=[{State}]",
|
|
||||||
isAdminRoute(), WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
var storedPanel = await js.InvokeAsync<string?>("rpgRollerApi.getSessionValue", MobilePanelSessionKey);
|
var storedPanel = await js.InvokeAsync<string?>("rpgRollerApi.getSessionValue", MobilePanelSessionKey);
|
||||||
if (string.Equals(storedPanel, "log", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(storedPanel, "log", StringComparison.OrdinalIgnoreCase))
|
||||||
state.MobilePanel = "log";
|
state.MobilePanel = "log";
|
||||||
@@ -42,29 +38,19 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
preferredCampaignId = parsedCampaignId;
|
preferredCampaignId = parsedCampaignId;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation(
|
|
||||||
"WorkspaceSessionCoordinator.InitializeAsync sessionValues panel={Panel} rollVisibility={RollVisibility} preferredCampaignId={PreferredCampaignId}",
|
|
||||||
state.MobilePanel, state.RollVisibility, preferredCampaignId);
|
|
||||||
|
|
||||||
await CheckHealthAsync();
|
await CheckHealthAsync();
|
||||||
|
|
||||||
var reloaded = await ReloadAuthenticatedSessionAsync(preferredCampaignId);
|
var reloaded = await ReloadAuthenticatedSessionAsync(preferredCampaignId);
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.InitializeAsync end reloaded={Reloaded} state=[{State}]",
|
|
||||||
reloaded, WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
if (!reloaded)
|
if (!reloaded)
|
||||||
await onLoggedOutAsync("Session expired. Please log in again.");
|
await onLoggedOutAsync("Session expired. Please log in again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RetryAfterHealthIssueAsync()
|
public async Task RetryAfterHealthIssueAsync()
|
||||||
{
|
{
|
||||||
logger.LogWarning("WorkspaceSessionCoordinator.RetryAfterHealthIssueAsync start state=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
await CheckHealthAsync();
|
await CheckHealthAsync();
|
||||||
if (!state.HasHealthIssue && state.User is not null)
|
if (!state.HasHealthIssue && state.User is not null)
|
||||||
{
|
{
|
||||||
var reloaded = await ReloadAuthenticatedSessionAsync(state.SelectedCampaignId);
|
var reloaded = await ReloadAuthenticatedSessionAsync(state.SelectedCampaignId);
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.RetryAfterHealthIssueAsync reloaded={Reloaded} state=[{State}]",
|
|
||||||
reloaded, WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
if (!reloaded)
|
if (!reloaded)
|
||||||
await onLoggedOutAsync("Session expired. Please log in again.");
|
await onLoggedOutAsync("Session expired. Please log in again.");
|
||||||
}
|
}
|
||||||
@@ -86,8 +72,6 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
|
|
||||||
public async Task LogoutAsync()
|
public async Task LogoutAsync()
|
||||||
{
|
{
|
||||||
logger.LogWarning("WorkspaceSessionCoordinator.LogoutAsync start state=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
if (state.IsMutating)
|
if (state.IsMutating)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -111,16 +95,12 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
|
|
||||||
public async Task OnRollVisibilityChangedAsync(string visibility)
|
public async Task OnRollVisibilityChangedAsync(string visibility)
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.OnRollVisibilityChangedAsync old={OldVisibility} new={NewVisibility}",
|
|
||||||
state.RollVisibility, visibility);
|
|
||||||
state.RollVisibility = NormalizeRollVisibility(visibility);
|
state.RollVisibility = NormalizeRollVisibility(visibility);
|
||||||
await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", RollVisibilitySessionKey, state.RollVisibility);
|
await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", RollVisibilitySessionKey, state.RollVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearAuthenticatedState()
|
public void ClearAuthenticatedState()
|
||||||
{
|
{
|
||||||
logger.LogWarning("WorkspaceSessionCoordinator.ClearAuthenticatedState stateBefore=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
state.User = null;
|
state.User = null;
|
||||||
state.ActiveCharacterId = null;
|
state.ActiveCharacterId = null;
|
||||||
state.SelectedCampaignId = null;
|
state.SelectedCampaignId = null;
|
||||||
@@ -146,8 +126,6 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
state.HasLoadedAdminUsers = false;
|
state.HasLoadedAdminUsers = false;
|
||||||
state.IsAdminDataLoading = false;
|
state.IsAdminDataLoading = false;
|
||||||
feedback.ClearToasts();
|
feedback.ClearToasts();
|
||||||
logger.LogWarning("WorkspaceSessionCoordinator.ClearAuthenticatedState stateAfter=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CheckHealthAsync()
|
private async Task CheckHealthAsync()
|
||||||
@@ -162,25 +140,18 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
state.Rulesets = (await workspaceQuery.GetRulesetsAsync()).ToList();
|
state.Rulesets = (await workspaceQuery.GetRulesetsAsync()).ToList();
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.LoadRulesetsAsync loadedRulesets={RulesetCount}",
|
|
||||||
state.Rulesets.Count);
|
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex)
|
catch (ApiRequestException ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "WorkspaceSessionCoordinator.LoadRulesetsAsync failed");
|
|
||||||
feedback.SetStatus(ex.Message, true);
|
feedback.SetStatus(ex.Message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> ReloadAuthenticatedSessionAsync(Guid? preferredCampaignId)
|
private async Task<bool> ReloadAuthenticatedSessionAsync(Guid? preferredCampaignId)
|
||||||
{
|
{
|
||||||
logger.LogInformation(
|
|
||||||
"WorkspaceSessionCoordinator.ReloadAuthenticatedSessionAsync start preferredCampaignId={PreferredCampaignId} isAdminRoute={IsAdminRoute}",
|
|
||||||
preferredCampaignId, isAdminRoute());
|
|
||||||
var me = await TryGetMeAsync();
|
var me = await TryGetMeAsync();
|
||||||
if (me is null)
|
if (me is null)
|
||||||
{
|
{
|
||||||
logger.LogWarning("WorkspaceSessionCoordinator.ReloadAuthenticatedSessionAsync unauthorized");
|
|
||||||
ClearAuthenticatedState();
|
ClearAuthenticatedState();
|
||||||
await stopStateEventsAsync();
|
await stopStateEventsAsync();
|
||||||
return false;
|
return false;
|
||||||
@@ -188,36 +159,22 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
|
|
||||||
state.User = me.User;
|
state.User = me.User;
|
||||||
state.ActiveCharacterId = me.ActiveCharacterId;
|
state.ActiveCharacterId = me.ActiveCharacterId;
|
||||||
logger.LogInformation(
|
|
||||||
"WorkspaceSessionCoordinator.ReloadAuthenticatedSessionAsync me user={User} activeCharacterId={ActiveCharacterId} currentCampaignId={CurrentCampaignId}",
|
|
||||||
me.User.Username, me.ActiveCharacterId, me.CurrentCampaignId);
|
|
||||||
if (!await EnsureRouteAccessAsync())
|
if (!await EnsureRouteAccessAsync())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (isAdminRoute())
|
if (isAdminRoute())
|
||||||
{
|
{
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.ReloadAuthenticatedSessionAsync adminRoute path");
|
|
||||||
await stopStateEventsAsync();
|
await stopStateEventsAsync();
|
||||||
state.ConnectionState = "offline";
|
state.ConnectionState = "offline";
|
||||||
await ensureAdminUsersLoadedAsync();
|
await ensureAdminUsersLoadedAsync();
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.ReloadAuthenticatedSessionAsync adminRoute end state=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await LoadRulesetsAsync();
|
await LoadRulesetsAsync();
|
||||||
await reloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId);
|
await reloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId);
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.ReloadAuthenticatedSessionAsync campaigns loaded state=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
await reloadCharacterCampaignOptionsAsync();
|
await reloadCharacterCampaignOptionsAsync();
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.ReloadAuthenticatedSessionAsync campaign options loaded count={Count}",
|
|
||||||
state.CharacterCampaignOptions.Count);
|
|
||||||
await refreshCampaignScopeAsync();
|
await refreshCampaignScopeAsync();
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.ReloadAuthenticatedSessionAsync campaign scope refreshed state=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
await syncStateEventsAsync();
|
await syncStateEventsAsync();
|
||||||
logger.LogInformation("WorkspaceSessionCoordinator.ReloadAuthenticatedSessionAsync end state=[{State}]",
|
|
||||||
WorkspaceDiagnosticSummary.DescribeState(state));
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -230,7 +187,6 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
}
|
}
|
||||||
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "WorkspaceSessionCoordinator.TryGetMeAsync unauthorized");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,7 +200,6 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
|
|
||||||
state.AdminUsers = [];
|
state.AdminUsers = [];
|
||||||
state.HasLoadedAdminUsers = false;
|
state.HasLoadedAdminUsers = false;
|
||||||
logger.LogWarning("WorkspaceSessionCoordinator.EnsureRouteAccessAsync redirecting non-admin away from admin route");
|
|
||||||
await redirectToPlayAsync();
|
await redirectToPlayAsync();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ using RpgRoller.Hosting;
|
|||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
|
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
|
||||||
builder.Services.AddRazorComponents().AddInteractiveServerComponents(options =>
|
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
||||||
{
|
|
||||||
options.DetailedErrors = builder.Environment.IsDevelopment();
|
|
||||||
});
|
|
||||||
builder.Services.AddResponseCompression(options =>
|
builder.Services.AddResponseCompression(options =>
|
||||||
{
|
{
|
||||||
options.EnableForHttps = true;
|
options.EnableForHttps = true;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
window.rpgRollerApi = (() => {
|
window.rpgRollerApi = (() => {
|
||||||
const sessionPrefix = "rpgroller.";
|
const sessionPrefix = "rpgroller.";
|
||||||
const debugPrefix = "[rpgroller-api]";
|
|
||||||
const stateStream = {
|
const stateStream = {
|
||||||
source: null,
|
source: null,
|
||||||
dotNetRef: null,
|
dotNetRef: null,
|
||||||
@@ -9,306 +8,6 @@ window.rpgRollerApi = (() => {
|
|||||||
reconnectTimer: null,
|
reconnectTimer: null,
|
||||||
stopped: true
|
stopped: true
|
||||||
};
|
};
|
||||||
const workspaceDiagnostics = {
|
|
||||||
observer: null,
|
|
||||||
route: null,
|
|
||||||
globalHandlersInstalled: false,
|
|
||||||
domOperationDiagnosticsInstalled: false,
|
|
||||||
mutationBatchCount: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
function debug(...args) {
|
|
||||||
console.info(debugPrefix, new Date().toISOString(), ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
function warn(...args) {
|
|
||||||
console.warn(debugPrefix, new Date().toISOString(), ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
function summarizeChildren(element) {
|
|
||||||
if (!element || !element.childNodes) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(element.childNodes).slice(0, 20).map(summarizeNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function summarizeNode(node) {
|
|
||||||
if (!node) {
|
|
||||||
return "<null>";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
|
||||||
const text = (node.textContent || "").trim().replace(/\s+/g, " ");
|
|
||||||
return text ? `#text(${text.slice(0, 40)})` : "#text";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(node instanceof Element)) {
|
|
||||||
return `nodeType:${node.nodeType}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = node.id ? `#${node.id}` : "";
|
|
||||||
const classes = node.classList && node.classList.length > 0
|
|
||||||
? `.${Array.from(node.classList).join(".")}`
|
|
||||||
: "";
|
|
||||||
return `${node.tagName.toLowerCase()}${id}${classes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function summarizeElementQuery(selector) {
|
|
||||||
const matches = Array.from(document.querySelectorAll(selector));
|
|
||||||
return {
|
|
||||||
count: matches.length,
|
|
||||||
first: matches.length > 0 ? summarizeNode(matches[0]) : null,
|
|
||||||
firstParent: matches.length > 0 ? summarizeNode(matches[0].parentElement) : null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorkspaceRoot() {
|
|
||||||
return document.querySelector("[data-workspace-root]");
|
|
||||||
}
|
|
||||||
|
|
||||||
function logWorkspaceSnapshot(label) {
|
|
||||||
const root = getWorkspaceRoot();
|
|
||||||
const trackedSelectors = [
|
|
||||||
".workspace-shell",
|
|
||||||
".workspace-header",
|
|
||||||
".play-screen",
|
|
||||||
".management-screen",
|
|
||||||
".mobile-bottom-nav",
|
|
||||||
"#skill-filter-input",
|
|
||||||
"#roll-visibility",
|
|
||||||
"#custom-roll-expression",
|
|
||||||
".modal-overlay",
|
|
||||||
".management-list"
|
|
||||||
];
|
|
||||||
const tracked = Object.fromEntries(trackedSelectors.map((selector) => [selector, summarizeElementQuery(selector)]));
|
|
||||||
debug("workspace snapshot", {
|
|
||||||
label,
|
|
||||||
route: workspaceDiagnostics.route,
|
|
||||||
root: summarizeNode(root),
|
|
||||||
rootAttributes: root
|
|
||||||
? {
|
|
||||||
route: root.getAttribute("data-workspace-route"),
|
|
||||||
sessionInitialized: root.getAttribute("data-workspace-session-initialized"),
|
|
||||||
campaignLoading: root.getAttribute("data-workspace-campaign-loading"),
|
|
||||||
adminLoading: root.getAttribute("data-workspace-admin-loading"),
|
|
||||||
user: root.getAttribute("data-workspace-user")
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
rootChildren: root ? Array.from(root.children).slice(0, 8).map(summarizeNode) : [],
|
|
||||||
activeElement: summarizeNode(document.activeElement),
|
|
||||||
tracked
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function installGlobalDiagnostics() {
|
|
||||||
if (workspaceDiagnostics.globalHandlersInstalled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceDiagnostics.globalHandlersInstalled = true;
|
|
||||||
window.addEventListener("error", (event) => {
|
|
||||||
warn("window error", {
|
|
||||||
message: event.message,
|
|
||||||
filename: event.filename,
|
|
||||||
lineno: event.lineno,
|
|
||||||
colno: event.colno,
|
|
||||||
target: summarizeNode(event.target)
|
|
||||||
});
|
|
||||||
logWorkspaceSnapshot("window-error");
|
|
||||||
});
|
|
||||||
window.addEventListener("unhandledrejection", (event) => {
|
|
||||||
warn("window unhandledrejection", {
|
|
||||||
reason: String(event.reason),
|
|
||||||
type: typeof event.reason
|
|
||||||
});
|
|
||||||
logWorkspaceSnapshot("unhandledrejection");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function installDomOperationDiagnostics() {
|
|
||||||
if (workspaceDiagnostics.domOperationDiagnosticsInstalled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceDiagnostics.domOperationDiagnosticsInstalled = true;
|
|
||||||
const originalInsertBefore = Node.prototype.insertBefore;
|
|
||||||
const originalAppendChild = Node.prototype.appendChild;
|
|
||||||
const originalRemoveChild = Node.prototype.removeChild;
|
|
||||||
const originalReplaceChild = Node.prototype.replaceChild;
|
|
||||||
|
|
||||||
Node.prototype.insertBefore = function (newNode, referenceNode) {
|
|
||||||
debug("dom insertBefore", {
|
|
||||||
parent: summarizeNode(this),
|
|
||||||
newNode: summarizeNode(newNode),
|
|
||||||
referenceNode: summarizeNode(referenceNode),
|
|
||||||
referenceParent: referenceNode ? summarizeNode(referenceNode.parentNode) : null
|
|
||||||
});
|
|
||||||
return originalInsertBefore.call(this, newNode, referenceNode);
|
|
||||||
};
|
|
||||||
|
|
||||||
Node.prototype.appendChild = function (child) {
|
|
||||||
debug("dom appendChild", {
|
|
||||||
parent: summarizeNode(this),
|
|
||||||
child: summarizeNode(child)
|
|
||||||
});
|
|
||||||
return originalAppendChild.call(this, child);
|
|
||||||
};
|
|
||||||
|
|
||||||
Node.prototype.removeChild = function (child) {
|
|
||||||
debug("dom removeChild", {
|
|
||||||
parent: summarizeNode(this),
|
|
||||||
child: summarizeNode(child)
|
|
||||||
});
|
|
||||||
return originalRemoveChild.call(this, child);
|
|
||||||
};
|
|
||||||
|
|
||||||
Node.prototype.replaceChild = function (newChild, oldChild) {
|
|
||||||
debug("dom replaceChild", {
|
|
||||||
parent: summarizeNode(this),
|
|
||||||
newChild: summarizeNode(newChild),
|
|
||||||
oldChild: summarizeNode(oldChild)
|
|
||||||
});
|
|
||||||
return originalReplaceChild.call(this, newChild, oldChild);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function summarizeMutation(mutation) {
|
|
||||||
return {
|
|
||||||
type: mutation.type,
|
|
||||||
target: summarizeNode(mutation.target),
|
|
||||||
attributeName: mutation.attributeName || null,
|
|
||||||
added: Array.from(mutation.addedNodes || []).slice(0, 5).map(summarizeNode),
|
|
||||||
removed: Array.from(mutation.removedNodes || []).slice(0, 5).map(summarizeNode)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function installWorkspaceDiagnostics(route) {
|
|
||||||
installGlobalDiagnostics();
|
|
||||||
workspaceDiagnostics.route = route;
|
|
||||||
workspaceDiagnostics.mutationBatchCount = 0;
|
|
||||||
|
|
||||||
if (workspaceDiagnostics.observer) {
|
|
||||||
workspaceDiagnostics.observer.disconnect();
|
|
||||||
workspaceDiagnostics.observer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = getWorkspaceRoot();
|
|
||||||
if (!root) {
|
|
||||||
warn("installWorkspaceDiagnostics skipped; no workspace root", { route });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceDiagnostics.observer = new MutationObserver((mutations) => {
|
|
||||||
workspaceDiagnostics.mutationBatchCount += 1;
|
|
||||||
debug("workspace mutations", {
|
|
||||||
route: workspaceDiagnostics.route,
|
|
||||||
mutationBatchCount: workspaceDiagnostics.mutationBatchCount,
|
|
||||||
mutations: mutations.slice(0, 20).map(summarizeMutation)
|
|
||||||
});
|
|
||||||
logWorkspaceSnapshot(`mutation-batch-${workspaceDiagnostics.mutationBatchCount}`);
|
|
||||||
});
|
|
||||||
workspaceDiagnostics.observer.observe(root, {
|
|
||||||
subtree: true,
|
|
||||||
childList: true,
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: [
|
|
||||||
"class",
|
|
||||||
"hidden",
|
|
||||||
"aria-expanded",
|
|
||||||
"data-workspace-route",
|
|
||||||
"data-workspace-session-initialized",
|
|
||||||
"data-workspace-campaign-loading",
|
|
||||||
"data-workspace-admin-loading",
|
|
||||||
"data-workspace-user"
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
debug("installWorkspaceDiagnostics attached", { route, root: summarizeNode(root) });
|
|
||||||
logWorkspaceSnapshot(`install:${route}`);
|
|
||||||
queueMicrotask(() => logWorkspaceSnapshot(`microtask:${route}`));
|
|
||||||
requestAnimationFrame(() => logWorkspaceSnapshot(`raf1:${route}`));
|
|
||||||
setTimeout(() => logWorkspaceSnapshot(`timeout25:${route}`), 25);
|
|
||||||
setTimeout(() => logWorkspaceSnapshot(`timeout100:${route}`), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function markWorkspacePhase(label) {
|
|
||||||
debug("workspace phase", label);
|
|
||||||
logWorkspaceSnapshot(`phase:${label}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bootstrapPreBlazorDiagnostics(requestPath) {
|
|
||||||
installGlobalDiagnostics();
|
|
||||||
installDomOperationDiagnostics();
|
|
||||||
|
|
||||||
const host = document.getElementById("rr-interactive-host");
|
|
||||||
workspaceDiagnostics.route = requestPath;
|
|
||||||
debug("bootstrapPreBlazorDiagnostics", {
|
|
||||||
requestPath,
|
|
||||||
readyState: document.readyState,
|
|
||||||
bodyChildren: summarizeChildren(document.body),
|
|
||||||
host: summarizeNode(host),
|
|
||||||
hostChildren: summarizeChildren(host)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!host) {
|
|
||||||
warn("bootstrapPreBlazorDiagnostics missing host", { requestPath });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const preconnectObserver = new MutationObserver((mutations) => {
|
|
||||||
debug("preblazor host mutations", {
|
|
||||||
requestPath,
|
|
||||||
mutations: mutations.slice(0, 20).map(summarizeMutation),
|
|
||||||
bodyChildren: summarizeChildren(document.body),
|
|
||||||
hostChildren: summarizeChildren(host)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
preconnectObserver.observe(host, {
|
|
||||||
subtree: true,
|
|
||||||
childList: true,
|
|
||||||
attributes: true,
|
|
||||||
characterData: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const bodyObserver = new MutationObserver((mutations) => {
|
|
||||||
debug("preblazor body mutations", {
|
|
||||||
requestPath,
|
|
||||||
mutations: mutations.slice(0, 20).map(summarizeMutation),
|
|
||||||
bodyChildren: summarizeChildren(document.body),
|
|
||||||
hostChildren: summarizeChildren(host)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
bodyObserver.observe(document.body, {
|
|
||||||
subtree: false,
|
|
||||||
childList: true,
|
|
||||||
attributes: false
|
|
||||||
});
|
|
||||||
|
|
||||||
queueMicrotask(() => {
|
|
||||||
debug("preblazor microtask snapshot", {
|
|
||||||
requestPath,
|
|
||||||
bodyChildren: summarizeChildren(document.body),
|
|
||||||
hostChildren: summarizeChildren(host)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
debug("preblazor raf snapshot", {
|
|
||||||
requestPath,
|
|
||||||
bodyChildren: summarizeChildren(document.body),
|
|
||||||
hostChildren: summarizeChildren(host)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
debug("preblazor timeout50 snapshot", {
|
|
||||||
requestPath,
|
|
||||||
bodyChildren: summarizeChildren(document.body),
|
|
||||||
hostChildren: summarizeChildren(host)
|
|
||||||
});
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toAppUrl(url) {
|
function toAppUrl(url) {
|
||||||
if (!url || typeof url !== "string") {
|
if (!url || typeof url !== "string") {
|
||||||
@@ -332,11 +31,9 @@ window.rpgRollerApi = (() => {
|
|||||||
|
|
||||||
function invokeDotNet(method, ...args) {
|
function invokeDotNet(method, ...args) {
|
||||||
if (!stateStream.dotNetRef) {
|
if (!stateStream.dotNetRef) {
|
||||||
warn("invokeDotNet skipped; missing dotNetRef", method, args);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("invokeDotNet", method, args);
|
|
||||||
stateStream.dotNetRef.invokeMethodAsync(method, ...args).catch(() => {
|
stateStream.dotNetRef.invokeMethodAsync(method, ...args).catch(() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -360,30 +57,25 @@ window.rpgRollerApi = (() => {
|
|||||||
|
|
||||||
function connectStateStream() {
|
function connectStateStream() {
|
||||||
if (stateStream.stopped || !stateStream.campaignId) {
|
if (stateStream.stopped || !stateStream.campaignId) {
|
||||||
debug("connectStateStream skipped", { stopped: stateStream.stopped, campaignId: stateStream.campaignId });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
debug("connectStateStream start", { campaignId: stateStream.campaignId });
|
|
||||||
invokeDotNet("OnConnectionStateChanged", "reconnecting");
|
invokeDotNet("OnConnectionStateChanged", "reconnecting");
|
||||||
|
|
||||||
const source = new EventSource(toAppUrl(`api/events/state?campaignId=${encodeURIComponent(stateStream.campaignId)}`));
|
const source = new EventSource(toAppUrl(`api/events/state?campaignId=${encodeURIComponent(stateStream.campaignId)}`));
|
||||||
stateStream.source = source;
|
stateStream.source = source;
|
||||||
|
|
||||||
source.onopen = () => {
|
source.onopen = () => {
|
||||||
debug("state stream open", { campaignId: stateStream.campaignId });
|
|
||||||
stateStream.reconnectDelayMs = 1000;
|
stateStream.reconnectDelayMs = 1000;
|
||||||
invokeDotNet("OnConnectionStateChanged", "connected");
|
invokeDotNet("OnConnectionStateChanged", "connected");
|
||||||
};
|
};
|
||||||
|
|
||||||
source.addEventListener("state", (event) => {
|
source.addEventListener("state", (event) => {
|
||||||
try {
|
try {
|
||||||
debug("state stream payload", event.data);
|
|
||||||
const payload = JSON.parse(event.data);
|
const payload = JSON.parse(event.data);
|
||||||
invokeDotNet("OnStateEventReceived", payload);
|
invokeDotNet("OnStateEventReceived", payload);
|
||||||
} catch {
|
} catch {
|
||||||
warn("state stream payload parse failed", event.data);
|
|
||||||
invokeDotNet("OnStateEventReceived", {
|
invokeDotNet("OnStateEventReceived", {
|
||||||
campaignId: stateStream.campaignId,
|
campaignId: stateStream.campaignId,
|
||||||
totalVersion: 0,
|
totalVersion: 0,
|
||||||
@@ -395,7 +87,6 @@ window.rpgRollerApi = (() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
source.onerror = () => {
|
source.onerror = () => {
|
||||||
warn("state stream error", { campaignId: stateStream.campaignId, stopped: stateStream.stopped });
|
|
||||||
if (stateStream.source === source) {
|
if (stateStream.source === source) {
|
||||||
source.close();
|
source.close();
|
||||||
stateStream.source = null;
|
stateStream.source = null;
|
||||||
@@ -411,7 +102,6 @@ window.rpgRollerApi = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stopStateEvents() {
|
function stopStateEvents() {
|
||||||
debug("stopStateEvents", { campaignId: stateStream.campaignId });
|
|
||||||
stateStream.stopped = true;
|
stateStream.stopped = true;
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
|
|
||||||
@@ -425,19 +115,16 @@ window.rpgRollerApi = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("offline", () => {
|
window.addEventListener("offline", () => {
|
||||||
warn("window offline");
|
|
||||||
invokeDotNet("OnConnectionStateChanged", "offline");
|
invokeDotNet("OnConnectionStateChanged", "offline");
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("online", () => {
|
window.addEventListener("online", () => {
|
||||||
debug("window online");
|
|
||||||
if (!stateStream.stopped) {
|
if (!stateStream.stopped) {
|
||||||
connectStateStream();
|
connectStateStream();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function request(method, url, body) {
|
async function request(method, url, body) {
|
||||||
debug("request start", { method, url, hasBody: body !== null && body !== undefined });
|
|
||||||
const options = {
|
const options = {
|
||||||
method,
|
method,
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
@@ -454,8 +141,7 @@ window.rpgRollerApi = (() => {
|
|||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(toAppUrl(url), options);
|
response = await fetch(toAppUrl(url), options);
|
||||||
} catch (error) {
|
} catch {
|
||||||
warn("request network error", { method, url, error: String(error) });
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 0,
|
status: 0,
|
||||||
@@ -474,7 +160,6 @@ window.rpgRollerApi = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
warn("request failed", { method, url, status: response.status, parsed });
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
@@ -483,7 +168,6 @@ window.rpgRollerApi = (() => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("request success", { method, url, status: response.status });
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
@@ -492,12 +176,10 @@ window.rpgRollerApi = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSessionValue(key) {
|
function getSessionValue(key) {
|
||||||
debug("getSessionValue", key);
|
|
||||||
return sessionStorage.getItem(`${sessionPrefix}${key}`);
|
return sessionStorage.getItem(`${sessionPrefix}${key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSessionValue(key, value) {
|
function setSessionValue(key, value) {
|
||||||
debug("setSessionValue", key, value);
|
|
||||||
const qualifiedKey = `${sessionPrefix}${key}`;
|
const qualifiedKey = `${sessionPrefix}${key}`;
|
||||||
if (value === null || value === undefined || value === "") {
|
if (value === null || value === undefined || value === "") {
|
||||||
sessionStorage.removeItem(qualifiedKey);
|
sessionStorage.removeItem(qualifiedKey);
|
||||||
@@ -508,7 +190,6 @@ window.rpgRollerApi = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startStateEvents(campaignId, dotNetRef) {
|
function startStateEvents(campaignId, dotNetRef) {
|
||||||
debug("startStateEvents", { campaignId });
|
|
||||||
stopStateEvents();
|
stopStateEvents();
|
||||||
stateStream.stopped = false;
|
stateStream.stopped = false;
|
||||||
stateStream.dotNetRef = dotNetRef;
|
stateStream.dotNetRef = dotNetRef;
|
||||||
@@ -519,32 +200,26 @@ window.rpgRollerApi = (() => {
|
|||||||
|
|
||||||
function scrollElementToBottom(element) {
|
function scrollElementToBottom(element) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
warn("scrollElementToBottom skipped; missing element");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("scrollElementToBottom");
|
|
||||||
element.scrollTop = element.scrollHeight;
|
element.scrollTop = element.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearInputValue(element) {
|
function clearInputValue(element) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
warn("clearInputValue skipped; missing element");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("clearInputValue");
|
|
||||||
element.value = "";
|
element.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeAuthPage() {
|
function initializeAuthPage() {
|
||||||
const root = document.querySelector("[data-auth-page]");
|
const root = document.querySelector("[data-auth-page]");
|
||||||
if (!root) {
|
if (!root) {
|
||||||
debug("initializeAuthPage skipped; no auth root");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("initializeAuthPage start");
|
|
||||||
const statusElement = root.querySelector("[data-auth-status]");
|
const statusElement = root.querySelector("[data-auth-status]");
|
||||||
const forms = root.querySelectorAll("[data-auth-form]");
|
const forms = root.querySelectorAll("[data-auth-form]");
|
||||||
forms.forEach((form) => {
|
forms.forEach((form) => {
|
||||||
@@ -632,7 +307,7 @@ window.rpgRollerApi = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setSubmitting(form, isSubmitting) {
|
function setSubmitting(form, isSubmitting) {
|
||||||
const submitButton = form.querySelector('button[type="submit"]');
|
const submitButton = form.querySelector("button[type=\"submit\"]");
|
||||||
if (!submitButton) {
|
if (!submitButton) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -715,9 +390,6 @@ window.rpgRollerApi = (() => {
|
|||||||
startStateEvents,
|
startStateEvents,
|
||||||
stopStateEvents,
|
stopStateEvents,
|
||||||
scrollElementToBottom,
|
scrollElementToBottom,
|
||||||
clearInputValue,
|
clearInputValue
|
||||||
installWorkspaceDiagnostics,
|
|
||||||
markWorkspacePhase,
|
|
||||||
bootstrapPreBlazorDiagnostics
|
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
8
TASKS.md
8
TASKS.md
@@ -27,6 +27,7 @@ The change is complete when a human can run the app, open `/`, observe the corre
|
|||||||
- [x] (2026-05-04) Extended the diagnostics to page-load time by wrapping the interactive host in a stable container and logging pre-Blazor body and host mutations before the first interactive batch applies.
|
- [x] (2026-05-04) Extended the diagnostics to page-load time by wrapping the interactive host in a stable container and logging pre-Blazor body and host mutations before the first interactive batch applies.
|
||||||
- [x] (2026-05-04) Reworked authenticated route startup into phased interactive batches so the first render mounts only a tiny shell, the second render mounts a simple header placeholder, the third render mounts route skeletons, and real control-heavy content appears only after route initialization completes.
|
- [x] (2026-05-04) Reworked authenticated route startup into phased interactive batches so the first render mounts only a tiny shell, the second render mounts a simple header placeholder, the third render mounts route skeletons, and real control-heavy content appears only after route initialization completes.
|
||||||
- [x] (2026-05-04 22:17Z) Removed global authenticated `Routes` interactivity, moved `InteractiveServerRenderMode(prerender: false)` onto the real authenticated pages, and switched to manual `Blazor.start({ ssr: { disableDomPreservation: true } })` startup based on the upstream Firefox guidance in `dotnet/aspnetcore#58824`.
|
- [x] (2026-05-04 22:17Z) Removed global authenticated `Routes` interactivity, moved `InteractiveServerRenderMode(prerender: false)` onto the real authenticated pages, and switched to manual `Blazor.start({ ssr: { disableDomPreservation: true } })` startup based on the upstream Firefox guidance in `dotnet/aspnetcore#58824`.
|
||||||
|
- [x] (2026-05-05) Confirmed the real fix in Firefox plus RoboForm, documented it in `README.md`, and removed the failed phased-render and diagnostics-only mitigation layers from the codebase.
|
||||||
|
|
||||||
## Surprises & Discoveries
|
## Surprises & Discoveries
|
||||||
|
|
||||||
@@ -63,6 +64,9 @@ The change is complete when a human can run the app, open `/`, observe the corre
|
|||||||
- Observation: once the authenticated pages moved to per-page interactivity, header route navigation needed full document reloads instead of in-circuit `NavigationManager.NavigateTo` transitions.
|
- Observation: once the authenticated pages moved to per-page interactivity, header route navigation needed full document reloads instead of in-circuit `NavigationManager.NavigateTo` transitions.
|
||||||
Evidence: the first Selenium run after the per-page render-mode change reached `/play` in the URL but never mounted `#skill-filter-input` after `/campaigns -> /play` until `Workspace.NavigateToRouteAsync` switched to `forceLoad: true`.
|
Evidence: the first Selenium run after the per-page render-mode change reached `/play` in the URL but never mounted `#skill-filter-input` after `/campaigns -> /play` until `Workspace.NavigateToRouteAsync` switched to `forceLoad: true`.
|
||||||
|
|
||||||
|
- Observation: the phased first-render shells and browser/server diagnostics were not part of the final fix.
|
||||||
|
Evidence: after the app switched to per-page interactive render modes plus manual `Blazor.start({ ssr: { disableDomPreservation: true } })`, the Firefox plus RoboForm repro stopped even after those extra mitigations were removed.
|
||||||
|
|
||||||
- Observation: the locally installed Snap Firefox build on this machine is viable for Selenium through `geckodriver`, but not for Playwright protocol control.
|
- Observation: the locally installed Snap Firefox build on this machine is viable for Selenium through `geckodriver`, but not for Playwright protocol control.
|
||||||
Evidence: Playwright stalled during the `-juggler-pipe` handshake, while a `geckodriver` plus Selenium session against `/snap/firefox/current/usr/lib/firefox/firefox` completed the same Milestone 1 verification successfully.
|
Evidence: Playwright stalled during the `-juggler-pipe` handshake, while a `geckodriver` plus Selenium session against `/snap/firefox/current/usr/lib/firefox/firefox` completed the same Milestone 1 verification successfully.
|
||||||
|
|
||||||
@@ -88,6 +92,10 @@ The change is complete when a human can run the app, open `/`, observe the corre
|
|||||||
Rationale: upstream issue `dotnet/aspnetcore#58824` identifies Firefox failures tied to Global interactivity and explicitly notes that PerPage mode does not share the problem. The Blazor startup guidance also documents manual `Blazor.start` configuration for SSR options such as `disableDomPreservation`.
|
Rationale: upstream issue `dotnet/aspnetcore#58824` identifies Firefox failures tied to Global interactivity and explicitly notes that PerPage mode does not share the problem. The Blazor startup guidance also documents manual `Blazor.start` configuration for SSR options such as `disableDomPreservation`.
|
||||||
Date/Author: 2026-05-04 / Codex
|
Date/Author: 2026-05-04 / Codex
|
||||||
|
|
||||||
|
- Decision: remove the phased render system and the crash-diagnostics scaffolding after the real fix was confirmed.
|
||||||
|
Rationale: those changes were useful for isolating the failure, but they increased code complexity without contributing to the final Firefox plus RoboForm solution.
|
||||||
|
Date/Author: 2026-05-05 / Codex
|
||||||
|
|
||||||
- Decision: stage the rewrite in two layers: first introduce real routes while preserving existing feature behavior, then split the large workspace tree into route-owned subtrees.
|
- Decision: stage the rewrite in two layers: first introduce real routes while preserving existing feature behavior, then split the large workspace tree into route-owned subtrees.
|
||||||
Rationale: the current workspace is dense and risk-prone. A staged rewrite keeps the app working while the route model changes, and it gives the test suite meaningful checkpoints.
|
Rationale: the current workspace is dense and risk-prone. A staged rewrite keeps the app working while the route model changes, and it gives the test suite meaningful checkpoints.
|
||||||
Date/Author: 2026-05-04 / Codex
|
Date/Author: 2026-05-04 / Codex
|
||||||
|
|||||||
Reference in New Issue
Block a user