from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends from sqlalchemy import func, select, text from sqlalchemy.ext.asyncio import AsyncSession from haunt_fm.db import get_session from haunt_fm.models.track import ( ListenEvent, Playlist, TasteProfile, Track, TrackEmbedding, ) from haunt_fm.config import settings from haunt_fm.services.embedding import is_model_loaded from haunt_fm.services.embedding_worker import is_running as is_worker_running from haunt_fm.services.embedding_worker import last_processed as worker_last_processed router = APIRouter(prefix="/api") @router.get("/status") async def status(session: AsyncSession = Depends(get_session)): # DB connectivity try: await session.execute(text("SELECT 1")) db_connected = True except Exception: db_connected = False if not db_connected: return {"healthy": False, "db_connected": False} now = datetime.now(timezone.utc) day_ago = now - timedelta(days=1) # Listen events total_events = (await session.execute(select(func.count(ListenEvent.id)))).scalar() or 0 events_24h = ( await session.execute( select(func.count(ListenEvent.id)).where(ListenEvent.listened_at >= day_ago) ) ).scalar() or 0 latest_event = ( await session.execute(select(func.max(ListenEvent.listened_at))) ).scalar() # Tracks total_tracks = (await session.execute(select(func.count(Track.id)))).scalar() or 0 from_history = ( await session.execute( select(func.count(func.distinct(ListenEvent.track_id))) ) ).scalar() or 0 from_discovery = total_tracks - from_history # Embeddings def _embedding_count(status_val: str): return select(func.count(Track.id)).where(Track.embedding_status == status_val) emb_done = (await session.execute(_embedding_count("done"))).scalar() or 0 emb_pending = (await session.execute(_embedding_count("pending"))).scalar() or 0 emb_failed = (await session.execute(_embedding_count("failed"))).scalar() or 0 emb_no_preview = (await session.execute(_embedding_count("no_preview"))).scalar() or 0 # Taste profile taste = (await session.execute(select(TasteProfile).where(TasteProfile.name == "default"))).scalar() # Playlists total_playlists = (await session.execute(select(func.count(Playlist.id)))).scalar() or 0 last_playlist = (await session.execute(select(func.max(Playlist.created_at)))).scalar() return { "healthy": db_connected, "db_connected": db_connected, "clap_model_loaded": is_model_loaded(), "pipeline": { "listen_events": { "total": total_events, "last_24h": events_24h, "latest": latest_event.isoformat() if latest_event else None, }, "tracks": { "total": total_tracks, "from_history": from_history, "from_discovery": from_discovery, }, "embeddings": { "done": emb_done, "pending": emb_pending, "failed": emb_failed, "no_preview": emb_no_preview, "worker_running": is_worker_running(), "worker_last_processed": worker_last_processed().isoformat() if worker_last_processed() else None, }, "taste_profile": { "exists": taste is not None, "track_count": taste.track_count if taste else 0, "updated_at": taste.updated_at.isoformat() if taste else None, }, "playlists": { "total_generated": total_playlists, "last_generated": last_playlist.isoformat() if last_playlist else None, }, }, "dependencies": { "lastfm_api": "configured" if settings.lastfm_api_key else "not_configured", "itunes_api": "ok", # no auth needed "ha_reachable": bool(settings.ha_token), "music_assistant_reachable": bool(settings.ha_token), }, }