192 lines
5.4 KiB
Dart
192 lines
5.4 KiB
Dart
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 <BackfillSyncItem>[],
|
|
});
|
|
|
|
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<BackfillSyncItem> 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<String, dynamic> 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<BackfillSyncItem> _parseBackfillItems(dynamic raw) {
|
|
if (raw is! List<dynamic>) {
|
|
return const <BackfillSyncItem>[];
|
|
}
|
|
return raw
|
|
.whereType<Map<String, dynamic>>()
|
|
.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: 7,
|
|
retentionDays: 7,
|
|
syncEnabled: false,
|
|
),
|
|
});
|
|
|
|
final List<SyncRunEvent> runs;
|
|
final List<SyncRunEvent> pinned;
|
|
final DateTime? nextBefore;
|
|
final MarketHistoryAdminConfig config;
|
|
}
|