feat: District Portals API (DB-based, no hardcodes)
Backend:
- GET /api/v1/districts - list all districts from DB
- GET /api/v1/districts/{slug} - district detail with lead agent, core team, rooms, nodes
repo_city methods:
- get_districts() - SELECT FROM microdaos WHERE dao_type='district'
- get_district_by_slug()
- get_district_lead_agent() - with fallback to orchestrator
- get_district_core_team()
- get_district_agents()
- get_district_rooms() - by slug prefix
- get_district_nodes()
- get_district_stats()
Task doc: TASK_PHASE_DISTRICT_PORTALS_v1.md
This commit is contained in:
451
docs/tasks/TASK_PHASE_DISTRICT_PORTALS_v1.md
Normal file
451
docs/tasks/TASK_PHASE_DISTRICT_PORTALS_v1.md
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
# TASK_PHASE_DISTRICT_PORTALS_v1
|
||||||
|
|
||||||
|
Version: 1.0
|
||||||
|
Status: Ready
|
||||||
|
Priority: High (City → District → MicroDAO контур)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. МЕТА
|
||||||
|
|
||||||
|
Зробити **District-и повноцінними "порталами платформ"** у DAARION.city:
|
||||||
|
|
||||||
|
- окремі сторінки District-ів (SOUL, GREENFOOD, ENERGYUNION),
|
||||||
|
- прив'язка до існуючих District-протоколів,
|
||||||
|
- інтеграція з Rooms, Matrix, Presence, Chat Widget,
|
||||||
|
- відображення MicroDAO всередині District-а.
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
користувач, заходячи на DAARION.space, може:
|
||||||
|
|
||||||
|
- потрапити в місто (/city),
|
||||||
|
- з міста — в District-портал (/soul, /greenfood, /energy-union),
|
||||||
|
- з District — у відповідні MicroDAO / кімнати / агентів.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. ВИХІДНІ ДАНІ
|
||||||
|
|
||||||
|
Уже є:
|
||||||
|
|
||||||
|
- Foundation-документи:
|
||||||
|
- `GREENFOOD_District_Protocol_v1.md`
|
||||||
|
- `ENERGYUNION_District_Protocol_v1.md`
|
||||||
|
- `SOUL_District_Protocol_v1.md`
|
||||||
|
- `District_Interface_Architecture_v1.md`
|
||||||
|
- Таблиця `microdaos` з полем `dao_type = 'district'` (SOUL, GREENFOOD, ENERGYUNION).
|
||||||
|
- Rooms Layer:
|
||||||
|
- District rooms для:
|
||||||
|
- SOUL (soul-lobby, soul-events, soul-masters, ...)
|
||||||
|
- GREENFOOD (greenfood-lobby, ... )
|
||||||
|
- ENERGYUNION (energyunion-lobby, energyunion-compute, ...)
|
||||||
|
- Matrix + Chat:
|
||||||
|
- `rooms.matrix_room_id` заповнено,
|
||||||
|
- Chat API працює,
|
||||||
|
- Presence API працює.
|
||||||
|
- Frontend:
|
||||||
|
- City Layer /city, /city/{slug}
|
||||||
|
- Agents, Nodes, MicroDAO базові сторінки
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. SCOPE
|
||||||
|
|
||||||
|
1. Backend District API:
|
||||||
|
- `GET /api/v1/districts`
|
||||||
|
- `GET /api/v1/districts/{slug}`
|
||||||
|
2. Frontend routing:
|
||||||
|
- `/districts` (список всіх District-ів)
|
||||||
|
- `/districts/[slug]` (універсальний портал)
|
||||||
|
- короткі alias-роути:
|
||||||
|
- `/soul` → SOUL District
|
||||||
|
- `/greenfood` → GREENFOOD District
|
||||||
|
- `/energy-union` → ENERGYUNION District
|
||||||
|
3. UI District-порталу:
|
||||||
|
- header (назва, опис, тип, lead agent),
|
||||||
|
- District rooms (список кімнат + переходи),
|
||||||
|
- host agents (lead/core team) + presence,
|
||||||
|
- chat widget (District lobby room),
|
||||||
|
- список MicroDAO всередині District-а.
|
||||||
|
4. Інтеграція з City Layer:
|
||||||
|
- посилання з City → District.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. МОДУЛЬ 1 — BACKEND: DISTRICT API (CITY-SERVICE)
|
||||||
|
|
||||||
|
## 4.1. `GET /api/v1/districts`
|
||||||
|
|
||||||
|
Повертає список всіх District-ів.
|
||||||
|
|
||||||
|
Приблизний вихід:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"slug": "soul",
|
||||||
|
"name": "SOUL Retreat District",
|
||||||
|
"description": "Wellness / Retreat / Metahuman",
|
||||||
|
"dao_type": "district",
|
||||||
|
"lead_agent": {
|
||||||
|
"id": "agent_id_soul",
|
||||||
|
"name": "SOUL"
|
||||||
|
},
|
||||||
|
"rooms": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"slug": "soul-lobby",
|
||||||
|
"name": "SOUL Lobby"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"microdaos_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"slug": "greenfood",
|
||||||
|
"name": "GREENFOOD District",
|
||||||
|
"description": "ERP / Supply Chains / Food",
|
||||||
|
"dao_type": "district",
|
||||||
|
"lead_agent": {
|
||||||
|
"id": "agent_id_greenfood",
|
||||||
|
"name": "ERP GREENFOOD"
|
||||||
|
},
|
||||||
|
"rooms": [ ... ],
|
||||||
|
"microdaos_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"slug": "energy-union",
|
||||||
|
"name": "Energy Union District",
|
||||||
|
"description": "DePIN / Energy / Compute",
|
||||||
|
"dao_type": "district",
|
||||||
|
"lead_agent": {
|
||||||
|
"id": "agent_id_helion",
|
||||||
|
"name": "Helion"
|
||||||
|
},
|
||||||
|
"rooms": [ ... ],
|
||||||
|
"microdaos_count": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Логіка:
|
||||||
|
|
||||||
|
- брати записи з `microdaos` де `dao_type = 'district'`;
|
||||||
|
- підвантажити lead-agent (згідно District-протоколів);
|
||||||
|
- підтягнути ключові кімнати (lobby тощо) через `rooms` та `district_id` / зв'язки.
|
||||||
|
|
||||||
|
## 4.2. `GET /api/v1/districts/{slug}`
|
||||||
|
|
||||||
|
Подробиці District-а.
|
||||||
|
|
||||||
|
Приблизний вихід:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"slug": "soul",
|
||||||
|
"name": "SOUL Retreat District",
|
||||||
|
"description": "Wellness / Retreat / Metahuman District",
|
||||||
|
"dao_type": "district",
|
||||||
|
"lead_agent": {
|
||||||
|
"id": "agent_id_soul",
|
||||||
|
"name": "SOUL",
|
||||||
|
"dais_id": "dais_soul"
|
||||||
|
},
|
||||||
|
"core_team": [
|
||||||
|
{
|
||||||
|
"id": "agent_spirit",
|
||||||
|
"name": "Spirit",
|
||||||
|
"role": "Guidance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "agent_logic",
|
||||||
|
"name": "Logic",
|
||||||
|
"role": "Information"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rooms": [
|
||||||
|
{
|
||||||
|
"id": "room_id_lobby",
|
||||||
|
"slug": "soul-lobby",
|
||||||
|
"name": "SOUL Lobby",
|
||||||
|
"matrix_room_id": "!room:matrix..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "room_id_events",
|
||||||
|
"slug": "soul-events",
|
||||||
|
"name": "Events"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"microdaos": [
|
||||||
|
{
|
||||||
|
"id": "microdao_id_1",
|
||||||
|
"slug": "soul-hub-1",
|
||||||
|
"name": "Retreat Hub #1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Якщо District не знайдено → 404.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. МОДУЛЬ 2 — FRONTEND API КЛІЄНТ
|
||||||
|
|
||||||
|
У `apps/web/src/lib/api/districts.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type DistrictSummary = {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
daoType: "district";
|
||||||
|
leadAgent: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
rooms: {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
microdaosCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DistrictDetail = {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
daoType: "district";
|
||||||
|
leadAgent: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
daisId?: string;
|
||||||
|
};
|
||||||
|
coreTeam: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
}[];
|
||||||
|
rooms: {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
matrixRoomId?: string;
|
||||||
|
}[];
|
||||||
|
microdaos: {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getDistricts(): Promise<DistrictSummary[]> { ... }
|
||||||
|
|
||||||
|
export async function getDistrictBySlug(slug: string): Promise<DistrictDetail> { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. МОДУЛЬ 3 — ROUTING
|
||||||
|
|
||||||
|
## 6.1. `/districts` (список)
|
||||||
|
|
||||||
|
Файл: `apps/web/src/app/districts/page.tsx`
|
||||||
|
|
||||||
|
Повинен:
|
||||||
|
|
||||||
|
- завантажити `getDistricts()`;
|
||||||
|
- показати карточки District-ів:
|
||||||
|
- SOUL
|
||||||
|
- GREENFOOD
|
||||||
|
- ENERGY UNION
|
||||||
|
- кожна картка:
|
||||||
|
- назва
|
||||||
|
- опис
|
||||||
|
- lead agent (ім'я + presence)
|
||||||
|
- кнопка/посилання "Перейти до District".
|
||||||
|
|
||||||
|
## 6.2. `/districts/[slug]` (портал District-а)
|
||||||
|
|
||||||
|
Файл: `apps/web/src/app/districts/[slug]/page.tsx`
|
||||||
|
|
||||||
|
Логіка:
|
||||||
|
|
||||||
|
- прочитати `params.slug`;
|
||||||
|
- викликати `getDistrictBySlug(slug)`;
|
||||||
|
- якщо 404 → `notFound()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. МОДУЛЬ 4 — UI DISTRICT-ПОРТАЛУ
|
||||||
|
|
||||||
|
Структура сторінки `/districts/[slug]`:
|
||||||
|
|
||||||
|
## 7.1. Header
|
||||||
|
|
||||||
|
- Назва District-а (`name`)
|
||||||
|
- Опис (`description`)
|
||||||
|
- Badge: "District"
|
||||||
|
- Breadcrumb:
|
||||||
|
- `City / Districts / {DistrictName}`
|
||||||
|
|
||||||
|
## 7.2. Lead Agent + Core Team
|
||||||
|
|
||||||
|
Секція "District Agents":
|
||||||
|
|
||||||
|
- Lead Agent:
|
||||||
|
- аватар,
|
||||||
|
- ім'я (SOUL / GREENFOOD / Helion),
|
||||||
|
- PresenceDot (online/offline/away),
|
||||||
|
- кнопка "Кабінет агента".
|
||||||
|
|
||||||
|
- Core Team (якщо є):
|
||||||
|
- список агентів (Spirit, Logic, Energia тощо),
|
||||||
|
- ролі (Guidance, Information, Energy, Scheduler...),
|
||||||
|
- presence.
|
||||||
|
|
||||||
|
## 7.3. Rooms
|
||||||
|
|
||||||
|
Секція "District Rooms":
|
||||||
|
|
||||||
|
- список кімнат District-а:
|
||||||
|
- назва,
|
||||||
|
- опис (якщо є),
|
||||||
|
- scope tag (lobby / events / labs / providers / compute / guidance / etc.),
|
||||||
|
- кнопка "Відкрити кімнату" → `/city/{slug}` або окремий `/rooms/{slug}`, залежно від моделі.
|
||||||
|
- Для lobby-кімнати:
|
||||||
|
- окремий акцент: "Головний портал District-а".
|
||||||
|
|
||||||
|
(Якщо кімнати реалізовані як `rooms.scope = 'district'`, на UI використовувати їх метадані з БД.)
|
||||||
|
|
||||||
|
## 7.4. MicroDAO
|
||||||
|
|
||||||
|
Секція "MicroDAO цього District-а":
|
||||||
|
|
||||||
|
- таблиця/карточки:
|
||||||
|
- назва MicroDAO,
|
||||||
|
- тип (якщо є),
|
||||||
|
- кнопка "Відкрити MicroDAO" → `/microdao/{slug}`.
|
||||||
|
|
||||||
|
## 7.5. Chat (District Lobby)
|
||||||
|
|
||||||
|
- Внизу чи праворуч: Chat Widget, прив'язаний до **District Lobby Room**:
|
||||||
|
- отримати lobby room з `rooms` (наприклад, slug `soul-lobby`, `greenfood-lobby`, `energyunion-lobby`);
|
||||||
|
- чат працює через `/api/v1/chat/rooms/{room_id}`.
|
||||||
|
- Неавторизованим показувати "Увійти, щоб писати".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. МОДУЛЬ 5 — SHORTCUT ROUTES
|
||||||
|
|
||||||
|
Щоб було зручно заходити на платформи:
|
||||||
|
|
||||||
|
- `/soul` → редірект або пряма сторінка, що використовує `getDistrictBySlug("soul")`.
|
||||||
|
- `/greenfood` → `getDistrictBySlug("greenfood")`.
|
||||||
|
- `/energy-union` → `getDistrictBySlug("energy-union")`.
|
||||||
|
|
||||||
|
Реалізація:
|
||||||
|
|
||||||
|
- створити файли:
|
||||||
|
- `apps/web/src/app/soul/page.tsx`
|
||||||
|
- `apps/web/src/app/greenfood/page.tsx`
|
||||||
|
- `apps/web/src/app/energy-union/page.tsx`
|
||||||
|
|
||||||
|
Які просто рендерять той самий компонент, що й `/districts/[slug]`, з фіксованим `slug`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. ІНТЕГРАЦІЯ З CITY LAYER
|
||||||
|
|
||||||
|
На сторінці `/city` або в City Square:
|
||||||
|
|
||||||
|
- для портальних точок (District Portals):
|
||||||
|
- додати кнопки/картки:
|
||||||
|
- "SOUL Retreat District"
|
||||||
|
- "GREENFOOD District"
|
||||||
|
- "Energy Union District"
|
||||||
|
- клік → переходить на:
|
||||||
|
- `/soul`
|
||||||
|
- `/greenfood`
|
||||||
|
- `/energy-union`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. SMOKE-ТЕСТИ
|
||||||
|
|
||||||
|
Після завершення:
|
||||||
|
|
||||||
|
1. `/districts`:
|
||||||
|
- показує 3 District-и (SOUL, GREENFOOD, ENERGYUNION).
|
||||||
|
|
||||||
|
2. `/districts/soul`:
|
||||||
|
- рендериться без помилок,
|
||||||
|
- видно SOUL як lead agent + presence,
|
||||||
|
- видно Spirit/Logic (як core team, якщо додані),
|
||||||
|
- видно список rooms (soul-lobby, soul-events, soul-guidance...),
|
||||||
|
- чат-простір для lobby.
|
||||||
|
|
||||||
|
3. `/districts/greenfood`:
|
||||||
|
- рендериться,
|
||||||
|
- видно ERP GREENFOOD agent,
|
||||||
|
- rooms згідно GREENFOOD_District_Protocol,
|
||||||
|
- microDAO (якщо є) у списку.
|
||||||
|
|
||||||
|
4. `/districts/energy-union`:
|
||||||
|
- рендериться,
|
||||||
|
- видно Helion, Energia,
|
||||||
|
- rooms (lobby, compute, providers, labs...),
|
||||||
|
- чат-простір.
|
||||||
|
|
||||||
|
5. `/soul`, `/greenfood`, `/energy-union`:
|
||||||
|
- відкривають відповідні портали District-ів.
|
||||||
|
|
||||||
|
6. City → District:
|
||||||
|
- з City UI є лінки, які ведуть на District Portals.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. ФІНАЛЬНИЙ ЗВІТ
|
||||||
|
|
||||||
|
Після виконання:
|
||||||
|
|
||||||
|
Створити файл:
|
||||||
|
|
||||||
|
`docs/debug/district_portals_report_<DATE>.md`
|
||||||
|
|
||||||
|
І включити в нього:
|
||||||
|
|
||||||
|
- список District-ів з `/api/v1/districts`,
|
||||||
|
- приклади `GET /api/v1/districts/{slug}`,
|
||||||
|
- скрін/опис `/districts`, `/districts/soul`, `/districts/greenfood`, `/districts/energy-union`,
|
||||||
|
- підтвердження, що `/soul`, `/greenfood`, `/energy-union` працюють.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 12. PROMPT ДЛЯ CURSOR
|
||||||
|
|
||||||
|
```text
|
||||||
|
Виконай TASK_PHASE_DISTRICT_PORTALS_v1.md.
|
||||||
|
|
||||||
|
Фокус:
|
||||||
|
1) Backend: /api/v1/districts, /api/v1/districts/{slug}
|
||||||
|
2) Frontend: /districts, /districts/[slug], /soul, /greenfood, /energy-union
|
||||||
|
3) District UI: lead agent, core team, rooms, microDAO list, chat (lobby room)
|
||||||
|
4) Інтеграція з City Layer (/city → District Portals)
|
||||||
|
|
||||||
|
Після завершення створи:
|
||||||
|
docs/debug/district_portals_report_<DATE>.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Target Date**: Immediate
|
||||||
|
**Priority**: High
|
||||||
|
**Dependencies**: City Layer complete, Matrix rooms synced
|
||||||
|
|
||||||
@@ -2349,3 +2349,191 @@ async def get_or_create_orchestrator_team_room(microdao_id: str) -> Optional[dic
|
|||||||
)
|
)
|
||||||
|
|
||||||
return dict(new_room)
|
return dict(new_room)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Districts Repository (DB-based, no hardcodes)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
async def get_districts() -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Отримати всі District-и з БД.
|
||||||
|
District = microdao з dao_type = 'district'
|
||||||
|
"""
|
||||||
|
pool = await get_pool()
|
||||||
|
query = """
|
||||||
|
SELECT id, slug, name, description, dao_type,
|
||||||
|
orchestrator_agent_id, created_at
|
||||||
|
FROM microdaos
|
||||||
|
WHERE dao_type = 'district'
|
||||||
|
ORDER BY name
|
||||||
|
"""
|
||||||
|
rows = await pool.fetch(query)
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_district_by_slug(slug: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Отримати District за slug.
|
||||||
|
"""
|
||||||
|
pool = await get_pool()
|
||||||
|
query = """
|
||||||
|
SELECT id, slug, name, description, dao_type,
|
||||||
|
orchestrator_agent_id, created_at
|
||||||
|
FROM microdaos
|
||||||
|
WHERE slug = $1
|
||||||
|
AND dao_type = 'district'
|
||||||
|
"""
|
||||||
|
row = await pool.fetchrow(query, slug)
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_district_lead_agent(district_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Отримати lead agent District-а.
|
||||||
|
Шукаємо спочатку role='district_lead', потім fallback на orchestrator.
|
||||||
|
"""
|
||||||
|
pool = await get_pool()
|
||||||
|
|
||||||
|
# Try district_lead first
|
||||||
|
query = """
|
||||||
|
SELECT a.id, a.display_name as name, a.kind, a.status,
|
||||||
|
a.avatar_url, a.gov_level,
|
||||||
|
ma.role as membership_role
|
||||||
|
FROM agents a
|
||||||
|
JOIN microdao_agents ma ON ma.agent_id = a.id
|
||||||
|
WHERE ma.microdao_id = $1
|
||||||
|
AND ma.role = 'district_lead'
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
row = await pool.fetchrow(query, district_id)
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
# Fallback: orchestrator
|
||||||
|
query = """
|
||||||
|
SELECT a.id, a.display_name as name, a.kind, a.status,
|
||||||
|
a.avatar_url, a.gov_level,
|
||||||
|
ma.role as membership_role
|
||||||
|
FROM agents a
|
||||||
|
JOIN microdao_agents ma ON ma.agent_id = a.id
|
||||||
|
WHERE ma.microdao_id = $1
|
||||||
|
AND (ma.role = 'orchestrator' OR ma.is_core = true)
|
||||||
|
ORDER BY ma.is_core DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
row = await pool.fetchrow(query, district_id)
|
||||||
|
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_district_core_team(district_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Отримати core team District-а.
|
||||||
|
"""
|
||||||
|
pool = await get_pool()
|
||||||
|
query = """
|
||||||
|
SELECT a.id, a.display_name as name, a.kind, a.status,
|
||||||
|
a.avatar_url, a.gov_level,
|
||||||
|
ma.role as membership_role
|
||||||
|
FROM agents a
|
||||||
|
JOIN microdao_agents ma ON ma.agent_id = a.id
|
||||||
|
WHERE ma.microdao_id = $1
|
||||||
|
AND (ma.role = 'core_team' OR ma.is_core = true)
|
||||||
|
AND ma.role != 'district_lead'
|
||||||
|
AND ma.role != 'orchestrator'
|
||||||
|
ORDER BY a.display_name
|
||||||
|
"""
|
||||||
|
rows = await pool.fetch(query, district_id)
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_district_agents(district_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Отримати всіх агентів District-а.
|
||||||
|
"""
|
||||||
|
pool = await get_pool()
|
||||||
|
query = """
|
||||||
|
SELECT a.id, a.display_name as name, a.kind, a.status,
|
||||||
|
a.avatar_url, a.gov_level,
|
||||||
|
ma.role as membership_role, ma.is_core
|
||||||
|
FROM agents a
|
||||||
|
JOIN microdao_agents ma ON ma.agent_id = a.id
|
||||||
|
WHERE ma.microdao_id = $1
|
||||||
|
ORDER BY
|
||||||
|
CASE ma.role
|
||||||
|
WHEN 'district_lead' THEN 0
|
||||||
|
WHEN 'orchestrator' THEN 1
|
||||||
|
WHEN 'core_team' THEN 2
|
||||||
|
ELSE 3
|
||||||
|
END,
|
||||||
|
ma.is_core DESC,
|
||||||
|
a.display_name
|
||||||
|
"""
|
||||||
|
rows = await pool.fetch(query, district_id)
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_district_rooms(district_slug: str) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Отримати кімнати District-а за slug-префіксом.
|
||||||
|
Наприклад: soul-lobby, soul-events, greenfood-lobby
|
||||||
|
"""
|
||||||
|
pool = await get_pool()
|
||||||
|
query = """
|
||||||
|
SELECT id, slug, name, description,
|
||||||
|
matrix_room_id, matrix_room_alias,
|
||||||
|
room_role, is_public
|
||||||
|
FROM city_rooms
|
||||||
|
WHERE slug LIKE $1
|
||||||
|
ORDER BY sort_order, name
|
||||||
|
"""
|
||||||
|
rows = await pool.fetch(query, f"{district_slug}-%")
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_district_nodes(district_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Отримати ноди District-а.
|
||||||
|
"""
|
||||||
|
pool = await get_pool()
|
||||||
|
query = """
|
||||||
|
SELECT n.id, n.display_name as name, n.node_type as kind,
|
||||||
|
n.status, n.hostname as location,
|
||||||
|
n.guardian_agent_id, n.steward_agent_id
|
||||||
|
FROM nodes n
|
||||||
|
WHERE n.owner_microdao_id = $1
|
||||||
|
ORDER BY n.display_name
|
||||||
|
"""
|
||||||
|
rows = await pool.fetch(query, district_id)
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_district_stats(district_id: str, district_slug: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Отримати статистику District-а.
|
||||||
|
"""
|
||||||
|
pool = await get_pool()
|
||||||
|
|
||||||
|
# Count agents
|
||||||
|
agents_count = await pool.fetchval(
|
||||||
|
"SELECT COUNT(*) FROM microdao_agents WHERE microdao_id = $1",
|
||||||
|
district_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Count rooms
|
||||||
|
rooms_count = await pool.fetchval(
|
||||||
|
"SELECT COUNT(*) FROM city_rooms WHERE slug LIKE $1",
|
||||||
|
f"{district_slug}-%"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Count nodes
|
||||||
|
nodes_count = await pool.fetchval(
|
||||||
|
"SELECT COUNT(*) FROM nodes WHERE owner_microdao_id = $1",
|
||||||
|
district_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"agents_count": agents_count or 0,
|
||||||
|
"rooms_count": rooms_count or 0,
|
||||||
|
"nodes_count": nodes_count or 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -1123,6 +1123,142 @@ async def get_city_room_by_slug(slug: str):
|
|||||||
raise HTTPException(status_code=500, detail="Failed to get city room")
|
raise HTTPException(status_code=500, detail="Failed to get city room")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Districts API (DB-based, no hardcodes)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@api_router.get("/districts")
|
||||||
|
async def get_districts():
|
||||||
|
"""
|
||||||
|
Отримати список всіх District-ів.
|
||||||
|
District = microdao з dao_type = 'district'
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
districts = await repo_city.get_districts()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for d in districts:
|
||||||
|
# Get lead agent for each district
|
||||||
|
lead_agent = await repo_city.get_district_lead_agent(d["id"])
|
||||||
|
|
||||||
|
# Get rooms count
|
||||||
|
rooms = await repo_city.get_district_rooms(d["slug"])
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
"id": d["id"],
|
||||||
|
"slug": d["slug"],
|
||||||
|
"name": d["name"],
|
||||||
|
"description": d.get("description"),
|
||||||
|
"dao_type": d["dao_type"],
|
||||||
|
"lead_agent": {
|
||||||
|
"id": lead_agent["id"],
|
||||||
|
"name": lead_agent["name"],
|
||||||
|
"avatar_url": lead_agent.get("avatar_url")
|
||||||
|
} if lead_agent else None,
|
||||||
|
"rooms_count": len(rooms),
|
||||||
|
"rooms": [{"id": r["id"], "slug": r["slug"], "name": r["name"]} for r in rooms[:3]]
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get districts: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to get districts")
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/districts/{slug}")
|
||||||
|
async def get_district_detail(slug: str):
|
||||||
|
"""
|
||||||
|
Отримати деталі District-а за slug.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
district = await repo_city.get_district_by_slug(slug)
|
||||||
|
if not district:
|
||||||
|
raise HTTPException(status_code=404, detail=f"District not found: {slug}")
|
||||||
|
|
||||||
|
# Get lead agent
|
||||||
|
lead_agent = await repo_city.get_district_lead_agent(district["id"])
|
||||||
|
|
||||||
|
# Get core team
|
||||||
|
core_team = await repo_city.get_district_core_team(district["id"])
|
||||||
|
|
||||||
|
# Get all agents
|
||||||
|
agents = await repo_city.get_district_agents(district["id"])
|
||||||
|
|
||||||
|
# Get rooms
|
||||||
|
rooms = await repo_city.get_district_rooms(district["slug"])
|
||||||
|
|
||||||
|
# Get nodes
|
||||||
|
nodes = await repo_city.get_district_nodes(district["id"])
|
||||||
|
|
||||||
|
# Get stats
|
||||||
|
stats = await repo_city.get_district_stats(district["id"], district["slug"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"district": {
|
||||||
|
"id": district["id"],
|
||||||
|
"slug": district["slug"],
|
||||||
|
"name": district["name"],
|
||||||
|
"description": district.get("description"),
|
||||||
|
"dao_type": district["dao_type"]
|
||||||
|
},
|
||||||
|
"lead_agent": {
|
||||||
|
"id": lead_agent["id"],
|
||||||
|
"name": lead_agent["name"],
|
||||||
|
"kind": lead_agent.get("kind"),
|
||||||
|
"status": lead_agent.get("status"),
|
||||||
|
"avatar_url": lead_agent.get("avatar_url"),
|
||||||
|
"gov_level": lead_agent.get("gov_level")
|
||||||
|
} if lead_agent else None,
|
||||||
|
"core_team": [
|
||||||
|
{
|
||||||
|
"id": a["id"],
|
||||||
|
"name": a["name"],
|
||||||
|
"kind": a.get("kind"),
|
||||||
|
"status": a.get("status"),
|
||||||
|
"avatar_url": a.get("avatar_url"),
|
||||||
|
"role": a.get("membership_role")
|
||||||
|
} for a in core_team
|
||||||
|
],
|
||||||
|
"agents": [
|
||||||
|
{
|
||||||
|
"id": a["id"],
|
||||||
|
"name": a["name"],
|
||||||
|
"kind": a.get("kind"),
|
||||||
|
"status": a.get("status"),
|
||||||
|
"avatar_url": a.get("avatar_url"),
|
||||||
|
"role": a.get("membership_role"),
|
||||||
|
"is_core": a.get("is_core", False)
|
||||||
|
} for a in agents
|
||||||
|
],
|
||||||
|
"rooms": [
|
||||||
|
{
|
||||||
|
"id": r["id"],
|
||||||
|
"slug": r["slug"],
|
||||||
|
"name": r["name"],
|
||||||
|
"description": r.get("description"),
|
||||||
|
"matrix_room_id": r.get("matrix_room_id"),
|
||||||
|
"room_role": r.get("room_role"),
|
||||||
|
"is_public": r.get("is_public", True)
|
||||||
|
} for r in rooms
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": n["id"],
|
||||||
|
"name": n["name"],
|
||||||
|
"kind": n.get("kind"),
|
||||||
|
"status": n.get("status"),
|
||||||
|
"location": n.get("location")
|
||||||
|
} for n in nodes
|
||||||
|
],
|
||||||
|
"stats": stats
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get district {slug}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to get district")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/rooms/{room_id}", response_model=CityRoomDetail)
|
@router.get("/rooms/{room_id}", response_model=CityRoomDetail)
|
||||||
async def get_city_room(room_id: str):
|
async def get_city_room(room_id: str):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user