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:
Apple
2025-11-30 11:37:56 -08:00
parent 066aae724a
commit 0fd05f678a
3 changed files with 775 additions and 0 deletions

View File

@@ -2349,3 +2349,191 @@ async def get_or_create_orchestrator_team_room(microdao_id: str) -> Optional[dic
)
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
}