feat: Upgrade Global Presence to SSE architecture

- matrix-presence-aggregator v2 with SSE endpoint
- Created @presence_daemon Matrix user
- SSE proxy in Next.js /api/presence/stream
- Updated frontend to use SSE instead of WebSocket
- Real-time city online count and room presence
This commit is contained in:
Apple
2025-11-26 14:43:46 -08:00
parent c456727d53
commit 5bed515852
18 changed files with 709 additions and 729 deletions

View File

@@ -0,0 +1,69 @@
"""Room source - reads rooms from database or static config"""
from sqlalchemy import create_engine, text
from typing import List, Dict
import logging
import yaml
logger = logging.getLogger(__name__)
class RoomsSource:
"""Reads room list from PostgreSQL database"""
def __init__(self, db_dsn: str):
self.engine = create_engine(db_dsn)
def get_rooms(self) -> List[Dict]:
"""
Get all rooms with matrix_room_id set.
Expected table structure:
- id (text)
- slug (text)
- name (text)
- matrix_room_id (text, nullable)
"""
query = text(
"""
SELECT id, slug, name, matrix_room_id
FROM city_rooms
WHERE matrix_room_id IS NOT NULL
"""
)
try:
with self.engine.connect() as conn:
rows = conn.execute(query).mappings().all()
return [
{
"room_id": str(r["id"]),
"slug": r["slug"],
"title": r["name"],
"matrix_room_id": r["matrix_room_id"],
}
for r in rows
]
except Exception as e:
logger.error(f"Failed to get rooms from database: {e}")
return []
class StaticRoomsSource:
"""Reads room list from YAML config file"""
def __init__(self, config_path: str):
self.config_path = config_path
self._rooms = self._load_config()
def _load_config(self) -> List[Dict]:
try:
with open(self.config_path, 'r') as f:
data = yaml.safe_load(f)
return data.get('rooms', [])
except Exception as e:
logger.error(f"Failed to load rooms config: {e}")
return []
def get_rooms(self) -> List[Dict]:
return self._rooms