Skip to content

UX/QA polish (PR9): rate-limit panel, retry-N counter, session-expired banner, payment schedule, contrast, gov-activity signpost#13

Merged
sidhujag merged 5 commits intomasterfrom
ux-polish-pr9
Apr 22, 2026
Merged

UX/QA polish (PR9): rate-limit panel, retry-N counter, session-expired banner, payment schedule, contrast, gov-activity signpost#13
sidhujag merged 5 commits intomasterfrom
ux-polish-pr9

Conversation

@sidhujag
Copy link
Copy Markdown
Member

Summary

Closes the UX/QA gaps identified in the SYSHUB001v3 audit. Pure UX/surface polish — no business-logic changes.

Scope

  1. ProposalStatus rate-limit terminal panel + failReason mapping fix. Dispatcher emits submit_rejected / collateral_not_found / duplicate_governance_hash; the page now maps those codes to accurate copy instead of the stale core_rejected label. A new classifyCoreRejection helper inspects failDetail and renders a dedicated, non-alarming terminal panel for the Object creation rate limit exceeded case (protocol-level quota, not a user bug).
  2. ProposalVoteModal explicit Retry-N-of-Y counter. Tracks failedCount, retryableCount, and dedupCount separately so the CTA reads "Retry 2 of 3 failed" when some rows can't be retried (MN no longer owned, missing collateral outpoint). Benign duplicate rows (already-voted) get their own clarifier instead of being presented as failures.
  3. Session-expired UX (QA v3-ER09). AuthContext now tracks a sessionExpired flag that flips true only when a 401 arrives while the user was already AUTHENTICATED (not on boot). New global SessionExpiredBanner renders a non-blocking notice with a "Sign in again" link that preserves the current path as location.state.from, and dismiss clears the flag without redirecting.
  4. Payment-schedule breakdown in wizard Review (QA v3-ER03). When paymentCount >= 2, the Review step renders an approximate superblock-cadence schedule (30-day stride) with each payment's date and amount, plus a total-budget line. A truncation warning appears when the voting window can't fit every requested payment.
  5. Footer wrap fix (QA v3-ER06). .site-footer__inner now flex-wraps and the copy block gets min-width: 0 so the copyright + social links don't overlap at mid-breakpoints.
  6. Contrast audit (QA v1-SG10 / v2-SG04). Darkened --muted (#63748d#525f76) and strengthened --panel-outline alpha (0.120.22) so every muted-text + panel combination clears WCAG AA 4.5:1 comfortably. Placeholder color pinned to --muted so UA defaults don't regress it on Safari.
  7. Account page governance-activity signpost (QA v2-SG02). New GovernanceActivityLink card deep-links to /governance where the canonical GovernanceActivity widget lives. Intentionally no second fetch — avoids two sources of truth for vote receipts.

Out of scope (deferred to PR10)

  • Pali dApp PSBT signing (sys_signAndSend). Requires craco + Node polyfills for syscoinjs-lib and live testnet validation; split out to keep PR9 reviewable.

Test plan

  • npm test -- --watchAll=false --testPathPattern=ProposalStatus — new rate-limit panel + updated failReason copy tests green.
  • npm test -- --watchAll=false --testPathPattern=ProposalVoteModal — new Retry-X-of-Y label + benign-dup note tests green.
  • npm test -- --watchAll=false --testPathPattern=SessionExpiredBanner — renders only on silent auth loss; not on boot; not on /login; dismiss clears flag; re-login clears flag.
  • npm test -- --watchAll=false --testPathPattern=NewProposal — new Review-step schedule + truncation-warning + single-payment hide tests green.
  • npm test -- --watchAll=false --testPathPattern=GovernanceActivityLink — renders heading; CTA is a real <a> with href="/governance".
  • npm test -- --watchAll=false --testPathPattern=pages/Account — 7/7 pass, no regression from the new card insert.
  • Manual pass on mobile breakpoints for the footer + schedule + banner.
  • Manual pass through the full proposal wizard + vote modal happy-path and failure path.

Made with Cursor

…d banner, payment schedule, contrast, account gov-activity signpost

Closes the UX/QA gaps flagged in the SYSHUB001v3 audit. No business-logic
changes — all edits are presentational or surface-level state wiring.

- ProposalStatus: fix failReason -> copy mapping to match dispatcher
  codes (submit_rejected, collateral_not_found, duplicate_governance_hash)
  and add a dedicated terminal panel for `Object creation rate limit
  exceeded` via classifyCoreRejection().
- ProposalVoteModal: track failedCount / retryableCount / dedupCount
  explicitly so the retry CTA reads "Retry X of Y failed" and a
  hint explains which rows are benign dupes vs real unrecoverable
  failures.
- AuthContext + new SessionExpiredBanner: on a 401 received while the
  user WAS authenticated, flag the session as expired and render a
  non-blocking global banner that preserves the return-path into
  /login. Does not fire on initial boot.
- NewProposal Review step: approximate payment-schedule breakdown
  + total budget when paymentCount>=2, with a truncation warning
  when the voting window can't fit every payment.
- App.css: footer copyright/social wrap fix, schedule styles,
  session-expired banner styles, vote-modal hint styles.
- index.css: darken --muted (#63748d -> #525f76) and strengthen
  --panel-outline to clear WCAG AA contrast on every panel/surface.
- Account page: new GovernanceActivityLink signpost card deep-linking
  to /governance (no second data fetch — receipts list stays
  canonical on the governance page).

Made-with: Cursor
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61ac831bb5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pages/NewProposal.js Outdated
Comment on lines +1102 to +1106
const paymentAmountNum = Number(form.paymentAmount);
const totalBudget =
Number.isFinite(paymentCountNum) &&
Number.isFinite(paymentAmountNum) &&
paymentCountNum > 0 &&
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use exact arithmetic for review total budget

The review total is derived from Number(form.paymentAmount)/Number(form.paymentCount), which introduces floating-point and integer-precision loss for valid large values; for example, amounts above 2^53 can display a different SYS total than the submitted proposal data. Because this block is the user’s final budget confirmation before collateral burn, showing rounded/incorrect totals is an accuracy regression. Consider computing totals in sats with BigInt (the same exact-math approach already used elsewhere in proposal form helpers).

Useful? React with 👍 / 👎.

Comment on lines +70 to +74
<Link
to={loginTo}
className="button button--primary button--small"
onClick={dismissSessionExpired}
data-testid="session-expired-signin"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't clear session-expired state on sign-in click

The sign-in CTA clears sessionExpired immediately via onClick, so if navigation is canceled or login fails, the user returns to anonymous/public pages without the expiry banner even though re-authentication never succeeded. This removes the only persistent context for why protected actions stopped working; the flag should remain until successful login (or explicit dismiss) rather than being cleared preemptively.

Useful? React with 👍 / 👎.

P2: Use exact BigInt sats arithmetic for the review-step total budget
(src/pages/NewProposal.js). `Number * Number` loses precision past
2^53 sats, which would show the user a rounded total right before
they burn 150 SYS on collateral. Route through sysToSatsString +
BigInt multiply + satsStringToSys so the display matches the
on-wire representation exactly.

P2: Don't pre-clear sessionExpired on the Sign-in CTA click
(src/components/SessionExpiredBanner.js). AuthContext.login()
already flips the flag off on a successful sign-in, and the
banner is suppressed on /login anyway. Clearing it in the Link's
onClick was destructive — if the user cancelled the nav or
login failed, they'd land back on a public page with no
context for why protected actions broke. Added a regression
test that verifies the flag persists past the click when the
login service is never actually called.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c43adaffaf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// normal "you are not logged in" state and showing an expiry
// banner would be confusing UX.
const handleAuthLost = useCallback(() => {
const wasAuthenticated = statusRef.current === AUTHENTICATED;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid reading stale auth status in auth-loss handler

handleAuthLost decides whether to raise sessionExpired from statusRef.current, but that ref is only synchronized in a passive useEffect. During status transitions (for example, right after a user-initiated logout), a 401 from an in-flight protected request can arrive before the effect runs, so this line still sees authenticated and incorrectly flips sessionExpired to true. That produces a misleading “session expired” banner for an intentional sign-out flow; the pre-transition status check needs to read from a value updated synchronously with setStatus.

Useful? React with 👍 / 👎.

P2 (AuthContext): eliminate the handleAuthLost / logout race that
could raise the "session expired" banner for a user-initiated
sign-out.

Two changes working together:

1. Synchronous status ref. `statusRef` was updated in a passive
   useEffect, which lagged one tick behind `setStatus`. Replaced
   all direct `setStatus(...)` calls with a `commitStatus(next)`
   helper that writes the ref AND schedules the React state
   update in the same tick. This closes a same-tick race where
   an interleaved handleAuthLost would read a still-AUTHENTICATED
   ref after logout had already committed ANONYMOUS.

2. `logoutPendingRef` guard. A user-initiated logout can still
   run for hundreds of ms on the network. During that window a
   parallel protected request can 401 because the SERVER is
   tearing down the session in response to our own /auth/logout.
   handleAuthLost now no-ops while logoutPendingRef is true, so
   the banner only rises for surprise session losses — never
   for a voluntary sign-out the user is already driving.

Added a dedicated regression test that drives the race with a
manually-resolved logout promise and asserts that
`sessionExpired` stays `false` both during and after the
overlapping handleAuthLost fire.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b035a010a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pages/NewProposal.js Outdated
Comment on lines +1095 to +1096
const at = start + i * step;
if (at > end) break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Align projected payments to superblock boundaries

buildApproximateSchedule currently counts startEpoch as payment #1 (at = start + i * step), but proposal payouts are executed on superblocks, so the first payable point is the next superblock after startEpoch, not the raw start timestamp. In windows where startEpoch is shortly after a superblock, this overestimates how many payments can land before endEpoch, which can hide the new truncation warning and mislead users into submitting a proposal that cannot pay all requested installments.

Useful? React with 👍 / 👎.

P1 (NewProposal): align projected payments to superblock
boundaries. `buildApproximateSchedule` was treating payment #1 as
if it landed on `startEpoch` (`at = start + i * step`), but Syscoin
governance payouts only execute on superblocks — the first payable
point is the next superblock after `startEpoch`, not the raw start
timestamp. For proposals whose `startEpoch` lands just after a
superblock, this overestimated how many payments could fit inside
the voting window and silently hid the truncation warning.

Fix:
  * Step by `(i + 1) * step` so the schedule starts at the NEXT
    superblock under the worst-case alignment assumption (the
    frontend has no network anchor for superblock timing, so
    erring conservative is correct — it may show one false
    "won't fit" warning but never hides a real one).
  * Render the schedule panel for ALL `paymentCount >= 2`
    proposals, not only those with a non-empty projected list.
    Otherwise a window shorter than one cycle would suppress the
    panel AND the truncation warning, recreating the very bug
    Codex flagged.
  * Add a dedicated copy variant for `schedule.length === 0` so
    users with short windows hear "too short to land any" in
    plain English.
  * Tighten surrounding explanatory copy to call out that the
    listed dates are worst-case estimates; real superblocks may
    run a bit earlier depending on start alignment.

Added a regression test for the zero-row case and updated the
existing truncation test to exercise a 45-day window that now
fits exactly one payment under the corrected math.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sidhujag sidhujag merged commit 06f9d04 into master Apr 22, 2026
4 checks passed
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.

1 participant