Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/components/GlassySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ type GlassyTileHeadingProps = {
className?: string;
};

export type GlassyStatusTone = "danger" | "neutral" | "ok" | "primary" | "warn";

type GlassyStatusChipProps = {
children: ReactNode;
className?: string;
tone?: "danger" | "neutral" | "ok" | "primary" | "warn";
tone?: GlassyStatusTone;
};

type GlassyProgressBarProps = {
Expand Down
20 changes: 19 additions & 1 deletion src/components/GovernanceStatusPills.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
import { governanceIdentityStatuses } from "@/lib/humanNodesUi";
import { getActiveGovernorReasonCopy } from "@/lib/governorOpportunityUi";
import type { ActiveGovernorReasonDto } from "@/types/api";
import { StatusPill } from "./StatusPill";

type GovernanceStatusPillsProps = {
governor: boolean;
activeGovernor: boolean;
activeGovernorReason?: ActiveGovernorReasonDto;
humanNode: boolean;
};

export function GovernanceStatusPills(props: GovernanceStatusPillsProps) {
const statuses = governanceIdentityStatuses(props);
const activeGovernorHint = props.activeGovernorReason
? getActiveGovernorReasonCopy(props.activeGovernorReason).detail
: undefined;
return (
<div className="flex flex-col items-center gap-2 text-sm lg:items-end">
{Object.values(statuses).map((status) => (
<StatusPill key={status.label} {...status} />
<StatusPill
key={status.label}
{...status}
hint={
status.label === "Active governor" && activeGovernorHint
? {
description: activeGovernorHint,
href: "/app/vortexopedia?term=governing_threshold",
title: "Active Governor assessment",
}
: undefined
}
/>
))}
</div>
);
Expand Down
23 changes: 22 additions & 1 deletion src/components/StatusPill.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import { ReferenceHint } from "@/components/Hint";
import { cn } from "@/lib/utils";
import { Surface } from "@/components/Surface";
import { Kicker } from "@/components/Kicker";
Expand All @@ -8,6 +9,11 @@ type StatusPillProps = {
value: string;
active?: boolean;
className?: string;
hint?: {
description: string;
href: string;
title: string;
};
widthClassName?: string;
};

Expand All @@ -16,6 +22,7 @@ export const StatusPill: React.FC<StatusPillProps> = ({
value,
active,
className,
hint,
widthClassName = "w-48",
}) => {
return (
Expand All @@ -29,7 +36,21 @@ export const StatusPill: React.FC<StatusPillProps> = ({
className,
)}
>
<Kicker as="span">{label}</Kicker>
<Kicker as="span">
{hint ? (
<ReferenceHint
actionLabel="Vortexopedia"
description={hint.description}
href={hint.href}
noUnderline
title={hint.title}
>
{label}
</ReferenceHint>
) : (
label
)}
</Kicker>
<span
className={cn(
"font-semibold",
Expand Down
51 changes: 46 additions & 5 deletions src/data/vortexopedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1410,21 +1410,23 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
name: "Governing threshold",
category: "governance",
short:
"Previous-era action quota used to decide who is counted as an active governor in quorums.",
"Previous-era Pool and Chamber quota used to decide who is counted as an Active Governor.",
long: [
"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.",
"A Governor is active for quorum purposes when both accountable voting requirements were met in the previous era.",
"Each category requires one-third of the Governor's eligible occurrences that remained open for the full accountability exposure period.",
"Actions are matched to their own occurrence. Fast-closing stages and stages where the Governor was legally excluded do not create requirements.",
"Current node liveness does not remove an Active Governor status already earned from the previous era.",
],
tags: ["threshold", "quorum", "activity", "governor"],
related: [
"governing_era",
"governor_opportunity_exposure",
"governor",
"quorum_of_vote",
"quorum_of_attention",
],
examples: [
"If the previous-era action threshold is met, the governor is counted as active in the next eras quorum.",
"If a Governor completes both previous-era category requirements, the Governor is active in the next era's quorum.",
],
stages: ["global"],
links: [
Expand All @@ -1434,7 +1436,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
},
],
source: "Proposition rights",
updated: "2025-12-04",
updated: "2026-08-25",
},
{
ref: 51,
Expand Down Expand Up @@ -1830,4 +1832,43 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
source: "Vortex Simulator Phase 90",
updated: "2026-06-27",
},
{
ref: 63,
id: "governor_opportunity_exposure",
name: "Governor opportunity exposure",
category: "governance",
short:
"Minimum time an eligible Pool or Chamber stage must remain actionable before it can create an Active Governor requirement.",
long: [
"Proposal progression remains immediate when its voting threshold is met. Opportunity exposure is a separate accountability rule.",
"An eligible stage counts only after it has remained open for the configured exposure period, currently 24 hours by default.",
"A stage that closes before exposure is still valid governance work, but it cannot penalize Governors who had no practical time to participate.",
"Proposal authors, Formation team members, and other legally excluded Governors do not receive an obligation for a stage they cannot vote on.",
],
tags: [
"governor",
"opportunity",
"exposure",
"threshold",
"accountability",
],
related: [
"governing_threshold",
"governing_era",
"proposal_pools",
"chamber_vote",
],
examples: [
"A Proposal Pool that advances in two hours creates no governing requirement. A Pool that stays open beyond 24 hours can count for eligible Governors.",
],
stages: ["global", "pool", "chamber"],
links: [
{
label: "My Governance",
url: "/app/my-governance",
},
],
source: "Vortex Simulator Phase 94",
updated: "2026-08-25",
},
];
26 changes: 24 additions & 2 deletions src/lib/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import type {
GetPublicProposalDraftsResponse,
GetProposalsResponse,
GetProposalTimelineResponse,
GovernorOpportunityStateDto,
GovernorOpportunityStageDto,
HumanNodeProfileDto,
ProposalDraftDetailDto,
PublicProposalDraftKindDto,
Expand Down Expand Up @@ -674,8 +676,28 @@ export async function apiInvision(): Promise<GetInvisionResponse> {
return await apiGet<GetInvisionResponse>("/api/invision");
}

export async function apiMyGovernance(): Promise<GetMyGovernanceResponse> {
return await apiGet<GetMyGovernanceResponse>("/api/my-governance");
export async function apiMyGovernance(input?: {
opportunityOffset?: number;
opportunityLimit?: number;
opportunityStage?: GovernorOpportunityStageDto | null;
opportunityState?: GovernorOpportunityStateDto | null;
}): Promise<GetMyGovernanceResponse> {
const params = new URLSearchParams();
if (input?.opportunityOffset !== undefined) {
params.set("opportunityOffset", String(input.opportunityOffset));
}
if (input?.opportunityLimit !== undefined) {
params.set("opportunityLimit", String(input.opportunityLimit));
}
if (input?.opportunityStage)
params.set("opportunityStage", input.opportunityStage);
if (input?.opportunityState)
params.set("opportunityState", input.opportunityState);
const query = params.toString();
if (!query) {
return await apiGet<GetMyGovernanceResponse>("/api/my-governance");
}
return await apiGet<GetMyGovernanceResponse>(`/api/my-governance?${query}`);
}

export async function apiLegitimacyObjectSet(input: {
Expand Down
42 changes: 42 additions & 0 deletions src/lib/feedUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,45 @@ export const toLimitedUrgentItems = (
safeLimit,
);
};

export function toGovernorAwareUrgentItems(input: {
eventItems: FeedItemDto[];
verifiedOpportunityItems: FeedItemDto[];
isGovernorActive: boolean;
viewerAddress?: string;
limit: number;
}): FeedItemDto[] {
const eventItems = input.eventItems.filter((item) => {
if (item.stage !== "pool" && item.stage !== "vote") return true;
return item.href?.includes("/referendum") === true;
});
const eligibleEvents = toUrgentItems(
eventItems,
input.isGovernorActive,
input.viewerAddress,
);
const verifiedOpportunities = toUrgentItems(
input.verifiedOpportunityItems,
true,
input.viewerAddress,
);
const byEntity = new Map<string, FeedItemDto>();

for (const item of [...eligibleEvents, ...verifiedOpportunities]) {
const key = urgentEntityKey(item);
const existing = byEntity.get(key);
if (
!existing ||
toTimestampMs(item.timestamp, -1) >= toTimestampMs(existing.timestamp, -1)
) {
byEntity.set(key, item);
}
}

return [...byEntity.values()]
.sort(
(left, right) =>
toTimestampMs(right.timestamp, -1) - toTimestampMs(left.timestamp, -1),
)
.slice(0, Math.max(0, Math.floor(input.limit)));
}
Loading
Loading