236 lines
15 KiB
Markdown
236 lines
15 KiB
Markdown
---
|
|||
|
|
phase: 01-federation-mesh-hardening
|
||
|
|
plan: 12
|
||
|
|
type: execute
|
||
|
|
wave: 7
|
||
|
|
depends_on: []
|
||
|
|
files_modified:
|
||
|
|
- neode-ui/src/views/web5/Web5ConnectedNodes.vue
|
||
|
|
- neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts
|
||
|
|
autonomous: true
|
||
|
|
requirements: [UIFIX-02]
|
||
|
|
gap_closure: true
|
||
|
|
|
||
|
|
must_haves:
|
||
|
|
truths:
|
||
|
|
- "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"
|
||
|
|
prohibitions:
|
||
|
|
- statement: "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"
|
||
|
|
category: safety
|
||
|
|
artifacts:
|
||
|
|
- path: neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts
|
||
|
|
provides: "Structural pin on the scroll contract for all three tab panes"
|
||
|
|
min_lines: 30
|
||
|
|
key_links:
|
||
|
|
- from: neode-ui/src/views/web5/Web5ConnectedNodes.vue
|
||
|
|
to: neode-ui/src/views/web5/Web5.vue
|
||
|
|
via: "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"
|
||
|
|
pattern: "overflow-y-auto"
|
||
|
|
---
|
||
|
|
|
||
|
|
<objective>
|
||
|
|
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.
|
||
|
|
</objective>
|
||
|
|
|
||
|
|
<execution_context>
|
||
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
||
|
|
</execution_context>
|
||
|
|
|
||
|
|
<context>
|
||
|
|
@.planning/PROJECT.md
|
||
|
|
@.planning/STATE.md
|
||
|
|
@.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md
|
||
|
|
@neode-ui/src/views/web5/Web5.vue
|
||
|
|
</context>
|
||
|
|
|
||
|
|
## 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 |
|
||
|
|
|
||
|
|
<tasks>
|
||
|
|
|
||
|
|
<task type="tracer" tdd="true">
|
||
|
|
<name>Task 1: End-to-end — the trusted pane scrolls at a sibling-matched height</name>
|
||
|
|
<files>neode-ui/src/views/web5/Web5ConnectedNodes.vue, neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts</files>
|
||
|
|
<read_first>
|
||
|
|
- `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).
|
||
|
|
</read_first>
|
||
|
|
<behavior>
|
||
|
|
- 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.
|
||
|
|
</behavior>
|
||
|
|
<action>
|
||
|
|
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.
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
<automated>cd neode-ui && test -f src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts && npx vitest run src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts</automated>
|
||
|
|
</verify>
|
||
|
|
<acceptance_criteria>
|
||
|
|
- 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.
|
||
|
|
</acceptance_criteria>
|
||
|
|
<done>All three panes carry the bounded scroll contract, the stacked layout is untouched, and a test pins it.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
<task type="auto">
|
||
|
|
<name>Task 2: Prove it against the real preview and settle the second "connected nodes" surface</name>
|
||
|
|
<files>neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts</files>
|
||
|
|
<precondition>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</precondition>
|
||
|
|
<read_first>
|
||
|
|
- `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.
|
||
|
|
</read_first>
|
||
|
|
<action>
|
||
|
|
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.
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
<automated>cd neode-ui && npx vitest run src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts && npm run build</automated>
|
||
|
|
</verify>
|
||
|
|
<acceptance_criteria>
|
||
|
|
- `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.
|
||
|
|
</acceptance_criteria>
|
||
|
|
<done>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.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
</tasks>
|
||
|
|
|
||
|
|
## 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>
|
||
|
|
|
||
|
|
<verification>
|
||
|
|
- `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.
|
||
|
|
|
||
|
|
</verification>
|
||
|
|
|
||
|
|
<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>
|
||
|
|
|
||
|
|
<output>
|
||
|
|
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`.
|
||
|
|
</output>
|