"""Public Codex endpoints — the browsable registry of every spirit ever contacted, shared across all users (spec §4).""" import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import desc, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.db import get_db from app.models.contact_session import ContactSession from app.models.entity import Entity from app.models.entity_sighting import EntitySighting from app.models.event import Event from app.models.user import User router = APIRouter(prefix="/api", tags=["codex"]) def _entity_card(entity: Entity, discoverer: str | None) -> dict: return { "id": str(entity.id), "name": entity.name, "epithet": entity.epithet, "rarity": entity.rarity_tier, "visual": entity.visual_profile, "quotes": entity.sample_quotes, "contact_count": entity.contact_count, "discovered_at": entity.discovered_at.isoformat(), "discovered_by": discoverer, } @router.get("/codex") async def list_codex( rarity: str | None = None, sort: str = "recent", limit: int = 60, db: AsyncSession = Depends(get_db), ): query = select(Entity) if rarity: query = query.where(Entity.rarity_tier == rarity) if sort == "contacted": query = query.order_by(desc(Entity.contact_count)) else: query = query.order_by(desc(Entity.discovered_at)) query = query.limit(min(limit, 200)) entities = (await db.execute(query)).scalars().all() discoverers = { user.id: user.username for user in ( await db.execute( select(User).where( User.id.in_({e.discovered_by for e in entities if e.discovered_by}) ) ) ).scalars() } return { "entities": [ _entity_card(e, discoverers.get(e.discovered_by)) for e in entities ] } @router.get("/codex/{entity_id}") async def get_entity(entity_id: uuid.UUID, db: AsyncSession = Depends(get_db)): entity = await db.get(Entity, entity_id) if entity is None: raise HTTPException(status.HTTP_404_NOT_FOUND, "no such spirit in the codex") discoverer = None if entity.discovered_by: user = await db.get(User, entity.discovered_by) discoverer = user.username if user else None sightings = await db.scalar( select(func.count(EntitySighting.id)).where(EntitySighting.entity_id == entity.id) ) card = _entity_card(entity, discoverer) card["persona"] = entity.persona card["voice"] = entity.voice_profile card["sightings"] = sightings or 0 return card @router.get("/stats") async def veil_stats(db: AsyncSession = Depends(get_db)): """Live counters for the landing page's 'veil activity' ticker.""" return { "entities": await db.scalar(select(func.count(Entity.id))) or 0, "sessions": await db.scalar(select(func.count(ContactSession.id))) or 0, "utterances": await db.scalar( select(func.count(Event.id)).where(Event.kind == "utterance") ) or 0, "anomalies": await db.scalar( select(func.count(Event.id)).where(Event.kind == "anomaly") ) or 0, }