From 3179a9cfd4ed0cdd5bb1f0b77c41cdc6d295b847 Mon Sep 17 00:00:00 2001 From: Cyber-preacher Date: Tue, 20 Jan 2026 14:18:15 +0400 Subject: [PATCH 1/4] feat(web): finish Hono migration alignment and cleanup legacy routes drop legacy /app redirect shim and old top-level routes align proposal stage labels, chips, and filters to pool/vote/build keep proposal draft flow intact while syncing API DTOs --- src/app/AppRoutes.tsx | 46 +---------------------- src/components/ProposalStageBar.tsx | 8 ++-- src/components/StageChip.tsx | 3 -- src/pages/proposals/ProposalChamber.tsx | 2 +- src/pages/proposals/ProposalFormation.tsx | 2 +- src/pages/proposals/Proposals.tsx | 2 - src/types/api.ts | 17 +-------- src/types/stages.ts | 20 +--------- 8 files changed, 11 insertions(+), 89 deletions(-) diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 0ddd52f..3c2e054 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -1,4 +1,4 @@ -import { Navigate, Outlet, Route, Routes, useLocation } from "react-router"; +import { Navigate, Outlet, Route, Routes } from "react-router"; import AppShell from "./AppShell"; import HumanNodes from "../pages/human-nodes/HumanNodes"; import Proposals from "../pages/proposals/Proposals"; @@ -28,18 +28,6 @@ import Landing from "../pages/Landing"; import Paper from "../pages/Paper"; import Guide from "../pages/Guide"; -// Backwards-compat redirects for old app URLs (pre `/app` split). -// Safe to delete once you no longer need to support old bookmarks/links. -const LegacyToAppRedirect: React.FC = () => { - const location = useLocation(); - return ( - - ); -}; - const AppRoutes: React.FC = () => { return ( @@ -81,38 +69,6 @@ const AppRoutes: React.FC = () => { } /> - {/* Legacy redirects (old app URLs -> /app/*). */} - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> ); diff --git a/src/components/ProposalStageBar.tsx b/src/components/ProposalStageBar.tsx index 6bf5bef..0e5f7db 100644 --- a/src/components/ProposalStageBar.tsx +++ b/src/components/ProposalStageBar.tsx @@ -1,7 +1,7 @@ import React from "react"; import { HintLabel } from "@/components/Hint"; -export type ProposalStage = "draft" | "pool" | "chamber" | "formation"; +export type ProposalStage = "draft" | "pool" | "vote" | "build"; type ProposalStageBarProps = { current: ProposalStage; @@ -24,12 +24,12 @@ export const ProposalStageBar: React.FC = ({ render: Proposal pool, }, { - key: "chamber", + key: "vote", label: "Chamber vote", render: Chamber vote, }, { - key: "formation", + key: "build", label: "Formation", render: Formation, }, @@ -44,7 +44,7 @@ export const ProposalStageBar: React.FC = ({ ? "bg-panel text-text border border-border shadow-[var(--shadow-control)] ring-1 ring-inset ring-[color:var(--glass-border)]" : stage.key === "pool" ? "bg-primary text-[var(--primary-foreground)]" - : stage.key === "chamber" + : stage.key === "vote" ? "bg-[var(--accent)] text-[var(--accent-foreground)]" : "bg-[var(--accent-warm)] text-[var(--text)]"; return ( diff --git a/src/components/StageChip.tsx b/src/components/StageChip.tsx index c46ed1c..6c9cd16 100644 --- a/src/components/StageChip.tsx +++ b/src/components/StageChip.tsx @@ -16,9 +16,6 @@ const chipClasses: Record = { thread: "bg-panel-alt text-muted", courts: "bg-[color:var(--accent-warm)]/15 text-[var(--accent-warm)]", faction: "bg-panel-alt text-muted", - draft: "bg-panel-alt text-muted", - final: "bg-[color:var(--accent)]/15 text-[var(--accent)]", - archived: "bg-panel-alt text-muted", }; const hintByKind: Partial> = { diff --git a/src/pages/proposals/ProposalChamber.tsx b/src/pages/proposals/ProposalChamber.tsx index a8c93b8..e40f8e5 100644 --- a/src/pages/proposals/ProposalChamber.tsx +++ b/src/pages/proposals/ProposalChamber.tsx @@ -136,7 +136,7 @@ const ProposalChamber: React.FC = () => { diff --git a/src/pages/proposals/ProposalFormation.tsx b/src/pages/proposals/ProposalFormation.tsx index 13e43e1..a1e307a 100644 --- a/src/pages/proposals/ProposalFormation.tsx +++ b/src/pages/proposals/ProposalFormation.tsx @@ -124,7 +124,7 @@ const ProposalFormation: React.FC = () => { diff --git a/src/pages/proposals/Proposals.tsx b/src/pages/proposals/Proposals.tsx index 26f8959..0f1fe9d 100644 --- a/src/pages/proposals/Proposals.tsx +++ b/src/pages/proposals/Proposals.tsx @@ -193,8 +193,6 @@ const Proposals: React.FC = () => { { value: "pool", label: "Proposal pool" }, { value: "vote", label: "Chamber vote" }, { value: "build", label: "Formation" }, - { value: "final", label: "Final vote" }, - { value: "archived", label: "Archived" }, ], }, { diff --git a/src/types/api.ts b/src/types/api.ts index 8abc2a3..dab32b8 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -3,7 +3,7 @@ import type { FeedStage } from "./stages"; -export type ProposalStageDto = "draft" | "pool" | "vote" | "build"; +export type ProposalStageDto = "pool" | "vote" | "build"; export type FeedStageDto = FeedStage; export type ToneDto = "ok" | "warn"; @@ -219,20 +219,7 @@ export type GetProposalsResponse = { items: ProposalListItemDto[] }; export type InvisionInsightDto = { role: string; bullets: string[] }; -export type ProposalTimelineEventTypeDto = - | "proposal.submitted" - | "proposal.stage.advanced" - | "proposal.vote.passed" - | "proposal.vote.finalized" - | "pool.vote" - | "chamber.vote" - | "veto.vote" - | "veto.applied" - | "formation.join" - | "formation.milestone.submitted" - | "formation.milestone.unlockRequested" - | "chamber.created" - | "chamber.dissolved"; +export type ProposalTimelineEventTypeDto = string; export type ProposalTimelineItemDto = { id: string; diff --git a/src/types/stages.ts b/src/types/stages.ts index 3c23597..23c51d1 100644 --- a/src/types/stages.ts +++ b/src/types/stages.ts @@ -1,11 +1,4 @@ -export const proposalStages = [ - "draft", - "pool", - "vote", - "build", - "final", - "archived", -] as const; +export const proposalStages = ["pool", "vote", "build"] as const; export type ProposalStage = (typeof proposalStages)[number]; @@ -28,30 +21,21 @@ export type StageChipKind = | "formation" | "thread" | "courts" - | "faction" - | "draft" - | "final" - | "archived"; + | "faction"; export const stageToChipKind = { - draft: "draft", pool: "proposal_pool", vote: "chamber_vote", build: "formation", - final: "final", - archived: "archived", thread: "thread", courts: "courts", faction: "faction", } as const satisfies Record; export const stageLabel = { - draft: "Draft", pool: "Proposal pool", vote: "Chamber vote", build: "Formation", - final: "Final vote", - archived: "Archived", thread: "Thread", courts: "Courts", faction: "Faction", From da4457be3c7cb3de3d2fedbe6956948d578ea978 Mon Sep 17 00:00:00 2001 From: Cyber-preacher Date: Tue, 20 Jan 2026 18:38:16 +0400 Subject: [PATCH 2/4] feat(web): align chamber UI with canonical roster + pipeline data - chamber detail now relies on canonical /api/chambers data (roster, pipeline, metadata) - chamber stats derived from real roster values (ACM/LCM/MCM/governors) - add rstest coverage for chamber stats helper --- package.json | 2 + rsbuild.config.ts | 3 + rstest.config.ts | 25 ++++ src/pages/chambers/Chamber.tsx | 196 ++++++++++++++++++------ src/types/api.ts | 17 +++ tests/api/api-client.test.js | 44 ++++++ tests/utils/cn.test.js | 9 ++ tests/utils/stat-grid.test.js | 24 +++ yarn.lock | 266 +++++++++++++++++++++++++++++++++ 9 files changed, 537 insertions(+), 49 deletions(-) create mode 100644 rstest.config.ts create mode 100644 tests/api/api-client.test.js create mode 100644 tests/utils/cn.test.js create mode 100644 tests/utils/stat-grid.test.js diff --git a/package.json b/package.json index 8531250..3e07770 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "rsbuild dev --open", "build": "rsbuild build", "preview": "rsbuild preview", + "test": "RSTEST=1 rstest", "typecheck": "tsc -p tsconfig.json --noEmit", "prettier:check": "prettier --list-different .", "prettier:fix": "prettier --write ." @@ -29,6 +30,7 @@ "devDependencies": { "@rsbuild/core": "^1.6.6", "@rsbuild/plugin-react": "^1.4.2", + "@rstest/core": "^0.7.9", "@tailwindcss/postcss": "^4.1.17", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 85939d8..cd3b4a7 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -1,9 +1,12 @@ import { defineConfig } from "@rsbuild/core"; import { pluginReact } from "@rsbuild/plugin-react"; +const isTest = process.env.RSTEST === "1"; + export default defineConfig({ plugins: [pluginReact()], server: { + ...(isTest ? { host: "127.0.0.1", port: 0 } : {}), proxy: { "/api": { target: process.env.API_PROXY_TARGET ?? "http://127.0.0.1:8788", diff --git a/rstest.config.ts b/rstest.config.ts new file mode 100644 index 0000000..b4d8848 --- /dev/null +++ b/rstest.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "@rstest/core"; +import type { RsbuildPlugin } from "@rsbuild/core"; + +const rstestServerPlugin = (): RsbuildPlugin => ({ + name: "rstest:server-host", + setup(api) { + api.modifyRsbuildConfig((config) => { + config.server = { + ...config.server, + host: "127.0.0.1", + port: 0, + strictPort: false, + middlewareMode: true, + }; + return config; + }); + }, +}); + +export default defineConfig({ + testMatch: ["tests/**/*.test.js"], + environment: "node", + browser: { enabled: false }, + plugins: [rstestServerPlugin()], +}); diff --git a/src/pages/chambers/Chamber.tsx b/src/pages/chambers/Chamber.tsx index abeb916..ef8f597 100644 --- a/src/pages/chambers/Chamber.tsx +++ b/src/pages/chambers/Chamber.tsx @@ -15,7 +15,9 @@ import { Surface } from "@/components/Surface"; import { PageHint } from "@/components/PageHint"; import { PageHeader } from "@/components/PageHeader"; import { TierLabel } from "@/components/TierLabel"; -import type { ChamberProposalStageDto } from "@/types/api"; +import { PipelineList } from "@/components/PipelineList"; +import { StatGrid, makeChamberStats } from "@/components/StatGrid"; +import type { ChamberProposalStageDto, GetChamberResponse } from "@/types/api"; import { apiChamber, apiChambers } from "@/lib/apiClient"; import { NoDataYetBar } from "@/components/NoDataYetBar"; @@ -25,28 +27,7 @@ const Chamber: React.FC = () => { id ? id.replace(/-/g, " ") : "Chamber", ); - const [data, setData] = useState<{ - proposals: { - id: string; - title: string; - meta: string; - summary: string; - lead: string; - nextStep: string; - timing: string; - stage: ChamberProposalStageDto; - }[]; - governors: { id: string; name: string; tier: string; focus: string }[]; - threads: { - id: string; - title: string; - author: string; - replies: number; - updated: string; - }[]; - chatLog: { id: string; author: string; message: string }[]; - stageOptions: { value: ChamberProposalStageDto; label: string }[]; - } | null>(null); + const [data, setData] = useState(null); const [loadError, setLoadError] = useState(null); const [stageFilter, setStageFilter] = @@ -65,7 +46,10 @@ const Chamber: React.FC = () => { if (!active) return; setData(chamberRes); const found = listRes.items.find((c) => c.id === id); - setChamberTitle(found?.name ?? id.replace(/-/g, " ")); + const fallbackTitle = chamberRes.chamber?.title; + setChamberTitle( + found?.name ?? fallbackTitle ?? id.replace(/-/g, " "), + ); setLoadError(null); } catch (error) { if (!active) return; @@ -92,10 +76,50 @@ const Chamber: React.FC = () => { (gov) => gov.name.toLowerCase().includes(term) || gov.tier.toLowerCase().includes(term) || - gov.focus.toLowerCase().includes(term), + gov.focus.toLowerCase().includes(term) || + String(gov.acm).includes(term) || + String(gov.lcm).includes(term) || + String(gov.mcm).includes(term) || + String(gov.delegatedWeight).includes(term), ); }, [data, governorSearch]); + const chamberStats = useMemo(() => { + const governors = data?.governors ?? []; + const totals = governors.reduce( + (acc, gov) => ({ + acm: acc.acm + (Number.isFinite(gov.acm) ? gov.acm : 0), + lcm: acc.lcm + (Number.isFinite(gov.lcm) ? gov.lcm : 0), + mcm: acc.mcm + (Number.isFinite(gov.mcm) ? gov.mcm : 0), + }), + { acm: 0, lcm: 0, mcm: 0 }, + ); + return { + governors: String(governors.length), + acm: totals.acm.toLocaleString(), + lcm: totals.lcm.toLocaleString(), + mcm: totals.mcm.toLocaleString(), + }; + }, [data]); + + const chamberMetaItems = useMemo(() => { + const chamber = data?.chamber; + if (!chamber) return []; + const items = [ + { label: "Status", value: chamber.status }, + { label: "Multiplier", value: chamber.multiplier.toFixed(1) }, + { label: "Created", value: chamber.createdAt.slice(0, 10) }, + { label: "Origin", value: chamber.createdByProposalId ?? "Genesis" }, + ]; + if (chamber.dissolvedAt) { + items.push({ label: "Dissolved", value: chamber.dissolvedAt.slice(0, 10) }); + } + if (chamber.dissolvedByProposalId) { + items.push({ label: "Dissolved by", value: chamber.dissolvedByProposalId }); + } + return items; + }, [data]); + return (
@@ -116,6 +140,64 @@ const Chamber: React.FC = () => { ) : null} + {data ? ( +
+ + + Chamber profile + Overview + + + {chamberMetaItems.map((item) => ( + + {item.label} +

+ {item.value} +

+
+ ))} +
+
+ + + + Chamber stats + Governance metrics + + + + + + + + + Pipeline + Proposal flow + + + {data.pipeline ? ( + + ) : ( + + No pipeline data yet. + + )} + + +
+ ) : null} +
@@ -179,30 +261,38 @@ const Chamber: React.FC = () => {

{proposal.summary}

-
- - Next step -

- {proposal.nextStep} -

-
- - Timing -

- {proposal.timing} -

-
-
+ {(() => { + const metaTiles = [ + { label: "Next step", value: proposal.nextStep }, + { label: "Timing", value: proposal.timing }, + ]; + if (typeof proposal.activeGovernors === "number") { + metaTiles.push({ + label: "Active governors", + value: proposal.activeGovernors.toLocaleString(), + }); + } + const columns = + metaTiles.length === 3 ? "sm:grid-cols-3" : "sm:grid-cols-2"; + return ( +
+ {metaTiles.map((tile) => ( + + {tile.label} +

+ {tile.value} +

+
+ ))} +
+ ); + })()} )) )} @@ -240,6 +330,14 @@ const Chamber: React.FC = () => {

· {gov.focus}

+

+ ACM {gov.acm.toLocaleString()} · LCM{" "} + {gov.lcm.toLocaleString()} · MCM{" "} + {gov.mcm.toLocaleString()} + {gov.delegatedWeight > 0 + ? ` · Delegated +${gov.delegatedWeight}` + : ""} +