From b30a30ecce190d14c95e396861b911b93588ee16 Mon Sep 17 00:00:00 2001 From: jack Date: Fri, 12 Sep 2025 23:24:54 +0200 Subject: [PATCH] Debug: add in-memory log buffer and AppInfoView debug section with copy/clear logs (DEBUG only) --- bitchat/Utils/SecureLogger.swift | 38 ++++++++++++++++- bitchat/Views/AppInfoView.swift | 71 ++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/bitchat/Utils/SecureLogger.swift b/bitchat/Utils/SecureLogger.swift index 3846a397..ce6e837a 100644 --- a/bitchat/Utils/SecureLogger.swift +++ b/bitchat/Utils/SecureLogger.swift @@ -86,6 +86,38 @@ final class SecureLogger { return level.order >= minimumLevel.order } + // MARK: - In-memory debug buffer (DEBUG builds only) + #if DEBUG + private static let logBufferQueue = DispatchQueue(label: "chat.bitchat.securelogger.buffer") + private static var logBuffer: [String] = [] + private static let logBufferCap: Int = 2000 + + /// Append a sanitized line to the in-memory debug buffer + private static func record(_ line: String) { + logBufferQueue.async { + logBuffer.append(line) + if logBuffer.count > logBufferCap { + logBuffer.removeFirst(logBuffer.count - logBufferCap) + } + } + } + + /// Get the current logs as an array (oldest first) + static func getLogs() -> [String] { + return logBufferQueue.sync { logBuffer } + } + + /// Get the current logs as a single string + static func getLogText() -> String { + return getLogs().joined(separator: "\n") + } + + /// Clear the in-memory debug buffer + static func clearLogs() { + logBufferQueue.async { logBuffer.removeAll(keepingCapacity: false) } + } + #endif + // MARK: - Security Event Types enum SecurityEvent { @@ -159,7 +191,9 @@ final class SecureLogger { let errorDesc = sanitize(error.localizedDescription) #if DEBUG - os_log("%{public}@ Error in %{public}@: %{public}@", log: category, type: .error, location, sanitized, errorDesc) + let line = "\(location) Error: \(sanitized) — \(errorDesc)" + os_log("%{public}@", log: category, type: .error, line) + record(line) #else os_log("%{private}@ Error in %{private}@: %{private}@", log: category, type: .error, location, sanitized, errorDesc) #endif @@ -203,6 +237,7 @@ private extension SecureLogger { #if DEBUG os_log("%{public}@", log: category, type: level.osLogType, sanitized) + record(sanitized) #else // In release builds, only log non-debug messages if level != .debug { @@ -220,6 +255,7 @@ private extension SecureLogger { #if DEBUG os_log("%{public}@", log: .security, type: level.osLogType, message) + record(message) #else // In release, use private logging to prevent sensitive data exposure os_log("%{private}@", log: .security, type: level.osLogType, message) diff --git a/bitchat/Views/AppInfoView.swift b/bitchat/Views/AppInfoView.swift index c8a4e779..d325e85f 100644 --- a/bitchat/Views/AppInfoView.swift +++ b/bitchat/Views/AppInfoView.swift @@ -1,4 +1,9 @@ import SwiftUI +#if os(iOS) +import UIKit +#elseif os(macOS) +import AppKit +#endif struct AppInfoView: View { @Environment(\.dismiss) var dismiss @@ -191,12 +196,78 @@ struct AppInfoView: View { .background(Color.red.opacity(0.1)) .cornerRadius(8) + #if DEBUG + // Debug section (visible only in Debug builds) + DebugLogsSection(textColor: textColor, secondaryTextColor: secondaryTextColor) + #endif + .padding(.top) } .padding() } } +#if DEBUG +private struct DebugLogsSection: View { + let textColor: Color + let secondaryTextColor: Color + @State private var logsText: String = SecureLogger.getLogText() + private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect() + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + SectionHeader("DEBUG") + + HStack(spacing: 12) { + Button { + copyToPasteboard(logsText) + } label: { + Text("copy logs") + .font(.system(size: 14, design: .monospaced)) + .foregroundColor(textColor) + } + .buttonStyle(.plain) + + Button { + SecureLogger.clearLogs() + logsText = "" + } label: { + Text("clear logs") + .font(.system(size: 14, design: .monospaced)) + .foregroundColor(textColor) + } + .buttonStyle(.plain) + } + + ScrollView { + Text(logsText.isEmpty ? "(no logs)" : logsText) + .font(.system(size: 12, design: .monospaced)) + .foregroundColor(secondaryTextColor) + .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) + .padding(8) + } + .frame(minHeight: 180) + .background(secondaryTextColor.opacity(0.08)) + .cornerRadius(8) + } + .onReceive(timer) { _ in + logsText = SecureLogger.getLogText() + } + } + + private func copyToPasteboard(_ text: String) { + #if os(iOS) + UIPasteboard.general.string = text + #elseif os(macOS) + let pb = NSPasteboard.general + pb.clearContents() + pb.setString(text, forType: .string) + #endif + } +} +#endif + struct SectionHeader: View { let title: String @Environment(\.colorScheme) var colorScheme