diff --git a/src/data/vortexopedia.ts b/src/data/vortexopedia.ts index a4b35d5..c0f236c 100644 --- a/src/data/vortexopedia.ts +++ b/src/data/vortexopedia.ts @@ -1379,11 +1379,11 @@ export const vortexopediaTerms: VortexopediaTerm[] = [ name: "Governing threshold", category: "governance", short: - "Action quota and uptime requirement per era to remain an active governor counted in quorums.", + "Previous-era action quota used to decide who is counted as an active governor in quorums.", long: [ - "A governor is active if bioauthenticated, node ran 164/168 epochs, and required actions were met in the previous era.", + "A governor is active for quorum purposes when the required governing actions were met in the previous era.", "Required actions per era include upvoting/downvoting proposals or voting on chamber proposals in Vortex.", - "Meeting the threshold keeps the governor eligible to be counted in quorums for the upcoming era.", + "Current node liveness does not remove an Active Governor status already earned from the previous era.", ], tags: ["threshold", "quorum", "activity", "governor"], related: [ @@ -1393,7 +1393,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [ "quorum_of_attention", ], examples: [ - "If the action threshold is met and uptime is 164/168 epochs, the governor is counted as active in the next era’s quorum.", + "If the previous-era action threshold is met, the governor is counted as active in the next era’s quorum.", ], stages: ["global"], links: [ @@ -1414,7 +1414,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [ "You are comfortably above the governing threshold pace for the current era. Status scale: Ahead → Stable → Falling behind → At risk → Losing status.", long: [ "Ahead means you have already met (or are well on track to exceed) the era’s action threshold early, leaving a buffer for the rest of the era.", - "Staying Ahead typically requires continuing normal participation (pool votes and chamber votes) while maintaining node uptime.", + "Staying Ahead typically requires continuing normal participation through pool votes and chamber votes.", "This status is based on your completed actions vs required actions for the current governing era, not on proposal outcomes.", ], tags: ["status", "governance", "threshold", "governor", "activity"], @@ -1449,7 +1449,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [ "You are on pace to meet the governing threshold for the era. Status scale: Ahead → Stable → Falling behind → At risk → Losing status.", long: [ "Stable means your completed actions are at or near the required threshold pace, and you are not currently trending toward inactivity for the next era.", - "If you stay Stable through the era (and maintain uptime), you remain counted as an active governor for quorum calculations in the next era.", + "If you stay Stable through the era, you remain counted as an active governor for quorum calculations in the next era.", "This status summarizes action progress for the current era; it can change as time passes and requirements are assessed.", ], tags: ["status", "governance", "threshold", "governor", "activity"], @@ -1519,7 +1519,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [ "You are unlikely to meet the governing threshold without immediate additional actions. Status scale: Ahead → Stable → Falling behind → At risk → Losing status.", long: [ "At risk means your current action count is far enough below the era requirement that you may lose active governor status for the next era if you do not act.", - "To improve: complete additional required actions (pool votes and chamber votes) before the era ends and maintain node uptime.", + "To improve: complete additional required actions, such as pool votes and chamber votes, before the era ends.", "This status summarizes your action deficit; it does not imply slashing or permanent removal—only loss of active quorum eligibility in the next era.", ], tags: ["status", "governance", "threshold", "governor", "activity"], @@ -1555,7 +1555,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [ long: [ "Losing status indicates a severe shortfall against the era action threshold and/or insufficient remaining time to realistically catch up.", "If this remains at era close, you may not be counted as an active governor for quorum calculations in the next era.", - "To recover, complete the highest-impact required actions immediately and maintain node uptime; otherwise you transition out of active quorum eligibility.", + "To recover, complete the highest-impact required actions immediately; otherwise you transition out of active quorum eligibility.", ], tags: ["status", "governance", "threshold", "governor", "activity"], related: [ diff --git a/src/lib/proposalUi.ts b/src/lib/proposalUi.ts index 62fca16..c3f1e82 100644 --- a/src/lib/proposalUi.ts +++ b/src/lib/proposalUi.ts @@ -30,6 +30,11 @@ type ProposalPoolVotingGateInput = { }; type ProposalOrdinaryVoteGateInput = { + auth?: { + authenticated: boolean; + enabled: boolean; + loading: boolean; + }; closedReason?: string; submitting: boolean; viewerIsProposer: boolean; @@ -54,18 +59,19 @@ export function getProposalPoolVotingGate({ }: ProposalPoolVotingGateInput): { allowed: boolean; disabledReason: string } { const allowed = !viewerIsProposer && - (!auth.enabled || (auth.authenticated && auth.eligible && !auth.loading)); + (!auth.enabled || (auth.authenticated && !auth.loading)); const disabledReason = viewerIsProposer ? "You cannot vote on your own proposal." : auth.enabled && auth.loading ? "Checking wallet status…" : auth.enabled && !auth.authenticated ? "Connect your wallet to vote." - : (auth.gateReason ?? "Only active human nodes can vote."); + : "Only chamber Governors can vote. Active Governors are counted for quorum."; return { allowed, disabledReason }; } export function getProposalOrdinaryVoteGate({ + auth, closedReason = "Ordinary voting is closed.", submitting, viewerIsProposer, @@ -74,13 +80,20 @@ export function getProposalOrdinaryVoteGate({ disabled: boolean; title: string | undefined; } { + const authBlocked = Boolean( + auth?.enabled && (auth.loading || !auth.authenticated), + ); return { - disabled: submitting || votingClosed || viewerIsProposer, + disabled: submitting || votingClosed || viewerIsProposer || authBlocked, title: viewerIsProposer ? "You cannot vote on your own proposal." : votingClosed ? closedReason - : undefined, + : auth?.enabled && auth.loading + ? "Checking wallet status…" + : auth?.enabled && !auth.authenticated + ? "Connect your wallet to vote." + : undefined, }; } diff --git a/src/pages/proposals/ProposalChamber.tsx b/src/pages/proposals/ProposalChamber.tsx index d3a00e5..fde85a7 100644 --- a/src/pages/proposals/ProposalChamber.tsx +++ b/src/pages/proposals/ProposalChamber.tsx @@ -75,6 +75,7 @@ const ProposalChamber: React.FC = () => { viewerAddress: auth.address, }); const ordinaryVoteGate = getProposalOrdinaryVoteGate({ + auth, closedReason: "Ordinary chamber voting is closed. Only veto actions remain in this window.", submitting, diff --git a/src/pages/proposals/ProposalPP.tsx b/src/pages/proposals/ProposalPP.tsx index 485c647..3ab9f44 100644 --- a/src/pages/proposals/ProposalPP.tsx +++ b/src/pages/proposals/ProposalPP.tsx @@ -128,6 +128,7 @@ const ProposalPP: React.FC = () => { tone="accent" icon="▲" label="Upvote" + requiresEligibility={false} disabled={!votingGate.allowed} title={votingGate.allowed ? undefined : votingGate.disabledReason} onClick={() => { @@ -142,6 +143,7 @@ const ProposalPP: React.FC = () => { tone="destructive" icon="▼" label="Downvote" + requiresEligibility={false} disabled={!votingGate.allowed} title={votingGate.allowed ? undefined : votingGate.disabledReason} onClick={() => { diff --git a/src/pages/proposals/shared/ProposalOrdinaryVoteActions.tsx b/src/pages/proposals/shared/ProposalOrdinaryVoteActions.tsx index 563a226..4bdd705 100644 --- a/src/pages/proposals/shared/ProposalOrdinaryVoteActions.tsx +++ b/src/pages/proposals/shared/ProposalOrdinaryVoteActions.tsx @@ -28,6 +28,7 @@ export function ProposalOrdinaryVoteActions({ onVote("yes", score?.value)} @@ -56,6 +57,7 @@ export function ProposalOrdinaryVoteActions({ onVote("no")} @@ -63,6 +65,7 @@ export function ProposalOrdinaryVoteActions({ onVote("abstain")} diff --git a/tests/unit/proposal-ui.test.ts b/tests/unit/proposal-ui.test.ts index bfd0565..169e338 100644 --- a/tests/unit/proposal-ui.test.ts +++ b/tests/unit/proposal-ui.test.ts @@ -187,7 +187,7 @@ test("getProposalChamberPageDerivation handles referendum and milestone titles", }); }); -test("getProposalPoolVotingGate blocks proposers and wallet gate states", () => { +test("getProposalPoolVotingGate blocks proposers and wallet connection states", () => { expect( getProposalPoolVotingGate({ viewerIsProposer: true, @@ -245,12 +245,13 @@ test("getProposalPoolVotingGate blocks proposers and wallet gate states", () => }, }), ).toEqual({ - allowed: false, - disabledReason: "Custom gate reason.", + allowed: true, + disabledReason: + "Only chamber Governors can vote. Active Governors are counted for quorum.", }); }); -test("getProposalPoolVotingGate allows eligible or auth-disabled voters", () => { +test("getProposalPoolVotingGate allows authenticated or auth-disabled voters", () => { expect( getProposalPoolVotingGate({ viewerIsProposer: false, @@ -263,7 +264,8 @@ test("getProposalPoolVotingGate allows eligible or auth-disabled voters", () => }), ).toEqual({ allowed: true, - disabledReason: "Only active human nodes can vote.", + disabledReason: + "Only chamber Governors can vote. Active Governors are counted for quorum.", }); expect( @@ -278,7 +280,8 @@ test("getProposalPoolVotingGate allows eligible or auth-disabled voters", () => }), ).toEqual({ allowed: true, - disabledReason: "Only active human nodes can vote.", + disabledReason: + "Only chamber Governors can vote. Active Governors are counted for quorum.", }); }); @@ -311,11 +314,31 @@ test("getProposalOrdinaryVoteGate blocks submit, proposer, and closed states", ( disabled: true, title: "Ordinary chamber voting is closed.", }); + + expect( + getProposalOrdinaryVoteGate({ + auth: { + enabled: true, + loading: false, + authenticated: false, + }, + submitting: false, + viewerIsProposer: false, + }), + ).toEqual({ + disabled: true, + title: "Connect your wallet to vote.", + }); }); test("getProposalOrdinaryVoteGate allows open non-proposer votes", () => { expect( getProposalOrdinaryVoteGate({ + auth: { + enabled: true, + loading: false, + authenticated: true, + }, submitting: false, viewerIsProposer: false, }),