Skip to content

Hub: keyed-morph rendering — DOM identity survives live repaints (#495) - #504

Merged
richard-devbot merged 2 commits into
mainfrom
claude/animation-uiux-skills-c5b356
Jul 31, 2026
Merged

Hub: keyed-morph rendering — DOM identity survives live repaints (#495)#504
richard-devbot merged 2 commits into
mainfrom
claude/animation-uiux-skills-c5b356

Conversation

@richard-devbot

@richard-devbot richard-devbot commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Closes #495 · foundation child of epic #499 · design + evidence trail on the issue.

Problem

The server broadcasts a full snapshot every 3 s (server.js:195-205) and applyState re-renders every registered page through setHTML = innerHTML replacement — destroying DOM node identity page-wide. Real-user consequences (all reproduced live before the fix): clicks whose press/release straddle a tick land on detached nodes and silently vanish (the Evidence tab pills took multiple attempts), keyboard focus drops to <body> mid-page, and text selection is wiped every tick.

Fix (client-only, zero new dependencies, no call-site churn)

  • setHTML now dirty-checks (per-element cache of the last painted HTML — an unchanged region on a quiet tick is zero DOM work) and morphs changed regions in place: attributes/text patch, children match by data-key → tag+position, so patched nodes keep identity. Focused inputs/textareas keep the user's in-progress value; options under a focused select keep the in-flight choice.
  • The remaining per-tick writers route through the same morph: both scope selects (the 3 s tick can no longer close an open dropdown), per-page "Updated" stamps, the run-workspace empty prompt + tab strip, the cockpit controls panel (previously removed-and-reinserted every tick — every [P3][SECURITY] Add authenticated, audited cockpit controls for run and recovery actions #285 button churned identity), the studio inspector, and run-report/run-analytics selectors + KPIs. Event-driven writers (drawer, artifact viewer, toast, cockpit status) unchanged.
  • Minimal DOM shims (the Node page-render suites) lack a fragment parser and keep the exact legacy innerHTML semantics via feature detection.
  • Security surface unchanged: template fragment parsing marks scripts already-started exactly like innerHTML, and the strings are the same esc()-disciplined HTML as before.

TDD evidence

New tests/browser/dashboard-495.test.js (dashboard-96 harness), verified failing on unfixed code first (RED transcript on #495):

  1. press → forced repaint (applyState(STATE), the WS code path) → release still lands (failed: EVIDENCE_VIEW stayed summary);
  2. focus survives a repaint (failed: activeElement fell to <body>);
  3. identical snapshot ⇒ 0 MutationObserver records + expando markers survive on deep nodes and scope-select options (failed: markers destroyed);
  4. morphHTML A→B→A round-trip: keyed reorder preserves node identity, attrs/text patch, focused-input value protected (failed: morphHTML undefined).

One pinned-source test updated to track the renamed call (dashboard-scope-client.test.js — the semantic-time contract itself is unchanged).

Verification

  • Core suite 1616/1616 (exit 0); the one intermittent is the known cockpit-285 temp-dir flake, 12/12 in isolation.
  • Browser regression 9/9 (4 new + all dashboard-96 journeys incl. axe + responsive) — the existing suite doubles as the painted-content parity net.
  • lint 0 errors · typecheck 0 · validate 196 agents PASS · validate:schemas 34 PASS/0 FAIL · security gate green (1 tolerated bundled advisory) · git diff --check clean.
  • Live-verified on a fixture-seeded hub: three physical tab clicks across live ticks, three first-try hits (previously required programmatic clicks); zero console errors.

Held for Richardson's review — please verify before merge.

🤖 Generated with Claude Code

@strix-security

strix-security Bot commented Jul 31, 2026

Copy link
Copy Markdown

Strix Security Review

No security issues found.

Updated for 06e77c3.


Reviewed by Strix
Re-run review · Configure security review settings

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@richard-devbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ce53d8ba-9d4d-4e95-b1d2-30c0926bc62b

📥 Commits

Reviewing files that changed from the base of the PR and between 7c690f1 and 6cb5bd7.

📒 Files selected for processing (8)
  • src/observability/dashboard/ui/client.js
  • src/observability/dashboard/ui/lib.js
  • src/observability/dashboard/ui/pages/run-analytics.js
  • src/observability/dashboard/ui/pages/run-report.js
  • src/observability/dashboard/ui/pages/run-workspace.js
  • src/observability/dashboard/ui/pages/studio.js
  • tests/browser/dashboard-495.test.js
  • tests/dashboard-scope-client.test.js

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Dashboard: preserve DOM identity with keyed morph rendering on live repaints

🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Replace innerHTML-based repaints with keyed DOM morphing to preserve node identity.
• Route 3s-tick UI writers through morphHTML to keep focus, clicks, and selections stable.
• Add Playwright regression coverage and bump ESLint to satisfy new advisory constraints.
Diagram

graph TD
  WS["WS snapshot (3s)"] --> AS["applyState()"] --> PR["Page renderers"] --> SH["setHTML()"] --> MH["morphHTML()"] --> DOM["Stable DOM nodes"]
  SH --> CACHE["__rsHtml cache"]
  T["Playwright tests (#495)"] --> MH
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Adopt a battle-tested DOM morph library (e.g., morphdom)
  • ➕ More mature handling of edge cases (SVG, weird attribute semantics, etc.)
  • ➕ Less custom code to maintain long-term
  • ➖ Introduces a new dependency and potential bundle/compat overhead
  • ➖ Harder to tailor focused-form-control rules to this app’s semantics
2. Stop sending full snapshots; send incremental patches/diffs
  • ➕ Reduces repaint workload and bandwidth at the source
  • ➕ Can avoid most DOM writes entirely for small changes
  • ➖ Larger architectural change (server protocol + client applyState logic)
  • ➖ Higher risk of state divergence and harder debugging

Recommendation: Keep the PR’s approach (client-only keyed morph + dirty-check) because it fixes the interaction regressions without protocol changes or new dependencies, and it centralizes repaint behavior behind setHTML/morphHTML. If edge cases emerge, consider swapping the internal morph engine for a library later while preserving the same setHTML/morphHTML surface and browser-contract tests.

Files changed (11) +538 / -276

Bug fix (6) +162 / -22
client.jsUse morphHTML for scope selectors and updated stamps +8/-5

Use morphHTML for scope selectors and updated stamps

• Switches the project/run scope <select> option rendering from innerHTML rebuilds to morphHTML to preserve option node identity and open dropdown state across live ticks. Routes per-page "Updated" stamps through morphHTML as well.

src/observability/dashboard/ui/client.js

lib.jsImplement keyed DOM morphing for setHTML updates +131/-2

Implement keyed DOM morphing for setHTML updates

• Changes setText to avoid redundant writes and makes setHTML delegate to a new morphHTML function. Adds a keyed morph engine (data-key matching, attribute/text patching, child reconciliation) with special handling to preserve in-progress focused form state, plus a per-element last-HTML cache to skip unchanged regions; falls back to legacy innerHTML when fragment parsing is unavailable.

src/observability/dashboard/ui/lib.js

run-analytics.jsMorph the run selector options instead of rebuilding +2/-2

Morph the run selector options instead of rebuilding

• Replaces select.innerHTML option list rebuild with morphHTML to preserve option identity and selection behavior across periodic repaints.

src/observability/dashboard/ui/pages/run-analytics.js

run-report.jsMorph report run selector and KPI panel +4/-4

Morph report run selector and KPI panel

• Updates the run selector rendering to use morphHTML for identity-preserving updates. Routes the KPI panel HTML through morphHTML to avoid tearing down and recreating KPI nodes on live ticks.

src/observability/dashboard/ui/pages/run-report.js

run-workspace.jsStabilize cockpit controls panel with a morphed slot +15/-7

Stabilize cockpit controls panel with a morphed slot

• Replaces remove-and-reinsert behavior with a persistent slot element whose contents are updated via morphHTML, preventing cockpit controls from being recreated every tick. Also routes the workspace empty-state HTML through morphHTML.

src/observability/dashboard/ui/pages/run-workspace.js

studio.jsMorph studio inspector panel content in place +2/-2

Morph studio inspector panel content in place

• Switches the inspector panel update from innerHTML replacement to morphHTML, preserving DOM identity and reducing churn during live updates.

src/observability/dashboard/ui/pages/studio.js

Tests (2) +277 / -1
dashboard-495.test.jsAdd browser regression suite for repaint identity contract (#495) +274/-0

Add browser regression suite for repaint identity contract (#495)

• Introduces Playwright-based dashboard tests that reproduce and lock in fixes for click straddling a repaint, focus persistence, zero-mutation behavior on unchanged snapshots, and keyed morph semantics (including focused-input value protection). Uses the existing dashboard harness pattern and cleanly skips when Chromium is unavailable.

tests/browser/dashboard-495.test.js

dashboard-scope-client.test.jsUpdate pinned source assertion for updated stamp writer +3/-1

Update pinned source assertion for updated stamp writer

• Adjusts the test expectation to match the new morphHTML(updated, ...) call site while keeping the semantic time provenance contract intact.

tests/dashboard-scope-client.test.js

Other (3) +99 / -253
package-lock.jsonBump eslint toolchain dependencies to ESLint 10 +91/-249

Bump eslint toolchain dependencies to ESLint 10

• Updates the lockfile for ESLint v10.8.0 and its transitive dependency graph (notably minimatch/brace-expansion versions and engine ranges). Removes older transitive packages no longer required by the updated ESLint stack.

package-lock.json

package.jsonUpgrade ESLint devDependency to v10.8.0 +1/-1

Upgrade ESLint devDependency to v10.8.0

• Bumps ESLint from ^9.0.0 to ^10.8.0 in devDependencies to align with updated tooling and vulnerability posture.

package.json

security-audit.mjsExtend tolerated advisory allowlist for brace-expansion +7/-3

Extend tolerated advisory allowlist for brace-expansion

• Documents and temporarily tolerates an additional brace-expansion advisory (GHSA-mh99-v99m-4gvg) alongside the existing DoS GHSA. Clarifies that the pinned vulnerable version comes from a shrinkwrap and notes when to re-check upstream.

scripts/security-audit.mjs

@qodo-code-review

qodo-code-review Bot commented Jul 31, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (3)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 17 invoked
  code-review-pr
  claude-api
  documentation-writing
  pptx
  docx
  performance-monitoring
  security-compliance
  cso
  plan-eng-review
  design-review
  prompt-engineering
  mcp-builder
  qa-testing
  code-patterns
  xlsx
  security-owasp
  testing-qa

Grey Divider


Remediation recommended

1. Special data-key breaks morph ✓ Resolved 🐞 Bug ≡ Correctness
Description
morphChildren() uses a plain object as a key→node map, so special data-key values like
__proto__ won’t store/retrieve correctly and can cause keyed reconciliation to misbehave (nodes
not reused, identity lost, or incorrect patching). This is reachable because the UI already emits
data-key attributes from external/state-derived strings (e.g. environment keys).
Code

src/observability/dashboard/ui/lib.js[R207-227]

+  var keyed = {};
+  var scan = oldParent.firstChild;
+  var key;
+  while (scan) {
+    if (scan.nodeType === 1) {
+      key = scan.getAttribute('data-key');
+      if (key != null && !Object.prototype.hasOwnProperty.call(keyed, key)) keyed[key] = scan;
+    }
+    scan = scan.nextSibling;
+  }
+  var refNode = oldParent.firstChild;
+  var newNode = newParent.firstChild;
+  while (newNode) {
+    var nextNew = newNode.nextSibling;
+    key = newNode.nodeType === 1 ? newNode.getAttribute('data-key') : null;
+    var target = null;
+    if (key != null) {
+      if (Object.prototype.hasOwnProperty.call(keyed, key) && keyed[key].nodeName === newNode.nodeName) {
+        target = keyed[key];
+        delete keyed[key];
+      }
Relevance

●● Moderate

No prior reviews found about using Map/Object.create(null) to avoid __proto__ data-key collisions.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The keyed reconciliation map is currently a plain object, and DOM keys come from data-key
attributes that are populated from state-derived strings; special values like __proto__ won’t be
stored as a normal own-property, so the later hasOwnProperty lookup won’t find them and the keyed
reuse path is skipped.

src/observability/dashboard/ui/lib.js[204-247]
src/observability/dashboard/ui/pages/environment.js[134-144]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`morphChildren()` builds a keyed lookup with `var keyed = {}` and assigns `keyed[key] = node` for `data-key` values. Special keys like `__proto__` won’t behave as normal own-properties on plain objects, so keyed matching can silently fail for those keys.

## Issue Context
This morph runs on all renderer output routed through `setHTML()` / `morphHTML()`, and `data-key` values can come from state-derived strings (e.g. environment entries).

## Fix Focus Areas
- src/observability/dashboard/ui/lib.js[204-247]
- src/observability/dashboard/ui/pages/environment.js[134-144]

### Suggested fix
Replace the object map with either:
- `var keyed = Object.create(null);` (and keep `hasOwnProperty` guards), or
- a real `Map` (`keyed.set(key, node)`, `keyed.get(key)`, `keyed.delete(key)`) to avoid all special-key pitfalls.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Magic number 15_000 ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
A new hardcoded timeout value 15_000 is introduced in test code instead of a named constant,
reducing readability and making future tuning error-prone. The compliance rule requires replacing
such magic numbers with named constants.
Code

tests/browser/dashboard-495.test.js[R60-62]

+    const timer = setTimeout(() => {
+      if (!settled) { settled = true; child.kill('SIGKILL'); rejectPromise(new Error(`server did not boot\n${out}`)); }
+    }, 15_000);
Relevance

●● Moderate

No historical evidence found that this repo enforces named constants for test timeouts/magic
numbers.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The rule requires literal numeric values used for timeouts/configuration to be replaced with named
constants. The new test introduces setTimeout(..., 15_000) as a raw literal.

tests/browser/dashboard-495.test.js[60-63]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Hardcoded numeric timeouts (e.g., `15_000`) were added directly in code. The rule requires extracting these to named constants.

## Issue Context
This affects maintainability of the test harness and clarifies intent (server boot timeout vs navigation timeout, etc.).

## Fix Focus Areas
- tests/browser/dashboard-495.test.js[41-74]
- tests/browser/dashboard-495.test.js[108-127]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Missing regression attribution comment 📜 Skill insight ⚙ Maintainability
Description
The new regression test file documents issue #495 and the failure context, but it does not include
the required “found date” and “QA report path” attribution metadata. This reduces traceability for
audit/debugging of regressions.
Code

tests/browser/dashboard-495.test.js[R1-19]

+/**
+ * #495 — keyed-morph rendering: DOM node identity must survive live repaints.
+ *
+ * The server broadcasts a full snapshot every 3s and applyState re-renders
+ * every registered page via setHTML (innerHTML replacement), destroying node
+ * identity page-wide. These tests pin the interaction contract the fix
+ * restores:
+ *   1. a click whose press/release straddles a repaint still lands;
+ *   2. keyboard focus inside a repainted region survives the repaint;
+ *   3. an unchanged snapshot causes zero DOM mutations (dirty-check) and
+ *      node identity persists (expando markers survive);
+ *   4. morphHTML patches correctly: attributes, text, keyed reorder with
+ *      identity preservation, focused-input value protection.
+ *
+ * Same harness as dashboard-96: real server on an ephemeral port, canonical
+ * fixtures, real Chromium via playwright-core; skips cleanly with no browser.
+ *
+ * owner: RStack developed by Richardson Gunde
+ */
Relevance

●● Moderate

No historical evidence found for required regression attribution blocks (found date/QA path) in test
headers.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400067 requires a regression test attribution block that includes the issue ID,
description of what broke, the date found, and a QA report path. The header comment in
tests/browser/dashboard-495.test.js includes the issue ID and description, but omits the date
found and QA report reference.

tests/browser/dashboard-495.test.js[1-19]
Skill: qa-testing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The regression test header comment is missing required attribution fields (found date and QA report path).

## Issue Context
Compliance requires regression test files to include: issue ID, what broke, date found, and QA report path reference.

## Fix Focus Areas
- tests/browser/dashboard-495.test.js[1-19]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Test names missing #495 ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
The new regression tests do not include the bug ID (#495) in their test descriptions, reducing
traceability to the original issue they prevent. This makes it harder to audit why the tests exist
and what regression they cover.
Code

tests/browser/dashboard-495.test.js[R138-208]

+browserTest('a click that straddles a live repaint still lands (press → repaint → release)', seedRichProject, async ({ page, server }) => {
+  await gotoDashboard(page, server);
+  await navigate(page, 'traceability');
+  const label = await page.locator(MATRIX_TAB).innerText();
+  assert.equal(label.trim(), 'Matrix', 'second Evidence view tab is Matrix');
+  assert.equal(await page.evaluate(() => window.EVIDENCE_VIEW), 'summary', 'starts on the summary view');
+
+  const box = await page.locator(MATRIX_TAB).boundingBox();
+  assert.ok(box, 'Matrix tab is visible');
+  await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
+  await page.mouse.down();
+  await forceRepaint(page); // a WS tick lands mid-gesture
+  await page.mouse.up();
+
+  assert.equal(
+    await page.evaluate(() => window.EVIDENCE_VIEW), 'matrix',
+    'the Matrix tab click took effect despite the repaint between press and release',
+  );
+});
+
+browserTest('keyboard focus inside a repainted region survives the repaint', seedRichProject, async ({ page, server }) => {
+  await gotoDashboard(page, server);
+  await navigate(page, 'traceability');
+  await page.focus(MATRIX_TAB);
+  assert.equal(
+    await page.evaluate(() => document.activeElement && document.activeElement.textContent.trim()),
+    'Matrix', 'the Matrix tab has focus before the repaint',
+  );
+  await forceRepaint(page);
+  assert.equal(
+    await page.evaluate(() => document.activeElement && document.activeElement.textContent.trim()),
+    'Matrix', 'the Matrix tab keeps focus across the repaint',
+  );
+});
+
+browserTest('an unchanged snapshot causes zero DOM mutations and node identity persists', seedRichProject, async ({ page, server }) => {
+  await gotoDashboard(page, server);
+  // Warm pass: with the fix, the first applyState populates the per-region
+  // caches; without it, every pass rebuilds everything.
+  await forceRepaint(page);
+
+  const result = await page.evaluate(() => {
+    const region = document.getElementById('page-command');
+    // Deep renderer-painted nodes (not the static shell): any button/card
+    // inside the command page, and a scope-select option in the topbar.
+    const deep = region.querySelector('button, .card, h3');
+    const option = document.querySelector('#scope-run option');
+    if (deep) deep.__rs495 = 'marker';
+    if (option) option.__rs495 = 'marker';
+
+    const records = [];
+    const observer = new MutationObserver((batch) => { records.push(...batch); });
+    observer.observe(region, { subtree: true, childList: true, attributes: true, characterData: true });
+    window.applyState(window.STATE, {});
+    observer.disconnect();
+
+    const deepAfter = region.querySelector('button, .card, h3');
+    const optionAfter = document.querySelector('#scope-run option');
+    return {
+      mutations: records.length,
+      deepMarkerSurvived: !!(deepAfter && deepAfter.__rs495 === 'marker'),
+      optionMarkerSurvived: !!(optionAfter && optionAfter.__rs495 === 'marker'),
+    };
+  });
+
+  assert.equal(result.mutations, 0, 'identical snapshot repaint mutates nothing in #page-command');
+  assert.equal(result.deepMarkerSurvived, true, 'renderer-painted node identity survives the repaint');
+  assert.equal(result.optionMarkerSurvived, true, 'scope select options are not rebuilt for an unchanged catalog');
+});
+
+browserTest('morphHTML patches attributes, text and keyed children while preserving identity', seedRichProject, async ({ page, server }) => {
Relevance

●● Moderate

No historical evidence found that regression test titles must include the issue id (e.g., “#495”).

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires regression tests to reference the bug ID in the test description
string. The added browserTest(...) descriptions omit #495 even though the file header references
it.

tests/browser/dashboard-495.test.js[138-214]
Skill: testing-qa

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Regression tests should include the bug/issue ID in the `test()`/`it()` description string. The new `browserTest(...)` descriptions are detailed but do not include `#495`.

## Issue Context
The file is explicitly a `#495` regression suite, but the rule requires the ID in the test description itself for traceability.

## Fix Focus Areas
- tests/browser/dashboard-495.test.js[138-171]
- tests/browser/dashboard-495.test.js[173-206]
- tests/browser/dashboard-495.test.js[208-214]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. ESLint 10 needs Node 20 ✓ Resolved 🐞 Bug ☼ Reliability
Description
The PR upgrades eslint to 10.8.0, but the resolved eslint package declares engines.node as
^20.19.0 || ^22.13.0 || >=24 while this repo still advertises and tests Node 18. This makes the
supported Node version story inconsistent and can break installs/linting in the Node 18 environments
the repo currently claims to support.
Code

package.json[R101-105]

  "devDependencies": {
    "@types/node": "^18.19.130",
-    "eslint": "^9.0.0",
+    "eslint": "^10.8.0",
    "pkg": "^5.8.1",
    "typescript": "^5.9.3"
Relevance

● Weak

Similar Node18 vs Node20 engines mismatch suggestion was rejected in PR #473.

PR-#473

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The repo explicitly supports Node 18 in engines/docs and runs CI/release steps on Node 18, but the
lockfile shows eslint@10.8.0 requires Node 20.19+ via its declared engines field, creating a
concrete compatibility mismatch introduced by the eslint bump.

package.json[41-43]
package.json[101-106]
package-lock.json[3928-3975]
docs/quick-start-guide.md[8-15]
docs/SETUP.md[32-36]
.github/workflows/ci.yml[10-35]
.github/workflows/release-observer.yml[47-59]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
eslint@10’s dependency tree requires Node 20.19+, but the repo still declares Node >=18, docs state Node >=18, CI runs on 18.x, and the observer release workflow installs Node 18. This mismatch will lead to engine warnings and can break lint/tooling on Node 18.

## Issue Context
The PR introduced eslint@10.8.0 in devDependencies.

## Fix Focus Areas
- package.json[41-43]
- package.json[101-105]
- package-lock.json[3928-3975]
- docs/quick-start-guide.md[8-12]
- docs/SETUP.md[32-36]
- .github/workflows/ci.yml[10-35]
- .github/workflows/release-observer.yml[47-59]

### Suggested fix options (pick one)
1) **Keep Node 18 support**: revert/pin eslint to a Node-18-compatible major (eslint 9.x) and address the underlying advisory via overrides if needed.
2) **Drop Node 18 support**: bump `package.json.engines.node` (and docs) to `>=20.19.0`, remove Node 18 from CI matrix, and update the observer release workflow (Node setup + `pkg_target`) to Node 20 targets.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
6. dashboard-scope-client.test.js modified 📜 Skill insight ⚙ Maintainability
Description
This PR adds a new regression test file but also modifies an existing test file, which violates the
rule requiring regression tests to be added only via new test files. Keeping regression-related
updates isolated reduces churn and avoids accidental changes to existing test suites.
Code

tests/dashboard-scope-client.test.js[R80-82]

+  // #495 routed the stamp through the identity-preserving morph; the pinned
+  // contract is unchanged — the stamp renders semantic timeHtml provenance.
+  assert.match(bundle, /morphHTML\(updated, s\.ts \? 'Updated ' \+ timeHtml\(s\.ts\) : ''\)/);
Relevance

● Weak

Team frequently tweaks existing tests alongside new coverage; no precedent for “new-file-only”
regression rule enforcement.

PR-#473

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires that when adding regression tests, existing test files should not be
modified. This PR adds tests/browser/dashboard-495.test.js and also changes assertions/comments in
tests/dashboard-scope-client.test.js.

tests/dashboard-scope-client.test.js[78-83]
tests/browser/dashboard-495.test.js[1-30]
Skill: qa-testing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A regression-test PR added `tests/browser/dashboard-495.test.js` but also modified an existing test file (`tests/dashboard-scope-client.test.js`), which violates the requirement that regression tests be added as new files without editing existing tests.

## Issue Context
This PR appears to be adding regression coverage for issue `#495`, but it also updates a pinned-source assertion in an existing test.

## Fix Focus Areas
- tests/dashboard-scope-client.test.js[78-83]
- tests/browser/dashboard-495.test.js[1-274]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. PII in test header 📜 Skill insight ⛨ Security
Description
The new test file includes a personal name in a comment header, which can be considered PII and
should not be committed in-repo per the documentation/sensitive-data rule. This increases privacy
and compliance risk if the repository is shared beyond the intended audience.
Code

tests/browser/dashboard-495.test.js[18]

+ * owner: RStack developed by Richardson Gunde
Relevance

● Weak

Repo commonly keeps “owner: … Richardson Gunde” comments; not treated as prohibited PII (e.g., PR
#317, #10).

PR-#317
PR-#10

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1399700 prohibits exposing sensitive data (including PII) in documentation/code
comments. The added file comment explicitly includes an individual's name in the header.

tests/browser/dashboard-495.test.js[18-18]
Skill: documentation-writing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new comment header includes a personal name (PII). Per compliance, documentation/code comments must not expose sensitive data.

## Issue Context
The header currently contains `owner: ... developed by <person name>`.

## Fix Focus Areas
- tests/browser/dashboard-495.test.js[18-18]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +1 to +19
/**
* #495 — keyed-morph rendering: DOM node identity must survive live repaints.
*
* The server broadcasts a full snapshot every 3s and applyState re-renders
* every registered page via setHTML (innerHTML replacement), destroying node
* identity page-wide. These tests pin the interaction contract the fix
* restores:
* 1. a click whose press/release straddles a repaint still lands;
* 2. keyboard focus inside a repainted region survives the repaint;
* 3. an unchanged snapshot causes zero DOM mutations (dirty-check) and
* node identity persists (expando markers survive);
* 4. morphHTML patches correctly: attributes, text, keyed reorder with
* identity preservation, focused-input value protection.
*
* Same harness as dashboard-96: real server on an ephemeral port, canonical
* fixtures, real Chromium via playwright-core; skips cleanly with no browser.
*
* owner: RStack developed by Richardson Gunde
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

2. Missing regression attribution comment 📜 Skill insight ⚙ Maintainability

The new regression test file documents issue #495 and the failure context, but it does not include
the required “found date” and “QA report path” attribution metadata. This reduces traceability for
audit/debugging of regressions.
Agent Prompt
## Issue description
The regression test header comment is missing required attribution fields (found date and QA report path).

## Issue Context
Compliance requires regression test files to include: issue ID, what broke, date found, and QA report path reference.

## Fix Focus Areas
- tests/browser/dashboard-495.test.js[1-19]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tests/browser/dashboard-495.test.js Outdated
Comment thread tests/browser/dashboard-495.test.js Outdated
Comment thread src/observability/dashboard/ui/lib.js Outdated
The server broadcasts a full snapshot every 3s and applyState re-rendered
every registered page via setHTML (innerHTML replacement), destroying DOM
node identity page-wide: clicks straddling a tick landed on detached nodes
and vanished, keyboard focus dropped to <body>, text selection was wiped.

setHTML now dirty-checks (per-element cache of the last painted HTML — an
unchanged region is zero DOM work) and morphs changed regions in place:
attributes and text patch, children match by data-key, then tag+position,
so patched nodes keep identity. Focused form controls keep the user's
in-progress value; options under a focused select keep the in-flight
choice. Minimal DOM shims (Node page-render tests) lack a fragment parser
and keep the legacy innerHTML-assignment semantics via feature detection.

Routed the remaining per-tick writers through the same morph: both scope
selects (the 3s tick can no longer close an open dropdown), per-page
Updated stamps, the run-workspace empty prompt and tab strip, the cockpit
controls panel (previously removed-and-reinserted every tick — every
button churned identity), the studio inspector, and the run-report /
run-analytics run selectors and KPIs. Event-driven writers (drawer,
artifact viewer, toast, cockpit status) are unchanged.

Browser regression (tests/browser/dashboard-495.test.js, verified failing
before the fix): a press→repaint→release click lands; focus survives a
repaint; an identical snapshot causes zero MutationObserver records and
expando markers survive; morphHTML round-trips attributes/text/keyed
reorders preserving node identity and protects focused input values.

Template parsing has identical script-execution semantics to innerHTML
(fragment parsing marks scripts already-started in both), so the security
surface is unchanged.

Refs #495 (closes it), foundation child of epic #499.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@richard-devbot
richard-devbot force-pushed the claude/animation-uiux-skills-c5b356 branch from 06e77c3 to 0f25730 Compare July 31, 2026 04:01
@richard-devbot

Copy link
Copy Markdown
Owner Author

Rebased onto main as a single commit: main independently fixed the brace-expansion advisory while this PR was open (root override → 5.0.8, GHSA tolerance) — my duplicate eslint@10 security commit is dropped; this PR is now purely the #495 morph fix. NOTE: the override approach has a latent CJS-interop landmine (brace-expansion@5.0.8 exports an object; minimatch@3 crashes on any brace glob like **/*.{js,ts} — none in the repo today) — filing separately with probe evidence. — Claude Main, 2026-07-31

Qodo (correctness, real): morphChildren used a plain object as the
data-key → node map, so a state-derived key like "__proto__" silently
failed to store (assignment hits the prototype slot) — the node lost
keyed matching and its identity on reorder. Object.create(null) removes
every special-key pitfall; a __proto__ round-trip case is pinned in the
morph browser test, verified failing on the plain-object map first.

Convention items (PR #490 precedent): browser-test timeouts are named
constants; test names carry their #495 attribution.

Refs #495 (PR #504 review follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@richard-devbot

Copy link
Copy Markdown
Owner Author

Qodo findings addressed in the follow-up commit: the keyed map is now null-prototype (Object.create(null)) — a state-derived data-key="__proto__" previously lost keyed matching and node identity; a round-trip regression case is pinned in the morph test (verified failing first). Timeouts are named constants and test names carry #495 per the #490 convention. Suite 1709/1709, browser 4/4 morph tests green. — Claude Main

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hub client: full-DOM live repaints destroy node identity — swallowed clicks, dropped focus, broken selection

2 participants