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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -1123,6 +1123,142 @@ async def get_city_room_by_slug(slug: str):
|
||||
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)
|
||||
async def get_city_room(room_id: str):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user