import '../models/market_history_admin_config.dart'; import '../models/sync_run_event.dart'; import '../services/market_history_admin_api.dart'; abstract class SyncRunLogController { SyncRunLogRepositoryState get state; Future loadInitial({int limit = 50}); Future refresh({int limit = 50}); Future loadMore({int limit = 50}); Future triggerResync(); Future triggerCleanup({bool archive = false}); } class SyncRunLogRepositoryState { const SyncRunLogRepositoryState({ required this.pinned, required this.history, required this.nextBefore, required this.isLoading, required this.errorMessage, this.config = const MarketHistoryAdminConfig( archiveEnabled: false, windowDays: 5, retentionDays: 5, syncEnabled: false, ), }); final List pinned; final List history; final DateTime? nextBefore; final bool isLoading; final String? errorMessage; final MarketHistoryAdminConfig config; bool get hasMore => nextBefore != null; bool get hasInProgressRun { bool inProgress(SyncRunEvent event) => event.status == SyncRunStatus.inProgress; return pinned.any(inProgress) || history.any(inProgress); } } class SyncRunLogRepository implements SyncRunLogController { SyncRunLogRepository({required MarketHistoryAdminApi api}) : _api = api; final MarketHistoryAdminApi _api; List _pinned = []; List _history = []; DateTime? _nextBefore; bool _isLoading = false; String? _errorMessage; MarketHistoryAdminConfig _config = const MarketHistoryAdminConfig( archiveEnabled: false, windowDays: 5, retentionDays: 5, syncEnabled: false, ); SyncRunLogRepositoryState get state => SyncRunLogRepositoryState( pinned: List.unmodifiable(_pinned), history: List.unmodifiable(_history), nextBefore: _nextBefore, isLoading: _isLoading, errorMessage: _errorMessage, config: _config, ); Future loadInitial({int limit = 50}) async { _isLoading = true; _errorMessage = null; try { final SyncRunLogPage page = await _api.fetchSyncRuns(limit: limit); _pinned = _dedupeById(page.pinned); _history = _dedupeById(_sortedNewestFirst(page.runs)); _nextBefore = page.nextBefore; _config = page.config; } on MarketHistoryAdminApiException catch (e) { _errorMessage = e.message; } catch (e) { _errorMessage = e.toString(); } finally { _isLoading = false; } return state; } Future refresh({int limit = 50}) async { return loadInitial(limit: limit); } Future loadMore({int limit = 50}) async { if (_isLoading || _nextBefore == null) { return state; } _isLoading = true; _errorMessage = null; try { final SyncRunLogPage page = await _api.fetchSyncRuns( limit: limit, before: _nextBefore, ); _pinned = _dedupeById(page.pinned); final List merged = [ ..._history, ...page.runs, ]; _history = _dedupeById(_sortedNewestFirst(merged)); _nextBefore = page.nextBefore; _config = page.config; } on MarketHistoryAdminApiException catch (e) { _errorMessage = e.message; } catch (e) { _errorMessage = e.toString(); } finally { _isLoading = false; } return state; } @override Future triggerResync() async { _isLoading = true; _errorMessage = null; try { await _api.triggerResync(); return refresh(); } on MarketHistoryAdminApiException catch (e) { _errorMessage = e.message; _isLoading = false; return state; } catch (e) { _errorMessage = e.toString(); _isLoading = false; return state; } } @override Future triggerCleanup({bool archive = false}) async { _isLoading = true; _errorMessage = null; try { await _api.triggerCleanup(archive: archive); return refresh(); } on MarketHistoryAdminApiException catch (e) { _errorMessage = e.message; _isLoading = false; return state; } catch (e) { _errorMessage = e.toString(); _isLoading = false; return state; } } static List _dedupeById(List events) { final Map byId = {}; for (final SyncRunEvent event in events) { byId[event.id] = event; } return byId.values.toList(); } static List _sortedNewestFirst(List events) { final List sorted = List.from(events); sorted.sort( (SyncRunEvent a, SyncRunEvent b) => b.startedAt.compareTo(a.startedAt), ); return sorted; } }