"""Analytics service for platform-wide statistics.""" import logging from datetime import datetime, timedelta, timezone from gatehouse_app.models.organization.organization import Organization from gatehouse_app.models.user.user import User from gatehouse_app.models.user.session import Session from gatehouse_app.models.superadmin_audit_log import SuperadminAuditLog logger = logging.getLogger(__name__) class SuperadminAnalyticsService: """Service for platform-wide analytics and statistics.""" @staticmethod def get_dashboard_stats() -> dict: """Get dashboard statistics for the overview page. Returns: Dashboard stats including org count, user count, etc. """ now = datetime.now(timezone.utc) thirty_days_ago = now - timedelta(days=30) # Total organizations total_orgs = Organization.query.filter(Organization.deleted_at.is_(None)).count() active_orgs = Organization.query.filter( Organization.deleted_at.is_(None), Organization.is_active == True, # noqa: E712 ).count() # Total users total_users = User.query.filter(User.deleted_at.is_(None)).count() # Active sessions active_sessions = Session.query.filter( Session.deleted_at.is_(None), Session.status == "active", ).count() # New signups in last 30 days new_users_30d = User.query.filter( User.deleted_at.is_(None), User.created_at >= thirty_days_ago, ).count() # New organizations in last 30 days new_orgs_30d = Organization.query.filter( Organization.deleted_at.is_(None), Organization.created_at >= thirty_days_ago, ).count() # Suspended organizations suspended_orgs = Organization.query.filter( Organization.deleted_at.is_(None), Organization.is_active == False, # noqa: E712 ).count() return { "total_organizations": total_orgs, "active_organizations": active_orgs, "suspended_organizations": suspended_orgs, "total_users": total_users, "active_sessions": active_sessions, "new_users_30d": new_users_30d, "new_orgs_30d": new_orgs_30d, "generated_at": now.isoformat() + "Z", } @staticmethod def get_signup_trends(days: int = 30) -> dict: """Get signup trends over time. Args: days: Number of days to analyze Returns: Daily signup data """ now = datetime.now(timezone.utc) start_date = now - timedelta(days=days) # Get all users created in period users = User.query.filter( User.deleted_at.is_(None), User.created_at >= start_date, ).all() # Group by day daily_signups = {} for i in range(days): date = (start_date + timedelta(days=i)).strftime("%Y-%m-%d") daily_signups[date] = 0 for user in users: date = user.created_at.strftime("%Y-%m-%d") if date in daily_signups: daily_signups[date] += 1 # Convert to list history = [ {"date": date, "value": count} for date, count in sorted(daily_signups.items()) ] return { "period_start": start_date.isoformat() + "Z", "period_end": now.isoformat() + "Z", "total": len(users), "history": history, } @staticmethod def get_org_distribution() -> dict: """Get distribution of organizations by size. Returns: Organization size distribution """ orgs = Organization.query.filter(Organization.deleted_at.is_(None)).all() distribution = { "solo": 0, # 1 user "small": 0, # 2-10 users "medium": 0, # 11-50 users "large": 0, # 51-200 users "enterprise": 0, # 200+ users } for org in orgs: count = org.get_member_count() if count == 1: distribution["solo"] += 1 elif count <= 10: distribution["small"] += 1 elif count <= 50: distribution["medium"] += 1 elif count <= 200: distribution["large"] += 1 else: distribution["enterprise"] += 1 return { "distribution": distribution, "total_orgs": len(orgs), } @staticmethod def get_recent_activity(limit: int = 20) -> list: """Get recent superadmin actions. Args: limit: Maximum number of actions to return Returns: List of recent audit log entries """ logs = SuperadminAuditLog.query.filter( SuperadminAuditLog.deleted_at.is_(None), ).order_by( SuperadminAuditLog.created_at.desc() ).limit(limit).all() return [ { "id": log.id, "superadmin_id": log.superadmin_id, "action": log.action, "resource_type": log.resource_type, "resource_id": log.resource_id, "extra_data": log.extra_data, "ip_address": log.ip_address, "user_agent": log.user_agent, "created_at": log.created_at.isoformat() + "Z" if log.created_at else None, } for log in logs ]