fix: scope startup by route
This commit is contained in:
@@ -14,11 +14,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="workspace-shell">
|
<div class="workspace-shell">
|
||||||
|
@if (HasSessionInitialized)
|
||||||
|
{
|
||||||
<AppHeader
|
<AppHeader
|
||||||
User="State.User"
|
User="State.User"
|
||||||
ShowCampaign="true"
|
ShowCampaign="@ShowCampaignInHeader"
|
||||||
CampaignName="@State.SelectedCampaignName"
|
CampaignName="@State.SelectedCampaignName"
|
||||||
ShowConnectionState="true"
|
ShowConnectionState="@ShowConnectionStateInHeader"
|
||||||
ConnectionStateLabel="@State.ConnectionStateLabel"
|
ConnectionStateLabel="@State.ConnectionStateLabel"
|
||||||
ConnectionStateCssClass="@State.ConnectionStateCssClass"
|
ConnectionStateCssClass="@State.ConnectionStateCssClass"
|
||||||
IsMenuOpen="State.IsScreenMenuOpen"
|
IsMenuOpen="State.IsScreenMenuOpen"
|
||||||
@@ -27,6 +29,16 @@
|
|||||||
MenuItems="HeaderMenuItems"
|
MenuItems="HeaderMenuItems"
|
||||||
ToggleMenuRequested="ToggleScreenMenu"
|
ToggleMenuRequested="ToggleScreenMenu"
|
||||||
LogoutRequested="Session.LogoutAsync"/>
|
LogoutRequested="Session.LogoutAsync"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<main class="management-screen">
|
||||||
|
<section class="card">
|
||||||
|
<p class="empty">Loading workspace...</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
|
||||||
@if (ChildContent is not null)
|
@if (ChildContent is not null)
|
||||||
{
|
{
|
||||||
@ChildContent(PageContext)
|
@ChildContent(PageContext)
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private bool IsCampaignsRoute => Route == WorkspaceRoute.Campaigns;
|
private bool IsCampaignsRoute => Route == WorkspaceRoute.Campaigns;
|
||||||
private bool IsAdminRoute => Route == WorkspaceRoute.Admin;
|
private bool IsAdminRoute => Route == WorkspaceRoute.Admin;
|
||||||
private string AppCssClass => IsPlayRoute ? "rr-app app-play" : "rr-app";
|
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,
|
private WorkspacePageContext PageContext => new(State, Play, Campaigns, Admin, Scope, Session,
|
||||||
InitializeRouteAsync, HasSessionInitialized, RequestRefreshAsync, AdminDatabaseDownloadUrl, HeaderMenuItems,
|
InitializeRouteAsync, HasSessionInitialized, RequestRefreshAsync, AdminDatabaseDownloadUrl, HeaderMenuItems,
|
||||||
|
|||||||
@@ -87,10 +87,23 @@ public sealed class WorkspaceCampaignScopeCoordinator(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await RefreshCampaignRosterAsync();
|
await RefreshCampaignRosterAsync();
|
||||||
|
if (isPlayRoute())
|
||||||
|
{
|
||||||
await refreshSelectedCharacterSheetAsync();
|
await refreshSelectedCharacterSheetAsync();
|
||||||
await refreshCampaignLogAsync(null);
|
await refreshCampaignLogAsync(null);
|
||||||
resetCampaignStateTracking();
|
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)
|
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
||||||
{
|
{
|
||||||
clearAuthenticatedState();
|
clearAuthenticatedState();
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public sealed class WorkspaceLiveStateController(
|
|||||||
|
|
||||||
public async Task SyncStateEventsAsync()
|
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();
|
await StopStateEventsAsync();
|
||||||
state.ConnectionState = "offline";
|
state.ConnectionState = "offline";
|
||||||
|
|||||||
@@ -31,12 +31,14 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
state.RollVisibility = NormalizeRollVisibility(storedRollVisibility);
|
state.RollVisibility = NormalizeRollVisibility(storedRollVisibility);
|
||||||
|
|
||||||
Guid? preferredCampaignId = null;
|
Guid? preferredCampaignId = null;
|
||||||
|
if (!isAdminRoute())
|
||||||
|
{
|
||||||
var storedCampaignId = await js.InvokeAsync<string?>("rpgRollerApi.getSessionValue", CampaignSessionKey);
|
var storedCampaignId = await js.InvokeAsync<string?>("rpgRollerApi.getSessionValue", CampaignSessionKey);
|
||||||
if (Guid.TryParse(storedCampaignId, out var parsedCampaignId))
|
if (Guid.TryParse(storedCampaignId, out var parsedCampaignId))
|
||||||
preferredCampaignId = parsedCampaignId;
|
preferredCampaignId = parsedCampaignId;
|
||||||
|
}
|
||||||
|
|
||||||
await CheckHealthAsync();
|
await CheckHealthAsync();
|
||||||
await LoadRulesetsAsync();
|
|
||||||
|
|
||||||
var reloaded = await ReloadAuthenticatedSessionAsync(preferredCampaignId);
|
var reloaded = await ReloadAuthenticatedSessionAsync(preferredCampaignId);
|
||||||
if (!reloaded)
|
if (!reloaded)
|
||||||
@@ -160,14 +162,20 @@ public sealed class WorkspaceSessionCoordinator(
|
|||||||
if (!await EnsureRouteAccessAsync())
|
if (!await EnsureRouteAccessAsync())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
if (isAdminRoute())
|
||||||
|
{
|
||||||
|
await stopStateEventsAsync();
|
||||||
|
state.ConnectionState = "offline";
|
||||||
|
await ensureAdminUsersLoadedAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LoadRulesetsAsync();
|
||||||
await reloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId);
|
await reloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId);
|
||||||
await reloadCharacterCampaignOptionsAsync();
|
await reloadCharacterCampaignOptionsAsync();
|
||||||
await refreshCampaignScopeAsync();
|
await refreshCampaignScopeAsync();
|
||||||
await syncStateEventsAsync();
|
await syncStateEventsAsync();
|
||||||
|
|
||||||
if (isAdminRoute())
|
|
||||||
await ensureAdminUsersLoadedAsync();
|
|
||||||
|
|
||||||
return true;
|
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.
|
- 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`.
|
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.
|
- 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.
|
||||||
|
|
||||||
@@ -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.
|
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.
|
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
|
## Context and Orientation
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ const {
|
|||||||
} = require("./lib/selenium-smoke");
|
} = require("./lib/selenium-smoke");
|
||||||
|
|
||||||
const domWrapAddonPath = path.join(__dirname, "dom-wrap-addon");
|
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) {
|
async function openAuthenticatedPlay(driver, sessionCookie) {
|
||||||
await seedAuthenticatedBrowser(driver, sessionCookie);
|
await seedAuthenticatedBrowser(driver, sessionCookie);
|
||||||
@@ -76,8 +87,7 @@ const tests = [
|
|||||||
{
|
{
|
||||||
name: "authenticated root document redirects to play",
|
name: "authenticated root document redirects to play",
|
||||||
run: async () => {
|
run: async () => {
|
||||||
const username = uniqueName("doc-auth");
|
const { sessionCookie } = await ensureAdminSession();
|
||||||
const { sessionCookie } = await registerAndLoginApi(username, "Document Auth");
|
|
||||||
const response = await request("/", {
|
const response = await request("/", {
|
||||||
cookie: sessionCookie,
|
cookie: sessionCookie,
|
||||||
redirect: "manual"
|
redirect: "manual"
|
||||||
@@ -142,6 +152,21 @@ const tests = [
|
|||||||
assert.equal(await hasSelector(driver, ".management-list"), false);
|
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",
|
name: "successful login transitions to play workspace",
|
||||||
run: async () => withDriver({}, async (driver) => {
|
run: async () => withDriver({}, async (driver) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user