feat(governance): District & MicroDAO Governance Panels

- Add DistrictGovernancePanel component
- Add MicroDAOGovernancePanel component with team management
- Add /governance/district/:id route
- Add /governance/microdao/:id route
- Seed City Governance Agents on NODE1
This commit is contained in:
Apple
2025-11-29 16:16:42 -08:00
parent 57749ac10c
commit 91a4c6be5b
6 changed files with 794 additions and 0 deletions

View File

@@ -45,6 +45,8 @@ import { CityRoomView } from './features/city/rooms/CityRoomView';
import { SecondMePage } from './features/secondme/SecondMePage';
// Governance Engine
import { GovernancePage } from './pages/GovernancePage';
import { DistrictGovernancePage } from './pages/DistrictGovernancePage';
import { MicroDAOGovernancePage } from './pages/MicroDAOGovernancePage';
function App() {
return (
@@ -60,6 +62,8 @@ function App() {
<Route path="/secondme" element={<SecondMePage />} />
{/* Governance Engine */}
<Route path="/governance" element={<GovernancePage />} />
<Route path="/governance/district/:districtId" element={<DistrictGovernancePage />} />
<Route path="/governance/microdao/:microdaoId" element={<MicroDAOGovernancePage />} />
<Route path="/space" element={<SpaceDashboard />} />
<Route path="/messenger" element={<MessengerPage />} />
{/* Task 039: Agent Console v2 */}

View File

@@ -0,0 +1,258 @@
/**
* District Governance Panel
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Shows district-level governance: Lead Agent, Sub-DAOs, District Incidents
*/
import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { governanceApi } from '../../../api/governance';
import { incidentsApi } from '../../../api/incidents';
import { auditApi } from '../../../api/audit';
import { GovernanceLevelBadge } from './GovernanceLevelBadge';
import type { Incident, GovernanceEvent } from '../../../types/governance';
interface DistrictGovernancePanelProps {
districtId: string;
districtName?: string;
actorId?: string;
}
export const DistrictGovernancePanel: React.FC<DistrictGovernancePanelProps> = ({
districtId,
districtName,
actorId,
}) => {
const [activeTab, setActiveTab] = useState<'overview' | 'daos' | 'incidents' | 'audit'>('overview');
// Fetch district lead
const { data: districtLeads } = useQuery({
queryKey: ['governance', 'district-leads', districtId],
queryFn: () => governanceApi.getDistrictLeadAgents(districtId),
});
const lead = districtLeads?.[0];
// Fetch district incidents
const { data: incidentsData } = useQuery({
queryKey: ['incidents', 'district', districtId],
queryFn: () => incidentsApi.listIncidents({
escalationLevel: 'district',
targetScopeId: districtId,
}),
});
// Fetch district audit events
const { data: auditData } = useQuery({
queryKey: ['audit', 'district', districtId],
queryFn: () => auditApi.getEventsByScope(`district:${districtId}`, 20),
});
return (
<div className="bg-slate-900 rounded-xl border border-slate-700 overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-pink-900/50 to-purple-900/50 px-6 py-4 border-b border-slate-700">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
🏘 {districtName || districtId} Governance
</h2>
<p className="text-sm text-slate-400 mt-1">
District-level управління
</p>
</div>
{lead && (
<div className="flex items-center gap-3 bg-slate-800/50 rounded-lg px-4 py-2">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-pink-500 to-purple-500 flex items-center justify-center text-lg">
👤
</div>
<div>
<div className="text-white font-medium">{lead.agentName}</div>
<div className="text-xs text-slate-400">District Lead</div>
</div>
<GovernanceLevelBadge level="district_lead" size="sm" />
</div>
)}
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-slate-700">
{[
{ id: 'overview', label: '📊 Overview', icon: '📊' },
{ id: 'daos', label: '🏢 Sub-DAOs', icon: '🏢' },
{ id: 'incidents', label: '⚠️ Incidents', icon: '⚠️' },
{ id: 'audit', label: '📋 Audit', icon: '📋' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as typeof activeTab)}
className={`px-6 py-3 text-sm font-medium transition-colors ${
activeTab === tab.id
? 'text-white bg-slate-800 border-b-2 border-pink-500'
: 'text-slate-400 hover:text-white'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Content */}
<div className="p-6">
{activeTab === 'overview' && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Stats Cards */}
<StatCard
icon="🏢"
label="Sub-DAOs"
value={0}
color="blue"
/>
<StatCard
icon="👥"
label="Agents"
value={0}
color="green"
/>
<StatCard
icon="⚠️"
label="Open Incidents"
value={incidentsData?.incidents.filter(i => i.status === 'open').length || 0}
color="red"
/>
{/* Lead Agent Info */}
{lead && (
<div className="col-span-full bg-slate-800/50 rounded-lg p-4 border border-slate-700">
<h3 className="text-sm font-medium text-slate-400 mb-3">District Lead Agent</h3>
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-pink-500 to-purple-500 flex items-center justify-center text-3xl">
👤
</div>
<div>
<div className="text-xl font-bold text-white">{lead.agentName}</div>
<div className="text-slate-400">{lead.districtName}</div>
<div className="mt-2">
<GovernanceLevelBadge level="district_lead" />
</div>
</div>
</div>
</div>
)}
</div>
)}
{activeTab === 'daos' && (
<div className="text-center py-12 text-slate-400">
<div className="text-4xl mb-4">🏢</div>
<p>Sub-DAOs для цього дистрикту будуть показані тут</p>
<p className="text-sm mt-2">Потрібна інтеграція з MicroDAO API</p>
</div>
)}
{activeTab === 'incidents' && (
<div className="space-y-4">
{incidentsData?.incidents.length === 0 ? (
<div className="text-center py-12 text-green-400">
<div className="text-4xl mb-4"></div>
<p>Немає відкритих інцидентів на рівні дистрикту</p>
</div>
) : (
incidentsData?.incidents.map((incident: Incident) => (
<IncidentRow key={incident.id} incident={incident} />
))
)}
</div>
)}
{activeTab === 'audit' && (
<div className="space-y-3">
{auditData?.length === 0 ? (
<div className="text-center py-12 text-slate-400">
<p>Немає подій для цього дистрикту</p>
</div>
) : (
auditData?.map((event: GovernanceEvent) => (
<AuditRow key={event.id} event={event} />
))
)}
</div>
)}
</div>
</div>
);
};
// Helper Components
const StatCard: React.FC<{
icon: string;
label: string;
value: number;
color: 'blue' | 'green' | 'red' | 'yellow';
}> = ({ icon, label, value, color }) => {
const colorClasses = {
blue: 'bg-blue-500/20 border-blue-500/30',
green: 'bg-green-500/20 border-green-500/30',
red: 'bg-red-500/20 border-red-500/30',
yellow: 'bg-yellow-500/20 border-yellow-500/30',
};
return (
<div className={`rounded-lg p-4 border ${colorClasses[color]}`}>
<div className="flex items-center gap-2 text-slate-400 text-sm mb-1">
<span>{icon}</span>
<span>{label}</span>
</div>
<div className="text-2xl font-bold text-white">{value}</div>
</div>
);
};
const IncidentRow: React.FC<{ incident: Incident }> = ({ incident }) => {
const priorityColors = {
low: 'border-gray-500/30',
medium: 'border-yellow-500/30',
high: 'border-orange-500/30',
critical: 'border-red-500/30',
};
return (
<div className={`rounded-lg p-4 border bg-slate-800/50 ${priorityColors[incident.priority]}`}>
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-white">{incident.title}</div>
<div className="text-sm text-slate-400 mt-1">
{incident.targetScopeType}: {incident.targetScopeId}
</div>
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${
incident.status === 'open' ? 'bg-red-500/20 text-red-300' :
incident.status === 'in_progress' ? 'bg-yellow-500/20 text-yellow-300' :
'bg-green-500/20 text-green-300'
}`}>
{incident.status}
</span>
</div>
</div>
);
};
const AuditRow: React.FC<{ event: GovernanceEvent }> = ({ event }) => (
<div className="rounded-lg p-3 bg-slate-800/50 border border-slate-700 text-sm">
<div className="flex items-center justify-between">
<span className="text-white">{event.eventType}</span>
<span className="text-slate-500 text-xs">
{new Date(event.createdAt).toLocaleString('uk-UA')}
</span>
</div>
<div className="text-slate-400 text-xs mt-1">
{event.actorId} {event.targetId}
</div>
</div>
);
export default DistrictGovernancePanel;

View File

@@ -0,0 +1,418 @@
/**
* MicroDAO Governance Panel
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
*
* Shows DAO-level governance: Orchestrator, Core-team, Members, Incidents
*/
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { governanceApi } from '../../../api/governance';
import { incidentsApi } from '../../../api/incidents';
import { auditApi } from '../../../api/audit';
import { GovernanceLevelBadge } from './GovernanceLevelBadge';
import type { Incident, GovernanceEvent, AgentGovLevel } from '../../../types/governance';
import { GOV_LEVEL_LABELS } from '../../../types/governance';
interface MicroDAOGovernancePanelProps {
microdaoId: string;
microdaoName?: string;
actorId?: string;
}
export const MicroDAOGovernancePanel: React.FC<MicroDAOGovernancePanelProps> = ({
microdaoId,
microdaoName,
actorId,
}) => {
const queryClient = useQueryClient();
const [activeTab, setActiveTab] = useState<'team' | 'incidents' | 'audit' | 'actions'>('team');
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
const [promoteLevel, setPromoteLevel] = useState<AgentGovLevel>('worker');
// Fetch orchestrators
const { data: orchestrators } = useQuery({
queryKey: ['governance', 'agents', 'orchestrator'],
queryFn: () => governanceApi.getAgentsByLevel('orchestrator', 20),
});
// Fetch core-team
const { data: coreTeam } = useQuery({
queryKey: ['governance', 'agents', 'core_team'],
queryFn: () => governanceApi.getAgentsByLevel('core_team', 50),
});
// Fetch workers
const { data: workers } = useQuery({
queryKey: ['governance', 'agents', 'worker'],
queryFn: () => governanceApi.getAgentsByLevel('worker', 50),
});
// Fetch incidents
const { data: incidentsData } = useQuery({
queryKey: ['incidents', 'microdao', microdaoId],
queryFn: () => incidentsApi.listIncidents({
escalationLevel: 'microdao',
targetScopeId: microdaoId,
}),
});
// Fetch audit events
const { data: auditData } = useQuery({
queryKey: ['audit', 'microdao', microdaoId],
queryFn: () => auditApi.getEventsByScope(`microdao:${microdaoId}`, 20),
});
// Promote mutation
const promoteMutation = useMutation({
mutationFn: (params: { targetId: string; newLevel: AgentGovLevel }) =>
governanceApi.promoteAgent({
actorId: actorId!,
targetId: params.targetId,
newLevel: params.newLevel,
scope: `microdao:${microdaoId}`,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['governance'] });
setSelectedAgent(null);
},
});
// Filter agents for this DAO (simplified - in real app would filter by home_microdao_id)
const daoOrchestrators = orchestrators?.filter(a => a.homeMicrodaoId === microdaoId) || [];
const daoCoreTeam = coreTeam?.filter(a => a.homeMicrodaoId === microdaoId) || [];
const daoWorkers = workers?.filter(a => a.homeMicrodaoId === microdaoId) || [];
return (
<div className="bg-slate-900 rounded-xl border border-slate-700 overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-blue-900/50 to-indigo-900/50 px-6 py-4 border-b border-slate-700">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
🏢 {microdaoName || microdaoId} Governance
</h2>
<p className="text-sm text-slate-400 mt-1">
MicroDAO-level управління
</p>
</div>
<div className="flex items-center gap-2">
<span className="px-3 py-1 bg-blue-500/20 text-blue-300 rounded-full text-sm">
{daoOrchestrators.length} Orchestrators
</span>
<span className="px-3 py-1 bg-purple-500/20 text-purple-300 rounded-full text-sm">
{daoCoreTeam.length} Core-team
</span>
<span className="px-3 py-1 bg-green-500/20 text-green-300 rounded-full text-sm">
{daoWorkers.length} Workers
</span>
</div>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-slate-700">
{[
{ id: 'team', label: '👥 Team' },
{ id: 'incidents', label: '⚠️ Incidents' },
{ id: 'audit', label: '📋 Audit' },
{ id: 'actions', label: '⚡ Actions' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as typeof activeTab)}
className={`px-6 py-3 text-sm font-medium transition-colors ${
activeTab === tab.id
? 'text-white bg-slate-800 border-b-2 border-blue-500'
: 'text-slate-400 hover:text-white'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Content */}
<div className="p-6">
{activeTab === 'team' && (
<div className="space-y-6">
{/* Orchestrators */}
<TeamSection
title="🎭 Orchestrators"
level="orchestrator"
agents={daoOrchestrators}
onSelect={actorId ? setSelectedAgent : undefined}
/>
{/* Core-team */}
<TeamSection
title="⭐ Core-team"
level="core_team"
agents={daoCoreTeam}
onSelect={actorId ? setSelectedAgent : undefined}
/>
{/* Workers */}
<TeamSection
title="👷 Workers"
level="worker"
agents={daoWorkers}
onSelect={actorId ? setSelectedAgent : undefined}
/>
{/* Empty state */}
{daoOrchestrators.length === 0 && daoCoreTeam.length === 0 && daoWorkers.length === 0 && (
<div className="text-center py-12 text-slate-400">
<div className="text-4xl mb-4">👥</div>
<p>Немає агентів, привʼязаних до цього DAO</p>
<p className="text-sm mt-2">Агенти зʼявляться після їх призначення</p>
</div>
)}
</div>
)}
{activeTab === 'incidents' && (
<div className="space-y-4">
{incidentsData?.incidents.length === 0 ? (
<div className="text-center py-12 text-green-400">
<div className="text-4xl mb-4"></div>
<p>Немає відкритих інцидентів у цьому DAO</p>
</div>
) : (
incidentsData?.incidents.map((incident: Incident) => (
<IncidentCard key={incident.id} incident={incident} />
))
)}
</div>
)}
{activeTab === 'audit' && (
<div className="space-y-3">
{auditData?.length === 0 ? (
<div className="text-center py-12 text-slate-400">
<p>Немає governance-подій для цього DAO</p>
</div>
) : (
auditData?.map((event: GovernanceEvent) => (
<AuditCard key={event.id} event={event} />
))
)}
</div>
)}
{activeTab === 'actions' && (
<div className="space-y-6">
{!actorId ? (
<div className="text-center py-12 text-slate-400">
<div className="text-4xl mb-4">🔒</div>
<p>Увійдіть щоб виконувати governance-дії</p>
</div>
) : (
<>
{/* Promote Agent */}
<div className="bg-slate-800/50 rounded-lg p-4 border border-slate-700">
<h3 className="font-medium text-white mb-4"> Підвищити агента</h3>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm text-slate-400 mb-1">Agent ID</label>
<input
type="text"
value={selectedAgent || ''}
onChange={(e) => setSelectedAgent(e.target.value)}
placeholder="agent-id..."
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Новий рівень</label>
<select
value={promoteLevel}
onChange={(e) => setPromoteLevel(e.target.value as AgentGovLevel)}
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm"
>
<option value="worker">{GOV_LEVEL_LABELS.worker}</option>
<option value="core_team">{GOV_LEVEL_LABELS.core_team}</option>
<option value="orchestrator">{GOV_LEVEL_LABELS.orchestrator}</option>
</select>
</div>
</div>
<button
onClick={() => selectedAgent && promoteMutation.mutate({
targetId: selectedAgent,
newLevel: promoteLevel
})}
disabled={!selectedAgent || promoteMutation.isPending}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded-lg text-sm font-medium"
>
{promoteMutation.isPending ? 'Підвищення...' : 'Підвищити'}
</button>
{promoteMutation.error && (
<div className="mt-2 text-red-400 text-sm">
{(promoteMutation.error as Error).message}
</div>
)}
</div>
{/* Quick Actions */}
<div className="bg-slate-800/50 rounded-lg p-4 border border-slate-700">
<h3 className="font-medium text-white mb-4"> Швидкі дії</h3>
<div className="flex flex-wrap gap-2">
<button className="px-3 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-sm">
📋 Експорт команди
</button>
<button className="px-3 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-sm">
📊 Аудит звіт
</button>
<button className="px-3 py-2 bg-orange-600/20 hover:bg-orange-600/30 text-orange-300 rounded-lg text-sm">
Створити інцидент
</button>
</div>
</div>
</>
)}
</div>
)}
</div>
{/* Promote Modal */}
{selectedAgent && activeTab === 'team' && actorId && (
<PromoteModal
agentId={selectedAgent}
onClose={() => setSelectedAgent(null)}
onPromote={(level) => promoteMutation.mutate({ targetId: selectedAgent, newLevel: level })}
isPending={promoteMutation.isPending}
/>
)}
</div>
);
};
// Helper Components
const TeamSection: React.FC<{
title: string;
level: AgentGovLevel;
agents: Array<{ id: string; name: string; level: AgentGovLevel; status: string }>;
onSelect?: (id: string) => void;
}> = ({ title, level, agents, onSelect }) => {
if (agents.length === 0) return null;
return (
<div>
<h3 className="text-sm font-medium text-slate-400 mb-3">{title}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{agents.map((agent) => (
<div
key={agent.id}
onClick={() => onSelect?.(agent.id)}
className={`bg-slate-800/50 rounded-lg p-3 border border-slate-700 ${
onSelect ? 'cursor-pointer hover:border-slate-500' : ''
}`}
>
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-white">{agent.name}</div>
<div className="text-xs text-slate-400">{agent.id}</div>
</div>
<GovernanceLevelBadge level={level} size="sm" />
</div>
</div>
))}
</div>
</div>
);
};
const IncidentCard: React.FC<{ incident: Incident }> = ({ incident }) => (
<div className="rounded-lg p-4 border bg-slate-800/50 border-slate-700">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-white">{incident.title}</div>
<div className="text-sm text-slate-400 mt-1">
{new Date(incident.createdAt).toLocaleDateString('uk-UA')}
</div>
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${
incident.priority === 'critical' ? 'bg-red-500 text-white' :
incident.priority === 'high' ? 'bg-orange-500 text-white' :
incident.priority === 'medium' ? 'bg-yellow-500 text-black' :
'bg-gray-500 text-white'
}`}>
{incident.priority}
</span>
</div>
</div>
);
const AuditCard: React.FC<{ event: GovernanceEvent }> = ({ event }) => (
<div className="rounded-lg p-3 bg-slate-800/50 border border-slate-700 text-sm">
<div className="flex items-center justify-between">
<span className="text-white">{event.eventType}</span>
<span className="text-slate-500 text-xs">
{new Date(event.createdAt).toLocaleString('uk-UA')}
</span>
</div>
</div>
);
const PromoteModal: React.FC<{
agentId: string;
onClose: () => void;
onPromote: (level: AgentGovLevel) => void;
isPending: boolean;
}> = ({ agentId, onClose, onPromote, isPending }) => {
const [level, setLevel] = useState<AgentGovLevel>('worker');
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-xl border border-slate-600 max-w-sm w-full">
<div className="flex items-center justify-between p-4 border-b border-slate-700">
<h3 className="font-bold text-white">Підвищити агента</h3>
<button onClick={onClose} className="text-slate-400 hover:text-white"></button>
</div>
<div className="p-6 space-y-4">
<div>
<div className="text-sm text-slate-400">Agent ID:</div>
<div className="text-white font-mono">{agentId}</div>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Новий рівень</label>
<select
value={level}
onChange={(e) => setLevel(e.target.value as AgentGovLevel)}
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
>
<option value="member">{GOV_LEVEL_LABELS.member}</option>
<option value="worker">{GOV_LEVEL_LABELS.worker}</option>
<option value="core_team">{GOV_LEVEL_LABELS.core_team}</option>
</select>
</div>
<div className="flex gap-3 pt-2">
<button
onClick={onClose}
className="flex-1 px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg"
>
Скасувати
</button>
<button
onClick={() => onPromote(level)}
disabled={isPending}
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded-lg font-medium"
>
{isPending ? '...' : 'Підвищити'}
</button>
</div>
</div>
</div>
</div>
);
};
export default MicroDAOGovernancePanel;

View File

@@ -5,6 +5,8 @@
// Components
export { GovernanceLevelBadge } from './components/GovernanceLevelBadge';
export { CityGovernancePanel } from './components/CityGovernancePanel';
export { DistrictGovernancePanel } from './components/DistrictGovernancePanel';
export { MicroDAOGovernancePanel } from './components/MicroDAOGovernancePanel';
export { AuditDashboard } from './components/AuditDashboard';
export { IncidentsList } from './components/IncidentsList';
export { ReportButton } from './components/ReportButton';

View File

@@ -0,0 +1,56 @@
/**
* District Governance Page
* /governance/district/:districtId
*/
import React from 'react';
import { useParams, Link } from 'react-router-dom';
import { DistrictGovernancePanel } from '../features/governance/components/DistrictGovernancePanel';
export function DistrictGovernancePage() {
const { districtId } = useParams<{ districtId: string }>();
// TODO: Get actual actorDaisId from auth context
const actorDaisId = 'dais-demo-user';
if (!districtId) {
return (
<div className="min-h-screen bg-slate-950 text-white flex items-center justify-center">
<div className="text-center">
<div className="text-4xl mb-4"></div>
<p>District ID не вказано</p>
<Link to="/governance" className="text-blue-400 hover:underline mt-4 block">
Назад до Governance
</Link>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-slate-950 text-white">
{/* Breadcrumb */}
<div className="bg-slate-900 border-b border-slate-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center gap-2 text-sm text-slate-400">
<Link to="/governance" className="hover:text-white">Governance</Link>
<span></span>
<span className="text-white">District: {districtId}</span>
</div>
</div>
</div>
{/* Panel */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<DistrictGovernancePanel
districtId={districtId}
districtName={districtId}
actorId={actorDaisId}
/>
</div>
</div>
);
}
export default DistrictGovernancePage;

View File

@@ -0,0 +1,56 @@
/**
* MicroDAO Governance Page
* /governance/microdao/:microdaoId
*/
import React from 'react';
import { useParams, Link } from 'react-router-dom';
import { MicroDAOGovernancePanel } from '../features/governance/components/MicroDAOGovernancePanel';
export function MicroDAOGovernancePage() {
const { microdaoId } = useParams<{ microdaoId: string }>();
// TODO: Get actual actorDaisId from auth context
const actorDaisId = 'dais-demo-user';
if (!microdaoId) {
return (
<div className="min-h-screen bg-slate-950 text-white flex items-center justify-center">
<div className="text-center">
<div className="text-4xl mb-4"></div>
<p>MicroDAO ID не вказано</p>
<Link to="/governance" className="text-blue-400 hover:underline mt-4 block">
Назад до Governance
</Link>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-slate-950 text-white">
{/* Breadcrumb */}
<div className="bg-slate-900 border-b border-slate-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center gap-2 text-sm text-slate-400">
<Link to="/governance" className="hover:text-white">Governance</Link>
<span></span>
<span className="text-white">MicroDAO: {microdaoId}</span>
</div>
</div>
</div>
{/* Panel */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<MicroDAOGovernancePanel
microdaoId={microdaoId}
microdaoName={microdaoId}
actorId={actorDaisId}
/>
</div>
</div>
);
}
export default MicroDAOGovernancePage;