feat: unified shell, diagnostics pack, native Office, sticky board
All checks were successful
CI / Windows build (push) Successful in 14m22s
All checks were successful
CI / Windows build (push) Successful in 14m22s
Make Surface remote debugging and classroom workflows viable: always-on structured logs with one-click zip export, a single AppShell chrome, OOXML PPTX/DOCX annotation without LibreOffice, and a first-class sticky board. Also drop spike/legacy ink widgets and tighten pen feel (predictor, PenInfoHistory, page-tile layer). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
164
lib/diagnostics/badnote_log.dart
Normal file
164
lib/diagnostics/badnote_log.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
// Global structured logging bus for BadNote.
|
||||
//
|
||||
// Always-on (unlike the old PDF-only DiagnosticLogger opt-in). Writes NDJSON
|
||||
// lines to a rotating session file under the app documents directory so a
|
||||
// Surface user can export a diagnostic pack without attaching a debugger.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum LogLevel { trace, debug, info, warn, error }
|
||||
|
||||
/// Known subsystems — keep the set small so filters stay useful.
|
||||
abstract final class LogSubsystem {
|
||||
static const shell = 'shell';
|
||||
static const ink = 'ink';
|
||||
static const arbiter = 'arbiter';
|
||||
static const penNative = 'pen_native';
|
||||
static const pdf = 'pdf';
|
||||
static const office = 'office';
|
||||
static const board = 'board';
|
||||
static const sync = 'sync';
|
||||
static const diag = 'diag';
|
||||
static const frame = 'frame';
|
||||
}
|
||||
|
||||
class BadNoteLog {
|
||||
BadNoteLog._();
|
||||
static final BadNoteLog instance = BadNoteLog._();
|
||||
|
||||
final String sessionId = const Uuid().v4();
|
||||
final List<Map<String, Object?>> _ring = <Map<String, Object?>>[];
|
||||
static const int _ringCap = 4000;
|
||||
|
||||
File? _file;
|
||||
Directory? _dir;
|
||||
Timer? _flushTimer;
|
||||
final List<String> _pending = <String>[];
|
||||
bool _started = false;
|
||||
LogLevel minLevel = LogLevel.debug;
|
||||
|
||||
/// Absolute path of the current session log, once [start] succeeds.
|
||||
String? get path => _file?.path;
|
||||
|
||||
Directory? get directory => _dir;
|
||||
|
||||
Future<void> start() async {
|
||||
if (_started) return;
|
||||
_started = true;
|
||||
try {
|
||||
Directory base;
|
||||
try {
|
||||
base = await getApplicationDocumentsDirectory();
|
||||
} catch (_) {
|
||||
base = await getTemporaryDirectory();
|
||||
}
|
||||
_dir = Directory(
|
||||
'${base.path}${Platform.pathSeparator}badnote_diagnostics',
|
||||
);
|
||||
if (!await _dir!.exists()) {
|
||||
await _dir!.create(recursive: true);
|
||||
}
|
||||
final stamp = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll(':', '-')
|
||||
.replaceAll('.', '-');
|
||||
_file = File(
|
||||
'${_dir!.path}${Platform.pathSeparator}session_$stamp.ndjson',
|
||||
);
|
||||
await _file!.writeAsString(
|
||||
'${jsonEncode({
|
||||
'ts': DateTime.now().toIso8601String(),
|
||||
'level': 'info',
|
||||
'subsystem': LogSubsystem.diag,
|
||||
'msg': 'session_start',
|
||||
'sessionId': sessionId,
|
||||
'platform': Platform.operatingSystem,
|
||||
'osVersion': Platform.operatingSystemVersion,
|
||||
})}\n',
|
||||
flush: true,
|
||||
);
|
||||
_flushTimer = Timer.periodic(const Duration(seconds: 1), (_) => _flush());
|
||||
info(LogSubsystem.diag, 'log file ready', fields: {'path': _file!.path});
|
||||
} catch (e) {
|
||||
// Logging must never crash the app.
|
||||
developer.log('BadNoteLog start failed: $e', name: 'badnote');
|
||||
}
|
||||
}
|
||||
|
||||
void trace(String subsystem, String msg, {Map<String, Object?>? fields}) =>
|
||||
_emit(LogLevel.trace, subsystem, msg, fields);
|
||||
|
||||
void debug(String subsystem, String msg, {Map<String, Object?>? fields}) =>
|
||||
_emit(LogLevel.debug, subsystem, msg, fields);
|
||||
|
||||
void info(String subsystem, String msg, {Map<String, Object?>? fields}) =>
|
||||
_emit(LogLevel.info, subsystem, msg, fields);
|
||||
|
||||
void warn(String subsystem, String msg, {Map<String, Object?>? fields}) =>
|
||||
_emit(LogLevel.warn, subsystem, msg, fields);
|
||||
|
||||
void error(String subsystem, String msg, {Map<String, Object?>? fields}) =>
|
||||
_emit(LogLevel.error, subsystem, msg, fields);
|
||||
|
||||
void _emit(
|
||||
LogLevel level,
|
||||
String subsystem,
|
||||
String msg,
|
||||
Map<String, Object?>? fields,
|
||||
) {
|
||||
if (level.index < minLevel.index) return;
|
||||
final entry = <String, Object?>{
|
||||
'ts': DateTime.now().toIso8601String(),
|
||||
'level': level.name,
|
||||
'subsystem': subsystem,
|
||||
'msg': msg,
|
||||
'sessionId': sessionId,
|
||||
if (fields != null) ...fields,
|
||||
};
|
||||
_ring.add(entry);
|
||||
if (_ring.length > _ringCap) {
|
||||
_ring.removeRange(0, _ring.length - _ringCap);
|
||||
}
|
||||
final line = jsonEncode(entry);
|
||||
developer.log(line, name: 'badnote.$subsystem');
|
||||
if (_file != null) {
|
||||
_pending.add(line);
|
||||
if (_pending.length >= 200) {
|
||||
unawaited(_flush());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _flush() async {
|
||||
final file = _file;
|
||||
if (file == null || _pending.isEmpty) return;
|
||||
final chunk = '${_pending.join('\n')}\n';
|
||||
_pending.clear();
|
||||
try {
|
||||
await file.writeAsString(chunk, mode: FileMode.append, flush: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Snapshot of the in-memory ring (newest last).
|
||||
List<Map<String, Object?>> snapshotRing() =>
|
||||
List<Map<String, Object?>>.unmodifiable(_ring);
|
||||
|
||||
Future<void> flush() => _flush();
|
||||
|
||||
Future<void> stop() async {
|
||||
_flushTimer?.cancel();
|
||||
_flushTimer = null;
|
||||
await _flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// Bridge for legacy call sites that still use plain strings.
|
||||
void logLegacyInputLine(String line) {
|
||||
BadNoteLog.instance.debug(LogSubsystem.penNative, line);
|
||||
}
|
||||
Reference in New Issue
Block a user