TASK PHASE 9 — LIVING MAP (LITE 2D UI)¶
Version: 1.0
Status: READY FOR IMPLEMENTATION
Scope: Frontend-Only 2D Interactive Map (React + Canvas)
1. Context¶
Існує або буде реалізовано living-map-service (Phase 9 FULL):
GET /living-map/snapshotWS /living-map/stream
Цей таск — чисто UI/Frontend, який:
- візуалізує стан мережі DAARION у вигляді 2D карти,
- дає змогу перемикатися між шарами (City / Space / Nodes / Agents),
- показує базові стани (online/offline, load, alerts),
- реагує на живі події (WS).
Цей 2D UI має працювати без 3D/Three.js, тільки React + Canvas.
2. Goals¶
- Створити 2D "Living Map" сторінку
/living-map. - Зробити Canvas-рендеринг 4 шарів:
- City layer (microDAO як "райони міста")
- Space layer (DAO-планети, орбіти нод)
- Nodes layer (ноди, їх завантаженість)
- Agents layer (агенти як точки/іконки)
- Підключити
useLivingMapFull(з FULL таску) або окремийuseLivingMapLite. - Забезпечити:
- панель шарів (Layer switcher),
- клік по сутності → панель деталей справа,
- zoom/pan базового рівня.
3. UI Structure¶
3.1. Routes¶
У src/App.tsx:
- Додати route:
/living-map→LivingMapPage.
3.2. Files (Frontend)¶
Створити:
src/features/livingMap/
├── LivingMapPage.tsx
├── hooks/useLivingMapLite.ts # або reuse useLivingMapFull
├── components/LivingMapCanvas.tsx
├── components/LayerSwitcher.tsx
├── components/EntityDetailsPanel.tsx
├── mini-engine/canvasRenderer.ts
└── mini-engine/layoutEngine.ts
4. Data Contract (UI Level)¶
Очікуваний формат snapshot (узгоджений з FULL таском):
type LivingMapSnapshot = {
generated_at: string;
layers: {
city: {
items: Array<{
id: string;
slug: string;
name: string;
status: "active" | "inactive" | "warning";
agents: number;
nodes: number;
}>;
};
space: {
planets: Array<{
id: string;
name: string;
type: "dao" | "platform" | "other";
status: "active" | "inactive" | "warning";
orbits: string[];
}>;
nodes: Array<{
id: string;
name: string;
status: "online" | "offline" | "warning";
cpu: number;
gpu: number;
}>;
};
nodes: {
items: Array<{
id: string;
microdao_id: string | null;
status: "online" | "offline" | "warning";
metrics: {
cpu: number;
gpu: number;
ram: number;
};
}>;
};
agents: {
items: Array<{
id: string;
name: string;
kind: string;
microdao_id: string | null;
status: "online" | "offline" | "idle";
usage: {
llm_calls_24h: number;
tokens_24h: number;
};
}>;
};
};
};
Якщо backend ще не повністю готовий — у hook'у передбачити fallback з mock-даними.
5. Hook: useLivingMapLite¶
Мета: інкапсулювати логіку:
- HTTP-запит snapshot
- WebSocket-підписка
- merge подій у локальний state
5.1. API¶
type UseLivingMapLiteResult = {
snapshot: LivingMapSnapshot | null;
isLoading: boolean;
error: string | null;
connectionStatus: "connecting" | "open" | "closed" | "error";
selectedLayer: "city" | "space" | "nodes" | "agents";
setSelectedLayer: (layer: "city" | "space" | "nodes" | "agents") => void;
selectedEntityId: string | null;
setSelectedEntityId: (id: string | null) => void;
};
5.2. Поведінка¶
-
При mount:
-
GET /living-map/snapshot - після успіху — зберегти в
snapshot - відкрити WS
/living-map/stream -
На WS повідомлення:
-
якщо
kind="event":- оновлювати відповідні
layers.*immutable-способом - При помилках:
- оновлювати відповідні
-
виставити
error - обережний reconnect (наприклад, через 5–10 сек).
6. Canvas Rendering¶
6.1. LivingMapCanvas.tsx¶
Компонент:
interface LivingMapCanvasProps {
snapshot: LivingMapSnapshot | null;
selectedLayer: "city" | "space" | "nodes" | "agents";
selectedEntityId: string | null;
onSelectEntity: (id: string | null) => void;
}
export function LivingMapCanvas(props: LivingMapCanvasProps) {
// створює <canvas>, підключає canvasRenderer
}
- Використати
useRef<HTMLCanvasElement>+useEffect. -
Передавати в
canvasRenderer: -
snapshot selectedLayerselectedEntityIdonSelectEntity- внутрішній state zoom/pan (можна зберігати тут або в hook'у).
6.2. mini-engine/canvasRenderer.ts¶
Експортувати функцію:
export function createLivingMapRenderer(opts: {
canvas: HTMLCanvasElement;
getState: () => {
snapshot: LivingMapSnapshot | null;
selectedLayer: "city" | "space" | "nodes" | "agents";
selectedEntityId: string | null;
zoom: number;
offsetX: number;
offsetY: number;
};
onSelectEntity: (id: string | null) => void;
}) {
// 1) ініціалізація контексту
// 2) підписка на mouse events
// 3) основний render loop (requestAnimationFrame)
}
Проста логіка:
-
Layer
"city": -
Рендерити прямокутники/кластери для кожного microDAO.
-
Layer
"space": -
Кола/"орбіти" для DAO-планет, ноди — точки на орбітах.
-
Layer
"nodes": -
Квадрати/іконки нод, колір залежить від
status+ bar дляcpu/gpu. -
Layer
"agents": -
Маленькі точки/іконки, колір за статусом, розмір за
usage.tokens_24h.
6.3. mini-engine/layoutEngine.ts¶
Нехай вміщає функції:
export function layoutCityLayer(/* items */) { /* x,y,w,h для кожного microDAO */ }
export function layoutSpaceLayer(/* planets, nodes */) { /* координати */ }
export function layoutNodesLayer(/* nodes */) { /* grid/cluster layout */ }
export function layoutAgentsLayer(/* agents */) { /* grid / spiral / random seeded */ }
Координати зберігати в локальному мапінгу (наприклад, Map<entityId, {x,y,w,h}>).
7. UI Components¶
7.1. LayerSwitcher.tsx¶
Простий компонент:
interface LayerSwitcherProps {
value: "city" | "space" | "nodes" | "agents";
onChange: (v: "city" | "space" | "nodes" | "agents") => void;
}
export function LayerSwitcher(props: LayerSwitcherProps) {
// 4 кнопки / pills / segmented control
}
7.2. EntityDetailsPanel.tsx¶
Показує деталі обраної сутності:
interface EntityDetailsPanelProps {
snapshot: LivingMapSnapshot | null;
selectedLayer: "city" | "space" | "nodes" | "agents";
selectedEntityId: string | null;
}
export function EntityDetailsPanel(props: EntityDetailsPanelProps) {
// шукає entity у відповідному layer
// показує name, type, status, basic metrics
// опційно: кнопки "Open Agent Hub", "Open microDAO Console", "Open DAO"
}
8. LivingMapPage.tsx¶
Складає все разом:
-
Layout:
-
Ліворуч — Canvas (70% ширини)
-
Праворуч — панель з:
- LayerSwitcher
- Connection status (WS)
- EntityDetailsPanel
- Підключає
useLivingMapLite.
Псевдокод:
export function LivingMapPage() {
const {
snapshot,
isLoading,
error,
connectionStatus,
selectedLayer,
setSelectedLayer,
selectedEntityId,
setSelectedEntityId,
} = useLivingMapLite();
return (
<div className="flex h-full">
<div className="flex-1">
<LivingMapCanvas
snapshot={snapshot}
selectedLayer={selectedLayer}
selectedEntityId={selectedEntityId}
onSelectEntity={setSelectedEntityId}
/>
</div>
<div className="w-96 border-l flex flex-col">
<LayerSwitcher value={selectedLayer} onChange={setSelectedLayer} />
{/* status + errors */}
<EntityDetailsPanel
snapshot={snapshot}
selectedLayer={selectedLayer}
selectedEntityId={selectedEntityId}
/>
</div>
</div>
);
}
9. TODO Checklist¶
- [ ] Додати route
/living-mapвApp.tsx. - [ ] Створити папку
src/features/livingMap/. - [ ] Реалізувати
useLivingMapLite(або обгорнутиuseLivingMapFull). - [ ] Створити
LivingMapPage.tsx. - [ ] Створити
LivingMapCanvas.tsx. -
[ ] Реалізувати
canvasRenderer.tsз базовим рендером: -
[ ] city layer
- [ ] space layer
- [ ] nodes layer
- [ ] agents layer
- [ ] Реалізувати
layoutEngine.ts. - [ ] Додати
LayerSwitcher.tsx(простий UI). - [ ] Додати
EntityDetailsPanel.tsx. - [ ] Підключити WebSocket stream (якщо backend вже готовий).
- [ ] Додати fallback на mock-дані, якщо API недоступний.
- [ ] Переконатись, що немає TypeScript/lint помилок.
10. Acceptance Criteria¶
- Route
/living-mapдоступний у UI. -
При відкритті сторінки:
-
робиться запит
GET /living-map/snapshot(або використовується mock), - на Canvas зʼявляються базові форми (місто/космос/ноди/агенти).
- LayerSwitcher перемикає режим рендерингу між
city,space,nodes,agents. - Клік по елементу на Canvas змінює
selectedEntityIdі панель деталей показує правильні дані. - WebSocket (якщо активний) змінює стан (наприклад, статус ноди, агента) без перезавантаження сторінки.
- FPS достатній (без явних лагів на базовому обсязі даних).
- Код компілюється без TypeScript та ESLint помилок.
END OF TASK