Files
archy/.planning/phases/01-federation-mesh-hardening/01-12-PLAN.md
T

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, gap_closure, must_haves
phase plan type wave depends_on files_modified autonomous requirements gap_closure must_haves
01-federation-mesh-hardening 12 execute 7
neode-ui/src/views/web5/Web5ConnectedNodes.vue
neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts
true
UIFIX-02
true
truths prohibitions artifacts key_links
On a wide viewport the connected-nodes card's height is set by its row sibling, not by how many nodes are in the list — adding nodes makes the inner list scroll instead of making the row taller (UIFIX-02)
The inner list scrolls within the matched height: with more rows than fit, a scrollbar appears inside the card and the card stays put
With a short sibling the card still has a usable list height rather than collapsing to its header and tabs (UIFIX-02 empty edge, sibling half)
With zero connected nodes the card renders its existing empty/loading row and does not collapse (UIFIX-02 empty edge, list half)
All three tabs — trusted, observers, requests — share the same scroll behaviour, so switching tabs never changes the card's height (UIFIX-02 adjacency edge)
The stacked single-column layout below the row breakpoint is unchanged: the list keeps its existing capped height and its existing scroll
statement category
Nothing outside the connected-nodes card's own height and overflow behaviour may change — the card's glass styling, padding, header, tab strip, row markup, counts, and every animation stay byte-identical, and no sibling card in any Web5 row is restyled to make the fix work safety
path provides min_lines
neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts Structural pin on the scroll contract for all three tab panes 30
from to via pattern
neode-ui/src/views/web5/Web5ConnectedNodes.vue neode-ui/src/views/web5/Web5.vue the card is a min-height-zero flex column whose scroll pane contributes no intrinsic height at the row breakpoint, so the grid row is sized by the sibling and stretch gives the card that height overflow-y-auto
Make the connected-nodes list obey the row: its height tracks the taller sibling beside it and the list scrolls inside that height, instead of growing until every node fits.

Purpose: UIFIX-02 is a BLOCKER, and it is a regression of an earlier request ("was still meant to scroll"). Quick task 260729-je5 made the list fill the card's height; what is missing is the other half — the list must not drive the card's height. Today all three tab panes carry max-h-72 xl:max-h-none, so at the xl breakpoint where the row becomes two columns the cap is lifted and nothing bounds the list: it grows to fit every row, stretches the grid row, and the scrollbar the user expects never appears. Output: a bounded, sibling-matched card with an internal scroll at the row breakpoint, an unchanged stacked layout below it, and a test that pins the contract so a future cleanup cannot undo it again.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/STATE.md @.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md @neode-ui/src/views/web5/Web5.vue

Artifacts this phase produces

Created or changed by this plan:

Symbol Kind File
scroll-contract classes on the three tab panes changed template classes neode-ui/src/views/web5/Web5ConnectedNodes.vue
row-breakpoint height floor on the card root changed template classes same
neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts new vitest suite new file
Task 1: End-to-end — the trusted pane scrolls at a sibling-matched height neode-ui/src/views/web5/Web5ConnectedNodes.vue, neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts - `neode-ui/src/views/web5/Web5ConnectedNodes.vue` lines 1-135: the card root (`glass-card p-6 scroll-mt-24 flex flex-col`), the desktop and mobile header blocks, the four-tab strip, and the three `v-show` tab panes at lines 57, 90 and 120 — all three currently carrying `space-y-2 flex-auto min-h-0 overflow-y-auto max-h-72 xl:max-h-none`. Also read the loading and empty rows inside the trusted pane so you know what renders when the list is empty. - `neode-ui/src/views/web5/Web5.vue` lines 57-74: the three `grid grid-cols-1 xl:grid-cols-2 gap-6` rows. The connected-nodes card is the left item of the first row and `Web5NodeVisibility` is its right sibling. Confirm no `items-start`/`self-start` is applied anywhere on that row — grid's default `align-items: stretch` is what makes the sibling-matched height work, and this plan must not add or remove alignment utilities on the row. - `neode-ui/src/views/web5/Web5NodeVisibility.vue` — read only far enough to see roughly how tall it renders (it is the sibling whose height the card must adopt). Do not modify it. - `neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts` — the house convention for a structural DOM/class pin test in this repo (this is the file the standing rule names as must-stay-green; read it for its mounting and assertion style, do not change it). - Mounting the component and reading the trusted pane's class list: it has `overflow-y-auto`, has `min-h-0`, and has no class that removes its height bound at the row breakpoint. - The same three assertions hold for the observers pane and the requests pane. - The pane keeps a capped height below the row breakpoint (the stacked layout is unchanged). - The card root is a flex column with a height floor at the row breakpoint, so a short sibling cannot collapse the list area. - With an empty node list the pane still renders (the existing empty/loading row is present) and the pane element is still in the tree. Write the test file first and confirm it fails.
In `Web5ConnectedNodes.vue`, change only the height/overflow contract:

On each of the three tab panes, replace the current sizing classes so that below the row
breakpoint nothing changes (keep the existing capped height and `overflow-y-auto`, keep basis
`auto` so the auto-height stacked column still sizes to content), and at the row breakpoint the
pane becomes a zero-basis growing flex child with no height cap — `xl:flex-1 xl:basis-0
xl:max-h-none` alongside the existing `min-h-0 overflow-y-auto`. Zero basis is the whole trick:
it makes the pane contribute nothing to the card's intrinsic height, so the grid row is sized by
the sibling alone, `align-items: stretch` gives the card that row height, and `flex-1` then hands
the leftover height to the pane, which scrolls inside it.

On the card root, keep `glass-card p-6 scroll-mt-24 flex flex-col` exactly as it is and add
`min-h-0` plus a row-breakpoint height floor (`xl:min-h-[20rem]`) so a sibling shorter than the
header-plus-tabs block still leaves a usable, scrolling list area rather than a collapsed strip.
Choose the floor to sit close to today's stacked cap so the visual weight of the card is familiar.

Change nothing else. Do not touch the header blocks, the tab strip, the per-row markup, the count
badges, the pulse dot on the requests tab, any `v-show`/`v-if` condition, any script logic, or any
class on `Web5.vue`'s grid rows. Do not add a scrollbar style — the list already scrolls with the
house default below the breakpoint and must look identical above it.
cd neode-ui && test -f src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts && npx vitest run src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts - The test file exists and `cd neode-ui && npx vitest run src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts` exits 0 (the `test -f` guard is required — `vitest.config.ts` sets `passWithNoTests: true`, so a missing file would pass vacuously). - `grep -c 'xl:max-h-none' neode-ui/src/views/web5/Web5ConnectedNodes.vue` equals 3 and each of those three lines also matches `xl:basis-0`. - `grep -c 'flex-auto' neode-ui/src/views/web5/Web5ConnectedNodes.vue` equals 0. - `grep -c 'max-h-72' neode-ui/src/views/web5/Web5ConnectedNodes.vue` equals 3 — the stacked cap is untouched. - `grep -c 'xl:min-h-' neode-ui/src/views/web5/Web5ConnectedNodes.vue` equals 1. - `git diff --stat -- neode-ui/src/views/web5/Web5.vue` reports no change. - `git diff -- neode-ui/src/views/web5/Web5ConnectedNodes.vue | grep -c '^[-+].*<script'` equals 0 — no script-block change. - `cd neode-ui && npx vitest run` exits 0 — every existing suite, including `src/views/dashboard/__tests__/keepAliveTabs.test.ts`, stays green. All three panes carry the bounded scroll contract, the stacked layout is untouched, and a test pins it. Task 2: Prove it against the real preview and settle the second "connected nodes" surface neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts The local dev preview can be started (`cd neode-ui && npm run dev:mock` serves the UI on :8100 against the mock backend) — jsdom cannot compute layout, so the height claim has to be observed in a real browser engine - `neode-ui/DEV-SCRIPTS.md` lines 1-40 — how the dev preview and mock backend are started and on which ports, and how to stop them cleanly. - `neode-ui/src/views/settings/AccountInfoSection.vue` (grep it for "connected" / "nodes" first) — the todo flags a second "connected nodes" block living in settings. Determine whether it is the same list in a different place or unrelated copy, and record the verdict. Start the dev preview, open the Web5 tab at a wide viewport (at or above the row breakpoint), and observe the first row directly. Confirm three things and record each in the SUMMARY with the viewport width you used:
1. The connected-nodes card and its right-hand sibling are the same height.
2. With more connected nodes than fit, the list scrolls inside the card and the card does not grow
   — if the mock backend does not supply enough nodes to overflow, temporarily add rows in the
   browser's element inspector to force the condition rather than editing the mock backend, and say
   so in the SUMMARY.
3. Narrowing below the row breakpoint restores exactly the previous stacked appearance.

Then settle the second surface: grep the settings section named above for a connected-nodes list.
If it is a genuinely separate list with the same grow-to-fit behaviour, fix it the same way in this
plan and add its file to the plan's `files_modified` in the SUMMARY. If it is unrelated (for
example a count or a link rather than a scrolling list), record that finding and leave it alone.
Do not silently skip this step — the todo explicitly flagged the ambiguity.

Extend the test file with a case for whichever surface the investigation confirmed, so the pin
covers what actually shipped.
cd neode-ui && npx vitest run src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts && npm run build - `cd neode-ui && npx vitest run src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts` exits 0. - `cd neode-ui && npm run build` exits 0 and `grep -rq 'xl:basis-0' ../web/dist/neode-ui/assets/` succeeds (per CLAUDE.md the frontend build can silently no-op, so grep the built bundle for a string this plan introduced). - The SUMMARY records all three dev-preview observations with the viewport width used for each. - The SUMMARY records an explicit verdict on the settings "connected nodes" block: same defect and fixed here, or unrelated and why. - `cd neode-ui && npx vitest run` exits 0. The behaviour is confirmed in a real browser at both sides of the breakpoint, and the second candidate surface has a recorded verdict rather than an assumption.

Planner Assumptions (flagged, unresolved)

  • The 20rem floor is a judgement call, not a measured value. The planner did not render Web5NodeVisibility.vue to learn its height. If the sibling is reliably taller than the floor the floor never binds and the exact value is invisible; if it is shorter, the floor is what the user sees. Task 2's dev-preview observation is where that gets confirmed — if the floor looks wrong on screen, adjust it there and record the final value.

<threat_model>

Trust Boundaries

Boundary Description
federated peer data → rendered node row The list renders peer-supplied names and identifiers

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-01-55 Information Disclosure a bounded, scrolling list hiding a connected node the operator needs to notice medium mitigate The tab strip's existing count badges stay untouched and remain visible above the scroll area, so the total is always readable without scrolling; the acceptance criteria forbid changing them
T-01-56 Spoofing a long peer-supplied node name overflowing the newly bounded pane and overlapping adjacent chrome low accept Row markup is unchanged by this plan; the panes already truncate as they do today, and this plan alters only the container's height and overflow
T-01-57 Denial of Service a very large peer list making the card expensive to render low accept The list is already fully rendered today; bounding the container reduces painted area rather than increasing it, and virtualisation is out of scope for a layout fix
T-01-SC Tampering npm/pip/cargo installs high mitigate This plan installs nothing — template class changes and one vitest file only. If an implementation choice would add a dependency, stop: RESEARCH.md's Package Legitimacy Audit must cover it first, with a blocking human checkpoint for any [ASSUMED]/[SUS] entry
</threat_model>
- `cd neode-ui && npx vitest run` — green, including `keepAliveTabs.test.ts`. - `cd neode-ui && npm run build` — green, and the built bundle carries the new class. - Dev-preview observation recorded at both sides of the row breakpoint.

<success_criteria>

  • The card's height comes from its row sibling; the list scrolls inside it and never grows to fit.
  • A short sibling still leaves a usable list height.
  • The stacked layout below the breakpoint is byte-identical to before.
  • The settings "connected nodes" block has a recorded verdict.
  • A test pins the contract so the behaviour cannot silently regress a third time. </success_criteria>
Create `.planning/phases/01-federation-mesh-hardening/01-12-SUMMARY.md` when done, recording the dev-preview observations, the final floor value, and the settings-surface verdict. Stage by explicit path, commit, and `git push gitea-ai main`.