feat(district): District Interface Architecture v1
## Documentation - District_Interface_Architecture_v1.md - повна архітектура District Layer - District Space, Campus Map, Sub-DAOs, Portals - Золотий трикутник: City → District → MicroDAO ## Database - Migration 028: District rooms for Energyunion & GREENFOOD - Portal rooms on City Square ## Frontend - src/api/districts.ts - Districts API client - DistrictDashboard.tsx - District Dashboard UI component ## Key concepts: - District = MicroDAO type='district' - District Lead Agent (Helion, ERP-Agent) - Campus Map (2D) - Sub-DAOs management - District-to-City portals
This commit is contained in:
211
src/api/districts.ts
Normal file
211
src/api/districts.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Districts API Client
|
||||
* Based on: docs/foundation/District_Interface_Architecture_v1.md
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import type {
|
||||
Microdao,
|
||||
Room,
|
||||
Node,
|
||||
Agent,
|
||||
AgentAssignment
|
||||
} from '../types/ontology';
|
||||
|
||||
const BASE_URL = '/api/v1/districts';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface DistrictStats {
|
||||
subDaoCount: number;
|
||||
activeAgents: number;
|
||||
nodeCount: number;
|
||||
memberCount: number;
|
||||
eventsThisWeek: number;
|
||||
gpuCapacity?: string;
|
||||
}
|
||||
|
||||
export interface DistrictMapElement {
|
||||
id: string;
|
||||
type: 'subdao' | 'room' | 'node' | 'agent' | 'portal';
|
||||
x: number;
|
||||
y: number;
|
||||
zone: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
targetId: string;
|
||||
}
|
||||
|
||||
export interface DistrictMap {
|
||||
districtId: string;
|
||||
name: string;
|
||||
dimensions: { width: number; height: number };
|
||||
zones: string[];
|
||||
elements: DistrictMapElement[];
|
||||
background?: string;
|
||||
}
|
||||
|
||||
export interface DistrictDashboard {
|
||||
district: Microdao & { type: 'district' };
|
||||
primaryAgent: Agent;
|
||||
teamAgents: Agent[];
|
||||
stats: DistrictStats;
|
||||
rooms: Room[];
|
||||
subdaos: Microdao[];
|
||||
nodes: Node[];
|
||||
recentEvents: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
timestamp: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface District extends Microdao {
|
||||
type: 'district';
|
||||
leadAgentId: string;
|
||||
portalRoomId?: string;
|
||||
campusMapId?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get all districts
|
||||
*/
|
||||
export async function getDistricts(): Promise<District[]> {
|
||||
const response = await apiClient.get<{ data: District[] }>(BASE_URL);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get district by ID
|
||||
*/
|
||||
export async function getDistrict(districtId: string): Promise<District> {
|
||||
const response = await apiClient.get<{ data: District }>(
|
||||
`${BASE_URL}/${districtId}`
|
||||
);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get district dashboard
|
||||
*/
|
||||
export async function getDistrictDashboard(
|
||||
districtId: string
|
||||
): Promise<DistrictDashboard> {
|
||||
const response = await apiClient.get<{ data: DistrictDashboard }>(
|
||||
`${BASE_URL}/${districtId}/dashboard`
|
||||
);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get district rooms
|
||||
*/
|
||||
export async function getDistrictRooms(districtId: string): Promise<Room[]> {
|
||||
const response = await apiClient.get<{ data: Room[] }>(
|
||||
`${BASE_URL}/${districtId}/rooms`
|
||||
);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get district sub-DAOs
|
||||
*/
|
||||
export async function getDistrictSubDaos(
|
||||
districtId: string
|
||||
): Promise<Microdao[]> {
|
||||
const response = await apiClient.get<{ data: Microdao[] }>(
|
||||
`${BASE_URL}/${districtId}/subdaos`
|
||||
);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get district campus map
|
||||
*/
|
||||
export async function getDistrictMap(districtId: string): Promise<DistrictMap> {
|
||||
const response = await apiClient.get<{ data: DistrictMap }>(
|
||||
`${BASE_URL}/${districtId}/map`
|
||||
);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get district agents
|
||||
*/
|
||||
export async function getDistrictAgents(districtId: string): Promise<Agent[]> {
|
||||
const response = await apiClient.get<{ data: Agent[] }>(
|
||||
`${BASE_URL}/${districtId}/agents`
|
||||
);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get district nodes
|
||||
*/
|
||||
export async function getDistrictNodes(districtId: string): Promise<Node[]> {
|
||||
const response = await apiClient.get<{ data: Node[] }>(
|
||||
`${BASE_URL}/${districtId}/nodes`
|
||||
);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get district stats
|
||||
*/
|
||||
export async function getDistrictStats(
|
||||
districtId: string
|
||||
): Promise<DistrictStats> {
|
||||
const response = await apiClient.get<{ data: DistrictStats }>(
|
||||
`${BASE_URL}/${districtId}/stats`
|
||||
);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add sub-DAO to district
|
||||
*/
|
||||
export async function addSubDao(
|
||||
districtId: string,
|
||||
microdaoId: string
|
||||
): Promise<void> {
|
||||
await apiClient.post(`${BASE_URL}/${districtId}/subdaos`, { microdaoId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create district room
|
||||
*/
|
||||
export async function createDistrictRoom(
|
||||
districtId: string,
|
||||
room: Partial<Room>
|
||||
): Promise<Room> {
|
||||
const response = await apiClient.post<{ data: Room }>(
|
||||
`${BASE_URL}/${districtId}/rooms`,
|
||||
room
|
||||
);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// React Query Keys
|
||||
// ============================================================================
|
||||
|
||||
export const districtKeys = {
|
||||
all: ['districts'] as const,
|
||||
list: () => [...districtKeys.all, 'list'] as const,
|
||||
detail: (id: string) => [...districtKeys.all, 'detail', id] as const,
|
||||
dashboard: (id: string) => [...districtKeys.all, 'dashboard', id] as const,
|
||||
rooms: (id: string) => [...districtKeys.all, 'rooms', id] as const,
|
||||
subdaos: (id: string) => [...districtKeys.all, 'subdaos', id] as const,
|
||||
map: (id: string) => [...districtKeys.all, 'map', id] as const,
|
||||
agents: (id: string) => [...districtKeys.all, 'agents', id] as const,
|
||||
nodes: (id: string) => [...districtKeys.all, 'nodes', id] as const,
|
||||
stats: (id: string) => [...districtKeys.all, 'stats', id] as const,
|
||||
};
|
||||
|
||||
290
src/features/district/components/DistrictDashboard.tsx
Normal file
290
src/features/district/components/DistrictDashboard.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* District Dashboard Component
|
||||
* Based on: docs/foundation/District_Interface_Architecture_v1.md
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { getDistrictDashboard, districtKeys } from '../../../api/districts';
|
||||
import { MicrodaoTypeBadge, NodeStatusBadge } from '../../ontology/components/OntologyBadge';
|
||||
|
||||
export function DistrictDashboard() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const { data: dashboard, isLoading, error } = useQuery({
|
||||
queryKey: districtKeys.dashboard(id || ''),
|
||||
queryFn: () => getDistrictDashboard(id || ''),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-gray-800 rounded w-1/3"></div>
|
||||
<div className="h-64 bg-gray-800 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !dashboard) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 p-6 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl text-red-400 mb-2">District not found</h2>
|
||||
<Link to="/city" className="text-blue-400 hover:underline">
|
||||
← Back to City
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { district, primaryAgent, teamAgents, stats, rooms, subdaos, nodes } = dashboard;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
{/* Header */}
|
||||
<header className="bg-gray-800 border-b border-gray-700 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/city" className="text-gray-400 hover:text-white">
|
||||
← City
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold">{district.name}</h1>
|
||||
<MicrodaoTypeBadge type="district" />
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-sm">
|
||||
⚙️ Settings
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Top Row: Lead Agent + Stats */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Lead Agent Card */}
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-xl p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-blue-500 rounded-full flex items-center justify-center text-2xl">
|
||||
🔋
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{primaryAgent?.name || 'Lead Agent'}</h3>
|
||||
<p className="text-gray-400 text-sm">District Lead</p>
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-500/20 text-green-400 rounded-full text-xs mt-1">
|
||||
● Online
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="mt-4 w-full py-2 bg-purple-600 hover:bg-purple-700 rounded-lg text-sm font-medium transition-colors">
|
||||
💬 Chat with {primaryAgent?.name || 'Lead'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats Card */}
|
||||
<div className="lg:col-span-2 bg-gray-800 border border-gray-700 rounded-xl p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">District Statistics</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatItem label="Sub-DAOs" value={stats.subDaoCount} icon="🏢" />
|
||||
<StatItem label="Agents" value={stats.activeAgents} icon="🤖" />
|
||||
<StatItem label="Nodes" value={stats.nodeCount} icon="🖥️" />
|
||||
<StatItem label="Members" value={stats.memberCount} icon="👥" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Campus Map Preview */}
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">🗺️ Campus Map</h3>
|
||||
<Link
|
||||
to={`/district/${id}/map`}
|
||||
className="text-blue-400 hover:text-blue-300 text-sm"
|
||||
>
|
||||
View Full Map →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="bg-gray-700/50 rounded-lg p-8 text-center border-2 border-dashed border-gray-600">
|
||||
<div className="text-4xl mb-2">🗺️</div>
|
||||
<p className="text-gray-400">Campus Map Preview</p>
|
||||
<p className="text-gray-500 text-sm mt-1">Interactive map coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rooms Row */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">💬 District Rooms</h3>
|
||||
<Link
|
||||
to={`/district/${id}/rooms`}
|
||||
className="text-blue-400 hover:text-blue-300 text-sm"
|
||||
>
|
||||
View All →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{rooms.slice(0, 4).map((room) => (
|
||||
<RoomCard key={room.id} room={room} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub-DAOs Row */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">🏢 Sub-DAOs</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
to={`/district/${id}/subdaos`}
|
||||
className="text-blue-400 hover:text-blue-300 text-sm"
|
||||
>
|
||||
View All →
|
||||
</Link>
|
||||
<button className="px-3 py-1 bg-green-600 hover:bg-green-700 rounded text-sm">
|
||||
+ Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{subdaos.length > 0 ? (
|
||||
subdaos.slice(0, 3).map((dao) => (
|
||||
<SubDaoCard key={dao.id} dao={dao} />
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-3 bg-gray-800 border border-dashed border-gray-600 rounded-xl p-8 text-center">
|
||||
<p className="text-gray-400">No sub-DAOs yet</p>
|
||||
<button className="mt-2 text-blue-400 hover:text-blue-300 text-sm">
|
||||
+ Add first Sub-DAO
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Team Agents */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">🤖 Team Agents</h3>
|
||||
<Link
|
||||
to={`/district/${id}/agents`}
|
||||
className="text-blue-400 hover:text-blue-300 text-sm"
|
||||
>
|
||||
View All →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{teamAgents.map((agent) => (
|
||||
<AgentCard key={agent.id} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nodes */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">🖥️ District Nodes</h3>
|
||||
<Link
|
||||
to={`/district/${id}/nodes`}
|
||||
className="text-blue-400 hover:text-blue-300 text-sm"
|
||||
>
|
||||
View All →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{nodes.slice(0, 3).map((node) => (
|
||||
<NodeCard key={node.id} node={node} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Components
|
||||
// ============================================================================
|
||||
|
||||
function StatItem({ label, value, icon }: { label: string; value: number; icon: string }) {
|
||||
return (
|
||||
<div className="bg-gray-700/50 rounded-lg p-4 text-center">
|
||||
<div className="text-2xl mb-1">{icon}</div>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
<div className="text-gray-400 text-sm">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RoomCard({ room }: { room: any }) {
|
||||
const icons: Record<string, string> = {
|
||||
'district-room': '💬',
|
||||
'front-room': '🚪',
|
||||
'city-room': '🏛️',
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/room/${room.id}`}
|
||||
className="bg-gray-800 border border-gray-700 hover:border-gray-600 rounded-xl p-4 transition-colors"
|
||||
>
|
||||
<div className="text-2xl mb-2">{icons[room.type] || '💬'}</div>
|
||||
<h4 className="font-medium text-sm">{room.name}</h4>
|
||||
<p className="text-gray-500 text-xs mt-1">{room.visibility}</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function SubDaoCard({ dao }: { dao: any }) {
|
||||
return (
|
||||
<Link
|
||||
to={`/microdao/${dao.id}`}
|
||||
className="bg-gray-800 border border-gray-700 hover:border-gray-600 rounded-xl p-4 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-lg flex items-center justify-center">
|
||||
🏢
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">{dao.name}</h4>
|
||||
<p className="text-gray-500 text-xs">{dao.slug}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentCard({ agent }: { agent: any }) {
|
||||
return (
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-full flex items-center justify-center">
|
||||
🤖
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-sm">{agent.name}</h4>
|
||||
<p className="text-gray-500 text-xs">{agent.kind}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NodeCard({ node }: { node: any }) {
|
||||
return (
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-sm">{node.name || node.nodeId}</h4>
|
||||
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 rounded text-xs">
|
||||
{node.status || 'active'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs">{node.kind || 'datacenter'}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DistrictDashboard;
|
||||
|
||||
Reference in New Issue
Block a user