from datetime import datetime import json from pathlib import Path import sqlite3 from database.job_store import JobStore from database.maintenance import ( backup_storage_status, check_managed_databases, ) from database.paths import BACKUP_ROOT from web.config import Config from web.database import query_db from web.services.integrity_service import IntegrityService from web.services.roster_version_service import RosterVersionService from web.services.team_context_service import TeamContextService class AdminService: DATABASE_LABELS = { 'l2': 'L2 Facts', 'l3': 'L3 Marts', 'web': 'Web App', } @staticmethod def _format_bytes(value): value = float(value or 0) for unit in ('B', 'KB', 'MB', 'GB'): if value < 1024 or unit == 'GB': return f'{value:.1f} {unit}' value /= 1024 @staticmethod def get_overview(): integrity = IntegrityService.build_report() store = JobStore(Config.DB_WEB_PATH) latest_report = query_db( 'l3', """ SELECT match_id, match_date, map_name, is_win, team_avg_rating, summary_text FROM dm_match_reports ORDER BY match_date DESC LIMIT 1 """, one=True, ) latest_match = dict(latest_report) if latest_report else None if latest_match: latest_match['date_text'] = datetime.fromtimestamp( latest_match['match_date'] ).strftime('%Y-%m-%d %H:%M') counts = integrity['counts'] return { 'integrity': integrity, 'jobs': store.list_jobs(8), 'job_summary': store.get_summary(), 'latest_match': latest_match, 'roster_size': len(TeamContextService.get_active_roster_ids()), 'roster_versions': len(RosterVersionService.list_versions()), 'metrics': { 'matches': counts.get('matches', 0), 'roster_players': counts.get('active_roster', 0), 'reports': counts.get('l3_match_reports', 0), 'awards': counts.get('l3_awards', 0), 'backup_size': AdminService._format_bytes( counts.get('backup_bytes', 0) ), }, } @staticmethod def get_jobs(status=None, job_type=None, limit=50): store = JobStore(Config.DB_WEB_PATH) return { 'jobs': store.list_jobs( limit=limit, status=status, job_type=job_type, ), 'summary': store.get_summary(), 'status': status, 'job_type': job_type, } @staticmethod def get_database_catalog(db_name): if db_name not in {'l2', 'l3', 'web'}: raise ValueError('Unknown database') rows = query_db( db_name, """ SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY type, name """, ) return [dict(row) for row in rows] @staticmethod def get_system_status(): managed = check_managed_databases() web_status = { 'path': Config.DB_WEB_PATH, 'exists': Path(Config.DB_WEB_PATH).exists(), 'size_bytes': ( Path(Config.DB_WEB_PATH).stat().st_size if Path(Config.DB_WEB_PATH).exists() else 0 ), } databases = [] for name, item in managed.items(): entry = dict(item) entry['name'] = name.upper() entry['size_text'] = AdminService._format_bytes( entry['size_bytes'] ) databases.append(entry) web_status.update({ 'name': 'WEB', 'quick_check': 'managed by app', 'size_text': AdminService._format_bytes(web_status['size_bytes']), }) databases.append(web_status) backup_dirs = sorted( [path for path in BACKUP_ROOT.glob('*') if path.is_dir()], key=lambda path: path.stat().st_mtime, reverse=True, ) backups = [] for directory in backup_dirs: manifest_path = directory / 'manifest.json' manifest = {} if manifest_path.exists(): try: manifest = json.loads( manifest_path.read_text(encoding='utf-8') ) except (OSError, json.JSONDecodeError): manifest = {} size = sum( file.stat().st_size for file in directory.rglob('*') if file.is_file() ) backups.append({ 'name': directory.name, 'created_at': manifest.get('created_at'), 'size_text': AdminService._format_bytes(size), 'databases': manifest.get('databases', {}), }) return { 'databases': databases, 'backups': backups, 'backup_summary': backup_storage_status(), 'config': { 'web_schema_version': Config.WEB_SCHEMA_VERSION, 'sqlite_timeout': Config.SQLITE_TIMEOUT_SECONDS, 'slow_query_threshold': Config.SLOW_QUERY_THRESHOLD_SECONDS, 'max_upload_mb': Config.MAX_CONTENT_LENGTH / 1024 / 1024, 'secret_key_configured': ( Config.SECRET_KEY != 'yrtv-dev-only-change-me' ), 'admin_token_configured': ( Config.ADMIN_TOKEN != 'yrtv-admin-dev' ), }, }