commit a6e2f40fd3f7b4cf34230d373b05e8c3853d7dcf
parent decd060e26895c988ee02ef08ede9e90da608e1a
Author: Michael Camilleri <[email protected]>
Date: Thu, 2 Jul 2026 22:23:51 +0900
Render the diagnostics share file only on demand
While the Diagnostics Log screen was open, every log event rebuilt the
full diagnostic dump and rewrote it to disk — an O(n) string join plus a
multi-MB atomic write, several times a second during a co-solve — purely
to keep a pre-built temp file fresh in case the user tapped 'Share Full
Log'. The share item is now a Transferable that renders the dump and
writes the file only when the user actually invokes the share, and the
disk write happens off the main thread. A log event now costs only the
visible fifty-row list update.
This commit also tightens the external login redirect check. The
coordinator previously treated any host containing `nytimes.com` as the
sign-in destination, which a lookalike such as
`nytimes.com.attacker.tld` would satisfy before session cookies were
extracted. The check now requires the exact host or a `.nytimes.com`
suffix.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat:
2 files changed, 29 insertions(+), 35 deletions(-)
diff --git a/Crossmate/Views/Browse/NYTLoginView.swift b/Crossmate/Views/Browse/NYTLoginView.swift
@@ -68,7 +68,8 @@ private struct NYTWebView: UIViewRepresentable {
) async -> WKNavigationActionPolicy {
guard !didComplete,
let url = navigationAction.request.url,
- url.host?.contains("nytimes.com") == true,
+ let host = url.host,
+ host == "nytimes.com" || host.hasSuffix(".nytimes.com"),
url.path.hasPrefix("/crosswords") else {
return .allow
}
diff --git a/Crossmate/Views/Settings/DiagnosticsView.swift b/Crossmate/Views/Settings/DiagnosticsView.swift
@@ -1,5 +1,7 @@
import CloudKit
+import CoreTransferable
import SwiftUI
+import UniformTypeIdentifiers
private enum TimestampTimeZone {
case local
@@ -39,7 +41,6 @@ struct DiagnosticsView: View {
@Environment(EventLog.self) private var eventLog
@State private var isSyncing = false
- @State private var diagnosticShareFile: URL?
/// The on-screen list is a quick glance, not the archive — the buffer now
/// spans a full day (tens of thousands of entries during a co-solve), and
@@ -123,51 +124,26 @@ struct DiagnosticsView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
- if let diagnosticShareFile {
- ShareLink(
- item: diagnosticShareFile,
- preview: SharePreview(
- "Crossmate Diagnostics",
- image: Image(systemName: "doc.text")
- )
- ) {
- Label("Share Full Log", systemImage: "square.and.arrow.up")
- }
+ ShareLink(
+ item: DiagnosticsShareFile(render: { diagnosticDump }),
+ preview: SharePreview(
+ "Crossmate Diagnostics",
+ image: Image(systemName: "doc.text")
+ )
+ ) {
+ Label("Share Full Log", systemImage: "square.and.arrow.up")
}
}
}
- .onAppear {
- refreshDiagnosticShareFile()
- }
- .onChange(of: diagnosticDump) { _, _ in
- refreshDiagnosticShareFile()
- }
.task {
guard let syncEngine else { return }
let snapshot = await syncEngine.diagnosticSnapshot()
syncMonitor.updateSnapshot(snapshot)
- refreshDiagnosticShareFile()
}
}
// MARK: - Actions
- private func refreshDiagnosticShareFile() {
- do {
- diagnosticShareFile = try writeDiagnosticShareFile()
- } catch {
- diagnosticShareFile = nil
- eventLog.note("diagnostics share file failed: \(error.localizedDescription)", level: "error")
- }
- }
-
- private func writeDiagnosticShareFile() throws -> URL {
- let url = FileManager.default.temporaryDirectory
- .appendingPathComponent("crossmate-diagnostics.txt")
- try diagnosticDump.write(to: url, atomically: true, encoding: .utf8)
- return url
- }
-
private func resetSyncState() async {
guard let syncEngine else { return }
await syncEngine.resetSyncState()
@@ -272,3 +248,20 @@ struct DiagnosticsView: View {
return lines.joined(separator: "\n")
}
}
+
+/// Renders the diagnostic dump only when the user actually shares it. The
+/// buffer spans a full day (tens of thousands of entries during a co-solve),
+/// so building the joined text and writing it to disk eagerly — as the view
+/// once did on every log event — is multi-MB of wasted work per second.
+private struct DiagnosticsShareFile: Transferable {
+ let render: @MainActor () -> String
+
+ static var transferRepresentation: some TransferRepresentation {
+ FileRepresentation(exportedContentType: .plainText) { file in
+ let url = FileManager.default.temporaryDirectory
+ .appendingPathComponent("crossmate-diagnostics.txt")
+ try await file.render().write(to: url, atomically: true, encoding: .utf8)
+ return SentTransferredFile(url)
+ }
+ }
+}