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
5 changes: 5 additions & 0 deletions src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import FactionInitiativeCreate from "../pages/factions/FactionInitiativeCreate";
import FactionCreate from "../pages/factions/FactionCreate";
import ProposalPP from "../pages/proposals/ProposalPP";
import ProposalChamber from "../pages/proposals/ProposalChamber";
import ProposalReferendum from "../pages/proposals/ProposalReferendum";
import ProposalFormation from "../pages/proposals/ProposalFormation";
import ProposalFinished from "../pages/proposals/ProposalFinished";
import Profile from "../pages/profile/Profile";
Expand Down Expand Up @@ -90,6 +91,10 @@ const AppRoutes: React.FC = () => {
<Route path="proposals/new" element={<ProposalCreation />} />
<Route path="proposals/:id/pp" element={<ProposalPP />} />
<Route path="proposals/:id/chamber" element={<ProposalChamber />} />
<Route
path="proposals/:id/referendum"
element={<ProposalReferendum />}
/>
<Route path="proposals/:id/formation" element={<ProposalFormation />} />
<Route path="proposals/:id/finished" element={<ProposalFinished />} />
<Route path="chambers" element={<Chambers />} />
Expand Down
56 changes: 56 additions & 0 deletions src/lib/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,39 @@ export async function apiProposalChamberPage(
return await apiGet<ChamberProposalPageDto>(`/api/proposals/${id}/chamber`);
}

export async function apiProposalReferendumPage(
id: string,
): Promise<ChamberProposalPageDto> {
return await apiGet<ChamberProposalPageDto>(
`/api/proposals/${id}/referendum`,
);
}

export async function apiReferendumVote(input: {
proposalId: string;
choice: ChamberVoteChoice;
idempotencyKey?: string;
}): Promise<{
ok: true;
type: "referendum.vote";
proposalId: string;
choice: ChamberVoteChoice;
counts: { yes: number; no: number; abstain: number };
systemReset?: boolean;
}> {
return await apiPost(
"/api/command",
{
type: "referendum.vote",
payload: { proposalId: input.proposalId, choice: input.choice },
idempotencyKey: input.idempotencyKey,
},
input.idempotencyKey
? { headers: { "idempotency-key": input.idempotencyKey } }
: undefined,
);
}

export async function apiProposalFormationPage(
id: string,
): Promise<FormationProposalPageDto> {
Expand Down Expand Up @@ -1218,6 +1251,29 @@ export async function apiMyGovernance(): Promise<GetMyGovernanceResponse> {
return await apiGet<GetMyGovernanceResponse>("/api/my-governance");
}

export async function apiLegitimacyObjectSet(input: {
active: boolean;
idempotencyKey?: string;
}): Promise<{
ok: true;
type: "legitimacy.object.set";
legitimacy: GetMyGovernanceResponse["legitimacy"];
}> {
return await apiPost(
"/api/command",
{
type: "legitimacy.object.set",
payload: {
active: input.active,
},
idempotencyKey: input.idempotencyKey,
},
input.idempotencyKey
? { headers: { "idempotency-key": input.idempotencyKey } }
: undefined,
);
}

export async function apiCmMe(): Promise<CmSummaryDto> {
return await apiGet<CmSummaryDto>("/api/cm/me");
}
Expand Down
106 changes: 106 additions & 0 deletions src/pages/MyGovernance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Kicker } from "@/components/Kicker";
import {
apiChambers,
apiClock,
apiLegitimacyObjectSet,
apiCmMe,
apiMyGovernance,
} from "@/lib/apiClient";
Expand Down Expand Up @@ -157,6 +158,8 @@ const MyGovernance: React.FC = () => {
const [clock, setClock] = useState<GetClockResponse | null>(null);
const [cmSummary, setCmSummary] = useState<CmSummaryDto | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
const [legitimacyPending, setLegitimacyPending] = useState(false);
const [legitimacyError, setLegitimacyError] = useState<string | null>(null);
const [nowMs, setNowMs] = useState<number>(() => Date.now());

useEffect(() => {
Expand Down Expand Up @@ -244,6 +247,32 @@ const MyGovernance: React.FC = () => {
)
: 100;

const legitimacy = gov?.legitimacy ?? {
percent: 100,
objecting: false,
objectingHumanNodes: 0,
eligibleHumanNodes: 0,
referendumTriggered: false,
triggerThresholdPercent: 33.3,
};

const handleLegitimacyToggle = async () => {
try {
setLegitimacyPending(true);
setLegitimacyError(null);
await apiLegitimacyObjectSet({
active: !legitimacy.objecting,
idempotencyKey: crypto.randomUUID(),
});
const fresh = await apiMyGovernance();
setGov(fresh);
} catch (error) {
setLegitimacyError(formatLoadError((error as Error).message));
} finally {
setLegitimacyPending(false);
}
};

return (
<div className="flex flex-col gap-6">
<PageHint pageId="my-governance" />
Expand Down Expand Up @@ -647,6 +676,83 @@ const MyGovernance: React.FC = () => {
)}
</CardContent>
</Card>

<Card>
<CardHeader className="pb-2">
<CardTitle>System legitimacy</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 sm:grid-cols-3">
{[
{
label: "Legitimacy",
value: `${legitimacy.percent}%`,
},
{
label: "Objectors",
value: `${legitimacy.objectingHumanNodes} / ${legitimacy.eligibleHumanNodes}`,
},
{
label: "Referendum trigger",
value: `< ${legitimacy.triggerThresholdPercent}%`,
},
].map((tile) => (
<Surface
key={tile.label}
variant="panelAlt"
radius="2xl"
shadow="tile"
className="flex h-full flex-col items-center justify-center px-4 py-4 text-center"
>
<p className="text-sm text-muted">{tile.label}</p>
<p className="text-xl font-semibold text-text">{tile.value}</p>
</Surface>
))}
</div>

<Surface
variant="panelAlt"
radius="2xl"
shadow="tile"
className="space-y-3 p-4"
>
<p className="text-sm text-muted">
Any active human node can object to Vortex legitimacy. Each
objector reduces legitimacy by their equal share of the active
human-node base.
</p>
<div className="flex flex-wrap items-center gap-3">
<Button
type="button"
variant={legitimacy.objecting ? "outline" : "ghost"}
className="border-[var(--danger)] text-[var(--danger)] hover:bg-[var(--danger)] hover:text-white"
disabled={legitimacyPending}
onClick={handleLegitimacyToggle}
>
{legitimacy.objecting
? "Withdraw illegitimacy objection"
: "VORTEX IS ILLEGITIMATE"}
</Button>
<Badge variant="outline">
{legitimacy.objecting
? "You are objecting"
: "You are not objecting"}
</Badge>
{legitimacy.referendumTriggered ? (
<Badge
variant="outline"
className="border-danger/40 text-danger"
>
Referendum threshold reached
</Badge>
) : null}
</div>
{legitimacyError ? (
<p className="text-sm text-danger">{legitimacyError}</p>
) : null}
</Surface>
</CardContent>
</Card>
</div>
);
};
Expand Down
3 changes: 2 additions & 1 deletion src/pages/feed/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const proposalIdFromHref = (href?: string) => {
? noQuery.slice("/app".length)
: noQuery;
const match = clean.match(
/^\/proposals\/([^/]+)\/(pp|chamber|formation|finished)$/,
/^\/proposals\/([^/]+)\/(pp|chamber|referendum|formation|finished)$/,
);
return match?.[1] ?? null;
};
Expand Down Expand Up @@ -115,6 +115,7 @@ const isUrgentItemInteractable = (
return Boolean(viewer && proposer && viewer === proposer);
}
if ((item.stage === "pool" || item.stage === "vote") && !isGovernorActive) {
if (item.href?.includes("/referendum")) return true;
return false;
}
return true;
Expand Down
51 changes: 40 additions & 11 deletions src/pages/invision/Invision.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,21 @@ const Invision: React.FC = () => {
});
}, [factions, search, factionSort]);

const hasInvisionContent =
Boolean(invision?.governanceState.metrics.length) ||
Boolean(invision?.economicIndicators.length) ||
Boolean(invision?.riskSignals.length) ||
Boolean(invision?.chamberProposals.length);
const primaryGovernanceMetrics = (
invision?.governanceState.metrics ?? []
).slice(0, 3);
const secondaryGovernanceMetrics = (
invision?.governanceState.metrics ?? []
).slice(3);

return (
<div className="relative">
<div className="pointer-events-none flex flex-col gap-5 opacity-35 blur-[6px] select-none">
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-5">
<PageHint pageId="invision" />
{invision === null ? (
<Card className="border-dashed px-4 py-6 text-center text-sm text-muted">
Expand All @@ -86,6 +98,7 @@ const Invision: React.FC = () => {
{invision !== null &&
factions !== null &&
factions.length === 0 &&
!hasInvisionContent &&
!loadError ? (
<NoDataYetBar label="Invision data" />
) : null}
Expand All @@ -94,23 +107,44 @@ const Invision: React.FC = () => {
variant="panelAlt"
className="px-6 py-5 text-center sm:col-span-2 lg:col-span-3"
>
<Kicker align="center">Governance model</Kicker>
<Kicker align="center">System state</Kicker>
<h1 className="text-2xl font-semibold text-text">
{invision?.governanceState.label ?? "—"}
</h1>
</Surface>
{(invision?.governanceState.metrics ?? []).map((metric) => (
{primaryGovernanceMetrics.map((metric) => (
<Surface
key={metric.label}
variant="panel"
className="px-3 py-3 text-center"
className="px-4 py-4 text-center"
>
<Kicker align="center">{metric.label}</Kicker>
<p className="text-2xl font-semibold text-text">{metric.value}</p>
<p className="text-3xl font-semibold text-text">{metric.value}</p>
</Surface>
))}
</div>

{secondaryGovernanceMetrics.length > 0 ? (
<Card>
<CardHeader className="pb-2">
<CardTitle>Governance signals</CardTitle>
</CardHeader>
<CardContent className="grid gap-3 text-sm text-text sm:grid-cols-2 lg:grid-cols-3">
{secondaryGovernanceMetrics.map((metric) => (
<div
key={metric.label}
className="rounded-xl border border-border px-3 py-3"
>
<Kicker>{metric.label}</Kicker>
<p className="text-lg font-semibold text-text">
{metric.value}
</p>
</div>
))}
</CardContent>
</Card>
) : null}

<SearchBar
value={search}
onChange={(e) => setSearch(e.target.value)}
Expand Down Expand Up @@ -254,11 +288,6 @@ const Invision: React.FC = () => {
</Card>
</div>
</div>
<div className="pointer-events-none absolute inset-0 z-20 grid place-items-center p-6">
<h2 className="text-center text-4xl font-semibold text-text sm:text-5xl lg:text-6xl">
coming sooner than you think...
</h2>
</div>
</div>
);
};
Expand Down
Loading