diff --git a/apps/web/src/components/nodes/NodeAgentsPanel.tsx b/apps/web/src/components/nodes/NodeAgentsPanel.tsx
new file mode 100644
index 00000000..3a3eb5db
--- /dev/null
+++ b/apps/web/src/components/nodes/NodeAgentsPanel.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import Link from "next/link";
+import { Shield, Wrench } from "lucide-react";
+
+interface NodeAgent {
+ id: string;
+ name: string;
+ kind?: string;
+ slug?: string;
+}
+
+interface NodeAgentsPanelProps {
+ guardian?: NodeAgent | null;
+ steward?: NodeAgent | null;
+}
+
+export function NodeAgentsPanel({ guardian, steward }: NodeAgentsPanelProps) {
+ if (!guardian && !steward) {
+ return null;
+ }
+
+ return (
+
+
+
+ Системні агенти ноди
+
+
+
+ {/* Guardian Agent */}
+
+
+
+ Node Guardian
+
+ {guardian ? (
+
+ {guardian.name}
+
+ ) : (
+
Не призначено
+ )}
+ {guardian?.kind && (
+
{guardian.kind}
+ )}
+
+
+ {/* Steward Agent */}
+
+
+
+ Node Steward
+
+ {steward ? (
+
+ {steward.name}
+
+ ) : (
+
Не призначено
+ )}
+ {steward?.kind && (
+
{steward.kind}
+ )}
+
+
+
+ );
+}
+
diff --git a/apps/web/src/lib/agent-dashboard.ts b/apps/web/src/lib/agent-dashboard.ts
index 6383b98a..8e2b2e5c 100644
--- a/apps/web/src/lib/agent-dashboard.ts
+++ b/apps/web/src/lib/agent-dashboard.ts
@@ -153,6 +153,13 @@ export interface AgentPublicProfile {
is_system?: boolean;
}
+export interface CityRoomSummary {
+ id: string;
+ slug: string;
+ name: string;
+ matrix_room_id?: string | null;
+}
+
export interface AgentDashboard {
profile: AgentProfile;
node?: AgentNode;
@@ -162,6 +169,7 @@ export interface AgentDashboard {
system_prompts?: AgentSystemPrompts;
public_profile?: AgentPublicProfile;
microdao_memberships?: AgentMicrodaoMembership[];
+ primary_city_room?: CityRoomSummary | null;
}
// ============================================================================
diff --git a/apps/web/src/lib/types/microdao.ts b/apps/web/src/lib/types/microdao.ts
index 5216caad..34d66bd2 100644
--- a/apps/web/src/lib/types/microdao.ts
+++ b/apps/web/src/lib/types/microdao.ts
@@ -72,6 +72,17 @@ export interface MicrodaoCitizenView {
primary_room_slug?: string | null;
}
+// =============================================================================
+// City Room Summary (for chat embedding)
+// =============================================================================
+
+export interface CityRoomSummary {
+ id: string;
+ slug: string;
+ name: string;
+ matrix_room_id?: string | null;
+}
+
// =============================================================================
// MicroDAO Detail (for /microdao/[slug])
// =============================================================================
@@ -102,6 +113,9 @@ export interface MicrodaoDetail {
agents: MicrodaoAgentView[];
channels: MicrodaoChannelView[];
public_citizens: MicrodaoCitizenView[];
+
+ // Primary city room for chat
+ primary_city_room?: CityRoomSummary | null;
}
// =============================================================================
diff --git a/apps/web/src/lib/types/nodes.ts b/apps/web/src/lib/types/nodes.ts
index f93b1ec9..17fc5522 100644
--- a/apps/web/src/lib/types/nodes.ts
+++ b/apps/web/src/lib/types/nodes.ts
@@ -1,3 +1,10 @@
+export interface NodeAgentSummary {
+ id: string;
+ name: string;
+ kind?: string;
+ slug?: string;
+}
+
export interface NodeProfile {
node_id: string;
name: string;
@@ -9,6 +16,10 @@ export interface NodeProfile {
agents_total: number;
agents_online: number;
last_heartbeat?: string | null;
+ guardian_agent_id?: string | null;
+ steward_agent_id?: string | null;
+ guardian_agent?: NodeAgentSummary | null;
+ steward_agent?: NodeAgentSummary | null;
}
export interface NodeListResponse {
diff --git a/docs/internal/agents/NODE_AGENTS_INVENTORY.md b/docs/internal/agents/NODE_AGENTS_INVENTORY.md
new file mode 100644
index 00000000..b6796a45
--- /dev/null
+++ b/docs/internal/agents/NODE_AGENTS_INVENTORY.md
@@ -0,0 +1,142 @@
+# Node Agents Inventory
+
+**Дата:** 28 листопада 2025
+**Статус:** ✅ Інвентаризація завершена
+**Результат TASK 031_NODE_AGENTS_DISCOVERY**
+
+---
+
+## 1. Node Monitoring Agents
+
+### 1.1. Monitor Agent (NODE1)
+
+| Поле | Значення |
+|------|----------|
+| **ID** | `agent-monitor-node1` |
+| **Display Name** | Monitor Agent (НОДА1) |
+| **Slug** | `monitor-node1` |
+| **Role** | System Monitoring & Event Logging (Node-1) |
+| **Model** | mistral-nemo:12b |
+| **Backend** | ollama |
+| **Node** | node-1-hetzner-gex44 |
+| **Kind** | infra_monitor |
+| **Department** | System |
+| **Файл опису** | `src/api/node1Agents.ts` (рядки 76-92) |
+| **Статус** | ✅ Існує в коді |
+
+**Функції:**
+- Моніторинг CPU, RAM, GPU, Disk
+- Відстеження стану сервісів (Router, Swapper, Ollama, Matrix, Postgres, NATS)
+- Генерація звітів про інциденти
+- Виявлення аномалій
+
+### 1.2. Monitor Agent (NODE2)
+
+| Поле | Значення |
+|------|----------|
+| **ID** | `agent-monitor-node2` / `monitor-node2` |
+| **Display Name** | Monitor Agent (НОДА2) |
+| **Slug** | `monitor-node2` |
+| **Role** | System Monitoring & Event Logging (Node-2) |
+| **Model** | mistral-nemo:12b |
+| **Backend** | ollama |
+| **Node** | node-2-macbook-m4max |
+| **Kind** | infra_monitor |
+| **Department** | System |
+| **Файли опису** | `src/api/node2Agents.ts` (рядки 37-52), `config/agents_city_mapping.yaml`, `router-config.yml` |
+| **Статус** | ✅ Існує в БД та коді |
+
+**Функції:**
+- Аналогічні до NODE1 Monitor
+- Додатково: архітектор-інспектор DAGI
+
+---
+
+## 2. Node Steward / NodeOps Agents
+
+### 2.1. Node Steward (NODE1)
+
+| Поле | Значення |
+|------|----------|
+| **ID** | `node-steward-node1` (пропонується) |
+| **Display Name** | Node Steward (НОДА1) |
+| **Slug** | `node-steward-node1` |
+| **Role** | Curator of Node Stack |
+| **Model** | mistral-nemo:12b (рекомендовано) |
+| **Node** | node-1-hetzner-gex44 |
+| **Kind** | infra_ops |
+| **Статус** | ❌ НЕ ІСНУЄ — потрібно створити |
+
+**Заплановані функції:**
+- Інвентаризація стеку ноди
+- Порівняння з DAOS стандартами
+- Планування оновлень та встановлень
+- Документування конфігурації
+
+### 2.2. Node Steward (NODE2)
+
+| Поле | Значення |
+|------|----------|
+| **ID** | `node-steward-node2` (пропонується) |
+| **Display Name** | Node Steward (НОДА2) |
+| **Slug** | `node-steward-node2` |
+| **Role** | Curator of Node Stack |
+| **Model** | mistral-nemo:12b (рекомендовано) |
+| **Node** | node-2-macbook-m4max |
+| **Kind** | infra_ops |
+| **Статус** | ❌ НЕ ІСНУЄ — потрібно створити |
+
+---
+
+## 3. Кандидати на офіційні ролі
+
+### Node Guardian (is_node_guardian = true)
+
+| Нода | Агент | ID |
+|------|-------|-----|
+| NODE1 | Monitor Agent (НОДА1) | `monitor-node1` |
+| NODE2 | Monitor Agent (НОДА2) | `monitor-node2` |
+
+### Node Steward (is_node_steward = true)
+
+| Нода | Агент | ID |
+|------|-------|-----|
+| NODE1 | Node Steward (НОДА1) | `node-steward-node1` (створити) |
+| NODE2 | Node Steward (НОДА2) | `node-steward-node2` (створити) |
+
+---
+
+## 4. Джерела даних
+
+### Файли з описом агентів:
+
+1. `src/api/node1Agents.ts` — агенти NODE1
+2. `src/api/node2Agents.ts` — агенти NODE2
+3. `config/agents_city_mapping.yaml` — маппінг агентів на кімнати
+4. `router-config.yml` — конфігурація DAGI Router
+5. `docs/NODE2_AGENTS_FULL_INVENTORY.md` — повна інвентаризація NODE2
+6. `docs/users/agents/SYSTEM_AGENTS_DAIS.md` — DAIS паспорти
+
+### Сервіси моніторингу:
+
+1. `services/monitor-agent-service/` — сервіс Monitor Agent
+2. `src/components/monitor/NodeMonitorChat.tsx` — UI компонент чату з Monitor
+
+---
+
+## 5. Рекомендації для TASK 032
+
+1. **Створити міграцію** з полями:
+ - `agents.is_node_guardian` (boolean)
+ - `agents.is_node_steward` (boolean)
+ - `node_cache.guardian_agent_id` (text)
+ - `node_cache.steward_agent_id` (text)
+
+2. **Створити агентів Node Steward** для NODE1 та NODE2
+
+3. **Оновити існуючих Monitor Agent** — встановити `is_node_guardian = true`
+
+4. **Прив'язати агентів до нод** через `guardian_agent_id` / `steward_agent_id`
+
+5. **Додати в Node Dashboard UI** панель з агентами ноди
+
diff --git a/docs/tasks/033_AGENT_AND_MICRODAO_CHAT_WIDGETS.md b/docs/tasks/033_AGENT_AND_MICRODAO_CHAT_WIDGETS.md
new file mode 100644
index 00000000..8ed5aa60
--- /dev/null
+++ b/docs/tasks/033_AGENT_AND_MICRODAO_CHAT_WIDGETS.md
@@ -0,0 +1,97 @@
+# TASK 033: Agent & MicroDAO Chat Widgets
+
+**Дата:** 28 листопада 2025
+**Статус:** ✅ Завершено
+
+## Мета
+
+1. У кожному Agent Dashboard (`/agents/[id]`) має бути **діалогове вікно (Matrix-чат) з цим агентом**.
+2. У кожному MicroDAO (`/microdao/[slug]`) має бути **публічний чат кімнати цього MicroDAO**, де оркестратор спілкується з користувачами.
+
+## Виконані зміни
+
+### 1. Backend: Agent Dashboard → primary_city_room
+
+**Файл:** `services/city-service/routes_city.py`
+
+- Оновлено endpoint `GET /city/agents/{id}/dashboard`
+- Додано поле `primary_city_room` до відповіді
+- Пріоритет визначення кімнати:
+ 1. Перша кімната агента з `city_rooms`
+ 2. Primary room MicroDAO агента (якщо є `primary_microdao_id`)
+ 3. `null` якщо немає
+
+### 2. Backend: MicroDAO Detail → primary_city_room
+
+**Файл:** `services/city-service/models_city.py`
+
+- Додано модель `CityRoomSummary`:
+ ```python
+ class CityRoomSummary(BaseModel):
+ id: str
+ slug: str
+ name: str
+ matrix_room_id: Optional[str] = None
+ ```
+- Оновлено `MicrodaoDetail` — додано поле `primary_city_room`
+
+**Файл:** `services/city-service/repo_city.py`
+
+- Додано функцію `get_microdao_primary_room(microdao_id)`:
+ - Шукає primary room MicroDAO
+ - Пріоритет: `room_type='primary'` → `room_type='public'` → будь-яка активна
+
+**Файл:** `services/city-service/routes_city.py`
+
+- Оновлено endpoint `GET /city/microdao/{slug}`
+- Додано виклик `get_microdao_primary_room()` та заповнення `primary_city_room`
+
+### 3. Frontend: Типи
+
+**Файл:** `apps/web/src/lib/agent-dashboard.ts`
+
+- Додано тип `CityRoomSummary`
+- Оновлено `AgentDashboard` — додано поле `primary_city_room`
+
+**Файл:** `apps/web/src/lib/types/microdao.ts`
+
+- Додано тип `CityRoomSummary`
+- Оновлено `MicrodaoDetail` — додано поле `primary_city_room`
+
+### 4. Frontend: Agent Console (`/agents/[agentId]`)
+
+**Файл:** `apps/web/src/app/agents/[agentId]/page.tsx`
+
+- Оновлено Chat Tab:
+ - Прямий чат з агентом через DAGI Router (існуючий)
+ - Нова секція "Публічна кімната агента" з `CityChatWidget`
+- Якщо `primary_city_room` є — показує Matrix-чат
+- Якщо немає — показує повідомлення про необхідність налаштування
+
+### 5. Frontend: MicroDAO Page (`/microdao/[slug]`)
+
+**Файл:** `apps/web/src/app/microdao/[slug]/page.tsx`
+
+- Додано секцію "Публічний чат MicroDAO"
+- Використовує `CityChatWidget` з `primary_city_room.slug`
+- Показує інформацію про оркестратора
+- Якщо кімната не налаштована — показує placeholder
+
+## Перевикористання
+
+Обидві сторінки використовують існуючий компонент `CityChatWidget` з `/components/city/CityChatWidget.tsx`, який вже працює на сторінці громадянина (`/citizens/[slug]`).
+
+## Acceptance Criteria
+
+- [x] `/agents/[id]` — секція "Публічна кімната агента" з Matrix-чатом
+- [x] `/microdao/[slug]` — секція "Публічний чат MicroDAO" з Matrix-чатом
+- [x] Перевикористано `CityChatWidget`
+- [x] Білд проходить успішно
+- [x] Типи оновлено на фронтенді та бекенді
+
+## Пов'язані завдання
+
+- **TASK 031:** Node Agents Discovery
+- **TASK 032:** Node Guardian/Steward Formalize
+- **Citizen Interact Layer v1:** Базовий функціонал чату для громадян
+
diff --git a/migrations/030_node_guardian_steward.sql b/migrations/030_node_guardian_steward.sql
new file mode 100644
index 00000000..72703720
--- /dev/null
+++ b/migrations/030_node_guardian_steward.sql
@@ -0,0 +1,97 @@
+-- Migration 030: Node Guardian and Steward
+-- Додає поля для прив'язки агентів Guardian/Steward до нод
+
+-- 1. Розширити таблицю agents полями для ролей Guardian/Steward
+ALTER TABLE agents
+ ADD COLUMN IF NOT EXISTS is_node_guardian boolean NOT NULL DEFAULT false,
+ ADD COLUMN IF NOT EXISTS is_node_steward boolean NOT NULL DEFAULT false;
+
+-- 2. Розширити node_cache полями для прив'язки агентів
+ALTER TABLE node_cache
+ ADD COLUMN IF NOT EXISTS guardian_agent_id text,
+ ADD COLUMN IF NOT EXISTS steward_agent_id text;
+
+-- 3. Індекси для швидкого пошуку
+CREATE INDEX IF NOT EXISTS idx_agents_is_node_guardian ON agents(is_node_guardian) WHERE is_node_guardian = true;
+CREATE INDEX IF NOT EXISTS idx_agents_is_node_steward ON agents(is_node_steward) WHERE is_node_steward = true;
+
+-- 4. Оновити існуючих Monitor Agent як Guardian
+UPDATE agents
+SET is_node_guardian = true
+WHERE id IN ('monitor-node1', 'monitor-node2', 'agent-monitor-node1', 'agent-monitor-node2');
+
+-- 5. Прив'язати Guardian до нод
+UPDATE node_cache
+SET guardian_agent_id = 'monitor-node2'
+WHERE node_id = 'node-2-macbook-m4max';
+
+UPDATE node_cache
+SET guardian_agent_id = 'monitor-node1'
+WHERE node_id = 'node-1-hetzner-gex44';
+
+-- 6. Створити агентів Node Steward (якщо ще не існують)
+INSERT INTO agents (
+ id, display_name, kind, status, node_id,
+ is_public, is_node_steward, public_slug,
+ created_at, updated_at
+) VALUES
+(
+ 'node-steward-node1',
+ 'Node Steward (НОДА1)',
+ 'infra_ops',
+ 'online',
+ 'node-1-hetzner-gex44',
+ true,
+ true,
+ 'node-steward-node1',
+ NOW(),
+ NOW()
+),
+(
+ 'node-steward-node2',
+ 'Node Steward (НОДА2)',
+ 'infra_ops',
+ 'online',
+ 'node-2-macbook-m4max',
+ true,
+ true,
+ 'node-steward-node2',
+ NOW(),
+ NOW()
+)
+ON CONFLICT (id) DO UPDATE SET
+ is_node_steward = true,
+ kind = 'infra_ops',
+ updated_at = NOW();
+
+-- 7. Прив'язати Steward до нод
+UPDATE node_cache
+SET steward_agent_id = 'node-steward-node1'
+WHERE node_id = 'node-1-hetzner-gex44';
+
+UPDATE node_cache
+SET steward_agent_id = 'node-steward-node2'
+WHERE node_id = 'node-2-macbook-m4max';
+
+-- 8. Переконатися, що Monitor Agent (NODE1) існує
+INSERT INTO agents (
+ id, display_name, kind, status, node_id,
+ is_public, is_node_guardian, public_slug,
+ created_at, updated_at
+) VALUES (
+ 'monitor-node1',
+ 'Node Monitor (НОДА1)',
+ 'infra_monitor',
+ 'online',
+ 'node-1-hetzner-gex44',
+ true,
+ true,
+ 'monitor-node1',
+ NOW(),
+ NOW()
+)
+ON CONFLICT (id) DO UPDATE SET
+ is_node_guardian = true,
+ kind = 'infra_monitor',
+ updated_at = NOW();
+
diff --git a/services/city-service/models_city.py b/services/city-service/models_city.py
index 810b6168..6b28d01f 100644
--- a/services/city-service/models_city.py
+++ b/services/city-service/models_city.py
@@ -396,6 +396,14 @@ class MicrodaoAgentView(BaseModel):
is_core: bool
+class CityRoomSummary(BaseModel):
+ """Summary of a city room for chat embedding"""
+ id: str
+ slug: str
+ name: str
+ matrix_room_id: Optional[str] = None
+
+
class MicrodaoDetail(BaseModel):
"""Full MicroDAO detail view"""
id: str
@@ -423,6 +431,9 @@ class MicrodaoDetail(BaseModel):
agents: List[MicrodaoAgentView] = []
channels: List[MicrodaoChannelView] = []
public_citizens: List[MicrodaoCitizenView] = []
+
+ # Primary city room for chat
+ primary_city_room: Optional[CityRoomSummary] = None
class AgentMicrodaoMembership(BaseModel):
diff --git a/services/city-service/repo_city.py b/services/city-service/repo_city.py
index 3c3ba078..f67337ac 100644
--- a/services/city-service/repo_city.py
+++ b/services/city-service/repo_city.py
@@ -1505,7 +1505,7 @@ async def get_microdao_by_slug(slug: str) -> Optional[dict]:
# =============================================================================
async def get_all_nodes() -> List[dict]:
- """Отримати список всіх нод з кількістю агентів"""
+ """Отримати список всіх нод з кількістю агентів та Guardian/Steward"""
pool = await get_pool()
query = """
@@ -1518,18 +1518,47 @@ async def get_all_nodes() -> List[dict]:
nc.status,
nc.gpu,
nc.last_sync AS last_heartbeat,
+ nc.guardian_agent_id,
+ nc.steward_agent_id,
(SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id) AS agents_total,
- (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online
+ (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online,
+ ga.display_name AS guardian_name,
+ sa.display_name AS steward_name
FROM node_cache nc
+ LEFT JOIN agents ga ON nc.guardian_agent_id = ga.id
+ LEFT JOIN agents sa ON nc.steward_agent_id = sa.id
ORDER BY nc.environment DESC, nc.node_name
"""
rows = await pool.fetch(query)
- return [dict(row) for row in rows]
+ result = []
+ for row in rows:
+ data = dict(row)
+ # Build guardian_agent object
+ if data.get("guardian_agent_id"):
+ data["guardian_agent"] = {
+ "id": data.get("guardian_agent_id"),
+ "name": data.get("guardian_name"),
+ }
+ else:
+ data["guardian_agent"] = None
+ # Build steward_agent object
+ if data.get("steward_agent_id"):
+ data["steward_agent"] = {
+ "id": data.get("steward_agent_id"),
+ "name": data.get("steward_name"),
+ }
+ else:
+ data["steward_agent"] = None
+ # Clean up
+ data.pop("guardian_name", None)
+ data.pop("steward_name", None)
+ result.append(data)
+ return result
async def get_node_by_id(node_id: str) -> Optional[dict]:
- """Отримати ноду по ID"""
+ """Отримати ноду по ID з Guardian та Steward агентами"""
pool = await get_pool()
query = """
@@ -1542,14 +1571,58 @@ async def get_node_by_id(node_id: str) -> Optional[dict]:
nc.status,
nc.gpu,
nc.last_sync AS last_heartbeat,
+ nc.guardian_agent_id,
+ nc.steward_agent_id,
(SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id) AS agents_total,
- (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online
+ (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online,
+ -- Guardian agent info
+ ga.display_name AS guardian_name,
+ ga.kind AS guardian_kind,
+ ga.public_slug AS guardian_slug,
+ -- Steward agent info
+ sa.display_name AS steward_name,
+ sa.kind AS steward_kind,
+ sa.public_slug AS steward_slug
FROM node_cache nc
+ LEFT JOIN agents ga ON nc.guardian_agent_id = ga.id
+ LEFT JOIN agents sa ON nc.steward_agent_id = sa.id
WHERE nc.node_id = $1
"""
row = await pool.fetchrow(query, node_id)
- return dict(row) if row else None
+ if not row:
+ return None
+
+ data = dict(row)
+
+ # Build guardian_agent object
+ if data.get("guardian_agent_id"):
+ data["guardian_agent"] = {
+ "id": data.get("guardian_agent_id"),
+ "name": data.get("guardian_name"),
+ "kind": data.get("guardian_kind"),
+ "slug": data.get("guardian_slug"),
+ }
+ else:
+ data["guardian_agent"] = None
+
+ # Build steward_agent object
+ if data.get("steward_agent_id"):
+ data["steward_agent"] = {
+ "id": data.get("steward_agent_id"),
+ "name": data.get("steward_name"),
+ "kind": data.get("steward_kind"),
+ "slug": data.get("steward_slug"),
+ }
+ else:
+ data["steward_agent"] = None
+
+ # Clean up intermediate fields
+ for key in ["guardian_name", "guardian_kind", "guardian_slug",
+ "steward_name", "steward_kind", "steward_slug"]:
+ data.pop(key, None)
+
+ return data
# =============================================================================
@@ -1656,3 +1729,38 @@ async def create_microdao_for_agent(
return dict(dao_row)
+
+async def get_microdao_primary_room(microdao_id: str) -> Optional[dict]:
+ """
+ Отримати основну кімнату MicroDAO для чату.
+ Пріоритет: primary room → перша публічна кімната → будь-яка кімната.
+ """
+ pool = await get_pool()
+
+ query = """
+ SELECT
+ cr.id,
+ cr.slug,
+ cr.name,
+ cr.matrix_room_id
+ FROM city_rooms cr
+ WHERE cr.microdao_id = $1
+ AND cr.is_active = true
+ ORDER BY
+ CASE WHEN cr.room_type = 'primary' THEN 0
+ WHEN cr.room_type = 'public' THEN 1
+ ELSE 2 END,
+ cr.created_at
+ LIMIT 1
+ """
+
+ row = await pool.fetchrow(query, microdao_id)
+ if row:
+ return {
+ "id": str(row["id"]),
+ "slug": row["slug"],
+ "name": row["name"],
+ "matrix_room_id": row.get("matrix_room_id")
+ }
+ return None
+
diff --git a/services/city-service/routes_city.py b/services/city-service/routes_city.py
index a0b77a30..94dbf8e4 100644
--- a/services/city-service/routes_city.py
+++ b/services/city-service/routes_city.py
@@ -1206,10 +1206,28 @@ async def get_agent_dashboard(agent_id: str):
for item in memberships_raw
]
+ # Get primary city room for agent
+ primary_city_room = None
+ # Priority 1: agent's primary room from city_rooms
+ if rooms and len(rooms) > 0:
+ primary_room = rooms[0] # First room as primary
+ primary_city_room = {
+ "id": primary_room.get("id"),
+ "slug": primary_room.get("slug"),
+ "name": primary_room.get("name"),
+ "matrix_room_id": primary_room.get("matrix_room_id")
+ }
+ # Priority 2: Get from primary MicroDAO's main room
+ elif agent.get("primary_microdao_id"):
+ microdao_room = await repo_city.get_microdao_primary_room(agent["primary_microdao_id"])
+ if microdao_room:
+ primary_city_room = microdao_room
+
# Build dashboard response
dashboard = {
"profile": profile,
"node": node_info,
+ "primary_city_room": primary_city_room,
"runtime": {
"health": "healthy" if agent.get("status") == "online" else "unknown",
"last_success_at": None,
@@ -1466,6 +1484,18 @@ async def get_microdao_by_slug(slug: str):
is_platform=child.get("is_platform", False)
))
+ # Get primary city room for MicroDAO
+ primary_city_room = await repo_city.get_microdao_primary_room(dao["id"])
+ primary_room_summary = None
+ if primary_city_room:
+ from models_city import CityRoomSummary
+ primary_room_summary = CityRoomSummary(
+ id=primary_city_room["id"],
+ slug=primary_city_room["slug"],
+ name=primary_city_room["name"],
+ matrix_room_id=primary_city_room.get("matrix_room_id")
+ )
+
return MicrodaoDetail(
id=dao["id"],
slug=dao["slug"],
@@ -1483,7 +1513,8 @@ async def get_microdao_by_slug(slug: str):
logo_url=dao.get("logo_url"),
agents=agents,
channels=channels,
- public_citizens=public_citizens
+ public_citizens=public_citizens,
+ primary_city_room=primary_room_summary
)
except HTTPException: