feat(city-map): Add 2D City Map with coordinates and agent presence

- Add migration 013_city_map_coordinates.sql with map coordinates, zones, and agents table
- Add /city/map API endpoint in city-service
- Add /city/agents and /city/agents/online endpoints
- Extend presence aggregator to include agents[] in snapshot
- Add AgentsSource for fetching agent data from DB
- Create CityMap component with interactive room tiles
- Add useCityMap hook for fetching map data
- Update useGlobalPresence to include agents
- Add map/list view toggle on /city page
- Add agent badges to room cards and map tiles
This commit is contained in:
Apple
2025-11-27 07:00:47 -08:00
parent 3de3c8cb36
commit 6bd769ef40
258 changed files with 1747 additions and 79 deletions

View File

@@ -4,9 +4,10 @@ from datetime import datetime, timezone
from typing import List, Optional
import logging
from .models import PresenceSnapshot, RoomPresence, CityPresence
from .models import PresenceSnapshot, RoomPresence, CityPresence, AgentPresence
from .matrix_client import MatrixClient
from .rooms_source import RoomsSource
from .agents_source import AgentsSource
logger = logging.getLogger(__name__)
@@ -16,6 +17,7 @@ class PresenceAggregator:
Aggregates presence data from Matrix and broadcasts to subscribers.
- Periodically polls Matrix for room members and presence
- Fetches agent status from database
- Caches the latest snapshot
- Broadcasts updates to SSE subscribers
"""
@@ -24,10 +26,12 @@ class PresenceAggregator:
self,
matrix_client: MatrixClient,
rooms_source: RoomsSource,
agents_source: Optional[AgentsSource] = None,
poll_interval_seconds: int = 5,
):
self.matrix_client = matrix_client
self.rooms_source = rooms_source
self.agents_source = agents_source
self.poll_interval_seconds = poll_interval_seconds
self._snapshot: Optional[PresenceSnapshot] = None
@@ -62,18 +66,47 @@ class PresenceAggregator:
pass
async def _compute_snapshot(self) -> PresenceSnapshot:
"""Compute a new presence snapshot from Matrix"""
"""Compute a new presence snapshot from Matrix and agents DB"""
rooms = self.rooms_source.get_rooms()
if not rooms:
logger.warning("No rooms with matrix_room_id found")
# Fetch agents from database
all_agents: List[AgentPresence] = []
agents_by_room: dict = {}
if self.agents_source:
try:
online_agents = self.agents_source.get_online_agents()
for agent in online_agents:
ap = AgentPresence(
agent_id=agent["agent_id"],
display_name=agent["display_name"],
kind=agent.get("kind", "assistant"),
status=agent.get("status", "online"),
room_id=agent.get("room_id"),
color=agent.get("color", "cyan")
)
all_agents.append(ap)
# Group by room
room_id = agent.get("room_id")
if room_id:
if room_id not in agents_by_room:
agents_by_room[room_id] = []
agents_by_room[room_id].append(ap)
except Exception as e:
logger.error(f"Error fetching agents: {e}")
room_presences: List[RoomPresence] = []
city_online_total = 0
rooms_online = 0
for r in rooms:
matrix_room_id = r["matrix_room_id"]
room_id = r["room_id"]
try:
# Get room members
@@ -99,24 +132,30 @@ class PresenceAggregator:
city_online_total += online_count
# Get agents for this room
room_agents = agents_by_room.get(room_id, [])
room_presences.append(
RoomPresence(
room_id=r["room_id"],
room_id=room_id,
matrix_room_id=matrix_room_id,
online=online_count,
typing=typing_count,
agents=room_agents,
)
)
except Exception as e:
logger.error(f"Error processing room {r['room_id']}: {e}")
# Add room with 0 online
logger.error(f"Error processing room {room_id}: {e}")
# Add room with 0 online but include agents
room_agents = agents_by_room.get(room_id, [])
room_presences.append(
RoomPresence(
room_id=r["room_id"],
room_id=room_id,
matrix_room_id=matrix_room_id,
online=0,
typing=0,
agents=room_agents,
)
)
@@ -125,11 +164,13 @@ class PresenceAggregator:
city=CityPresence(
online_total=city_online_total,
rooms_online=rooms_online,
agents_online=len(all_agents),
),
rooms=room_presences,
agents=all_agents,
)
logger.info(f"Computed snapshot: {city_online_total} online in {rooms_online} rooms")
logger.info(f"Computed snapshot: {city_online_total} online, {len(all_agents)} agents in {rooms_online} rooms")
return snapshot
async def run_forever(self):
@@ -152,3 +193,4 @@ class PresenceAggregator:
self._running = False
logger.info("Stopping presence aggregator")