diff --git a/src/App.tsx b/src/App.tsx index f44f4eaf..0fd7714a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> {/* Governance Engine */} } /> + } /> + } /> } /> } /> {/* Task 039: Agent Console v2 */} diff --git a/src/features/governance/components/DistrictGovernancePanel.tsx b/src/features/governance/components/DistrictGovernancePanel.tsx new file mode 100644 index 00000000..cc4316b0 --- /dev/null +++ b/src/features/governance/components/DistrictGovernancePanel.tsx @@ -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 = ({ + 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 ( +
+ {/* Header */} +
+
+
+

+ 🏘️ {districtName || districtId} Governance +

+

+ District-level управління +

+
+ + {lead && ( +
+
+ 👤 +
+
+
{lead.agentName}
+
District Lead
+
+ +
+ )} +
+
+ + {/* Tabs */} +
+ {[ + { id: 'overview', label: '📊 Overview', icon: '📊' }, + { id: 'daos', label: '🏢 Sub-DAOs', icon: '🏢' }, + { id: 'incidents', label: '⚠️ Incidents', icon: '⚠️' }, + { id: 'audit', label: '📋 Audit', icon: '📋' }, + ].map((tab) => ( + + ))} +
+ + {/* Content */} +
+ {activeTab === 'overview' && ( +
+ {/* Stats Cards */} + + + i.status === 'open').length || 0} + color="red" + /> + + {/* Lead Agent Info */} + {lead && ( +
+

District Lead Agent

+
+
+ 👤 +
+
+
{lead.agentName}
+
{lead.districtName}
+
+ +
+
+
+
+ )} +
+ )} + + {activeTab === 'daos' && ( +
+
🏢
+

Sub-DAOs для цього дистрикту будуть показані тут

+

Потрібна інтеграція з MicroDAO API

+
+ )} + + {activeTab === 'incidents' && ( +
+ {incidentsData?.incidents.length === 0 ? ( +
+
+

Немає відкритих інцидентів на рівні дистрикту

+
+ ) : ( + incidentsData?.incidents.map((incident: Incident) => ( + + )) + )} +
+ )} + + {activeTab === 'audit' && ( +
+ {auditData?.length === 0 ? ( +
+

Немає подій для цього дистрикту

+
+ ) : ( + auditData?.map((event: GovernanceEvent) => ( + + )) + )} +
+ )} +
+
+ ); +}; + +// 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 ( +
+
+ {icon} + {label} +
+
{value}
+
+ ); +}; + +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 ( +
+
+
+
{incident.title}
+
+ {incident.targetScopeType}: {incident.targetScopeId} +
+
+ + {incident.status} + +
+
+ ); +}; + +const AuditRow: React.FC<{ event: GovernanceEvent }> = ({ event }) => ( +
+
+ {event.eventType} + + {new Date(event.createdAt).toLocaleString('uk-UA')} + +
+
+ {event.actorId} → {event.targetId} +
+
+); + +export default DistrictGovernancePanel; + diff --git a/src/features/governance/components/MicroDAOGovernancePanel.tsx b/src/features/governance/components/MicroDAOGovernancePanel.tsx new file mode 100644 index 00000000..d834a58c --- /dev/null +++ b/src/features/governance/components/MicroDAOGovernancePanel.tsx @@ -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 = ({ + microdaoId, + microdaoName, + actorId, +}) => { + const queryClient = useQueryClient(); + const [activeTab, setActiveTab] = useState<'team' | 'incidents' | 'audit' | 'actions'>('team'); + const [selectedAgent, setSelectedAgent] = useState(null); + const [promoteLevel, setPromoteLevel] = useState('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 ( +
+ {/* Header */} +
+
+
+

+ 🏢 {microdaoName || microdaoId} Governance +

+

+ MicroDAO-level управління +

+
+ +
+ + {daoOrchestrators.length} Orchestrators + + + {daoCoreTeam.length} Core-team + + + {daoWorkers.length} Workers + +
+
+
+ + {/* Tabs */} +
+ {[ + { id: 'team', label: '👥 Team' }, + { id: 'incidents', label: '⚠️ Incidents' }, + { id: 'audit', label: '📋 Audit' }, + { id: 'actions', label: '⚡ Actions' }, + ].map((tab) => ( + + ))} +
+ + {/* Content */} +
+ {activeTab === 'team' && ( +
+ {/* Orchestrators */} + + + {/* Core-team */} + + + {/* Workers */} + + + {/* Empty state */} + {daoOrchestrators.length === 0 && daoCoreTeam.length === 0 && daoWorkers.length === 0 && ( +
+
👥
+

Немає агентів, привʼязаних до цього DAO

+

Агенти зʼявляться після їх призначення

+
+ )} +
+ )} + + {activeTab === 'incidents' && ( +
+ {incidentsData?.incidents.length === 0 ? ( +
+
+

Немає відкритих інцидентів у цьому DAO

+
+ ) : ( + incidentsData?.incidents.map((incident: Incident) => ( + + )) + )} +
+ )} + + {activeTab === 'audit' && ( +
+ {auditData?.length === 0 ? ( +
+

Немає governance-подій для цього DAO

+
+ ) : ( + auditData?.map((event: GovernanceEvent) => ( + + )) + )} +
+ )} + + {activeTab === 'actions' && ( +
+ {!actorId ? ( +
+
🔒
+

Увійдіть щоб виконувати governance-дії

+
+ ) : ( + <> + {/* Promote Agent */} +
+

⬆️ Підвищити агента

+ +
+
+ + 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" + /> +
+
+ + +
+
+ + + + {promoteMutation.error && ( +
+ {(promoteMutation.error as Error).message} +
+ )} +
+ + {/* Quick Actions */} +
+

⚡ Швидкі дії

+
+ + + +
+
+ + )} +
+ )} +
+ + {/* Promote Modal */} + {selectedAgent && activeTab === 'team' && actorId && ( + setSelectedAgent(null)} + onPromote={(level) => promoteMutation.mutate({ targetId: selectedAgent, newLevel: level })} + isPending={promoteMutation.isPending} + /> + )} +
+ ); +}; + +// 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 ( +
+

{title}

+
+ {agents.map((agent) => ( +
onSelect?.(agent.id)} + className={`bg-slate-800/50 rounded-lg p-3 border border-slate-700 ${ + onSelect ? 'cursor-pointer hover:border-slate-500' : '' + }`} + > +
+
+
{agent.name}
+
{agent.id}
+
+ +
+
+ ))} +
+
+ ); +}; + +const IncidentCard: React.FC<{ incident: Incident }> = ({ incident }) => ( +
+
+
+
{incident.title}
+
+ {new Date(incident.createdAt).toLocaleDateString('uk-UA')} +
+
+ + {incident.priority} + +
+
+); + +const AuditCard: React.FC<{ event: GovernanceEvent }> = ({ event }) => ( +
+
+ {event.eventType} + + {new Date(event.createdAt).toLocaleString('uk-UA')} + +
+
+); + +const PromoteModal: React.FC<{ + agentId: string; + onClose: () => void; + onPromote: (level: AgentGovLevel) => void; + isPending: boolean; +}> = ({ agentId, onClose, onPromote, isPending }) => { + const [level, setLevel] = useState('worker'); + + return ( +
+
+
+

Підвищити агента

+ +
+ +
+
+
Agent ID:
+
{agentId}
+
+ +
+ + +
+ +
+ + +
+
+
+
+ ); +}; + +export default MicroDAOGovernancePanel; + diff --git a/src/features/governance/index.ts b/src/features/governance/index.ts index 1e740a9e..a0e3be38 100644 --- a/src/features/governance/index.ts +++ b/src/features/governance/index.ts @@ -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'; diff --git a/src/pages/DistrictGovernancePage.tsx b/src/pages/DistrictGovernancePage.tsx new file mode 100644 index 00000000..1b3ee18f --- /dev/null +++ b/src/pages/DistrictGovernancePage.tsx @@ -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 ( +
+
+
+

District ID не вказано

+ + ← Назад до Governance + +
+
+ ); + } + + return ( +
+ {/* Breadcrumb */} +
+
+
+ Governance + + District: {districtId} +
+
+
+ + {/* Panel */} +
+ +
+
+ ); +} + +export default DistrictGovernancePage; + diff --git a/src/pages/MicroDAOGovernancePage.tsx b/src/pages/MicroDAOGovernancePage.tsx new file mode 100644 index 00000000..608f7608 --- /dev/null +++ b/src/pages/MicroDAOGovernancePage.tsx @@ -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 ( +
+
+
+

MicroDAO ID не вказано

+ + ← Назад до Governance + +
+
+ ); + } + + return ( +
+ {/* Breadcrumb */} +
+
+
+ Governance + + MicroDAO: {microdaoId} +
+
+
+ + {/* Panel */} +
+ +
+
+ ); +} + +export default MicroDAOGovernancePage; +