beta.21: add DiagnosticsView.swift

This commit is contained in:
SuperDooper
2026-05-11 12:40:47 +02:00
parent d0b33e8f43
commit a4dff4bde9
+226
View File
@@ -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<Content: View>: 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)
}
}
}