import 'market_history_admin_config.dart'; import 'backfill_sync_item.dart'; enum SyncRunSeverity { ok, warning, error, rateLimit } enum SyncRunStatus { success, failed, partial, inProgress } class SyncRunEvent { const SyncRunEvent({ required this.id, required this.kind, required this.startedAt, required this.finishedAt, required this.rowsWritten, required this.rowsRemoved, required this.error, required this.severity, required this.status, required this.durationMs, required this.summary, this.backfillItems = const [], }); final int id; final String kind; final DateTime startedAt; final DateTime? finishedAt; final int rowsWritten; final int rowsRemoved; final String? error; final SyncRunSeverity severity; final SyncRunStatus status; final int? durationMs; final String summary; final List backfillItems; String get displayTitle { switch (kind) { case 'universe': return 'Asset universe sync'; case 'backfill': return 'Market history backfill'; case 'cleanup': return 'Data retention cleanup'; default: return kind; } } factory SyncRunEvent.fromJson(Map json) { return SyncRunEvent( id: (json['id'] as num).toInt(), kind: json['kind'] as String, startedAt: DateTime.parse(json['startedAt'] as String).toUtc(), finishedAt: (json['finishedAt'] as String?) == null ? null : DateTime.parse(json['finishedAt'] as String).toUtc(), rowsWritten: (json['rowsWritten'] as num?)?.toInt() ?? 0, rowsRemoved: (json['rowsRemoved'] as num?)?.toInt() ?? 0, error: json['error'] as String?, severity: _parseSeverity( json['severity'] as String?, json['error'] as String?, (json['finishedAt'] as String?) == null, ), status: _parseStatus( json['status'] as String?, json['error'] as String?, (json['finishedAt'] as String?) == null, (json['rowsWritten'] as num?)?.toInt() ?? 0, ), durationMs: (json['durationMs'] as num?)?.toInt(), summary: (json['summary'] as String?)?.trim().isNotEmpty == true ? (json['summary'] as String) : _fallbackSummary( kind: json['kind'] as String, rowsWritten: (json['rowsWritten'] as num?)?.toInt() ?? 0, rowsRemoved: (json['rowsRemoved'] as num?)?.toInt() ?? 0, error: json['error'] as String?, ), backfillItems: _parseBackfillItems(json['backfillItems']), ); } static List _parseBackfillItems(dynamic raw) { if (raw is! List) { return const []; } return raw .whereType>() .map(BackfillSyncItem.fromJson) .toList(growable: false); } static SyncRunSeverity _parseSeverity( String? wire, String? error, bool inProgress, ) { switch (wire) { case 'ok': return SyncRunSeverity.ok; case 'warning': return SyncRunSeverity.warning; case 'error': return SyncRunSeverity.error; case 'rate_limit': return SyncRunSeverity.rateLimit; default: final String normalized = (error ?? '').toLowerCase(); if (normalized.contains('429') || normalized.contains('rate limit') || normalized.contains('rate limited')) { return SyncRunSeverity.rateLimit; } if (error != null && error.trim().isNotEmpty) { return SyncRunSeverity.error; } if (inProgress) { return SyncRunSeverity.warning; } return SyncRunSeverity.ok; } } static SyncRunStatus _parseStatus( String? wire, String? error, bool inProgress, int rowsWritten, ) { switch (wire) { case 'success': return SyncRunStatus.success; case 'failed': return SyncRunStatus.failed; case 'partial': return SyncRunStatus.partial; case 'in_progress': return SyncRunStatus.inProgress; default: if (inProgress) { return SyncRunStatus.inProgress; } if (error != null && error.trim().isNotEmpty) { return rowsWritten > 0 ? SyncRunStatus.partial : SyncRunStatus.failed; } return SyncRunStatus.success; } } static String _fallbackSummary({ required String kind, required int rowsWritten, required int rowsRemoved, required String? error, }) { if (error != null && error.trim().isNotEmpty && rowsWritten > 0) { return 'Partial success: $rowsWritten rows written'; } if (error != null && error.trim().isNotEmpty) { return 'Run failed'; } if (kind == 'cleanup') { return '$rowsRemoved rows removed'; } if (kind == 'universe') { return '$rowsWritten assets refreshed'; } return '$rowsWritten bar rows written'; } } class SyncRunLogPage { const SyncRunLogPage({ required this.runs, required this.pinned, required this.nextBefore, this.config = const MarketHistoryAdminConfig( archiveEnabled: false, windowDays: 5, retentionDays: 5, syncEnabled: false, ), }); final List runs; final List pinned; final DateTime? nextBefore; final MarketHistoryAdminConfig config; }