From f86ac43153d4f842eebf8c487abc62c1efb25498 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 4 May 2026 22:53:14 +0200 Subject: [PATCH] chore: add pre-blazor crash diagnostics --- README.md | 1 + RpgRoller/Components/App.razor | 9 +- RpgRoller/wwwroot/js/rpgroller-api.js | 132 +++++++++++++++++++++++++- TASKS.md | 1 + 4 files changed, 141 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 056bfed..7c4baa1 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ SQLite migration rule: - 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`. - 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`. +- 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. - 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`. - Log rows return compact summary data first and lazy-load full detail from `/api/rolls/{rollId}` when expanded. diff --git a/RpgRoller/Components/App.razor b/RpgRoller/Components/App.razor index 73e211d..9c1bffa 100644 --- a/RpgRoller/Components/App.razor +++ b/RpgRoller/Components/App.razor @@ -25,11 +25,16 @@ } else { - +
+ +
} @if (UseInteractiveApp) { + } @@ -55,6 +60,8 @@ else private bool AuthStatusIsError => string.Equals(ReadAuthQueryValue("kind"), "error", StringComparison.OrdinalIgnoreCase); + private string RequestPath => HttpContext?.Request.Path.Value ?? "/"; + private string BaseHref { get diff --git a/RpgRoller/wwwroot/js/rpgroller-api.js b/RpgRoller/wwwroot/js/rpgroller-api.js index 478c1ad..f465970 100644 --- a/RpgRoller/wwwroot/js/rpgroller-api.js +++ b/RpgRoller/wwwroot/js/rpgroller-api.js @@ -13,6 +13,7 @@ window.rpgRollerApi = (() => { observer: null, route: null, globalHandlersInstalled: false, + domOperationDiagnosticsInstalled: false, mutationBatchCount: 0 }; @@ -24,6 +25,14 @@ window.rpgRollerApi = (() => { 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 ""; @@ -117,6 +126,53 @@ window.rpgRollerApi = (() => { }); } + 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, @@ -181,6 +237,79 @@ window.rpgRollerApi = (() => { 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) { if (!url || typeof url !== "string") { return url; @@ -588,6 +717,7 @@ window.rpgRollerApi = (() => { scrollElementToBottom, clearInputValue, installWorkspaceDiagnostics, - markWorkspacePhase + markWorkspacePhase, + bootstrapPreBlazorDiagnostics }; })(); diff --git a/TASKS.md b/TASKS.md index 8bf8da2..400f869 100644 --- a/TASKS.md +++ b/TASKS.md @@ -24,6 +24,7 @@ The change is complete when a human can run the app, open `/`, observe the corre - [x] (2026-05-04 21:58Z) Removed shell-level `OnAfterRenderAsync` bootstrapping, moved the JS-dependent authenticated startup into a route-owned `WorkspaceRouteView`, removed shell-owned staged control renders, restored the missing development database fixture, and updated README to describe the completed route-first architecture. - [x] (2026-05-04) Updated host tests, Selenium smoke tests, and docs so the real-route model is the documented and verified Milestone 2 behavior. - [x] (2026-05-04) Added expanded workspace startup diagnostics across Blazor lifecycle logging, route-content render logging, and browser-side DOM mutation snapshots to narrow the remaining Firefox batch-2 crash. +- [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. ## Surprises & Discoveries