Protocol 15: one shared session log on the server, chat never logged, Mark a problem

Every app streams its redacted diagnostic lines (server time, batched
every 10 s, gzip, buffered while offline) to POST /l/; the server merges
them with its own into session-YYYY-MM-DD.log in time order, tagged by
who. Opt-out in Settings > About; Mark a problem (Ctrl+Shift+B, the
player menus, About) writes a marker with an optional note. Copy
diagnostics can include the server's session log. Chat text never
reaches any log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-12 19:41:06 +03:00
1 parent 4ab37c55cf
commit 7487b4deb1
29 files changed
+1380 -47

No files matched your search

@@ -17,6 +17,7 @@ import '../playback/playback_controller.dart';
import '../platform/attention.dart';
import '../playback/yt_dlp_app.dart';
import '../schedule/anilist.dart';
import '../screens/home/ui.dart' show appName, displayVersion;
import '../screens/home/picked_media.dart';
import '../sfx/sfx.dart';
import '../state/settings.dart';
@@ -174,6 +175,32 @@ class SessionCoordinator {
/// The app's yt-dlp and link probes (tests pass their own).
final AppYtDlp ytDlp;
final SyncClient sync = SyncClient();
/// This app's log lines to the server's session log (Settings: "Share
/// diagnostics with the server"), and "Mark a problem".
late final DiagUploader diag = DiagUploader(
target: _diagTarget, version: '$appName ${displayVersion()} on ${Platform.operatingSystem}');
DiagTarget? _diagTarget() {
final uri = sync.wsUri;
if (sync.token.isEmpty || uri == null || !sync.clockSync.hasSample) return null;
return (base: DiagUploader.baseOf(uri), token: sync.token, offsetMs: sync.clockSync.offsetMs);
}
/// "Mark a problem": a marker with [note] in the server's session log
/// (and this app's log). False when not in a session: then only this
/// app's log has it.
Future<bool> markProblem(String note) async {
if (!diag.running) {
createLogger(DiagUploader.markLogger).warning('=== PROBLEM marked${note.trim().isEmpty ? '' : ': ${note.trim()}'} ===');
return false;
}
await diag.markProblem(note);
return true;
}
/// The server's session log of the last [hours], null when not in a
/// session or the server has none.
Future<String?> sessionLog({int hours = 6}) => diag.fetchSessionLog(hours: hours);
/// Up next: the server's queue, adding to it, and putting items up.
late final QueueController queue = QueueController(this);
/// The server has a password (it fetches links only then).
@@ -498,6 +525,9 @@ class SessionCoordinator {
}
markInput();
_subscribe();
diag
..share = AppSettings.instance.shareDiagnostics.peek()
..start();
feedLog.value = [];
toasts.value = [];
await initPlayer();
@@ -1201,6 +1231,9 @@ class SessionCoordinator {
toasts.peek().where((t) => now.difference(t.at).inSeconds < 4).toList();
final r = serverState.peek();
if (sync.token.isNotEmpty) sync.sendPing();
diag
..share = AppSettings.instance.shareDiagnostics.peek()
..tick();
if (sync.token.isNotEmpty && r != null && _playerReady) {
_sendReport(r);
if (!_lastOut) {
@@ -2235,6 +2268,8 @@ class SessionCoordinator {
try {
await playback.dispose();
} catch (_) {}
// The last lines go before the token does.
await diag.stop();
try {
await sync.disconnect();
} catch (_) {}