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:
@@ -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 */}
|
||||
|
||||
258
src/features/governance/components/DistrictGovernancePanel.tsx
Normal file
258
src/features/governance/components/DistrictGovernancePanel.tsx
Normal 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;
|
||||
|
||||
418
src/features/governance/components/MicroDAOGovernancePanel.tsx
Normal file
418
src/features/governance/components/MicroDAOGovernancePanel.tsx
Normal 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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
56
src/pages/DistrictGovernancePage.tsx
Normal file
56
src/pages/DistrictGovernancePage.tsx
Normal 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;
|
||||
|
||||
56
src/pages/MicroDAOGovernancePage.tsx
Normal file
56
src/pages/MicroDAOGovernancePage.tsx
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user