"""Identity Hub overview — the landing cockpit aggregation. Composes the existing identity subsystems into one read so the `/admin/identity` landing page (which replaced the "Coming soon" shell) is the navigation root of the Identity Hub: headcount by lifecycle state, login bindings by provider, downstream sync-target health, the most recent sync runs, and the data-quality posture. This is a pure composition over existing models + :class:`IdentityDataQualityService` — no new read model (the Identity-Hub spine principle). """ from __future__ import annotations from datetime import datetime from typing import Any from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.identity_sync.models import ( IdentitySyncRun, IdentitySyncTarget, ) from app.identity.data_quality_service import IdentityDataQualityService from app.identity.person_models import ( IdentityExternalIdentity, IdentityPerson, ) def _iso(v: datetime | None) -> str | None: return v.isoformat() if v is not None else None class IdentityHubService: def __init__(self, db: AsyncSession): self.db = db async def overview(self) -> dict[str, Any]: return { "people": await self._people_by_status(), "login_identities": await self._identities_by_provider(), "sync_targets": await self._sync_targets(), "recent_runs": await self._recent_runs(), "data_quality": await self._data_quality(), } async def _people_by_status(self) -> dict[str, Any]: rows = (await self.db.execute( select( IdentityPerson.employment_status, func.count().label("n"), ) .where(IdentityPerson.deleted_at.is_(None)) .group_by(IdentityPerson.employment_status) )).all() by_status = {status: int(n) for status, n in rows} return { "by_status": by_status, "total": sum(by_status.values()), } async def _identities_by_provider(self) -> dict[str, Any]: rows = (await self.db.execute( select( IdentityExternalIdentity.provider, func.count().label("n"), ) .where(IdentityExternalIdentity.deleted_at.is_(None)) .group_by(IdentityExternalIdentity.provider) )).all() by_provider = {provider: int(n) for provider, n in rows} # Login-disabled + suspended counts — the admin-kill-switch surface. login_disabled = (await self.db.execute( select(func.count()).select_from(IdentityExternalIdentity).where( IdentityExternalIdentity.deleted_at.is_(None), IdentityExternalIdentity.is_login_identity.is_(False), ) )).scalar_one() suspended = (await self.db.execute( select(func.count()).select_from(IdentityExternalIdentity).where( IdentityExternalIdentity.deleted_at.is_(None), IdentityExternalIdentity.identity_status == "suspended", ) )).scalar_one() return { "by_provider": by_provider, "total": sum(by_provider.values()), "login_disabled": int(login_disabled), "suspended": int(suspended), } async def _sync_targets(self) -> list[dict[str, Any]]: targets = (await self.db.execute( select(IdentitySyncTarget) .where(IdentitySyncTarget.deleted_at.is_(None)) .order_by(IdentitySyncTarget.name) )).scalars().all() return [ { "id": str(t.id), "name": t.name, "slug": t.slug, "kind": t.kind, "enabled": t.enabled, "last_run_status": t.last_run_status, "last_run_at": _iso(t.last_run_at), "next_run_at": _iso(t.next_run_at), } for t in targets ] async def _recent_runs(self, *, limit: int = 10) -> list[dict[str, Any]]: rows = (await self.db.execute( select(IdentitySyncRun, IdentitySyncTarget.name) .join(IdentitySyncTarget, IdentitySyncTarget.id == IdentitySyncRun.target_id) .order_by(IdentitySyncRun.started_at.desc()) .limit(limit) )).all() return [ { "id": str(run.id), "target_id": str(run.target_id), "target_name": name, "status": run.status, "trigger_source": run.trigger_source, "started_at": _iso(run.started_at), "persons_in_scope": run.persons_in_scope, "created_count": run.created_count, "updated_count": run.updated_count, "failed_count": run.failed_count, } for run, name in rows ] async def _data_quality(self) -> dict[str, Any]: summary = await IdentityDataQualityService(self.db).summary() # Trim the trend — the hub tile only needs the headline numbers; # the dedicated panel owns the full trend. return { "open_total": summary["open_total"], "open_high": summary["open_high"], "by_type": summary["by_type"], "last_run_at": summary["last_run_at"], }