Skip to content

Dev - #2093

Open
pikonha wants to merge 74 commits into
mainfrom
dev
Open

Dev#2093
pikonha wants to merge 74 commits into
mainfrom
dev

Conversation

@pikonha

@pikonha pikonha commented Jul 29, 2026

Copy link
Copy Markdown
Member

Note

High Risk
Wallet/session decoupling and immediate stale-session gating change core auth UX and security boundaries; per-user Prometheus labels carry PII and require protected metrics and Grafana access.

Overview
This PR bundles several product and platform changes across the dashboard, offchain indexer, authful, user-api, and monitoring.

Wallet vs session: Connecting a wallet no longer implies signing in. Vote, delegate, and publish flows open RainbowKit directly without SIWE; header Connect opens the wallet picker instead of the login modal. Session-gated surfaces (drafts, API keys) still use the sign-in modal. useAuthSession and isWalletSessionStale hide wallet-born sessions immediately when the wallet disconnects or the account switches, instead of waiting on sign-out or force-disconnecting connected wallets without a session.

Snapshot / offchain governance: The indexer and API persist scoresTotal and quorum, with a historical metadata backfill job. The dashboard derives Passed, No quorum, and non-stale Ongoing states instead of mapping winning Snapshot proposals to on-chain-style Executed / queue labels.

API keys UX: Save-key modal layout, MCP section copy, animated client tabs, truncated names, usage dropdown/BlankSlate, shorter chart, and scrollable chart legend on mobile.

Validation metrics: Authful adds GET /tokens/active (usage from daily counters). User API exposes bearer-protected /metrics with per-user gauges (email/wallet identifiers), a Grafana User API Validation dashboard, and USER_API_METRICS_TOKEN required when Authful provisioning is enabled.

Monitoring: Removes the non-functional railway-exporter (scrape job, alerts, panels). Extends Prometheus retention; PR Grafana can use basic auth instead of Google OAuth.

Reviewed by Cursor Bugbot for commit 6b63c61. Configure here.

pikonha and others added 30 commits July 27, 2026 11:10
Delegating, voting and publishing a proposal are settled on-chain by the
wallet's own transaction signature, so they only ever needed a connected
account. They were routed through the sign-in modal instead, forcing a
SIWE message on anyone who just wanted to act on-chain.

Connecting and signing in are now separate:

- useWalletPrompt always opens the wallet picker, never the sign-in modal
- LoginProvider no longer force-disconnects a wallet that has no session,
  and no longer re-opens the sign-in modal when the account changes
- the header connect buttons (Anticapture and whitelabel) open the wallet
  picker directly
- the sign-in modal, still raised by the surfaces that need a server
  session (proposal drafts, API keys), labels its wallet option
  "Sign in with wallet"

Wallet-born sessions stay bound to their wallet: disconnecting or
switching accounts still signs them out, it just never prompts again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each Snapshot vote type now renders its own designed ballot instead of the
generic option list (DEV-1131, Figma frames 01-04 and 07):

- shared BallotSection shell owns the "Your vote" label row, so ballots can
  place a counter or chip beside it, and adds a filter input plus a
  fixed-height scroll area once a list passes 8 options
- approval shows an "N of M selected" counter
- weighted/quadratic share an allocation ballot: per-option stepper, chart-token
  dots, stacked allocation bar, amber "N% Remaining" chip, running total and
  "Must equal 100% to vote"; the stepper cannot exceed 100%
- ranked adds a drag grip and drag-to-reorder, keeping the chevrons as the
  keyboard path, plus the reorder helper line
- comment field now uses the design-system Textarea and FormLabel

Allocation colors come from the existing --base-chart-* tokens, no raw hex.
Storybook stories cover each ballot against its Figma frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mistic states

Adds the standalone CURRENT RESULTS card that off-chain proposals never had
(DEV-1131, Figma frames 06, 10 and 11). OffchainVotesContent stays a per-voter
table; this card is the tally.

- ranked rows with rank number, voting power and share; leading option gets the
  brand-orange bar and orange percentage, no LEADING chip and no quorum row,
  matching the final frame
- shutter aware: encrypted while voting is open, reveal-pending once it closes
  with the tally still empty, then the normal card once the reveal lands. Both
  concealed states render dashes over empty tracks and never zeros, drop the
  rank numbers, and keep the authored choice order so nothing leaks the winner
- optimistic votes: optimisticScores apply on top of the indexed tally so a vote
  shows the moment the signature returns, and useOffchainVoteIndexing drives the
  header chip through "Indexing your vote..." to "Indexed" and out after 1.2s,
  falling back to the indexer-catching-up chip past two minutes

Storybook stories cover every state against its Figma frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Off-chain proposals derived their status through the on-chain enum, which is how
a Snapshot vote could render as "Executed" (DEV-1131, Figma frame 12).

getOffchainProposalStatus mirrors snapshot.box's getProposalState and implements
the frame's derivation table row for row:

- not started -> pending, voting open -> active
- basic (For/Against/Abstain) resolves to passed/rejected and is quorum-aware:
  below quorum rejects even when For leads, and a rejection-type quorum inverts
  the test so reaching it rejects the proposal
- ties reject, since passing needs strictly more For than Against
- every other vote type closes and surfaces "winner: X . NN%"
- no input can produce an executed state

OffchainProposalBadge renders the five badges, with Active as a white pill on
black text so it never reads as the green passed badge.

The quorum branches are inert until the API exposes quorum and quorumType on
OffchainProposal, which it does not today; the logic and its tests are in place
for when it does. Label vocabulary is still OQ 2 and lives in one constant map.

16 unit tests, one per derivation row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Casting an off-chain vote showed no indication of what the vote would do, while
on-chain voting shows percentages and impact (DEV-1131, Figma frame 09).

SingleChoiceOptions now takes an optional liveImpact, and when given renders a
per-row bar, voting power and share, with a green up-arrow delta on the selected
row. The projection grows the denominator too, so picking an option raises its
share and dips every other row's, and the shares always sum to 100%.

Opt-in by design: only single choice has a designed frame for this, so ballots
01-04 keep shipping without per-row results, and the approval preview waits on
its design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing told a connected wallet it had already voted on an off-chain proposal,
and the ballot reset to null on open with no prop carrying the prior choice
(DEV-1131, Figma frames 05 and 13).

- useMyOffchainVote reads the wallet's own vote through the votes endpoint's
  voterAddresses filter, so the prior choice is finally available
- OffchainVotedModal is the read-only "Your Vote" state: green "You voted"
  banner with date and voting power, per-option bars with the chosen shares,
  unchosen options dimmed at 0%, the static comment, and Close / Change vote
- OffchainVotedChip is the green "You voted X" chip with a hover tooltip that
  opens the read-only modal; long labels collapse through the existing
  presentOffchainVoteLabel, and rows without a vote get no placeholder

deriveOffchainVoteWeights only returns shares for basic and single choice.
Approval and ranked are not share-based, and weighted/quadratic weights are not
recoverable because the API models `choice` as bare choice indices and drops the
per-option weights Snapshot stores. Rather than invent numbers, the modal takes
weights explicitly and falls back to listing the chosen options. Surfacing those
weights needs an API change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The status bug in the ticket had a single source: getOffchainProposalStatus
returned ProposalStatus.EXECUTED whenever a basic Snapshot ballot had more For
than Against, so a proposal that was never executed anywhere read "Executed"
(DEV-1131, Figma frame 12).

That util now delegates to the quorum-aware derivation and maps onto the view
enum, which gains PASSED and REJECTED members with their own labels and colors.
Both call sites are updated, so the fix lands on the proposals list and the
proposal detail page, not just on new components.

Status is derived once in ProposalSection and shared, so the badge, its winner
copy, and the results card cannot disagree about the outcome.

Also drops useMyOffchainVote: ProposalSection already queries the wallet's own
vote through the same voterAddresses filter, and shipping a second hook for it
would have meant two queries for one answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the ad-hoc off-chain "Current Results" block in ProposalInfoSection
with OffchainResultsCard (DEV-1131, Figma frames 06, 10 and 11).

This is what makes the shutter fix reach users: ProposalSection now resolves
proposal privacy and passes isShutter through, so an encrypted or reveal-pending
election renders dashes over empty tracks instead of the 0/0.0% it used to show.

The old block's quorum row is not a loss: the off-chain adapter hardcodes quorum
to "0", so that row could never render, and the final card drops it by design
(OQ 6). VotingProgressBar stays. Drops the now-dead ChoiceIcon and its color
helper import along with the replaced markup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A wallet that already voted reopened the interactive ballot with its prior
choice thrown away, because the modal reset to null on open (DEV-1131, frame 05).

It now lands on the read-only "Your Vote" state and opts into editing through
"Change vote", which swaps in the ballot. Closing either modal resets that
choice, so the next open starts from the read-only state again.

Shares come from deriveOffchainVoteWeights, which only answers for basic and
single choice. Approval and ranked list their chosen options instead, and
weighted stays unresolved until the API exposes the per-option weights.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The list rendered off-chain status as plain colored text through the on-chain
helpers, so it could not show the white Active pill or the winner on a closed
vote (DEV-1131, Figma frame 12).

getOffchainProposalStatusView now also returns the off-chain status itself, and
the list renders OffchainProposalBadge with it plus the winner. The on-chain path
keeps using getStatusText and the color helpers, untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the has-voted indicator (DEV-1131, Figma frame 13). The header and the
mobile bottom bar showed a "You voted" label stacked over a plain choice chip;
both now render OffchainVotedChip, one green pill carrying the choice, with a
tooltip quoting voting power and date and a click into the read-only modal.

The chip needs more than the label, so ProposalHeader takes a single grouped
OffchainVoteIndicator prop rather than threading three separate ones through two
component levels. It is only built once the indexed vote exists, since the
tooltip quotes its values, and both call sites fall back to the previous label
rendering until then.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closed Shutter elections showed 0 / 0.0% forever, e.g. "6.47 ENS Security
Council" (DEV-1131).

Root cause: both sync passes are forward-only on `created`. A Shutter proposal
is ingested while its votes are still encrypted and its tally is zero, and the
reveal only happens after voting closes, by which point the cursors have moved
past it. Nothing ever re-read it.

Adds a reveal reconciliation pass to each cycle: closed proposals inside the
existing 14-day window whose tally is still all zeros are re-fetched by id along
with their votes, then upserted without advancing either cursor. The upserts
already overwrite choice, vp and scores, so the revealed values replace the
encrypted ones with no schema change.

A zero-tally proposal that genuinely had no votes is re-read cheaply and stays
zero, so no extra state is needed to distinguish the two cases.

Provider gains fetchProposalsByIds and fetchVotesByProposalIds, the latter
paginated since one proposal can exceed a page of votes. Proposal and vote GraphQL
field lists are extracted so the by-id queries cannot drift from the sync ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Weighted and quadratic ballots are stored as {choiceIndex: weight}, but the
response mapper ran Object.keys over that, keeping the picked indices and
throwing the weights away. The data was in the database the whole time; nothing
could read a voter's split back to them (DEV-1131).

Off-chain votes now also carry `weights`, null for the vote types that have no
weights. `choice` is unchanged, so existing consumers are unaffected. The
repository selects the same column twice under both aliases and each schema
interprets it its own way, so the two can never disagree.

This unblocks the weighted case of the read-only voted modal, which currently
falls back to listing chosen options because the weights were unavailable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…anges

Switching accounts on a SIWE session fired signOut() and left the old
session usable while the request was in flight, and indefinitely if it
failed: the header showed wallet B while drafts and API keys still acted
for wallet A.

Sign-out is now the cleanup, not the gate. isWalletSessionStale derives
the broken binding synchronously from wallet state, and useAuthSession
serves the session through it, so a mismatched or disconnected wallet
reads as no session from the render it happens. The API-key create and
delete modals live outside the auth branch, so they close on the account
change instead of submitting against the previous account.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four fixes from testing the real app (DEV-1131):

- modal bodies were padded per section, so stacked sections produced 32px gaps
  where the frames specify 16px. One padded body with gap-4 now wraps unpadded
  sections, in both the ballot and the read-only voted modal
- the results card had no height cap, so a 15-candidate election pushed the
  sidebar off screen. It caps at ~7 rows and scrolls, with the padding outside
  the scroll container so the designed 6-row card still fits without a scrollbar
- ballot overflow used the native scrollbar while the results card used the
  design system's; both now use scrollbar-custom
- ranked ballots no longer offer the filter: a ranking is an ordering, and
  hiding rows hides positions the voter still has to place

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Padding sits inside the scroll container so the scrollbar tracks the card border
rather than floating inset, with the cap raised to 300px to absorb it. The
designed 6-row card still does not scroll; a 15-candidate election does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both features were built, verified in Storybook and then never connected. They
were exposed as optional props with silent defaults, so nothing failed: the
preview never rendered and the indexing chip resolved to "idle", which returns
null. Two acceptance criteria looked delivered and were not (DEV-1131, frames
09 and 10).

- OffchainVotingModal now passes liveImpact to the single-choice ballot, built
  from the proposal's tally plus the voter's power. Suppressed on shutter
  proposals, where a preview would leak the concealed tally
- the modal reports the voter's power spread across the choices they picked;
  weighted and quadratic split it by allocation, other types apply it in full to
  each pick, matching how Snapshot tallies them
- ProposalSection records the signature time, runs useOffchainVoteIndexing and
  feeds the tally delta and chip state through to the results card
- the delta is dropped as soon as the API returns the vote for the wallet, so it
  is never counted twice on top of the now-indexed tally

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- serve before initial metrics refresh so slow Authful can't keep /health down
- publish DB counts independently of Authful in metrics refresh
- derive daily-active from usage records, not lastUsedAt (survives Gateful cache hits)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- export user_api_keys_created_total as a real counter incremented on
  creation instead of a mutable row count (cascade delete on account
  removal read as a counter reset / false increase() spike)
- guard MetricsSnapshotService.refresh against overlapping runs so a
  slow Authful can't clobber a newer snapshot or pile up fetches

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It never emitted a single railway_service_* sample in either environment —
its account-level RAILWAY_API_KEY was rejected for the whole retained history
(~15d), so the only visible effect was a permanently firing RailwayExporterStale
critical alert and three blank Grafana panels.

Drops the scrape job, the three Railway alert rules, the three dashboard panels,
and the Dockerfile/railway.toml pair. Also removes the need for a workspace-wide
Railway API token in service env vars, and a build-time git clone of a
third-party repo.

Railway's own dashboard still shows per-service CPU/RAM/egress; only alerting
on them is gone.

Co-Authored-By: Claude <noreply@anthropic.com>
pikonha and others added 4 commits July 30, 2026 15:29
isIndexed was "the API returned a vote for this wallet", which is already
true when a voter changes an existing vote: the chip flipped to Indexed
immediately, the optimistic delta was dropped and the results fell back to
the old tally while the label claimed the new choice. Snapshot votes carry
no id, so compare a created+choice fingerprint against the vote the
submission replaced, and poll the query until that replacement arrives
instead of relying on the single refetch fired at signature time.

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

Snapshot recomputes ranked/Copeland results from elimination or pairwise
rounds and aggregates quadratic votes as renormalized square roots, so
adding the full voting power to every ranked pick or a linear split to
quadratic scores published wrong totals — and sometimes a wrong winner —
until indexing caught up. Those types now wait for the indexer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The voted chip opens the read-only modal whatever the proposal status, and
that modal always offered "Change vote", swapping in a ballot whose confirm
gating never checked the voting window — so users were invited to compose
and sign a vote Snapshot can only reject. The modal now takes whether voting
is still open, and the ballot refuses to submit outside the window.

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

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

Copy link
Copy Markdown

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: ff22247263

ℹ️ 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 infra/monitoring/grafana/dashboards/validation.json Outdated
Comment thread .changeset/pr-2093-review-fixes.md
pikonha and others added 2 commits July 30, 2026 15:41
The snapshot service now asks Authful for activity since UTC midnight, so a
dashboard rendered in America/Sao_Paulo and captioned GMT-3 claimed a
calendar day the data doesn't describe between 00:00 and 02:59 UTC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The indexed scores already contain this wallet's previous ballot, while the
delta only describes the new one, so adding it counted the voter's power
twice — keeping the old choice and inflating the new one, potentially naming
the wrong leader — until indexing completed. Revotes now wait for the
indexer, which the indexing chip already communicates.

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

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

Copy link
Copy Markdown

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: ea015053ce

ℹ️ 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".

scores: offchainScores,
scoresTotal: rawOffchainProposal.scoresTotal,
choices: offchainChoices,
quorum: rawOffchainProposal.quorum,

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 Thread rejection quorum type through status derivation

For Snapshot spaces configured with voting.quorumType = "rejection", this call supplies only the numeric quorum, so getOffchainProposalStatus always takes the participation-quorum branch and can label a proposal rejected merely because total turnout is below the threshold, even though rejection quorum is measured against Against votes. A repo-wide search shows quorumType is not indexed or exposed by the API at all; carry the space setting through the REST/client contract and pass it at both status call sites.

AGENTS.md reference: AGENTS.md:L22-L31

Useful? React with 👍 / 👎.

Comment on lines +41 to +44
const projectedTotal =
liveImpact && value !== null
? currentTotal + liveImpact.votingPower
: currentTotal;

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 Replace the prior vote in the live-impact preview

When an already-indexed single-choice voter opens Change Vote, liveImpact.scores already includes that wallet's previous voting power, but the preview increases both the denominator and the newly selected score without subtracting the old contribution. The fresh remaining path is this newly added live-impact preview: the later optimistic-results handling does not affect it, so a revote from A to B can still display totals such as A=60, B=50 out of 110 instead of A=50, B=50 out of 100. Pass the prior choice into the preview and model a replacement, or disable the projection for revotes.

Useful? React with 👍 / 👎.

The preview added the voter's power to the selected choice and to the
denominator without removing the ballot already counted in the indexed
scores, so a revote from A to B projected A=60, B=50 of 110 instead of
A=50, B=50 of 100. The projection now starts from the tally with this
wallet's previous ballot taken back out.

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

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

Copy link
Copy Markdown

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: bb165ca349

ℹ️ 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 apps/offchain-indexer/src/indexer.ts Outdated
Comment on lines +132 to +133
await this.repository.upsertProposals(proposals);
await this.repository.upsertVotes(votes);

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 Upsert revealed proposals and votes atomically

If upsertProposals succeeds but upsertVotes fails—for example because of a transient database error or an oversized vote batch—the proposal now has a nonzero tally and getRevealPendingProposalIds will exclude it from every subsequent reconciliation. Its encrypted vote choices therefore remain stale permanently even though the log says the pass will retry; write both sets in one transaction, or persist votes before changing the predicate that selects the proposal.

Useful? React with 👍 / 👎.

</p>
{!isComplete && (
<p className="text-secondary font-inter text-[12px] font-normal not-italic leading-[18px]">
Must equal 100% to vote

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 Require full allocation before enabling quadratic votes

For a quadratic ballot, entering any positive allocation such as 5% enables the Vote button because OffchainVotingModal checks only total > 0, while this new shared control still displays “95% Remaining” and explicitly says the total must equal 100%. The user can therefore submit a ballot the UI identifies as incomplete; make the quadratic confirmation gate match the 100% requirement, or give quadratic ballots controls and copy that reflect their actual completion rule.

Useful? React with 👍 / 👎.

pikonha and others added 2 commits July 30, 2026 16:20
Persisting the revealed scores is what removes a proposal from
getRevealPendingProposalIds, so a failing vote upsert after a successful
proposal upsert dropped the proposal out of every later reconciliation and
left its encrypted choices stale permanently. Votes now land first, so any
failure leaves the zero tally in place and the next pass retries.

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

Quadratic reuses the weighted allocation control, which clamps at 100 and
states the total must equal 100%, but confirmation only required a positive
total — so a 5% ballot was submittable while the UI still said 95% remaining.
Snapshot normalizes both types' weights, so requiring 100 loses nothing.

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

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

Copy link
Copy Markdown

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: a777566e46

ℹ️ 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".

);
}, [choices, scores, optimisticScores]);

const total = effectiveScores.reduce((sum, score) => sum + score, 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 Pass turnout into the detail results card

For approval proposals, this separate detail-card path still divides each score by the sum of all choice scores instead of the newly exposed scoresTotal. Because one voter's full power can appear on several approved choices, a proposal where everyone approves two options renders each option as 50% rather than 100%; pass scoresTotal through ProposalInfoSection and use it as the denominator for these ballots.

Useful? React with 👍 / 👎.

const hasClosed = Date.now() >= end * 1000;
if (!hasClosed) return "encrypted";
// Once the reveal lands the tally is non-zero and the card renders normally.
return total > 0 ? "none" : "reveal-pending";

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 Stop treating every zero tally as a pending reveal

When a closed Shutter proposal legitimately received no voting power, its final tally remains zero even after decryption and indexing have completed. This condition therefore keeps the card concealed behind “decrypting votes, results in a few minutes” forever; determine reveal completion from an explicit finality/reveal signal rather than requiring a positive tally.

Useful? React with 👍 / 👎.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants