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:
138
lib/diagnostics/diagnostic_export.dart
Normal file
138
lib/diagnostics/diagnostic_export.dart
Normal file
@@ -0,0 +1,138 @@
|
||||
// Build a zip diagnostic pack the user can hand back for remote debugging.
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../editor/canvas/input_diagnostics.dart';
|
||||
import '../editor/input/pen_input_service.dart';
|
||||
import 'badnote_log.dart';
|
||||
import 'frame_sampler.dart';
|
||||
import 'pen_event_ring.dart';
|
||||
|
||||
class DiagnosticExportResult {
|
||||
DiagnosticExportResult({required this.zipPath, required this.bytes});
|
||||
|
||||
final String zipPath;
|
||||
final int bytes;
|
||||
}
|
||||
|
||||
class DiagnosticExport {
|
||||
DiagnosticExport._();
|
||||
static final DiagnosticExport instance = DiagnosticExport._();
|
||||
|
||||
/// Flush logs and write a zip under Documents/badnote_diagnostics/.
|
||||
Future<DiagnosticExportResult> exportPack({
|
||||
Duration penWindow = const Duration(minutes: 5),
|
||||
}) async {
|
||||
final log = BadNoteLog.instance;
|
||||
await log.flush();
|
||||
|
||||
final meta = <String, Object?>{
|
||||
'exportedAt': DateTime.now().toIso8601String(),
|
||||
'sessionId': log.sessionId,
|
||||
'platform': Platform.operatingSystem,
|
||||
'osVersion': Platform.operatingSystemVersion,
|
||||
'localHostname': Platform.localHostname,
|
||||
'numberOfProcessors': Platform.numberOfProcessors,
|
||||
'flutter': {
|
||||
'foundationDebug': kDebugMode,
|
||||
'foundationProfile': kProfileMode,
|
||||
'foundationRelease': kReleaseMode,
|
||||
},
|
||||
'penNative': PenInputService.instance.debugSummary,
|
||||
'penActive': PenInputService.instance.isActive,
|
||||
'zoom': InputDiagnostics.instance.summary(),
|
||||
'frames': FrameSampler.instance.summary(),
|
||||
'instructions':
|
||||
'Reproduce the issue for ~3 minutes with diagnostics on, then share this zip.',
|
||||
};
|
||||
|
||||
final archive = Archive();
|
||||
void addText(String name, String body) {
|
||||
final bytes = utf8.encode(body);
|
||||
archive.addFile(ArchiveFile(name, bytes.length, bytes));
|
||||
}
|
||||
|
||||
addText('meta.json', const JsonEncoder.withIndent(' ').convert(meta));
|
||||
addText(
|
||||
'pen_events.json',
|
||||
const JsonEncoder.withIndent(' ').convert(
|
||||
PenEventRing.instance.toJsonList(window: penWindow),
|
||||
),
|
||||
);
|
||||
addText(
|
||||
'frame_samples.json',
|
||||
const JsonEncoder.withIndent(' ').convert(FrameSampler.instance.toJsonList()),
|
||||
);
|
||||
addText(
|
||||
'log_ring.json',
|
||||
const JsonEncoder.withIndent(' ').convert(log.snapshotRing()),
|
||||
);
|
||||
|
||||
// Include on-disk session NDJSON if present.
|
||||
final sessionPath = log.path;
|
||||
if (sessionPath != null) {
|
||||
try {
|
||||
final f = File(sessionPath);
|
||||
if (await f.exists()) {
|
||||
final bytes = await f.readAsBytes();
|
||||
archive.addFile(
|
||||
ArchiveFile('session.ndjson', bytes.length, bytes),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Legacy input log if it exists alongside.
|
||||
try {
|
||||
Directory dir;
|
||||
try {
|
||||
dir = await getApplicationDocumentsDirectory();
|
||||
} catch (_) {
|
||||
dir = await getTemporaryDirectory();
|
||||
}
|
||||
final legacy = File(
|
||||
'${dir.path}${Platform.pathSeparator}badnote_input_log.txt',
|
||||
);
|
||||
if (await legacy.exists()) {
|
||||
final bytes = await legacy.readAsBytes();
|
||||
archive.addFile(
|
||||
ArchiveFile('legacy_input_log.txt', bytes.length, bytes),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
final encoded = ZipEncoder().encode(archive);
|
||||
if (encoded.isEmpty) {
|
||||
throw StateError('Failed to encode diagnostic zip');
|
||||
}
|
||||
|
||||
Directory outDir = log.directory ??
|
||||
Directory(
|
||||
'${(await getApplicationDocumentsDirectory()).path}'
|
||||
'${Platform.pathSeparator}badnote_diagnostics',
|
||||
);
|
||||
if (!await outDir.exists()) {
|
||||
await outDir.create(recursive: true);
|
||||
}
|
||||
final stamp = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll(':', '-')
|
||||
.replaceAll('.', '-');
|
||||
final zipPath =
|
||||
'${outDir.path}${Platform.pathSeparator}badnote_diag_$stamp.zip';
|
||||
await File(zipPath).writeAsBytes(encoded, flush: true);
|
||||
|
||||
BadNoteLog.instance.info(
|
||||
LogSubsystem.diag,
|
||||
'export_pack',
|
||||
fields: {'path': zipPath, 'bytes': encoded.length},
|
||||
);
|
||||
|
||||
return DiagnosticExportResult(zipPath: zipPath, bytes: encoded.length);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user