Skip to content

fix(web): Revenue-by-Route truncation, refund button state, explorer links, modal a11y - #249

Open
mhikel66 wants to merge 10 commits into
accensa:mainfrom
mhikel66:fix/issues-185-186-188-191
Open

fix(web): Revenue-by-Route truncation, refund button state, explorer links, modal a11y#249
mhikel66 wants to merge 10 commits into
accensa:mainfrom
mhikel66:fix/issues-185-186-188-191

Conversation

@mhikel66

Copy link
Copy Markdown

Fixes four apps/web bugs from the Stellar Wave batch. They share files
(dashboard/page.tsx, refund-panel.tsx) and two small helpers, so they land
as one PR of 10 commits rather than four.

Closes #185
Closes #186
Closes #188
Closes #191


#185 — Revenue by Route aggregated only the newest 100 payments

app/dashboard/routes/page.tsx fetched /api/payments with no limit (newest
100) and still offered an "All time" range selector, so every figure — total
settled, attributed split, per-route revenue, share bars, chart — was computed
from a truncated sample while presenting itself as complete.

  • New lib/payments-fetch.tsfetchAllPayments walks /api/payments'
    next_cursor to the end (1,000 rows/page), reports a running count while it
    loads, and caps at 1,000 pages (flags truncated rather than looping on a
    bad cursor).
  • New filterByRange in lib/revenue-analytics.ts scopes the payments to the
    selected window, its start aligned to midnight UTC to match
    buildRevenueSeries' bucketing. The route breakdown and its share
    percentages are now computed against the in-range total.
  • The page shows Loading payment history — N so far while paging.

Acceptance: each range aggregates every payment in that range ✓ · "All time"
covers a >100 fixture (payments-fetch.test.ts uses 1,200) ✓ · shares computed
against the full range total ✓ · render cost: revenue-analytics.perf.test.ts
measures the aggregation across all three ranges at ~8 ms for 600 payments
and ~15 ms for 3,000 on the dev machine (best-of-5, warm) ✓

#186 — Refund button stayed live during submitting

RefundPanel modelled five phases but handled four; submitting fell through
to the idle branch and rendered an enabled "Refund this payment" button while a
wallet prompt was open.

  • Split into a stateful RefundPanel and a pure RefundPanelView that renders
    one phase via an exhaustive switch. submitting returns early with "a
    signing prompt is open" and no action.
  • default: calls assertNever(phase), so adding a Phase variant without a
    case is a type error (pnpm typecheck).
  • submitRefund / the preflight flow are untouched.

Acceptance: no refund-starting control while submitting ✓ · panel says a wallet
prompt is open ✓ · unhandled new phase is a type error ✓ · test drives the
panel into submitting and asserts no actionable control ✓

#188 — Explorer links hardcoded to testnet in five places

  • New lib/explorer.ts — the only module in apps/web/src that contains
    the string stellar.expert (grep -rn confirms). resolveStellarNetwork
    reads NEXT_PUBLIC_STELLAR_NETWORK (+ public/pubnet aliases), throws
    on an unrecognised value, and falls back to testnet with a one-time
    console.warn when unset — the "clearly-labelled default" the issue allows
    (a hard throw would take down statically-rendered pages at build time).
  • All five call sites (dashboard/page.tsx, refund-panel.tsx ×2,
    batches/[id]/page.tsx, page.tsx) now use explorerTxUrl /
    explorerContractUrl.
  • Documented in apps/web/README.md.

Acceptance: exactly one module contains stellar.expert ✓ · links resolve to
the configured network ✓ · unset fails loudly (warns, not silent) ✓ ·
documented ✓ · explorer.test.ts asserts construction for testnet and
mainnet ✓

#191 — Payment details modal was not a real dialog

  • New lib/dialog-focus.tsgetFocusable, wrapTabTarget (Tab wrap logic),
    focusRestorer (capture + restore). Plain functions, unit-tested without a
    DOM.
  • The modal card now carries role="dialog", aria-modal, aria-labelledby
    → its heading id, and tabindex="-1". A mount effect moves focus in, traps
    Tab, keeps Escape and backdrop-click, and restores focus to the opener on
    unmount. Dead closeButtonRef and the parent's modal effect removed;
    onClose is memoised so the 15 s poll can't re-trap focus.
  • Kept the custom overlay rather than native <dialog>: matching the existing
    full-screen blur + zoom-in + max-h-[90vh] styling on <dialog>/::backdrop
    would be a visible restyle, which the issue puts out of scope.

Acceptance: announced as a dialog with an accessible name ✓ · Tab/Shift+Tab
trapped ✓ · focus returns to opener ✓ · Escape + backdrop both dismiss ✓ ·
tests cover trapping (wrapTabTarget) and restoration (focusRestorer) ✓


Verification

Run in apps/web against main at 27c0932:

Check Result
pnpm typecheck pass
pnpm lint pass (5 pre-existing warnings, unchanged)
pnpm test 300 pass; db.integration.test.ts fails locally with no Postgres — pre-existing, identical on clean main, green in CI
pnpm build pass
pnpm format:check pass

Commit 10 (chore(web): reformat verify/page.tsx…) is a prettier --write on
one file this PR otherwise doesn't touch: format:check is currently red on
main
(an unwrapped line left by #248), which turns the format job red on
every PR. Happy to drop it into a separate PR if you'd rather.

Per CONTRIBUTING I'd normally wait for assignment on each issue — flagging that
here; glad to split this back into four PRs if the Wave process needs it.

Adds `lib/explorer.ts`. `resolveStellarNetwork` reads
`NEXT_PUBLIC_STELLAR_NETWORK` (accepting the `public`/`pubnet` aliases), throws
on an unrecognised value, and falls back to testnet with a one-time console
warning when unset — a labelled default, not a silent one. `explorerTxUrl` and
`explorerContractUrl` build stellar.expert links for the resolved network,
mapping mainnet to the explorer's `public` path.

vitest gets `NEXT_PUBLIC_STELLAR_NETWORK=testnet` so the suite runs against a
defined network rather than the warning path.

Refs accensa#188
…the var

The landing page and the batch page each built a testnet stellar.expert URL
inline. Both now call `explorerContractUrl`, and the app README documents
`NEXT_PUBLIC_STELLAR_NETWORK` — its values, the testnet default, and why a
mainnet deployment must set it.

Refs accensa#188
`RefundPanel` modelled five phases but handled four. `submitting` fell through
to the idle branch, which rendered an enabled "Refund this payment" button
while a wallet prompt was open and a transaction in flight; clicking it
re-entered the preflight and could reach a second signing prompt for a refund
the vault would reject.

Splits the component into a stateful `RefundPanel` and a pure `RefundPanelView`
that renders one phase via an exhaustive `switch`. `submitting` returns an
early note that a signing prompt is open and offers no action; the `default`
branch calls `assertNever(phase)`, so a new unhandled phase is a type error.
Also routes the panel's two explorer links through `explorerTxUrl`.

Closes accensa#186
Renders `RefundPanelView` at each phase: idle offers the action, checking
disables it, submitting exposes no button at all (only the "signing prompt is
open" note), done shows the outcome. Documents the compile-time exhaustiveness
guard a runtime test cannot replace.

Refs accensa#186
`lib/dialog-focus.ts`: `getFocusable` lists tabbable descendants,
`wrapTabTarget` returns where Tab / Shift+Tab should wrap to keep focus in a
dialog (or null when the browser already would), and `focusRestorer` captures
the active element and returns a function that hands focus back. Plain
functions, unit tested without a DOM.

Refs accensa#191
The modal was a plain <div> overlay: no role, no accessible name, no focus
trap, and on close focus fell to <body> instead of the row that opened it — a
screen-reader user could Tab out and read the page behind a dialog they were
never told about.

The card now carries role="dialog", aria-modal, aria-labelledby pointing at its
heading, and a tabindex so it can receive focus. A mount effect moves focus in,
traps Tab with `wrapTabTarget`, keeps Escape working, and restores focus with
`focusRestorer` on unmount. The dead `closeButtonRef` and the parent's modal
effect are gone; `onClose` is memoised so a 15s poll re-render cannot re-trap
focus. Kept the custom overlay rather than native <dialog> to avoid restyling,
which is out of scope. Also swaps the explorer link to `explorerTxUrl`.

Closes accensa#191
SSR-renders PaymentModal and checks role="dialog", aria-modal, the
aria-labelledby / id pairing, the focusable container, and the labelled close
control. Focus trapping and restoration are covered by `dialog-focus.test.ts`.

Refs accensa#191
`fetchAllPayments` follows `/api/payments`' `next_cursor` to the end (it serves
at most 1,000 rows per request), reporting progress per page and capping at
1,000 pages so a non-advancing cursor cannot loop. `filterByRange` returns the
payments inside a `RangeKey` window, its start aligned to midnight UTC to match
`buildRevenueSeries`' bucketing; `'all'` is a passthrough.

Refs accensa#185
The page fetched `/api/payments` with no limit, received the newest 100 rows,
then offered an "All time" range selector — so every figure (total settled,
attributed split, per-route revenue, share bars, chart) was computed from at
most 100 payments while presenting itself as complete. On a page whose purpose
is proportions this is the worst kind of wrong: a truncated sample distorts the
relative share between routes.

Now pages the full history with `fetchAllPayments` (showing a running count
while it loads), then filters to the selected range with `filterByRange` before
building the breakdown and series, so shares are computed against the in-range
total. A perf test records the cost: ~8 ms for 600 payments across all three
ranges, ~15 ms for 3,000, on the dev machine.

Closes accensa#185
`pnpm format:check` fails on `main` (a long line in verify/page.tsx left
unwrapped by accensa#248), which turns the `format` job red on every PR. `prettier
--write` on the one file, no behavioural change.

Refs accensa#188
@drips-wave

drips-wave Bot commented Aug 26, 2026

Copy link
Copy Markdown

@mhikel66 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

@mhikel66 is attempting to deploy a commit to the ACCENSA Team on Vercel.

A member of the Team first needs to authorize it.

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

Labels

None yet

Projects

None yet

1 participant