git.iko.soy/parseable-macos
on 2026.02.16.0019 help sign in
parseable-macos/CLAUDE.md
109 lines 5.7 kB history raw
1# Parseable Viewer - macOS App
3Native macOS log viewer for [Parseable](https://www.parseable.com/) built with Swift and SwiftUI.
5## Build
7Requires macOS with Xcode 16+ and [XcodeGen](https://github.com/yonaskolb/XcodeGen).
9```bash
10brew install xcodegen
11xcodegen generate
12xcodebuild -project ParseableViewer.xcodeproj -scheme ParseableViewer -configuration Release build
13```
15Run tests:
17```bash
18xcodebuild test \
19 -project ParseableViewer.xcodeproj \
20 -scheme ParseableViewer \
21 -configuration Debug \
22 -destination 'platform=macOS'
23```
25CI builds on `macos-15` runners via `.github/workflows/build.yml`, producing arm64, x86_64, and universal binaries.
27## Architecture
29- **Language**: Swift 5.9, SwiftUI, macOS 14.0+ (Sonoma)
30- **Pattern**: MVVM with `@Observable` classes and `@Environment` injection
31- **Concurrency**: Swift structured concurrency (`async/await`, `@MainActor`, strict `Sendable` conformance)
32- **Networking**: `URLSession` async/await, no external dependencies
33- **Project generation**: XcodeGen from `project.yml`
34- **Persistence**: `UserDefaults` for connections and saved queries; Keychain for passwords
36### Source layout
38```
39ParseableViewer/
40 App/ # @main entry point + AppState (central @Observable)
41 Models/ # Codable data types (JSONValue, LogStream, ServerInfo, etc.)
42 Services/ # ParseableClient (REST API), ConnectionStore (persistence), KeychainService
43 ViewModels/ # QueryViewModel, LiveTailViewModel
44 Views/ # All SwiftUI views
45 Resources/ # Asset catalog
46ParseableViewerTests/ # Unit tests for models, view models, and services
47```
49### Key types
51- `AppState` — singleton `@Observable` injected via `.environment()`. Holds connections, streams, selected stream, navigation state. Clears stream-specific state when switching servers.
52- `ParseableClient` — `Sendable` HTTP client for all Parseable REST API calls. Uses Basic Auth. Created per-connection. Uses `finishTasksAndInvalidate()` on deinit so in-flight requests complete gracefully.
53- `JSONValue` — recursive `Codable`, `Hashable`, `Comparable` enum for type-safe arbitrary JSON (`null | bool | int | double | string | array | object`). Provides `displayString` (UI), `exportString` (CSV/JSON export with full nested serialization), and type-aware comparison (numeric values sort numerically, strings use `localizedStandardCompare`).
54- `QueryViewModel` — manages SQL text, time range, query execution, result columns, filtering, CSV/JSON export, and query history. Auto-updates the default query when the user switches streams.
55- `LiveTailViewModel` — timer-based polling that queries recent logs, deduplicates by FNV-1a content fingerprint, and appends to a capped buffer. Auto-stops after 5 consecutive poll failures.
57### Parseable API endpoints used
59All under `/api/v1/`. Auth: `Authorization: Basic <base64(user:pass)>`.
61| Method | Path | Purpose |
62|--------|------|---------|
63| HEAD | `/liveness` | Health check |
64| GET | `/about` | Server version, mode, storage info |
65| GET | `/logstream` | List all streams |
66| PUT/DELETE | `/logstream/{name}` | Create/delete stream |
67| GET | `/logstream/{name}/schema` | Field names and types |
68| GET | `/logstream/{name}/stats` | Ingestion/storage statistics |
69| GET | `/logstream/{name}/info` | Creation time, partitioning config |
70| POST | `/query` | SQL query with time range |
71| GET | `/alerts` | Alert rules (also tries legacy `/logstream/{name}/alert`) |
72| GET | `/logstream/{name}/retention` | Retention policy |
73| GET | `/user` | List users |
75The query endpoint accepts `{"query": "SQL", "startTime": "ISO8601", "endTime": "ISO8601"}` and returns either `[LogRecord]` or `{"records": [LogRecord], "fields": [...]}`.
77Live tail in Parseable uses gRPC Arrow Flight streaming. This app approximates it via HTTP polling with a sliding time window.
79## Conventions
81- No external dependencies; the app uses only Apple frameworks
82- All model types conform to `Sendable` for strict concurrency safety
83- Models use defensive `try?` decoding for optional fields to tolerate API version differences
84- Views follow the macOS pattern: `NavigationSplitView` sidebar + detail, with tab switching in the detail pane
85- All API calls are `async` and dispatched from views via `Task { }`
86- Passwords are stored in the Keychain, never in UserDefaults; `ServerConnection.password` is excluded from `Codable`
87- Stream names are SQL-escaped via `escapeSQLIdentifier` (double-quote wrapping) and URL-encoded via `encodePathComponent` for API paths
88- Views clear stale data before loading new content (e.g., switching streams clears previous schema/stats)
89- Entitlements: App Sandbox enabled with network.client for outbound connections
91## Common patterns
93### Adding a new API endpoint
951. Add the method to `ParseableClient` using `performRequest(method:path:body:)` — it handles auth, status codes, and error mapping
962. Add any new response types to the appropriate model file with defensive `try?` decoding for optional fields
973. Call from the view layer via `Task { }` with appropriate loading/error state management
99### Adding a new detail tab
1011. Add a case to `AppState.AppTab` with a `systemImage`
1022. Add the view to `StreamDetailView`'s tab switching logic
1033. The sidebar tab bar auto-renders from `AppTab.allCases`
105### Error handling
107- Use `ParseableError.userFriendlyMessage(for:)` to convert any error (including `NSURLError` codes) into a human-readable string
108- API methods throw `ParseableError` variants; views catch and display via `errorMessage` state
109- Cancelled tasks (e.g., disconnect during auto-reconnect) are detected via `Task.isCancelled` to suppress spurious error alerts