Skip to content

Release: mobile/UX foundations, governance vote UX + DB-cached tallies, signing & infra fixes#303

Open
QSchlegel wants to merge 63 commits into
mainfrom
preprod
Open

Release: mobile/UX foundations, governance vote UX + DB-cached tallies, signing & infra fixes#303
QSchlegel wants to merge 63 commits into
mainfrom
preprod

Conversation

@QSchlegel

Copy link
Copy Markdown
Collaborator

Promotes everything currently on preprod to main (41 commits). Summary by theme:

Governance & voting

Transactions & assets UX

Mobile / UX foundations (#287#292)

Infra & deps

⚠️ Deploy checklist for main

🤖 Generated with Claude Code

QSchlegel and others added 30 commits June 13, 2026 10:44
…NNSESSION)

Production was 500ing every query with "(EMAXCONNSESSION) max clients
reached in session mode - max clients are limited to pool_size: 15".

Verified root cause (live + multi-agent audit):
- src/server/db.ts created the @prisma/adapter-pg pool with no `max`, so
  node-postgres defaulted to 10 connections per warm Vercel instance. A
  couple of instances overrun Supabase's session-mode pool (15 client
  slots) -> EMAXCONNSESSION on every query, including user.createUser.
- The retry wrapper amplified it: it called $connect() against the dead
  pool between retries and, once connectionTimeoutMillis is finite, would
  treat the "timeout exceeded when trying to connect" acquire error as a
  retryable connection error.

Changes:
- Cap the pool: max: 2, idleTimeoutMillis 10s, connectionTimeoutMillis 10s
  (finite timeout fails fast instead of pg's default infinite wait).
- isConnectionError(): never retry pool-saturation errors (max clients
  reached / pool_size / EMAXCONNSESSION / connect-timeout).
- Drop the $connect() reconnect between retries (the driver-adapter pool
  reconnects lazily; forcing connect just adds load).

The Prisma globalThis singleton was verified correct and left unchanged.
The 5 interactive $transaction blocks are pure-DB (no external I/O held),
so no leak fix is required for this to hold.

NOTE (maintainer action, env — cannot be done in code): point production
DATABASE_URL at the Supabase TRANSACTION pooler (port 6543, ?pgbouncer=true)
and keep DIRECT_URL on the direct connection (5432) for migrations. The
session pooler (5432) is the wrong mode for serverless; this code change is
the necessary client-side cap and works as an interim mitigation too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Break grouped quarters into individual Month 2–12 sections, each with
  Quirin/Andre task tables matching Month 1's format
- Add Document Sign-Off flagship feature (MVP→v1→v2→v3) woven across months
- Add Mesh 2.0 upgrade; extend FROST research to include Lemour PQC multi-sig
- Shift schedule one month earlier (June work completed); April is buffer
- Drop completed items (Aiken crowdfund, full address, pagination, collateral, 404)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(roadmap): month-by-month breakdown with per-owner tasks
…e signature mismatch

DRep votes (and any tx with Conway voting_procedures) failed client-side with
"Wallet returned witness that does not verify against tx body hash", and would
be rejected on-chain (InvalidWitnessesUTXOW).

Root cause: the tx is BUILT with core-cst (MeshTxBuilder's default CardanoSDK
serializer), so the wallet signs the body hash core-cst produces. But the
witness verify + merge used core-csl (whisky) calculateTxHash / Transaction
reconstruction, which re-serializes the body to different bytes (voting_procedures
map order, set tag 258) → a different hash → valid witnesses fail to verify. The
two serializers agree for ordinary txs, so only Conway-vote-shaped txs broke.

Move the verify/merge/hash onto core-cst (the 2.0 stack, already a dependency),
so build and verify use one encoder and the original body bytes every signer
signed are preserved:
- mergeSignerWitnesses: verify new witnesses against resolveTxHash(originalTx)
  and merge via addVKeyWitnessSetToTransaction (preserves body bytes). Drops the
  body-swap workaround — every co-signer now signs the same stored body.
- filterWitnessesToScripts: rebuild the witness set with core-cst so dropping
  extraneous vkeys no longer re-encodes the body.
- diagnoseTxWitnesses + the server signature check in transactions.ts: hash with
  resolveTxHash so vote signatures are recognised (equal to the old hash for
  ordinary txs).

Verified invariants (tests): adding/filtering vkeys via core-cst preserves
resolveTxHash; a witness over resolveTxHash verifies; co-signers accumulate
without re-encoding. tsc clean; full suite 362 passed.

Follow-up (not in this PR): the server v1 bot path (signTransaction.ts +
addUniqueVkeyWitnessToTx) still uses core-csl calculateTxHash and needs the same
core-cst migration for bot-submitted votes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(governance): verify/merge witnesses with core-cst (DRep-vote signature mismatch)
…sets

Foundational mobile fixes (PR 1 of the UX/mobile quick-wins pass):
- viewport meta gains viewport-fit=cover so env(safe-area-inset-*) resolves
  (without it the insets are always 0).
- Full-height containers use 100dvh instead of 100vh/h-screen so the layout
  isn't clipped by mobile-Safari / wallet-webview dynamic toolbars
  (_app, layout root + inner content column).
- Main header grows by the safe-area top inset on mobile so it clears the
  notch/status bar, and honors side insets in landscape.
- Mobile nav drawer offsets by the safe-area top, uses 100dvh, and pads the
  bottom for the home indicator.
- Bottom Sheet variant pads the bottom safe area.

Desktop is unchanged (insets are 0; header rule is scoped below md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR 2 of the UX/mobile quick-wins pass. Buttons/inputs were 32-36px, below
the recommended 44px touch target. A single @media (pointer: coarse) rule
enlarges interactive controls on touch devices only — no per-call-site
edits and zero change to desktop density. Plain text links are
intentionally excluded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…loading/empty conflation

PR 3 of the UX/mobile quick-wins pass.
- Add shared `Skeleton` (shadcn) and `EmptyState` (Card-based) primitives.
- Wallet detail routes (info/transactions/governance/assets) rendered a blank
  fragment while `appWallet` loaded — replace with a `WalletDetailSkeleton` so
  there's no white flash on every wallet open.
- all-transactions: it showed "No transactions yet" *while still loading*
  (undefined === loading). Split into a skeleton (loading) vs an `EmptyState`
  (loaded and empty).
- proposals: plain-text "No proposals found" → `EmptyState`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR 6 of the UX/mobile quick-wins pass (input correctness).
- Recipient address fields used invalid type="string" with mobile autocorrect/
  autocapitalize active — which can silently corrupt case-sensitive bech32
  addresses. Now type="text" with inputMode="text" autoCapitalize="off"
  autoCorrect="off" spellCheck={false}.
- Amount fields get inputMode="decimal" (numeric keypad on mobile).
- Base Input uses text-base on mobile (sm:text-sm on desktop) so iOS Safari
  no longer zooms in on focus (<16px triggers it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(mobile): viewport-fit + dvh + safe-area insets (foundations)
feat(mobile): >=44px touch targets on coarse-pointer devices
feat(ux): Skeleton + EmptyState primitives; fix blank pages & loading/empty bug
fix(mobile): bech32-safe address inputs + decimal keypad + no iOS zoom
… + scroll)

PR 4 of the UX/mobile quick-wins pass. Centered Dialogs overflowed small
screens — content (and action buttons) got clipped off-screen, especially the
wide governance modals.

Make the base DialogContent mobile-safe without changing desktop layout:
w-[calc(100%-1.5rem)] (stay within the viewport with a small margin),
max-h-[90dvh] + overflow-y-auto (scroll internally instead of off-screen). The
max-w-lg cap and centered position are unchanged at desktop. BallotModal and
RegisterDrepModal switch their max-h from vh to dvh so the mobile toolbar
doesn't hide the bottom.

(Kept dialogs centered rather than converting to a bottom sheet to avoid
restructuring positioning on the shared base that backs every dialog; the
overflow fix is the critical part. Needs a quick on-device / Chrome-MCP visual
check before promoting past preprod.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…egister

PR 5 of the UX/mobile quick-wins pass.
- src/utils/errors.ts `getFriendlyError(error)` maps the common raw errors
  (CIP-30 {code:-2}, account-changed, 429/too-many-requests, insufficient
  funds, Blockfrost/UTXOS, user-decline) to short human messages, falling back
  to the raw message.
- src/utils/toast-error.ts `toastError(error, title?)` — destructive toast with
  the normalized message (additive; doesn't touch the TOAST_LIMIT=1 reducer).
- Adopt in DRep registration's catch (raw e.message -> getFriendlyError), while
  keeping the "Copy Error" action for raw debug details.

Other error-prone flows (new-transaction, WalletAuthModal) can adopt the
helper incrementally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(mobile): keep dialogs within the viewport (responsive width + dvh + scroll)
feat(ux): friendly-error helper + toastError wrapper
The transactions pagination bar overflowed its card on the right and used
hardcoded dark colors + a raw <select>. Rewrite the body (props unchanged):
- container: themed `border bg-card`, `flex-wrap min-w-0 gap-3` so the groups
  reflow instead of spilling off the right edge.
- sort toggle: outline Button, icon-only on mobile (label hidden < sm).
- page size: shadcn Select (was a native unstyled <select>); same options +
  reset-to-page-1 behavior.
- nav: compact icon-only prev/next (h-9 w-9), muted tabular-nums page indicator;
  rely on Button's native disabled styling.

PaginationProps is unchanged, so both call sites (transactions + DRep list)
behave identically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The on-chain history rows (desktop table + mobile cards) led with the raw
truncated tx hash — the least human-meaningful field — while the actual
description ("Ballot Vote: …") was buried below. Flip the hierarchy to match
the pending-tx cards:
- primary: the dbTransaction description, or a "Sent"/"Received" fallback (and
  the cert label on desktop) so rows without a DB record aren't identified by
  a bare hash.
- date below.
- hash demoted to a quiet muted mono link with the external-link arrow.

Also standardize the hash truncation on getFirstAndLast(hash, 8, 8) (was two
different inline substring schemes) and drop the now-redundant standalone
description block on mobile. Cardanoscan links, outputs, certs, signers, and
the row actions menu are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Long token names ($drep.collective) and raw quantities pushed the value/ticker
past the card's right edge (the clipped "↗1 $S…"). Root cause: the flex row +
left group had no min-w-0, so text couldn't shrink, and the name/quantity/ticker
were never truncated.

- min-w-0 on the row + flex-1 min-w-0 on the left group (lets text shrink).
- 60px avatar wrappers flex-shrink-0 (so the image never compresses instead).
- name h3 truncates (+ title tooltip) and is bounded via truncateTokenSymbol
  for the raw-unit hex fallback; remove ml-auto from the link.
- value block flex-shrink-0; quantity via numberWithCommas (tabular-nums);
  ticker truncates with a max width + title.

Full name stays reachable via tooltip + the Cardanoscan token link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ry per action

The per-proposal voting controls were confusing: a raw OS <select> for the
Yes/No/Abstain choice, plus TWO competing actions (a green "Add to Ballot"
button in proposals.tsx AND a mystery ballot icon in VoteButton).

- Replace the native <select> with a shadcn segmented Yes/No/Abstain control
  (color-coded selected state, icons). Guard `(v) => v && setVoteKind(v)` so a
  re-click can't blank the vote read into the tx.
- One clear primary: the Vote button now states the choice ("Vote Yes",
  "Vote Yes (Proxy)") and uses the themed primary style.
- One ballot entry: the icon-only ballot button becomes a labeled "Add to
  ballot" / "In N ballots" secondary, with a tooltip distinguishing the two
  flows (vote on-chain now vs collect for co-signers).
- Remove the now-duplicate green "Add to Ballot" buttons (mobile + desktop) in
  proposals.tsx; keep the View links.

vote()/voteProxy() bodies, proxy path, keepRelevant, metadata label 674, the
closed-vote Lock state, and all toasts are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The proposal meta row showed the governance action type as plain uppercase
text ("TREASURY WITHDRAWALS"), visually indistinguishable from the other meta
fields and hard to scan in a long list.

Add a GovernanceTypeChip that renders each Conway action type as a color-coded
outline Badge with an icon:
- treasury_withdrawals  amber   Coins
- info_action           blue    FileText
- parameter_change      purple  Settings2
- hard_fork_initiation  orange  GitBranch
- no_confidence         red     XCircle
- new_constitution      teal    FileText
- new/update_committee  indigo  Users
- unknown               slate   Hash (Title-Cased fallback label)

Used in both the mobile and desktop meta rows (single shared component, no
view drift). Purely presentational — no data or vote-flow changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(ux): themed, non-overflowing, mobile-friendly pagination
fix(ux): lead transaction rows with the label, demote the hash
fix(assets): stop token rows overflowing the Assets card
# Conflicts:
#	src/components/pages/wallet/governance/proposals.tsx
# Conflicts:
#	src/components/pages/wallet/governance/proposals.tsx
feat(governance): color-coded proposal type chips
# Conflicts:
#	src/components/pages/wallet/governance/proposals.tsx
feat(governance): clearer ballot voting UX (segmented vote + one entry per action)
QSchlegel and others added 3 commits June 15, 2026 11:49
The glass design system (.glass-nav/.glass-card/.glass-subtle) was already
applied to the header and sidebar, but the shared Card surface stayed opaque
(bg-white / dark:bg-zinc-950). Make Card a glass surface — translucent +
backdrop blur — so the (now default-on) animated background shows through and
the whole platform reads as one glass theme.

Opacity is kept high (80% light / 60% dark) to preserve text contrast on
content-dense cards; call sites can still override the bg via className. This
propagates to every CardUI/Card across the app via the one shared component.

Nested content surfaces (stat tiles, list rows) intentionally stay solid —
glass-over-glass blur is muddy and GPU-heavy; solid blocks on a glass container
is the correct pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… state

The landing recomputed aurora/marble opacity from a scrollY React state set on
every scroll event, re-rendering the whole (large) homepage tree each frame —
the main source of choppy scrolling. On top of that, a 700ms opacity transition
fought the frequent updates, smearing the fade.

Now the two fixed background layers are updated by writing opacity straight to
their DOM nodes (refs) inside a requestAnimationFrame callback. Scrolling no
longer triggers any React re-render; the transition is removed (per-frame rAF
updates are already smooth) and the layers get will-change: opacity so the
compositor handles the fade. Behavior (fade 500→1500px) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(ui): unify Overview copy affordance + signer button hierarchy
@railway-app railway-app Bot temporarily deployed to victorious-warmth / preprod June 15, 2026 09:57 Inactive
style(theme): extend glass-morphism to all cards
@railway-app railway-app Bot temporarily deployed to victorious-warmth / preprod June 15, 2026 09:57 Inactive
perf(landing): smooth scroll by driving hero fade via rAF + refs
QSchlegel and others added 2 commits June 15, 2026 12:24
The rAF/ref scroll fade wasn't the real bottleneck — the landing ran two
GPU-saturating things every frame, even idle:

- MarbleField is a full-viewport WebGL shader (5-octave fbm, double domain
  warp) on an unthrottled rAF loop at up to 1.5x DPR.
- A backdrop-blur pane sat over that live canvas (re-blur every frame), and the
  aurora animated background-position on 300%-size, 40-64px-blurred layers
  (continuous repaints) plus mix-blend-soft-light.

Changes:
- Marble: render the backing store at ~0.6x scale, DPR capped at 1 (~5x fewer
  fragments); cap the loop to ~30fps; skip all shader work while the tab is
  hidden or the user is scrolling. Look is unchanged (it sits under a soft wash).
- Drop the backdrop-blur frost pane → a plain translucent wash (the low-res
  marble already reads soft).
- Aurora: make the two base gradient layers static (kills the background-position
  repaints) and drop mix-blend-soft-light. Life now comes only from the
  transform/opacity orbs, sheen and bloom, which composite cheaply.
- Remove will-change:opacity from the two full-viewport hero layers (it forced
  giant permanent compositor layers and hurt more than helped).
- Pause the aurora's compositor animations (and the marble) while actively
  scrolling via an html[data-scrolling] flag, resuming ~140ms after scroll stops.

Net: near-zero idle cost and the GPU is free during scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
perf(landing): cut continuous GPU cost for butter-smooth scrolling
extractCidPath ran `s.match(/\/ipfs\/(.+)$/i)` on caller-supplied strings
(anchor URLs, CIDs). As a search-anywhere pattern it retries `/ipfs/` at many
start positions with a backtracking `.+$`, which CodeQL flags as polynomial
ReDoS on hostile inputs like "/ipfs/a/ipfs/a/ipfs/a…".

Replace it with a linear `indexOf("/ipfs/")` + `slice`, preserving the exact
behavior (everything after the first "/ipfs/" segment that has content,
case-insensitive). Add an extractCidPath test covering the equivalence cases
plus a hostile-input case that must terminate quickly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QSchlegel and others added 2 commits June 15, 2026 12:53
…e-on-scroll)

The earlier pass paused the marble shader and froze the aurora animations while
scrolling (html[data-scrolling]) as a belt-and-suspenders measure. It works but
the animation visibly stops on scroll, which feels worse than a continuous one.

The real performance came from the other changes — low-res + 30fps marble, no
backdrop-blur over the canvas, static aurora base layers, no mix-blend, no
will-change on the hero layers. Those make the whole background cheap enough to
keep animating during scroll: the aurora orbs/sheen/bloom are transform/opacity
(compositor-only), and the marble at 0.6x scale + 30fps has plenty of GPU
headroom. So just remove the pause:

- marble loop: drop the data-scrolling skip (keep the 30fps cap + tab-hidden pause)
- homepage onScroll: drop the data-scrolling flag toggling (keep the rAF fade)
- globals.css: drop the html[data-scrolling] animation-play-state:paused rule

Net: the animation runs smoothly and continuously, including while scrolling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
perf(landing): keep the background animating during scroll (drop pause-on-scroll)
QSchlegel and others added 3 commits June 15, 2026 13:13
The cursor reactivity was a touch strong. Dial both effects down:
- aurora orb parallax offset 26/22px → 10/8px
- marble cursor swell amplitude 0.18 → 0.07 with a tighter falloff (exp -3.5 →
  -4.5), so it affects a smaller area more subtly

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s out

Builds on dropping the pause-on-scroll: instead of stopping the animation while
scrolling (jarring), keep it running the whole time it's visible or fading, and
only stop it once it has fully faded out — a beat later (~800ms), when it's
invisible so the stop is imperceptible.

PageHomepage sets data-bg-hidden on <html> ~800ms after the scroll fade reaches
0 opacity, and clears it instantly the moment the background starts fading back
in. While the flag is set the marble shader skips its draws and the aurora's
animations are paused (globals.css). Net: smooth continuous motion whenever you
can see it, zero GPU spent on it once it's gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(landing): reduce the mouse-over effect on the background
@railway-app railway-app Bot temporarily deployed to victorious-warmth / preprod June 15, 2026 11:18 Inactive
perf(landing): stop the background animation only after it fully fades out
fix(security): remove ReDoS-prone regex in extractCidPath (CodeQL high)
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.

2 participants