fix: scope startup by route
This commit is contained in:
@@ -14,19 +14,31 @@
|
||||
}
|
||||
|
||||
<div class="workspace-shell">
|
||||
<AppHeader
|
||||
User="State.User"
|
||||
ShowCampaign="true"
|
||||
CampaignName="@State.SelectedCampaignName"
|
||||
ShowConnectionState="true"
|
||||
ConnectionStateLabel="@State.ConnectionStateLabel"
|
||||
ConnectionStateCssClass="@State.ConnectionStateCssClass"
|
||||
IsMenuOpen="State.IsScreenMenuOpen"
|
||||
MenuButtonId="workspace-screen-menu-button"
|
||||
MenuId="workspace-screen-menu"
|
||||
MenuItems="HeaderMenuItems"
|
||||
ToggleMenuRequested="ToggleScreenMenu"
|
||||
LogoutRequested="Session.LogoutAsync"/>
|
||||
@if (HasSessionInitialized)
|
||||
{
|
||||
<AppHeader
|
||||
User="State.User"
|
||||
ShowCampaign="@ShowCampaignInHeader"
|
||||
CampaignName="@State.SelectedCampaignName"
|
||||
ShowConnectionState="@ShowConnectionStateInHeader"
|
||||
ConnectionStateLabel="@State.ConnectionStateLabel"
|
||||
ConnectionStateCssClass="@State.ConnectionStateCssClass"
|
||||
IsMenuOpen="State.IsScreenMenuOpen"
|
||||
MenuButtonId="workspace-screen-menu-button"
|
||||
MenuId="workspace-screen-menu"
|
||||
MenuItems="HeaderMenuItems"
|
||||
ToggleMenuRequested="ToggleScreenMenu"
|
||||
LogoutRequested="Session.LogoutAsync"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<main class="management-screen">
|
||||
<section class="card">
|
||||
<p class="empty">Loading workspace...</p>
|
||||
</section>
|
||||
</main>
|
||||
}
|
||||
|
||||
@if (ChildContent is not null)
|
||||
{
|
||||
@ChildContent(PageContext)
|
||||
|
||||
@@ -137,6 +137,8 @@ public partial class Workspace : IAsyncDisposable
|
||||
private bool IsCampaignsRoute => Route == WorkspaceRoute.Campaigns;
|
||||
private bool IsAdminRoute => Route == WorkspaceRoute.Admin;
|
||||
private string AppCssClass => IsPlayRoute ? "rr-app app-play" : "rr-app";
|
||||
private bool ShowCampaignInHeader => !IsAdminRoute;
|
||||
private bool ShowConnectionStateInHeader => IsPlayRoute;
|
||||
|
||||
private WorkspacePageContext PageContext => new(State, Play, Campaigns, Admin, Scope, Session,
|
||||
InitializeRouteAsync, HasSessionInitialized, RequestRefreshAsync, AdminDatabaseDownloadUrl, HeaderMenuItems,
|
||||
|
||||
@@ -87,9 +87,22 @@ public sealed class WorkspaceCampaignScopeCoordinator(
|
||||
try
|
||||
{
|
||||
await RefreshCampaignRosterAsync();
|
||||
await refreshSelectedCharacterSheetAsync();
|
||||
await refreshCampaignLogAsync(null);
|
||||
resetCampaignStateTracking();
|
||||
if (isPlayRoute())
|
||||
{
|
||||
await refreshSelectedCharacterSheetAsync();
|
||||
await refreshCampaignLogAsync(null);
|
||||
resetCampaignStateTracking();
|
||||
}
|
||||
else
|
||||
{
|
||||
state.SelectedCharacterSkills = [];
|
||||
state.SelectedCharacterSkillGroups = [];
|
||||
state.CampaignLog = [];
|
||||
state.ConnectionState = "offline";
|
||||
state.CurrentCampaignState = null;
|
||||
state.CampaignLogCursor = null;
|
||||
resetCampaignLogDetailState();
|
||||
}
|
||||
}
|
||||
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
||||
{
|
||||
|
||||
@@ -82,7 +82,7 @@ public sealed class WorkspaceLiveStateController(
|
||||
|
||||
public async Task SyncStateEventsAsync()
|
||||
{
|
||||
if (state.User is null || !state.SelectedCampaignId.HasValue || isAdminRoute())
|
||||
if (state.User is null || !state.SelectedCampaignId.HasValue || isAdminRoute() || !isPlayRoute())
|
||||
{
|
||||
await StopStateEventsAsync();
|
||||
state.ConnectionState = "offline";
|
||||
|
||||
@@ -31,12 +31,14 @@ public sealed class WorkspaceSessionCoordinator(
|
||||
state.RollVisibility = NormalizeRollVisibility(storedRollVisibility);
|
||||
|
||||
Guid? preferredCampaignId = null;
|
||||
var storedCampaignId = await js.InvokeAsync<string?>("rpgRollerApi.getSessionValue", CampaignSessionKey);
|
||||
if (Guid.TryParse(storedCampaignId, out var parsedCampaignId))
|
||||
preferredCampaignId = parsedCampaignId;
|
||||
if (!isAdminRoute())
|
||||
{
|
||||
var storedCampaignId = await js.InvokeAsync<string?>("rpgRollerApi.getSessionValue", CampaignSessionKey);
|
||||
if (Guid.TryParse(storedCampaignId, out var parsedCampaignId))
|
||||
preferredCampaignId = parsedCampaignId;
|
||||
}
|
||||
|
||||
await CheckHealthAsync();
|
||||
await LoadRulesetsAsync();
|
||||
|
||||
var reloaded = await ReloadAuthenticatedSessionAsync(preferredCampaignId);
|
||||
if (!reloaded)
|
||||
@@ -160,14 +162,20 @@ public sealed class WorkspaceSessionCoordinator(
|
||||
if (!await EnsureRouteAccessAsync())
|
||||
return true;
|
||||
|
||||
if (isAdminRoute())
|
||||
{
|
||||
await stopStateEventsAsync();
|
||||
state.ConnectionState = "offline";
|
||||
await ensureAdminUsersLoadedAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
await LoadRulesetsAsync();
|
||||
await reloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId);
|
||||
await reloadCharacterCampaignOptionsAsync();
|
||||
await refreshCampaignScopeAsync();
|
||||
await syncStateEventsAsync();
|
||||
|
||||
if (isAdminRoute())
|
||||
await ensureAdminUsersLoadedAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
5
TASKS.md
5
TASKS.md
@@ -44,6 +44,9 @@ The change is complete when a human can run the app, open `/`, observe the corre
|
||||
- Observation: once the route-owned components controlled their own modal and page subtree rendering, the extra shell-owned play-control staging was no longer necessary for the DOM-wrap smoke coverage.
|
||||
Evidence: after moving authenticated startup into a route-owned wrapper and rendering play controls directly, `node ./scripts/run-selenium.js` still passed the extension-like DOM-wrap coverage against `/play`.
|
||||
|
||||
- Observation: the first Milestone 4 attempt was still incomplete because authenticated startup remained route-agnostic behind `Session.InitializeAsync()`.
|
||||
Evidence: `/admin` and `/play` could still hit the Firefox `insertBefore` circuit crash until admin and campaign-management routes stopped preloading play-only campaign scope, selected sheets, logs, and SSE startup during their first interactive batch.
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -87,6 +90,8 @@ After Milestone 3, `Workspace.razor` is now a shell that owns shared chrome, hea
|
||||
|
||||
After Milestone 4, authenticated startup is now triggered by a route-owned wrapper instead of `Workspace.razor.cs`, the shared shell no longer uses `OnAfterRenderAsync`, and the play route renders its controls directly without shell-driven follow-up batches. The route-first rewrite is now functionally complete: host tests pass, the Selenium smoke suite passes, and the restored development-database fixture lets the backend coverage suite validate the full repo behavior again.
|
||||
|
||||
Follow-up: the first pass at Milestone 4 removed shell-level `OnAfterRenderAsync`, but did not yet split `Session.InitializeAsync()` by route. The final follow-up fix made startup genuinely route-scoped by keeping `/admin` off play-only campaign scope and SSE startup, gating the full shell behind authenticated initialization, and adding direct `/admin` smoke coverage so this regression path stays visible.
|
||||
|
||||
This section must be updated after each major milestone. When the implementation is complete, summarize which parts of the old workspace architecture were fully removed, which compatibility constraints remain, and whether the final startup path still depends on any multi-batch structural rendering.
|
||||
|
||||
## Context and Orientation
|
||||
|
||||
@@ -32,6 +32,17 @@ const {
|
||||
} = require("./lib/selenium-smoke");
|
||||
|
||||
const domWrapAddonPath = path.join(__dirname, "dom-wrap-addon");
|
||||
let bootstrapAdminSession = null;
|
||||
|
||||
async function ensureAdminSession() {
|
||||
if (bootstrapAdminSession) {
|
||||
return bootstrapAdminSession;
|
||||
}
|
||||
|
||||
const username = uniqueName("bootstrap-admin");
|
||||
bootstrapAdminSession = await registerAndLoginApi(username, "Bootstrap Admin");
|
||||
return bootstrapAdminSession;
|
||||
}
|
||||
|
||||
async function openAuthenticatedPlay(driver, sessionCookie) {
|
||||
await seedAuthenticatedBrowser(driver, sessionCookie);
|
||||
@@ -76,8 +87,7 @@ const tests = [
|
||||
{
|
||||
name: "authenticated root document redirects to play",
|
||||
run: async () => {
|
||||
const username = uniqueName("doc-auth");
|
||||
const { sessionCookie } = await registerAndLoginApi(username, "Document Auth");
|
||||
const { sessionCookie } = await ensureAdminSession();
|
||||
const response = await request("/", {
|
||||
cookie: sessionCookie,
|
||||
redirect: "manual"
|
||||
@@ -142,6 +152,21 @@ const tests = [
|
||||
assert.equal(await hasSelector(driver, ".management-list"), false);
|
||||
})
|
||||
},
|
||||
{
|
||||
name: "admin route mounts directly without play UI",
|
||||
run: async () => withDriver({}, async (driver) => {
|
||||
const { sessionCookie } = await ensureAdminSession();
|
||||
|
||||
await seedAuthenticatedBrowser(driver, sessionCookie);
|
||||
await driver.get(absoluteUrl("/admin"));
|
||||
|
||||
await waitForUrl(driver, "/admin");
|
||||
await waitForText(driver, "User Management");
|
||||
assert.equal(await hasSelector(driver, ".management-list"), true);
|
||||
assert.equal(await hasSelector(driver, "#skill-filter-input"), false);
|
||||
assert.equal(await hasSelector(driver, ".log-panel"), false);
|
||||
})
|
||||
},
|
||||
{
|
||||
name: "successful login transitions to play workspace",
|
||||
run: async () => withDriver({}, async (driver) => {
|
||||
|
||||
Reference in New Issue
Block a user