Skip to content

M3: SwiftUI app — TrafficList, Inspector, SQLite store#74

Open
kalil0321 wants to merge 13 commits into
claude/proxy-monitor-m2-ca-keychainfrom
claude/proxy-monitor-m3-swiftui-ui
Open

M3: SwiftUI app — TrafficList, Inspector, SQLite store#74
kalil0321 wants to merge 13 commits into
claude/proxy-monitor-m2-ca-keychainfrom
claude/proxy-monitor-m3-swiftui-ui

Conversation

@kalil0321
Copy link
Copy Markdown
Owner

@kalil0321 kalil0321 commented May 19, 2026

Stacked on top of #73.

First UI slice — a working SwiftUI macOS app on top of the proxy engine.

What's in

  • New executable target ReverseAPI (alongside rae-proxy) with @main SwiftUI App
  • AppState (@MainActor + @Observable) — owns the engine, flow store, CA installer, system proxy controller. Handles toggle capture / install CA / enable system proxy / clear, all backgrounded off the main thread where they may block.
  • FlowStore — GRDB-backed SQLite store at ~/Library/Application Support/ReverseAPI/flows.sqlite. Subscribes to FlowBus, keeps flows: [CapturedFlow] reactive, persists finished flows asynchronously, restores the last 500 on launch.
  • TrafficListView — SwiftUI Table with Time / Method / Host / Path / Status / Size / Duration columns + search + filter menu (errors-only, by host / method / status bucket).
  • InspectorView — Overview / Request / Response tabs; JSON-aware pretty printer for bodies, text/binary fallback.
  • CaptureToolbar — capture button, CA trust toggle, system proxy toggle, clear, status line.

ProxyEngine fix

Split stop() (just closes the listening channel) from terminate() (also shuts down the event-loop group). Without this, capture could only be toggled once.

Run

cd macos
swift run ReverseAPI

First launch: click Install CA, then System proxy, then Start capture. Every app that respects the macOS proxy will now show up in the list.

Out of scope

  • Multi-session sidebar (later)
  • HTTP/2 + WebSocket (M4)
  • Agent panel (M5)
  • Signing / notarization / DMG (M6)

Generated by Claude Code


Summary by cubic

Adds a SwiftUI macOS app named rae with a live traffic list, inspector, and SQLite persistence, plus safer system-proxy lifecycle and clean shutdown. Responses stream to the client while capturing to cut latency, and the UI does less work during live capture.

  • New Features

    • New ReverseAPI app target and rae executable; AppState manages the proxy engine, CA trust, system proxy, reactive store, and a boot-failure view.
    • Capture modes: Device auto-enables the system proxy and restores it on stop/exit; toolbar shows status/port/flow count/errors; refined filter bar and a collapsible sidebar with clearer empty/filtered states.
    • FlowStore: GRDB.swift-backed SQLite (last 500 flows), subscribes to FlowBus, writes finished flows off the main thread, restores recent flows on launch; exposes incremental host/method options to avoid recompute churn.
    • TrafficListView: table with search and filters (errors, host, method, status buckets, resource types).
    • InspectorView: Overview/Request/Response tabs with JSON pretty print, text/binary fallback, and timings.
    • CA storage: file-based private key (no Keychain) with simpler install/uninstall.
    • System-proxy UX: detect and recover a stale device proxy pointing at rae on launch; targeted disable and isEnabled(host:port) check; lifecycle restores the proxy on window close and quit.
  • Bug Fixes

    • Stream upstream responses to the client while capturing (with a size cap) to avoid buffering delays; force Accept-Encoding: identity for clearer bodies.
    • Split ProxyEngine.stop() (close channel) from terminate() (also shuts down the event-loop group) for repeatable toggling and clean app exit; hook app lifecycle for graceful shutdown.
    • FlowStore: guard persist/clear races via a generation counter so in-flight writes are skipped after Clear; reduce UI work by caching host/method options and size formatting.

Written for commit 773e59f. Summary will update on new commits. Review in cubic

Greptile Summary

Adds the first SwiftUI macOS UI slice on top of the existing proxy engine: a live traffic list (TrafficListView), a request/response inspector (InspectorView), a GRDB-backed SQLite flow store (FlowStore), and an AppState object that ties capture toggling, CA trust installation, and system-proxy control together. The ProxyEngine is also fixed so stop() and terminate() are separated, enabling repeated capture toggling.

  • AppState / FlowStore: @MainActor+@Observable design is clean; persist() uses a generation counter to guard against clear-vs-write races; synchronous loadInitial() / clear() DB calls on the main actor and silent swallowing of persist errors are flagged in existing review comments.
  • TrafficListView: SwiftUI Table with seven columns, a filter bar for host/method/status/errors, and search; ByteCountFormatter is re-allocated per-cell-render and hostOptions/methodOptions are O(n) scanned on every flow update (noted below).
  • ProxyEngine: terminate() is correctly split out but is not yet wired into the SwiftUI app lifecycle (existing comment).

Confidence Score: 5/5

The changes are well-structured and the new UI layer correctly delegates blocking work off the main thread; no regressions are introduced.

The new code introduces no data-loss or correctness regressions; the generation counter correctly handles clear-vs-persist races, the subscription and weak-self patterns are correct, and the filter logic is well-tested. The only new findings are minor render-efficiency nits in TrafficListView.

FlowStore.swift carries the most open risk from prior review comments (synchronous DB I/O on main actor, silent persist failures); TrafficListView.swift has the two new render-efficiency notes.

Important Files Changed

Filename Overview
macos/Package.swift Adds GRDB.swift dependency and new ReverseAPI executable + test targets; straightforward and correct.
macos/Sources/ReverseAPI/App/AppState.swift MainActor-bound app state handling capture, CA trust, system proxy, and clear; blocking calls and missing terminate() at app exit noted in prior review comments.
macos/Sources/ReverseAPI/App/ReverseAPIApp.swift Clean SwiftUI @main entry point with AppSession enum for boot failure handling.
macos/Sources/ReverseAPI/Storage/FlowStore.swift GRDB-backed reactive store; loadInitial() and clear() perform synchronous DB I/O on the main actor, and persist() silently swallows write errors — both flagged in prior review.
macos/Sources/ReverseAPI/Storage/PersistedFlow.swift Clean GRDB record with correct round-trip serialization of headers as JSON blobs and Date as TimeInterval.
macos/Sources/ReverseAPI/UI/CaptureToolbar.swift Capture toolbar with async task dispatch for CA and system proxy toggling; correct and straightforward.
macos/Sources/ReverseAPI/UI/ContentView.swift Minimal content view wiring toolbar, list, and inspector into an HSplitView.
macos/Sources/ReverseAPI/UI/InspectorView.swift Well-structured inspector with Overview/Request/Response tabs, JSON pretty-printing, and binary fallback; no issues found.
macos/Sources/ReverseAPI/UI/TrafficFilter.swift Composable filter struct with correct onlyErrors / host / method / statusBucket logic; well-covered by tests.
macos/Sources/ReverseAPI/UI/TrafficListView.swift SwiftUI Table with filter bar; ByteCountFormatter allocated per-render and host/method option sets recomputed O(n) on every flow update.
macos/Sources/ReverseAPIProxy/ProxyEngine.swift Splits stop() from terminate() correctly; terminate() is never invoked from AppState or SwiftUI lifecycle (flagged in prior review).
macos/Sources/ReverseAPI/Helpers/JSONFormatter.swift Simple JSON pretty-printer with content-type and heuristic shape detection; correct and well-tested.
macos/Tests/ReverseAPITests/JSONFormatterTests.swift Good coverage of empty data, valid JSON, non-JSON content type, nil content type, whitespace prefix, and invalid JSON.
macos/Tests/ReverseAPITests/TrafficFilterTests.swift Thorough filter tests covering all filter types, case-insensitive search, nil status handling, and status bucket boundaries.

Comments Outside Diff (3)

  1. macos/Sources/ReverseAPIProxy/ProxyEngine.swift, line 1149-1152 (link)

    P2 terminate() is never called at app exit

    terminate() was added to cleanly shut down the NIO EventLoopGroup, but nothing in the SwiftUI app or AppState ever invokes it. stopCapture() only calls engine.stop(), which closes the listener but leaves the thread pool running. When the user quits the app, NIO's threads are abandoned to OS process cleanup rather than being gracefully drained. Since terminate() is the only path to group.shutdownGracefully(), an AppState.terminate() method (hooked into the SwiftUI scene lifecycle via onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) or NSApplicationDelegate.applicationWillTerminate) would close this out cleanly.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: macos/Sources/ReverseAPIProxy/ProxyEngine.swift
    Line: 1149-1152
    
    Comment:
    **`terminate()` is never called at app exit**
    
    `terminate()` was added to cleanly shut down the NIO `EventLoopGroup`, but nothing in the SwiftUI app or `AppState` ever invokes it. `stopCapture()` only calls `engine.stop()`, which closes the listener but leaves the thread pool running. When the user quits the app, NIO's threads are abandoned to OS process cleanup rather than being gracefully drained. Since `terminate()` is the only path to `group.shutdownGracefully()`, an `AppState.terminate()` method (hooked into the SwiftUI scene lifecycle via `onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification))` or `NSApplicationDelegate.applicationWillTerminate`) would close this out cleanly.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. macos/Sources/ReverseAPI/Storage/FlowStore.swift, line 358-370 (link)

    P2 Persistence failures are silently swallowed

    Both error paths in persist() discard the error: the PersistedFlow(from:) conversion bails out with a bare return, and the GRDB write uses try? which drops any write failure. If the database is full, the path is wrong, or the file is corrupt, finished flows are lost with no signal to the user or any log output. Even a print / Logger call on the catch branch would make this diagnosable.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: macos/Sources/ReverseAPI/Storage/FlowStore.swift
    Line: 358-370
    
    Comment:
    **Persistence failures are silently swallowed**
    
    Both error paths in `persist()` discard the error: the `PersistedFlow(from:)` conversion bails out with a bare `return`, and the GRDB write uses `try?` which drops any write failure. If the database is full, the path is wrong, or the file is corrupt, finished flows are lost with no signal to the user or any log output. Even a `print` / `Logger` call on the catch branch would make this diagnosable.
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. macos/Sources/ReverseAPI/Storage/FlowStore.swift, line 327-332 (link)

    P2 Synchronous DB operations block the main actor

    loadInitial() (called from init) and clear() both use the synchronous database.read / database.write APIs, which block the calling thread. Because FlowStore is @MainActor, these block the main thread. With 500 rows on cold start or a large DB on clear, this can visibly stall the UI. persist() correctly offloads to a detached task; the same pattern (or database.asyncRead / asyncWrite) should be applied here.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: macos/Sources/ReverseAPI/Storage/FlowStore.swift
    Line: 327-332
    
    Comment:
    **Synchronous DB operations block the main actor**
    
    `loadInitial()` (called from `init`) and `clear()` both use the synchronous `database.read` / `database.write` APIs, which block the calling thread. Because `FlowStore` is `@MainActor`, these block the main thread. With 500 rows on cold start or a large DB on clear, this can visibly stall the UI. `persist()` correctly offloads to a detached task; the same pattern (or `database.asyncRead` / `asyncWrite`) should be applied here.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
macos/Sources/ReverseAPI/UI/TrafficListView.swift:1004-1008
**`ByteCountFormatter` allocated on every cell render**

A fresh `ByteCountFormatter` is created every time the `Size` column renders a cell. `ByteCountFormatter` is an `NSObject` subclass with non-trivial initialization overhead; constructing one per-cell per render pass on every new flow event adds unnecessary allocations. Making it a static property (safe here since `TrafficListView` is `@MainActor`) avoids this entirely.

### Issue 2 of 2
macos/Sources/ReverseAPI/UI/TrafficListView.swift:945-951
**`hostOptions` / `methodOptions` recomputed on every render**

Both computed properties perform a full O(n) scan of `state.store.flows` (via `map` + `Set` construction + `sorted`) every time `TrafficListView.body` is re-evaluated. Under live capture where new flows arrive frequently, `@Observable` will re-evaluate `body` on each update, triggering two complete scans per incoming request. Caching these as a derived property inside `FlowStore` (or memoizing them in the view with `@State` + `onChange`) would avoid repeated work as the flow list grows.

Reviews (3): Last reviewed commit: "M3 review fixes + tests" | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 12 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread macos/Sources/ReverseAPI/Storage/FlowStore.swift
@kind-agent
Copy link
Copy Markdown

kind-agent Bot commented May 19, 2026

⚠️ Error — The test run failed unexpectedly.

Grok 4 Fast is deprecated. xAI recommends switching to Grok 4.3 (https://openrouter.ai/x-ai/grok-4.3)

This is likely a transient issue. You can re-trigger a run from the dashboard.

kalil0321 pushed a commit that referenced this pull request May 19, 2026
Fixes for PR #74 review comments (cubic):

- FlowStore: protect against the persist/clear race. Each clear()
  bumps a generation counter; in-flight persistence tasks check the
  counter inside the database.write closure and skip the save if
  the user has cleared since the task was scheduled. Without this,
  a "Clear" click could be silently undone by a still-pending
  insert that landed afterwards.
- New thread-safe GenerationCounter helper (NSLock-backed).

Tests:
- TrafficFilterTests: 10 cases covering search, host/method/status
  bucket filters, errors-only, status bucket boundaries
- JSONFormatterTests: empty / valid JSON / non-JSON content types /
  shape detection / leading whitespace / invalid JSON
- Package.swift: ReverseAPITests testTarget
Copy link
Copy Markdown
Owner Author

@greptile review


Generated by Claude Code

@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m2-ca-keychain branch from e78f8e9 to fbf9bd7 Compare May 19, 2026 12:57
kalil0321 pushed a commit that referenced this pull request May 19, 2026
Fixes for PR #74 review comments (cubic):

- FlowStore: protect against the persist/clear race. Each clear()
  bumps a generation counter; in-flight persistence tasks check the
  counter inside the database.write closure and skip the save if
  the user has cleared since the task was scheduled. Without this,
  a "Clear" click could be silently undone by a still-pending
  insert that landed afterwards.
- New thread-safe GenerationCounter helper (NSLock-backed).

Tests:
- TrafficFilterTests: 10 cases covering search, host/method/status
  bucket filters, errors-only, status bucket boundaries
- JSONFormatterTests: empty / valid JSON / non-JSON content types /
  shape detection / leading whitespace / invalid JSON
- Package.swift: ReverseAPITests testTarget
@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m3-swiftui-ui branch from 33bfb0d to 3c7f333 Compare May 19, 2026 12:57
Copy link
Copy Markdown
Owner Author

@greptile review


Generated by Claude Code

kalil0321 pushed a commit that referenced this pull request May 19, 2026
Fixes for PR #74 review comments (cubic):

- FlowStore: protect against the persist/clear race. Each clear()
  bumps a generation counter; in-flight persistence tasks check the
  counter inside the database.write closure and skip the save if
  the user has cleared since the task was scheduled. Without this,
  a "Clear" click could be silently undone by a still-pending
  insert that landed afterwards.
- New thread-safe GenerationCounter helper (NSLock-backed).

Tests:
- TrafficFilterTests: 10 cases covering search, host/method/status
  bucket filters, errors-only, status bucket boundaries
- JSONFormatterTests: empty / valid JSON / non-JSON content types /
  shape detection / leading whitespace / invalid JSON
- Package.swift: ReverseAPITests testTarget
@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m3-swiftui-ui branch from 3c7f333 to d9d7ff5 Compare May 19, 2026 13:20
@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m2-ca-keychain branch from 6a29aaf to 1d3b348 Compare May 19, 2026 16:46
claude and others added 3 commits May 19, 2026 18:50
- ReverseAPI app target wired through Package.swift
- AppState (@mainactor @observable) owns engine, flow store,
  installer, system proxy, capture toggles, CA install/uninstall
- FlowStore: GRDB-backed SQLite persistence with reactive @observable
  array of CapturedFlow, subscribes to FlowBus, loads last 500 on boot
- TrafficListView: SwiftUI Table with method/host/path/status/size/duration
  columns, search box, host/method/status/errors filters
- InspectorView: Overview / Request / Response tabs with header table
  and JSON-aware body pretty printer
- CaptureToolbar: capture, CA trust, system proxy, clear; status line
- ProxyEngine: split stop() (channel close) from terminate()
  (also shuts down the event-loop group) so capture can be toggled
- NIOFoundationCompat added to ReverseAPIProxy deps
Fixes for PR #74 review comments (cubic):

- FlowStore: protect against the persist/clear race. Each clear()
  bumps a generation counter; in-flight persistence tasks check the
  counter inside the database.write closure and skip the save if
  the user has cleared since the task was scheduled. Without this,
  a "Clear" click could be silently undone by a still-pending
  insert that landed afterwards.
- New thread-safe GenerationCounter helper (NSLock-backed).

Tests:
- TrafficFilterTests: 10 cases covering search, host/method/status
  bucket filters, errors-only, status bucket boundaries
- JSONFormatterTests: empty / valid JSON / non-JSON content types /
  shape detection / leading whitespace / invalid JSON
- Package.swift: ReverseAPITests testTarget
@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m3-swiftui-ui branch from d9d7ff5 to c27a3ee Compare May 19, 2026 16:53
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread macos/Sources/ReverseAPI/App/AppState.swift Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread macos/Sources/ReverseAPI/UI/TrafficFilter.swift Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift Outdated
Comment thread macos/Sources/ReverseAPI/UI/InspectorView.swift Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread macos/Sources/ReverseAPIProxy/CA/CAStore.swift Outdated
Comment thread macos/Sources/ReverseAPI/UI/ContentView.swift Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread macos/Sources/ReverseAPI/App/ReverseAPIApp.swift Outdated
@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented May 19, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants