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

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-federation-mesh-hardening 03 execute 1
neode-ui/src/components/ScreensaverRing.vue
neode-ui/src/components/SendBitcoinModal.vue
neode-ui/src/components/WalletScanModal.vue
neode-ui/src/components/__tests__/ScreensaverRing.test.ts
neode-ui/src/components/__tests__/PaidTick.test.ts
true
FED-06
truths prohibitions artifacts key_links
The payment-success tick in SendBitcoinModal renders the screensaver EQ-segment ring, not a CSS ripple burst
The payment-success tick in WalletScanModal renders the same EQ-segment ring, so the paid tick is identical on every surface it appears
ScreensaverRing exposes a third badge size variant sized 160px on mobile and 192px from 768px up, with --viz-radius 80px/96px, alongside the untouched default and compact variants
The badge ring fits inside the modal card without clipping — the success pane's ring container is no larger than the badge diameter at either breakpoint
The success amount numerals and SENT / Done copy are unchanged — only the ring geometry behind the checkmark changes
SystemDangerZone and Screensaver continue to render the compact and default variants unchanged
statement verification
ScreensaverRing's segment animation is disabled under prefers-reduced-motion for every size variant including the new badge, matching the site-wide reduced-motion convention backstop
statement category
The paid-tick change MUST NOT alter what the success pane asserts about the payment — the ring is decoration; it must never render a success state for a payment that has not actually settled, and no success-gating condition may be relaxed to make the animation easier to trigger safety
path provides contains
neode-ui/src/components/ScreensaverRing.vue badge size variant + reduced-motion guard viz-ring-badge
path provides min_lines
neode-ui/src/components/__tests__/PaidTick.test.ts Assertions that both paid-tick surfaces render the badge ring 25
from to via pattern
neode-ui/src/components/SendBitcoinModal.vue neode-ui/src/components/ScreensaverRing.vue success pane renders <ScreensaverRing size="badge" /> layered under the checkmark core ScreensaverRing
from to via pattern
neode-ui/src/components/WalletScanModal.vue neode-ui/src/components/ScreensaverRing.vue success pane renders <ScreensaverRing size="badge" /> in place of the plain circle ScreensaverRing
Make the invoice/payment "paid" tick on-brand: the circle around the checkmark becomes the screensaver ring with its outer EQ-segment lines, everywhere the paid tick appears.

Purpose: FED-06, locked by the user in CONTEXT.md — the paid-tick circle is the ScreensaverRing style, applied consistently to every paid/success tick surface. RESEARCH.md flagged that a naive drop-in overflows the modal card (the existing compact variant is 240-320px against a 96-112px badge); 01-UI-SPEC.md resolved that by deciding on a new badge size variant rather than a transform hack, and also recorded that ScreensaverRing has no prefers-reduced-motion guard at all today — a real gap this phase must close. Output: a third size variant plus a reduced-motion guard in the shared component, both paid-tick call sites swapped, and component tests pinning the result.

<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 @.planning/phases/01-federation-mesh-hardening/01-PATTERNS.md @neode-ui/src/components/ScreensaverRing.vue @neode-ui/src/components/Screensaver.vue

Artifacts this phase produces

Created or changed by this plan:

Symbol Kind File
size: 'default' | 'compact' | 'badge' widened prop union neode-ui/src/components/ScreensaverRing.vue
.viz-ring-badge CSS class (160px / 192px, --viz-radius 80px / 96px) same
reduced-motion media guard on .viz-segment CSS same
neode-ui/src/components/__tests__/ScreensaverRing.test.ts new vitest suite new file
neode-ui/src/components/__tests__/PaidTick.test.ts new vitest suite new file
Task 1: End-to-end — the badge ring variant renders as the payment-success tick neode-ui/src/components/ScreensaverRing.vue, neode-ui/src/components/SendBitcoinModal.vue, neode-ui/src/components/__tests__/ScreensaverRing.test.ts, neode-ui/src/components/__tests__/PaidTick.test.ts - `neode-ui/src/components/ScreensaverRing.vue` — the whole file (about 115 lines): the `withDefaults(defineProps<{ size?: ... }>())` union, the `sizeClass` computed, the two existing size CSS classes with their `min-width: 768px` breakpoints and `--viz-radius` custom properties, and the `segment-pulse` keyframes. - `neode-ui/src/components/SendBitcoinModal.vue` lines 1-30 (the success pane markup: the success-burst container, its three ripple span elements, and the core circle plus checkmark) and lines 680-740 (the corresponding CSS block, including the existing `@media (prefers-reduced-motion: reduce)` rule — copy that exact media-query syntax into ScreensaverRing). - `neode-ui/src/components/Screensaver.vue` — the existing `ScreensaverRing` + `ScreensaverLogo` centred-absolute layering pattern (`position: relative` wrapper, `position: absolute; inset: 0` inner content) to reuse for the checkmark core. - `neode-ui/src/components/__tests__/BaseModal.test.ts` — the house vitest + `@vue/test-utils` conventions for mounting a component in this repo. - `.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md` — the "FED-06 Sizing Decision" table (exact diameters and radii) and the "UI Considerations" rows for the paid-tick ring. - `ScreensaverRing.test.ts`: mounting with `size="badge"` puts `viz-ring-badge` on the root element; mounting with `size="compact"` still yields `viz-ring-compact`; the default mount still yields `viz-ring-default`; the rendered segment count matches the `segmentCount` prop. - `PaidTick.test.ts`: SendBitcoinModal driven into its payment-success state renders exactly one `ScreensaverRing` with `size="badge"`, renders the checkmark core, and renders zero ripple elements; the success amount text is unchanged. Write both test files first and confirm they fail before implementing.
In `ScreensaverRing.vue`: widen the `size` prop union with a third member `'badge'`, extend
`sizeClass` to map it to `viz-ring-badge`, and add a `.viz-ring-badge` CSS rule following the
exact shape of the existing two — `width`/`height` 160px and `--viz-radius: 80px` at mobile,
then a `@media (min-width: 768px)` block with 192px and `--viz-radius: 96px`. Do not touch
`.viz-ring-default` or `.viz-ring-compact`; `Screensaver.vue` and `SystemDangerZone.vue` must
keep their current rendering.

Also inside `ScreensaverRing.vue`, add the missing motion guard so it applies to every variant:
a `@media (prefers-reduced-motion: reduce)` block that sets `animation: none` and a static
reduced opacity on `.viz-segment`. Use the same media-query syntax as the guard already present
in `SendBitcoinModal.vue` so the two read identically.

In `SendBitcoinModal.vue`'s payment-success pane: import `ScreensaverRing`, replace the three
ripple span elements with `<ScreensaverRing size="badge" />`, keep the existing core circle and
checkmark markup untouched, and wrap the pair in the Screensaver-style layering (a
`position: relative` container sized to the badge diameter, with the core absolutely centred over
the ring). Remove the ripple elements' now-dead CSS rules and their keyframes; keep the core and
checkmark rules, and keep the existing reduced-motion rule but drop the clause that referenced
the removed elements. Do not change the success amount numerals, the SENT copy, the Done button,
or any condition that decides when the success pane is shown.
cd neode-ui && test -f src/components/__tests__/ScreensaverRing.test.ts && test -f src/components/__tests__/PaidTick.test.ts && npx vitest run src/components/__tests__/ScreensaverRing.test.ts src/components/__tests__/PaidTick.test.ts - Both test files exist and `npx vitest run src/components/__tests__/ScreensaverRing.test.ts src/components/__tests__/PaidTick.test.ts` exits 0 (the explicit `test -f` guards are required — `vitest.config.ts` sets `passWithNoTests: true`, so a missing file would otherwise pass vacuously). - `grep -c 'viz-ring-badge' neode-ui/src/components/ScreensaverRing.vue` is at least 2 (computed mapping + CSS rule). - `grep -c 'prefers-reduced-motion' neode-ui/src/components/ScreensaverRing.vue` equals 1. - `grep -Eq '160px' neode-ui/src/components/ScreensaverRing.vue` and `grep -Eq '192px' neode-ui/src/components/ScreensaverRing.vue` both succeed. - `grep -c 'viz-ring-compact' neode-ui/src/components/ScreensaverRing.vue` is unchanged from before the edit (the compact variant is untouched). - `grep -c 'ScreensaverRing' neode-ui/src/components/SendBitcoinModal.vue` is at least 2 (import + usage). - `grep -c 'burst-ring' neode-ui/src/components/SendBitcoinModal.vue` equals 0. - `cd neode-ui && npx vitest run` exits 0 — no existing suite regressed. - `cd neode-ui && npm run build` exits 0 and `grep -rq 'viz-ring-badge' ../web/dist/neode-ui/assets/` succeeds (per CLAUDE.md: the build can silently no-op, so grep the built bundle for the new string). The badge variant exists, the send-payment success tick renders it, and both are pinned by tests that failed before the change. Task 2: Bring the scan-modal paid tick to the same ring neode-ui/src/components/WalletScanModal.vue, neode-ui/src/components/__tests__/PaidTick.test.ts - `neode-ui/src/components/WalletScanModal.vue` around line 232 (the success circle markup — a fixed 24-unit inline-flex circle with the success-ring class) and around line 861 (its CSS rule). Note it has no ripple animation at all today, unlike the send modal. - `neode-ui/src/components/SendBitcoinModal.vue` as left by Task 1 — the layering wrapper to copy verbatim. - `.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md` — the FED-06 sizing table row confirming this call site also uses the badge variant. Replace WalletScanModal's fixed success circle with the same composition Task 1 established: a `position: relative` container sized to the badge diameter holding `` with the existing checkmark content absolutely centred over it. Import `ScreensaverRing`. Drop the now-unused fixed-size utility classes and the plain-circle CSS rule; keep the checkmark glyph, its colour, and the surrounding copy exactly as they are.
Extend `PaidTick.test.ts` with a WalletScanModal case asserting its success state renders one
`ScreensaverRing` with `size="badge"` and still renders the checkmark.

Verify on the dev preview before considering this done, per the user requirement recorded in
CONTEXT.md: run the dev preview and confirm neither ring is clipped by the modal card's
scrolling container at a narrow viewport and at desktop width. Record the observation in the
SUMMARY. The blocking human sign-off for this is consolidated into plan 01-07.
cd neode-ui && npx vitest run src/components/__tests__/PaidTick.test.ts && npm run build - `cd neode-ui && npx vitest run src/components/__tests__/PaidTick.test.ts` exits 0 and the suite contains both a SendBitcoinModal case and a WalletScanModal case. - `grep -c 'ScreensaverRing' neode-ui/src/components/WalletScanModal.vue` is at least 2. - `grep -c 'success-ring' neode-ui/src/components/WalletScanModal.vue` equals 0. - `cd neode-ui && npx vitest run` exits 0. - `cd neode-ui && npm run build` exits 0. - The SUMMARY records the dev-preview observation for both surfaces at a narrow and a desktop viewport. Both paid-tick surfaces render the identical branded ring, with no clipping at either breakpoint.

Planner Assumptions (flagged, unresolved)

  • FED-06 / spec-less probe, category unclassified: the probe surfaced an unclassified edge for FED-06 that no defensible acceptance criterion covers. Surfaced rather than dropped: the phase requirement says the ring applies "everywhere the paid tick appears", and a repo-wide grep found exactly two paid-tick surfaces (SendBitcoinModal.vue, WalletScanModal.vue). If a third success-tick surface is added between planning and execution — or exists under markup this grep did not match — it will not be covered by this plan. The FED-03 review in plan 01-07 re-runs the grep as a check.

<threat_model>

Trust Boundaries

Boundary Description
payment result → success pane render The only security-relevant edge: what the UI asserts about a payment's settlement

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-01-08 Spoofing success pane rendered for a payment that has not settled high mitigate This plan changes decoration only; the acceptance criteria forbid touching any condition that gates the success pane, and npx vitest run on the existing suites must stay green
T-01-09 Denial of Service 48 animated segments rendered inside a modal degrading low-power devices low mitigate The badge variant is the smallest of the three; the new prefers-reduced-motion guard disables the animation entirely for users who ask for it
T-01-10 Repudiation the success amount or recipient text changing as a side effect of the swap medium mitigate Tests assert the success amount text is unchanged; the action forbids touching the numerals and copy
</threat_model>
- `cd neode-ui && npx vitest run` — green. - `cd neode-ui && npm run build` — green, and the built bundle contains the new class name. - Dev-preview observation recorded for both modals at narrow and desktop widths.

<success_criteria>

  • A third badge size variant exists on the shared ring component; existing variants and their consumers are untouched.
  • Both paid-tick surfaces render the branded ring with the checkmark layered centred.
  • A reduced-motion guard covers every variant.
  • Component tests pin all of the above and were proven to fail before the change. </success_criteria>
Create `.planning/phases/01-federation-mesh-hardening/01-03-SUMMARY.md` when done. Stage by explicit path, commit, and `git push gitea-ai main`.