Files
sharedinbox/lib/data/repositories/sync_log_repository_impl.dart
T
Thomas SharedInboxandClaude Sonnet 4.6 33949b92c0 feat: syncLog add Copy button, stack trace, isPermanent, Android device info (#237)
- 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 Android device
  info (manufacturer, model, OS version) via device_info_plus
- AboutScreen uses the utility and adds Android device info row
- 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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:33:45 +02:00

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);
}
}