// // RequestsView.swift // Zyquo Router // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Live traffic: streaming log table (filters, pause/clear, export) and a // detail inspector with redacted-by-default bodies (explicit per-session // reveal) and the timing waterfall. // import SwiftUI import UniformTypeIdentifiers struct RequestsView: View { @EnvironmentObject private var server: ServerController @State private var entries: [RequestLogEntry] = [] @State private var revision = -1 @State private var paused = false @State private var filterText = "" @State private var providerFilter: ProviderID? @State private var errorsOnly = false @State private var selectedID: UUID? @AppStorage("logBodiesRevealed") private var bodiesRevealed = false @FocusState private var filterFocused: Bool private let refresh = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect() private var filtered: [RequestLogEntry] { entries.reversed().filter { entry in if let providerFilter, entry.provider != providerFilter { return false } if errorsOnly, entry.status < 400 { return false } if !filterText.isEmpty, !entry.namespacedModelID.localizedCaseInsensitiveContains(filterText) { return false } return true } } var body: some View { VStack(alignment: .leading, spacing: 0) { toolbar ZyquoHairline() if filtered.isEmpty { EmptyState( systemImage: "antenna.radiowaves.left.and.right", title: entries.isEmpty ? "No traffic yet" : "Nothing matches the filters", message: entries.isEmpty ? (server.isRunning ? "Requests routed through \(server.endpointURL) will stream in here live." : "Start the server, then point any OpenAI client at the endpoint.") : "Adjust or clear the filters above." ) } else { HSplitView { table .frame(minWidth: 460) if let selected = filtered.first(where: { $0.id == selectedID }) { RequestDetailPane(entry: selected, bodiesRevealed: $bodiesRevealed) .frame(minWidth: 320) } } } } .onReceive(refresh) { _ in guard !paused else { return } let log = server.requestLog Task { let newRevision = await log.revision if newRevision != revision { revision = newRevision entries = await log.entries } } } } private var toolbar: some View { HStack(spacing: ZyquoSpacing.sm) { SectionHeader(title: "Requests") Spacer() Toggle("Errors only", isOn: $errorsOnly) .toggleStyle(.checkbox) .font(ZyquoFont.caption) Picker("", selection: $providerFilter) { Text("All providers").tag(ProviderID?.none) ForEach(ProviderID.builtIn) { provider in Text(provider.displayName).tag(ProviderID?.some(provider)) } } .labelsHidden() .frame(width: 150) TextField("Filter by model… (⌘F)", text: $filterText) .textFieldStyle(.roundedBorder) .frame(width: 190) .focused($filterFocused) Button { paused.toggle() } label: { Image(systemName: paused ? "play.fill" : "pause.fill") } .help(paused ? "Resume live updates" : "Pause live updates") Button { let log = server.requestLog Task { await log.clear() entries = [] selectedID = nil } } label: { Image(systemName: "trash") } .help("Clear the log") Button { exportLog() } label: { Image(systemName: "square.and.arrow.up") } .help("Export log metadata as JSON") } .padding(.horizontal, ZyquoMetrics.contentInset) .padding(.vertical, ZyquoSpacing.sm) .background( // ⌘F routes here from the app menu. Button("") { filterFocused = true } .keyboardShortcut("f", modifiers: .command) .hidden() ) } private var table: some View { ScrollView { LazyVStack(spacing: 0) { ForEach(filtered) { entry in RequestRow(entry: entry, selected: entry.id == selectedID) .contentShape(Rectangle()) .onTapGesture { selectedID = selectedID == entry.id ? nil : entry.id } ZyquoHairline() .padding(.leading, ZyquoMetrics.contentInset) } } } } private func exportLog() { let log = server.requestLog Task { let data = await log.exportJSON() let panel = NSSavePanel() panel.allowedContentTypes = [.json] panel.nameFieldStringValue = "zyquo-router-log.json" if panel.runModal() == .OK, let url = panel.url { try? data.write(to: url) } } } } // MARK: - Row private struct RequestRow: View { let entry: RequestLogEntry let selected: Bool @State private var hovering = false var body: some View { HStack(spacing: ZyquoSpacing.sm) { Text(entry.date, format: .dateTime.hour().minute().second()) .font(ZyquoFont.mono(size: 11)) .foregroundStyle(ZyquoColor.textSecondary) .frame(width: 64, alignment: .leading) HStack(spacing: ZyquoSpacing.xs) { Circle() .fill(ZyquoColor.providerHue(entry.provider)) .frame(width: 6, height: 6) Text(entry.namespacedModelID) .font(ZyquoFont.mono(size: 11.5)) .foregroundStyle(ZyquoColor.textPrimary) .lineLimit(1) .truncationMode(.middle) if entry.streamed { CapabilityBadge(label: "SSE", tint: ZyquoColor.accent) } if entry.usageEstimated { CapabilityBadge(label: "EST", tint: ZyquoColor.warning) } } .frame(maxWidth: .infinity, alignment: .leading) Text("\(entry.status)") .font(ZyquoFont.mono(size: 11, weight: .semibold)) .foregroundStyle(entry.status < 400 ? ZyquoColor.success : ZyquoColor.danger) .frame(width: 36, alignment: .trailing) Text(String(format: "%.2fs", entry.latency)) .font(ZyquoFont.mono(size: 11)) .foregroundStyle(ZyquoColor.textSecondary) .frame(width: 56, alignment: .trailing) Text("\(entry.usage.inputTokens)→\(entry.usage.outputTokens)") .font(ZyquoFont.mono(size: 11)) .foregroundStyle(ZyquoColor.textSecondary) .frame(width: 84, alignment: .trailing) Text(costText) .font(ZyquoFont.mono(size: 11)) .foregroundStyle(ZyquoColor.textSecondary) .frame(width: 58, alignment: .trailing) } .padding(.horizontal, ZyquoMetrics.contentInset) .padding(.vertical, 6) .background(selected ? ZyquoColor.accentSubtle : (hovering ? ZyquoColor.surfaceSecondary : .clear)) .onHover { hover in withAnimation(ZyquoMotion.hover) { hovering = hover } } } private var costText: String { guard let cost = entry.estimatedCost, cost > 0 else { return "—" } return cost < 0.01 ? "<$0.01" : String(format: "$%.2f", cost) } } // MARK: - Detail inspector private struct RequestDetailPane: View { let entry: RequestLogEntry @Binding var bodiesRevealed: Bool var body: some View { ScrollView { VStack(alignment: .leading, spacing: ZyquoSpacing.md) { Text(entry.namespacedModelID) .font(ZyquoFont.mono(size: 13, weight: .medium)) .foregroundStyle(ZyquoColor.textPrimary) .textSelection(.enabled) waterfall if let error = entry.errorMessage { Label(error, systemImage: "xmark.octagon") .font(ZyquoFont.body()) .foregroundStyle(ZyquoColor.danger) .textSelection(.enabled) } Toggle("Reveal request/response bodies (this session)", isOn: $bodiesRevealed) .toggleStyle(.switch) .controlSize(.small) .font(ZyquoFont.caption) bodySection(title: "REQUEST", body: entry.requestBody) bodySection(title: "RESPONSE", body: entry.responseBody) } .padding(ZyquoSpacing.md) } .background(ZyquoColor.surface) } private var waterfall: some View { VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) { caption("TIMING") GeometryReader { proxy in let total = max(entry.latency, 0.001) let ttfb = min(entry.upstreamTTFB ?? entry.latency, total) HStack(spacing: 1) { RoundedRectangle(cornerRadius: 2) .fill(ZyquoColor.graphite.opacity(0.55)) .frame(width: max(proxy.size.width * (ttfb / total), 2)) RoundedRectangle(cornerRadius: 2) .fill(ZyquoColor.accent) .frame(maxWidth: .infinity) } } .frame(height: 8) HStack { Text(entry.upstreamTTFB.map { String(format: "upstream TTFB %.0f ms", $0 * 1000) } ?? "buffered call") Spacer() Text(String(format: "total %.2f s", entry.latency)) } .font(ZyquoFont.mono(size: 10.5)) .foregroundStyle(ZyquoColor.textSecondary) } } @ViewBuilder private func bodySection(title: String, body text: String) -> some View { VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) { HStack { caption(title) Spacer() if bodiesRevealed, !text.isEmpty { CopyButton(value: text) } } Group { if !bodiesRevealed { Text("Redacted — enable reveal above to inspect.") .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textTertiary) .frame(maxWidth: .infinity, alignment: .leading) } else { Text(text.isEmpty ? "(empty)" : text) .font(ZyquoFont.mono(size: 10.5)) .foregroundStyle(ZyquoColor.textPrimary) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) } } .padding(ZyquoSpacing.xs) .background( RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous) .fill(ZyquoColor.surfaceSecondary) ) } } private func caption(_ text: String) -> some View { Text(text) .font(.system(size: 9.5, weight: .semibold)) .foregroundStyle(ZyquoColor.textTertiary) .kerning(0.4) } }