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:
Apple
2025-11-29 01:35:54 -08:00
parent 86f5b58de5
commit 3ccc0e2d43
11 changed files with 862 additions and 360 deletions

View File

@@ -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):

View File

@@ -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"]:

View 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())