Release: mobile/UX foundations, governance vote UX + DB-cached tallies, signing & infra fixes#303
Conversation
…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)
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
style(theme): extend glass-morphism to all cards
perf(landing): smooth scroll by driving hero fade via rAF + refs
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>
…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)
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
perf(landing): stop the background animation only after it fully fades out
fix(security): remove ReDoS-prone regex in extractCidPath (CodeQL high)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes everything currently on
preprodtomain(41 commits). Summary by theme:Governance & voting
ProposalTallytable via a read-through tRPC router, refreshed on user activity with a 10-min TTL); "Voted X" pill; VoteButton no longer pre-selects Abstain.Transactions & assets UX
Mobile / UX foundations (#287–#292)
toastError.Infra & deps
EMAXCONNSESSIONpool exhaustion (fix(db): cap pg pool + stop retrying pool saturation (Supabase EMAXCONNSESSION) #284).prisma migrate deploy— feat(governance): proposal cards + DB-cached live vote tallies + current-vote UX #302 adds theProposalTallytable (migration20260614120000_add_proposal_tally). The governance tally router errors until it exists. Confirm the production deploy runs migrations.multisig-v1-smokehas been flaky on a depleted on-chain test wallet (UTxO Balance Insufficient), unrelated to these diffs.🤖 Generated with Claude Code