feat: TASK 037A/B - MicroDAO Multi-Room Cleanup & UI Polish
TASK 037A: Backend Consistency - Added db/sql/037_microdao_agent_audit.sql - Added services/city-service/tools/fix_microdao_agent_consistency.py - Updated repo_city.get_public_citizens with stricter filtering (node_id, microdao_membership) - Updated PublicCitizenSummary model to include home_microdao and primary_city_room - Updated NodeProfile model and get_node_by_id to include microdaos list TASK 037B: UI Polish - Updated MicrodaoRoomsSection with role-based colors/icons and mini-map - Updated /microdao/[slug] with new Hero Block (badges, stats, orchestrator) - Updated /citizens/[slug] with MicroDAO cross-link in DAIS profile - Updated /nodes/[nodeId] with MicroDAO Presence section
This commit is contained in:
@@ -208,6 +208,14 @@ class NodeAgentSummary(BaseModel):
|
||||
slug: Optional[str] = None
|
||||
|
||||
|
||||
class NodeMicrodaoSummary(BaseModel):
|
||||
"""Summary of a MicroDAO hosted on a node (via orchestrator)"""
|
||||
id: str
|
||||
slug: str
|
||||
name: str
|
||||
rooms_count: int = 0
|
||||
|
||||
|
||||
class NodeProfile(BaseModel):
|
||||
"""Node profile for Node Directory"""
|
||||
node_id: str
|
||||
@@ -224,6 +232,7 @@ class NodeProfile(BaseModel):
|
||||
steward_agent_id: Optional[str] = None
|
||||
guardian_agent: Optional[NodeAgentSummary] = None
|
||||
steward_agent: Optional[NodeAgentSummary] = None
|
||||
microdaos: List[NodeMicrodaoSummary] = []
|
||||
|
||||
|
||||
class ModelBindings(BaseModel):
|
||||
@@ -302,6 +311,12 @@ class PublicCitizenSummary(BaseModel):
|
||||
status: Optional[str] = None # backward compatibility
|
||||
# Home node info
|
||||
home_node: Optional[HomeNodeView] = None
|
||||
node_id: Optional[str] = None
|
||||
|
||||
# TASK 037A: Alignment
|
||||
home_microdao_slug: Optional[str] = None
|
||||
home_microdao_name: Optional[str] = None
|
||||
primary_city_room: Optional["CityRoomSummary"] = None
|
||||
|
||||
|
||||
class PublicCitizenProfile(BaseModel):
|
||||
|
||||
@@ -941,7 +941,10 @@ async def get_public_citizens(
|
||||
"a.public_slug IS NOT NULL",
|
||||
"COALESCE(a.is_archived, false) = false",
|
||||
"COALESCE(a.is_test, false) = false",
|
||||
"a.deleted_at IS NULL"
|
||||
"a.deleted_at IS NULL",
|
||||
# TASK 037A: Stricter filtering for Citizens Layer
|
||||
"a.node_id IS NOT NULL",
|
||||
"EXISTS (SELECT 1 FROM microdao_agents ma WHERE ma.agent_id = a.id)"
|
||||
]
|
||||
|
||||
if district:
|
||||
@@ -978,9 +981,28 @@ async def get_public_citizens(
|
||||
nc.hostname AS home_node_hostname,
|
||||
nc.roles AS home_node_roles,
|
||||
nc.environment AS home_node_environment,
|
||||
-- MicroDAO info
|
||||
m.slug AS home_microdao_slug,
|
||||
m.name AS home_microdao_name,
|
||||
-- Room info
|
||||
cr.id AS room_id,
|
||||
cr.slug AS room_slug,
|
||||
cr.name AS room_name,
|
||||
cr.matrix_room_id AS room_matrix_id,
|
||||
COUNT(*) OVER() AS total_count
|
||||
FROM agents a
|
||||
LEFT JOIN node_cache nc ON a.node_id = nc.node_id
|
||||
-- Join primary MicroDAO
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT ma.agent_id, md.slug, md.name
|
||||
FROM microdao_agents ma
|
||||
JOIN microdaos md ON ma.microdao_id = md.id
|
||||
WHERE ma.agent_id = a.id
|
||||
ORDER BY ma.is_core DESC, md.name
|
||||
LIMIT 1
|
||||
) m ON true
|
||||
-- Join primary room (by public_primary_room_slug)
|
||||
LEFT JOIN city_rooms cr ON cr.slug = a.public_primary_room_slug
|
||||
WHERE {where_sql}
|
||||
ORDER BY a.display_name
|
||||
LIMIT ${len(params) + 1} OFFSET ${len(params) + 2}
|
||||
@@ -1011,8 +1033,21 @@ async def get_public_citizens(
|
||||
}
|
||||
else:
|
||||
data["home_node"] = None
|
||||
|
||||
# Build primary_city_room object
|
||||
if data.get("room_id"):
|
||||
data["primary_city_room"] = {
|
||||
"id": str(data["room_id"]),
|
||||
"slug": data["room_slug"],
|
||||
"name": data["room_name"],
|
||||
"matrix_room_id": data.get("room_matrix_id")
|
||||
}
|
||||
else:
|
||||
data["primary_city_room"] = None
|
||||
|
||||
# Clean up intermediate fields
|
||||
for key in ["home_node_name", "home_node_hostname", "home_node_roles", "home_node_environment"]:
|
||||
for key in ["home_node_name", "home_node_hostname", "home_node_roles", "home_node_environment",
|
||||
"room_id", "room_slug", "room_name", "room_matrix_id"]:
|
||||
data.pop(key, None)
|
||||
items.append(data)
|
||||
|
||||
@@ -1595,6 +1630,19 @@ async def get_node_by_id(node_id: str) -> Optional[dict]:
|
||||
|
||||
data = dict(row)
|
||||
|
||||
# Fetch MicroDAOs where orchestrator is on this node
|
||||
microdaos = await pool.fetch("""
|
||||
SELECT m.id, m.slug, m.name, COUNT(cr.id) as rooms_count
|
||||
FROM microdaos m
|
||||
JOIN agents a ON m.orchestrator_agent_id = a.id
|
||||
LEFT JOIN city_rooms cr ON cr.microdao_id = m.id
|
||||
WHERE a.node_id = $1
|
||||
GROUP BY m.id, m.slug, m.name
|
||||
ORDER BY m.name
|
||||
""", node_id)
|
||||
|
||||
data["microdaos"] = [dict(m) for m in microdaos]
|
||||
|
||||
# Build guardian_agent object
|
||||
if data.get("guardian_agent_id"):
|
||||
data["guardian_agent"] = {
|
||||
@@ -1616,7 +1664,7 @@ async def get_node_by_id(node_id: str) -> Optional[dict]:
|
||||
}
|
||||
else:
|
||||
data["steward_agent"] = None
|
||||
|
||||
|
||||
# Clean up intermediate fields
|
||||
for key in ["guardian_name", "guardian_kind", "guardian_slug",
|
||||
"steward_name", "steward_kind", "steward_slug"]:
|
||||
|
||||
177
services/city-service/tools/fix_microdao_agent_consistency.py
Normal file
177
services/city-service/tools/fix_microdao_agent_consistency.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import asyncpg
|
||||
from uuid import UUID
|
||||
|
||||
# Add parent directory to path to allow importing from app if needed,
|
||||
# though we will try to use direct SQL for maintenance script ease.
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@postgres:5432/daarion")
|
||||
|
||||
async def get_connection():
|
||||
return await asyncpg.connect(DATABASE_URL)
|
||||
|
||||
async def fix_agents(conn, apply=False):
|
||||
print("--- CHECKING AGENTS ---")
|
||||
|
||||
# 1. Agents with missing public_slug
|
||||
rows = await conn.fetch("""
|
||||
SELECT id, display_name FROM agents
|
||||
WHERE is_public = true AND (public_slug IS NULL OR public_slug = '')
|
||||
""")
|
||||
|
||||
if rows:
|
||||
print(f"Found {len(rows)} public agents without public_slug.")
|
||||
for r in rows:
|
||||
print(f" - {r['display_name']} ({r['id']}) -> will set to {r['id']}")
|
||||
if apply:
|
||||
await conn.execute("""
|
||||
UPDATE agents SET public_slug = id
|
||||
WHERE id = $1
|
||||
""", r['id'])
|
||||
else:
|
||||
print("OK: All public agents have public_slug.")
|
||||
|
||||
# 2. Agents without node_id
|
||||
# We assume NODE1 as default fallback if we must set it, but per task we mostly log it.
|
||||
rows = await conn.fetch("""
|
||||
SELECT id, display_name FROM agents
|
||||
WHERE is_public = true AND (node_id IS NULL OR node_id = '')
|
||||
""")
|
||||
if rows:
|
||||
print(f"Found {len(rows)} public agents without node_id (WARNING).")
|
||||
for r in rows:
|
||||
print(f" - {r['display_name']} ({r['id']})")
|
||||
# Option: could set to default node if needed, but task says "optionally try to set".
|
||||
# We will skip auto-setting for now to avoid assigning wrong node.
|
||||
else:
|
||||
print("OK: All public agents have node_id.")
|
||||
|
||||
# 3. Agents without microdao memberships
|
||||
rows = await conn.fetch("""
|
||||
SELECT a.id, a.display_name
|
||||
FROM agents a
|
||||
WHERE a.is_public = true
|
||||
AND NOT EXISTS (SELECT 1 FROM microdao_agents ma WHERE ma.agent_id = a.id)
|
||||
""")
|
||||
if rows:
|
||||
print(f"Found {len(rows)} public agents without MicroDAO membership (WARNING).")
|
||||
for r in rows:
|
||||
print(f" - {r['display_name']} ({r['id']})")
|
||||
else:
|
||||
print("OK: All public agents have at least one MicroDAO membership.")
|
||||
|
||||
|
||||
async def fix_microdaos(conn, apply=False):
|
||||
print("\n--- CHECKING MICRODAOS ---")
|
||||
|
||||
# 1. MicroDAO without rooms
|
||||
rows = await conn.fetch("""
|
||||
SELECT m.id, m.slug, m.name
|
||||
FROM microdaos m
|
||||
WHERE m.is_public = true
|
||||
AND NOT EXISTS (SELECT 1 FROM city_rooms cr WHERE cr.microdao_id = m.id)
|
||||
""")
|
||||
if rows:
|
||||
print(f"Found {len(rows)} public MicroDAOs without rooms (WARNING).")
|
||||
for r in rows:
|
||||
print(f" - {r['name']} ({r['slug']})")
|
||||
else:
|
||||
print("OK: All public MicroDAOs have at least one room.")
|
||||
|
||||
# 2. MicroDAO without PRIMARY room (but has rooms)
|
||||
rows = await conn.fetch("""
|
||||
SELECT m.id, m.slug, m.name
|
||||
FROM microdaos m
|
||||
WHERE m.is_public = true
|
||||
AND EXISTS (SELECT 1 FROM city_rooms cr WHERE cr.microdao_id = m.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM city_rooms cr WHERE cr.microdao_id = m.id AND cr.room_role = 'primary')
|
||||
""")
|
||||
|
||||
if rows:
|
||||
print(f"Found {len(rows)} MicroDAOs with rooms but NO primary room.")
|
||||
for r in rows:
|
||||
print(f" - {r['name']} ({r['slug']})")
|
||||
|
||||
# Find candidate: lowest sort_order
|
||||
candidate = await conn.fetchrow("""
|
||||
SELECT id, name, sort_order FROM city_rooms
|
||||
WHERE microdao_id = $1
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
LIMIT 1
|
||||
""", r['id'])
|
||||
|
||||
if candidate:
|
||||
print(f" -> Candidate: {candidate['name']} (sort: {candidate['sort_order']})")
|
||||
if apply:
|
||||
print(" -> Setting as primary...")
|
||||
await conn.execute("""
|
||||
UPDATE city_rooms SET room_role = 'primary', sort_order = 0
|
||||
WHERE id = $1
|
||||
""", candidate['id'])
|
||||
else:
|
||||
print("OK: All MicroDAOs with rooms have a primary room.")
|
||||
|
||||
# 3. MicroDAO with MULTIPLE primary rooms
|
||||
rows = await conn.fetch("""
|
||||
SELECT m.id, m.slug, m.name
|
||||
FROM microdaos m
|
||||
JOIN city_rooms cr ON cr.microdao_id = m.id
|
||||
WHERE cr.room_role = 'primary'
|
||||
GROUP BY m.id, m.slug, m.name
|
||||
HAVING COUNT(*) > 1
|
||||
""")
|
||||
|
||||
if rows:
|
||||
print(f"Found {len(rows)} MicroDAOs with MULTIPLE primary rooms.")
|
||||
for r in rows:
|
||||
print(f" - {r['name']} ({r['slug']})")
|
||||
|
||||
primaries = await conn.fetch("""
|
||||
SELECT id, name, sort_order FROM city_rooms
|
||||
WHERE microdao_id = $1 AND room_role = 'primary'
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
""", r['id'])
|
||||
|
||||
# Keep the first one, demote others
|
||||
keep = primaries[0]
|
||||
others = primaries[1:]
|
||||
|
||||
print(f" -> Keeping: {keep['name']}")
|
||||
for o in others:
|
||||
print(f" -> Demoting: {o['name']}")
|
||||
if apply:
|
||||
await conn.execute("""
|
||||
UPDATE city_rooms SET room_role = 'team'
|
||||
WHERE id = $1
|
||||
""", o['id'])
|
||||
else:
|
||||
print("OK: No MicroDAOs have multiple primary rooms.")
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description="Fix MicroDAO/Agent consistency")
|
||||
parser.add_argument("--apply", action="store_true", help="Apply changes to DB")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Connecting to {DATABASE_URL}...")
|
||||
try:
|
||||
conn = await get_connection()
|
||||
except Exception as e:
|
||||
print(f"Failed to connect to DB: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
await fix_agents(conn, args.apply)
|
||||
await fix_microdaos(conn, args.apply)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
print("\nDone.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
Reference in New Issue
Block a user