diff --git a/src/App.tsx b/src/App.tsx
index ead6a21f..f44f4eaf 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -43,6 +43,8 @@ import { SettingsPage } from './pages/SettingsPage';
import { CityRoomsPage } from './features/city/rooms/CityRoomsPage';
import { CityRoomView } from './features/city/rooms/CityRoomView';
import { SecondMePage } from './features/secondme/SecondMePage';
+// Governance Engine
+import { GovernancePage } from './pages/GovernancePage';
function App() {
return (
@@ -56,6 +58,8 @@ function App() {
} />
} />
} />
+ {/* Governance Engine */}
+ } />
} />
} />
{/* Task 039: Agent Console v2 */}
diff --git a/src/features/agentHub/AgentCabinet.tsx b/src/features/agentHub/AgentCabinet.tsx
index 1952d849..c4f560d7 100644
--- a/src/features/agentHub/AgentCabinet.tsx
+++ b/src/features/agentHub/AgentCabinet.tsx
@@ -1,9 +1,14 @@
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
+import { useQuery } from '@tanstack/react-query';
import { useAgentDashboard } from './hooks/useAgentDashboard';
import { VisibilityCard } from './components/VisibilityCard';
import { AgentChatWidget } from './components/AgentChatWidget';
import { MicroDaoWizard } from './components/MicroDaoWizard';
+import { GovernanceLevelBadge } from '../governance/components/GovernanceLevelBadge';
+import { governanceApi } from '../../api/governance';
+import { GOV_LEVEL_LABELS, POWER_LABELS } from '../../types/governance';
+import type { AgentGovLevel, GovernancePower } from '../../types/governance';
export function AgentCabinet() {
const { agentId } = useParams<{ agentId: string }>();
@@ -213,6 +218,9 @@ export function AgentCabinet() {
+ {/* Governance & Roles Block */}
+
+
{/* Node Info */}
{node && (
@@ -254,4 +262,106 @@ export function AgentCabinet() {
);
}
+// ============================================================================
+// Governance Roles Block
+// ============================================================================
+function GovernanceRolesBlock({ agentId }: { agentId: string }) {
+ const { data: roles, isLoading, error } = useQuery({
+ queryKey: ['governance', 'agent', agentId, 'roles'],
+ queryFn: () => governanceApi.getAgentRoles(agentId),
+ retry: false,
+ });
+
+ if (isLoading) {
+ return (
+
+
🛡️ Ролі та Повноваження
+
+
+ );
+ }
+
+ if (error || !roles) {
+ return (
+
+
🛡️ Ролі та Повноваження
+
Governance API недоступний
+
+ );
+ }
+
+ const levelLabel = GOV_LEVEL_LABELS[roles.level as AgentGovLevel] || roles.level;
+ const powerLabels = roles.powers.map((p: GovernancePower) => POWER_LABELS[p] || p);
+
+ return (
+
+
🛡️ Ролі та Повноваження
+
+ {/* Level Badge */}
+
+ Рівень:
+
+
+
+ {/* Status */}
+
+ Статус:
+
+ {roles.status === 'active' ? '✅ Активний' :
+ roles.status === 'suspended' ? '⏸️ Призупинено' :
+ '🚫 Заблоковано'}
+
+
+
+ {/* Powers */}
+ {roles.powers.length > 0 && (
+
+
Повноваження:
+
+ {powerLabels.map((power: string, i: number) => (
+
+ {power}
+
+ ))}
+
+
+ )}
+
+ {/* Assignments */}
+ {roles.assignments.length > 0 && (
+
+
Призначення:
+
+ {roles.assignments.map((a: { microdaoId: string; role: string; scope: string }, i: number) => (
+
+ {a.microdaoId}
+
+ {a.role}
+
+
+ ))}
+
+
+ )}
+
+ {/* Actions (placeholder for governance actions) */}
+
+
+ );
+}
diff --git a/src/features/governance/components/ReportButton.tsx b/src/features/governance/components/ReportButton.tsx
new file mode 100644
index 00000000..6aea1ad9
--- /dev/null
+++ b/src/features/governance/components/ReportButton.tsx
@@ -0,0 +1,192 @@
+/**
+ * Report Button
+ * Universal button to create incidents from any context
+ */
+
+import React, { useState } from 'react';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { incidentsApi } from '../../../api/incidents';
+import type { IncidentPriority, TargetScopeType } from '../../../types/governance';
+import { INCIDENT_PRIORITY_LABELS } from '../../../types/governance';
+
+interface ReportButtonProps {
+ targetScopeType: TargetScopeType;
+ targetScopeId: string;
+ actorDaisId?: string;
+ variant?: 'icon' | 'text' | 'full';
+ className?: string;
+ defaultTitle?: string;
+}
+
+export const ReportButton: React.FC
= ({
+ targetScopeType,
+ targetScopeId,
+ actorDaisId,
+ variant = 'icon',
+ className = '',
+ defaultTitle = '',
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [form, setForm] = useState({
+ title: defaultTitle,
+ description: '',
+ priority: 'medium' as IncidentPriority,
+ });
+
+ const queryClient = useQueryClient();
+
+ const createMutation = useMutation({
+ mutationFn: () => {
+ if (!actorDaisId) {
+ throw new Error('Actor DAIS ID is required');
+ }
+ return incidentsApi.createIncident({
+ createdByDaisId: actorDaisId,
+ targetScopeType,
+ targetScopeId,
+ priority: form.priority,
+ title: form.title,
+ description: form.description || undefined,
+ });
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['incidents'] });
+ setIsOpen(false);
+ setForm({ title: defaultTitle, description: '', priority: 'medium' });
+ },
+ });
+
+ if (!actorDaisId) {
+ return null;
+ }
+
+ const renderButton = () => {
+ switch (variant) {
+ case 'icon':
+ return (
+
+ );
+ case 'text':
+ return (
+
+ );
+ case 'full':
+ return (
+
+ );
+ }
+ };
+
+ return (
+ <>
+ {renderButton()}
+
+ {isOpen && (
+
+
+
+
Повідомити про проблему
+
+
+
+
+
+
Об'єкт скарги:
+
+ {targetScopeType}: {targetScopeId}
+
+
+
+
+
+ setForm(prev => ({ ...prev, title: e.target.value }))}
+ placeholder="Коротко опишіть проблему..."
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-red-500 focus:border-red-500"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {createMutation.error && (
+
+ {(createMutation.error as Error).message}
+
+ )}
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+};
+
+export default ReportButton;
+
diff --git a/src/features/governance/index.ts b/src/features/governance/index.ts
new file mode 100644
index 00000000..1e740a9e
--- /dev/null
+++ b/src/features/governance/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Governance Feature Exports
+ */
+
+// Components
+export { GovernanceLevelBadge } from './components/GovernanceLevelBadge';
+export { CityGovernancePanel } from './components/CityGovernancePanel';
+export { AuditDashboard } from './components/AuditDashboard';
+export { IncidentsList } from './components/IncidentsList';
+export { ReportButton } from './components/ReportButton';
+
diff --git a/src/pages/GovernancePage.tsx b/src/pages/GovernancePage.tsx
new file mode 100644
index 00000000..b7efadf0
--- /dev/null
+++ b/src/pages/GovernancePage.tsx
@@ -0,0 +1,116 @@
+/**
+ * Governance Page
+ * Main governance dashboard with tabs for City, Audit, Incidents
+ */
+
+import React, { useState } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import { CityGovernancePanel } from '../features/governance/components/CityGovernancePanel';
+import { AuditDashboard } from '../features/governance/components/AuditDashboard';
+import { IncidentsList } from '../features/governance/components/IncidentsList';
+
+type Tab = 'city' | 'audit' | 'incidents';
+
+export function GovernancePage() {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const initialTab = (searchParams.get('tab') as Tab) || 'city';
+ const [activeTab, setActiveTab] = useState(initialTab);
+
+ const handleTabChange = (tab: Tab) => {
+ setActiveTab(tab);
+ setSearchParams({ tab });
+ };
+
+ // TODO: Get actual actorDaisId from auth context
+ const actorDaisId = 'dais-demo-user';
+
+ return (
+
+ {/* Header */}
+
+
+
+ 🏛️ DAARION.city Governance
+
+
+ Управління агентами, ролями, інцидентами та аудит подій
+
+
+
+
+ {/* Tabs */}
+
+
+
+ handleTabChange('city')}
+ icon="🏛️"
+ label="City Governance"
+ />
+ handleTabChange('audit')}
+ icon="📊"
+ label="Audit"
+ />
+ handleTabChange('incidents')}
+ icon="⚠️"
+ label="Incidents"
+ />
+
+
+
+
+ {/* Content */}
+
+ {activeTab === 'city' && (
+
+ )}
+
+ {activeTab === 'audit' && (
+
+ )}
+
+ {activeTab === 'incidents' && (
+
+ )}
+
+
+ );
+}
+
+// Tab Button Component
+function TabButton({
+ active,
+ onClick,
+ icon,
+ label
+}: {
+ active: boolean;
+ onClick: () => void;
+ icon: string;
+ label: string;
+}) {
+ return (
+
+ );
+}
+
+export default GovernancePage;
+