- Schema v33: add error_stack_trace and is_permanent columns to sync_logs - SyncLogEntry gains stackTrace and isPermanent fields; SyncLogRepository.log() gains matching optional parameters; IMAP and JMAP sync loops forward the stack trace string and isPermanent flag when writing error entries - New lib/ui/utils/about_markdown.dart utility shared by AboutScreen and the sync log copy feature; builds the markdown table including device info - AboutScreen uses the utility (refactored to remove duplicate _buildMarkdown) - SyncLogScreen: subtitle shows "Error (permanent)" for permanent errors; expanded view shows stack trace in red monospace; each tile has a Copy button that copies a markdown summary of the entry plus the About section - Migration test updated for v33; new repo test for stackTrace/isPermanent - check_coverage.dart excludes lib/ui/utils/about_markdown.dart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
123 lines
4.1 KiB
Dart
123 lines
4.1 KiB
Dart
import 'package:drift/drift.dart';
|
|
|
|
import 'package:sharedinbox/core/repositories/sync_log_repository.dart';
|
|
import 'package:sharedinbox/data/db/database.dart';
|
|
|
|
class SyncLogRepositoryImpl implements SyncLogRepository {
|
|
SyncLogRepositoryImpl(this._db);
|
|
|
|
final AppDatabase _db;
|
|
|
|
@override
|
|
Future<void> log({
|
|
required String accountId,
|
|
required bool success,
|
|
String? errorMessage,
|
|
String? stackTrace,
|
|
bool isPermanent = false,
|
|
required String protocol,
|
|
required int emailsFetched,
|
|
required int emailsSkipped,
|
|
required int mailboxesSynced,
|
|
required int pendingFlushed,
|
|
required int bytesTransferred,
|
|
required DateTime startedAt,
|
|
required DateTime finishedAt,
|
|
List<MailboxSyncStats> mailboxStats = const [],
|
|
String? protocolLog,
|
|
}) async {
|
|
await _db.transaction(() async {
|
|
final logId = await _db.into(_db.syncLogs).insert(
|
|
SyncLogsCompanion.insert(
|
|
accountId: accountId,
|
|
result: success ? 'ok' : 'error',
|
|
errorMessage: Value(errorMessage),
|
|
errorStackTrace: Value(stackTrace),
|
|
isPermanent: Value(isPermanent),
|
|
protocol: Value(protocol),
|
|
itemsSynced: Value(emailsFetched),
|
|
emailsSkipped: Value(emailsSkipped),
|
|
mailboxesSynced: Value(mailboxesSynced),
|
|
pendingFlushed: Value(pendingFlushed),
|
|
bytesTransferred: Value(bytesTransferred),
|
|
startedAt: startedAt,
|
|
finishedAt: finishedAt,
|
|
protocolLog: Value(protocolLog),
|
|
),
|
|
);
|
|
for (final s in mailboxStats) {
|
|
await _db.into(_db.syncLogMailboxes).insert(
|
|
SyncLogMailboxesCompanion.insert(
|
|
syncLogId: logId,
|
|
mailboxPath: s.mailboxPath,
|
|
fetched: Value(s.fetched),
|
|
skipped: Value(s.skipped),
|
|
bytesTransferred: Value(s.bytesTransferred),
|
|
durationMs: Value(s.duration?.inMilliseconds),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) {
|
|
final logsQuery = _db.select(_db.syncLogs)
|
|
..where((t) => t.accountId.equals(accountId))
|
|
..orderBy([(t) => OrderingTerm.desc(t.startedAt)])
|
|
..limit(100);
|
|
|
|
return logsQuery.watch().asyncMap((rows) async {
|
|
final entries = <SyncLogEntry>[];
|
|
for (final r in rows) {
|
|
final mailboxRows = await (_db.select(_db.syncLogMailboxes)
|
|
..where((t) => t.syncLogId.equals(r.id))
|
|
..orderBy([(t) => OrderingTerm.asc(t.mailboxPath)]))
|
|
.get();
|
|
entries.add(
|
|
SyncLogEntry(
|
|
id: r.id,
|
|
result: r.result,
|
|
errorMessage: r.errorMessage,
|
|
stackTrace: r.errorStackTrace,
|
|
isPermanent: r.isPermanent,
|
|
protocol: r.protocol,
|
|
emailsFetched: r.itemsSynced,
|
|
emailsSkipped: r.emailsSkipped,
|
|
mailboxesSynced: r.mailboxesSynced,
|
|
pendingFlushed: r.pendingFlushed,
|
|
bytesTransferred: r.bytesTransferred,
|
|
startedAt: r.startedAt,
|
|
finishedAt: r.finishedAt,
|
|
protocolLog: r.protocolLog,
|
|
mailboxStats: mailboxRows
|
|
.map(
|
|
(m) => MailboxSyncStats(
|
|
mailboxPath: m.mailboxPath,
|
|
fetched: m.fetched,
|
|
skipped: m.skipped,
|
|
bytesTransferred: m.bytesTransferred,
|
|
duration: m.durationMs != null
|
|
? Duration(milliseconds: m.durationMs!)
|
|
: null,
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
);
|
|
}
|
|
return entries;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Stream<String?> observeLastError(String accountId) {
|
|
return (_db.select(_db.syncLogs)
|
|
..where((t) => t.accountId.equals(accountId))
|
|
..orderBy([(t) => OrderingTerm.desc(t.startedAt)])
|
|
..limit(1))
|
|
.watchSingleOrNull()
|
|
.map((row) => (row?.result == 'error') ? row?.errorMessage : null);
|
|
}
|
|
}
|