From a4dff4bde9d8e9da33805519f83cb21b7afee4f0 Mon Sep 17 00:00:00 2001 From: SuperDooper <37051355+superdooper86@users.noreply.github.com> Date: Mon, 11 May 2026 12:40:47 +0200 Subject: [PATCH] beta.21: add DiagnosticsView.swift --- ClaudeChecker/DiagnosticsView.swift | 226 ++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 ClaudeChecker/DiagnosticsView.swift diff --git a/ClaudeChecker/DiagnosticsView.swift b/ClaudeChecker/DiagnosticsView.swift new file mode 100644 index 0000000..7248715 --- /dev/null +++ b/ClaudeChecker/DiagnosticsView.swift @@ -0,0 +1,226 @@ +import SwiftUI +import WebKit + +struct DiagnosticsView: View { + @EnvironmentObject var vm: UsageViewModel + @Binding var isPresented: Bool + @State private var liveAllCookies: [HTTPCookie] = [] + @State private var loadingCookies = false + @State private var copied = false + + var body: some View { + VStack(spacing: 0) { + // Header + HStack { + Text("Diagnostics") + .font(.system(size: 13, weight: .semibold)) + Spacer() + Button("Copy All") { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(fullDiagText, forType: .string) + copied = true + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { copied = false } + } + .buttonStyle(.bordered) + .controlSize(.small) + if copied { + Text("Copied!") + .font(.system(size: 11)) + .foregroundColor(.green) + } + Button("Close") { isPresented = false } + .buttonStyle(.bordered) + .controlSize(.small) + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background(Color(nsColor: .windowBackgroundColor)) + + Divider() + + ScrollView { + VStack(alignment: .leading, spacing: 14) { + + // App info + DiagSection(title: "App") { + DiagRow("Version", Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?") + DiagRow("Build", Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?") + DiagRow("Signed in", vm.isSignedIn ? "Yes" : "No") + DiagRow("Email", vm.userEmail.isEmpty ? "(none)" : vm.userEmail) + DiagRow("Org ID", UserDefaults.standard.string(forKey: "claude_org_id") ?? "(none)") + DiagRow("Error", vm.errorMessage ?? "(none)") + } + + Divider() + + // Last request + DiagSection(title: "Last Request") { + DiagRow("Path", vm.diagLastPath.isEmpty ? "(none)" : vm.diagLastPath) + DiagRow("Status", vm.diagLastStatus == 0 ? "(none)" : "\(vm.diagLastStatus)") + DiagRow("Error", vm.diagLastError.isEmpty ? "(none)" : vm.diagLastError) + if let t = vm.diagLastFetch { + DiagRow("Time", t.formatted(date: .omitted, time: .standard)) + } + } + + Divider() + + // Response body + if !vm.diagLastBody.isEmpty { + DiagSection(title: "Response Body (first 500 chars)") { + Text(vm.diagLastBody) + .font(.system(size: 10, design: .monospaced)) + .foregroundColor(.secondary) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(8) + .background(Color.primary.opacity(0.04)) + .cornerRadius(5) + } + Divider() + } + + // Cookie summary + DiagSection(title: "Cookie Store") { + DiagRow("Total cookies", "\(vm.diagCookieCount)") + DiagRow("claude.ai cookies", "\(vm.diagClaudeCookieCount)") + DiagRow("All domains", vm.diagCookieDomains.isEmpty + ? "(none — never fetched)" + : vm.diagCookieDomains.joined(separator: ", ")) + } + + Divider() + + // Live cookie list + DiagSection(title: "Live Cookie Names (WKWebsiteDataStore.default)") { + if loadingCookies { + ProgressView().scaleEffect(0.7) + } else if liveAllCookies.isEmpty { + Text("No cookies found") + .font(.system(size: 11)) + .foregroundColor(.secondary) + } else { + let claudeCookies = liveAllCookies.filter { + $0.domain.contains("claude.ai") || $0.domain.contains("anthropic.com") + } + let otherCookies = liveAllCookies.filter { + !$0.domain.contains("claude.ai") && !$0.domain.contains("anthropic.com") + } + if !claudeCookies.isEmpty { + Text("claude.ai / anthropic.com (\(claudeCookies.count))") + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.orange) + ForEach(claudeCookies, id: \.name) { c in + Text("• \(c.domain) \(c.name)") + .font(.system(size: 10, design: .monospaced)) + .foregroundColor(.secondary) + } + } + if !otherCookies.isEmpty { + Text("Other domains (\(otherCookies.count))") + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.secondary) + .padding(.top, 4) + ForEach(otherCookies.prefix(10), id: \.name) { c in + Text("• \(c.domain) \(c.name)") + .font(.system(size: 10, design: .monospaced)) + .foregroundColor(Color.secondary.opacity(0.6)) + } + if otherCookies.count > 10 { + Text("… and \(otherCookies.count - 10) more") + .font(.system(size: 10)) + .foregroundColor(Color.secondary.opacity(0.5)) + } + } + } + + Button("Refresh cookies") { loadLiveCookies() } + .buttonStyle(.bordered) + .controlSize(.small) + .padding(.top, 4) + } + } + .padding(14) + } + } + .frame(width: 480, height: 560) + .onAppear { loadLiveCookies() } + } + + private func loadLiveCookies() { + loadingCookies = true + WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in + DispatchQueue.main.async { + liveAllCookies = cookies.sorted { $0.domain < $1.domain } + loadingCookies = false + } + } + } + + private var fullDiagText: String { + var lines: [String] = [] + lines.append("=== ClaudeChecker Diagnostics ===") + lines.append("Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?") (\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"))") + lines.append("Signed in: \(vm.isSignedIn ? "Yes" : "No")") + lines.append("Email: \(vm.userEmail.isEmpty ? "(none)" : vm.userEmail)") + lines.append("Org ID: \(UserDefaults.standard.string(forKey: "claude_org_id") ?? "(none)")") + lines.append("Error: \(vm.errorMessage ?? "(none)")") + lines.append("") + lines.append("Last path: \(vm.diagLastPath)") + lines.append("Last status: \(vm.diagLastStatus)") + lines.append("Last error: \(vm.diagLastError)") + lines.append("Total cookies: \(vm.diagCookieCount)") + lines.append("Claude cookies: \(vm.diagClaudeCookieCount)") + lines.append("Domains: \(vm.diagCookieDomains.joined(separator: ", "))") + lines.append("") + lines.append("Response body:") + lines.append(vm.diagLastBody) + lines.append("") + lines.append("Live cookies:") + for c in liveAllCookies { + lines.append(" \(c.domain) \(c.name) httpOnly=\(c.isHTTPOnly) secure=\(c.isSecure)") + } + return lines.joined(separator: "\n") + } +} + +// MARK: - Helpers + +private struct DiagSection: View { + let title: String + @ViewBuilder let content: () -> Content + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(.secondary) + .textCase(.uppercase) + content() + } + } +} + +private struct DiagRow: View { + let label: String + let value: String + + init(_ label: String, _ value: String) { + self.label = label + self.value = value + } + + var body: some View { + HStack(alignment: .top, spacing: 8) { + Text(label) + .font(.system(size: 11)) + .foregroundColor(.secondary) + .frame(width: 100, alignment: .leading) + Text(value) + .font(.system(size: 11, design: .monospaced)) + .foregroundColor(.primary) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } + } +}