feat: fork additions — self-hosted server, Coach multi-provider, @Observable migration, pt-PT localisation (v1.0–v4.0)#28
Closed
tigercraft4 wants to merge 430 commits into
Closed
Conversation
added 30 commits
June 3, 2026 23:44
…rse_device_type - Add HrMonitor variant to DeviceType enum in protocol.rs - Update header_len, expected_frame_len, declared_len, header_crc_valid match arms - Add DeviceType::HrMonitor => "HR_MONITOR" to device_type_name in store.rs - Add "HR_MONITOR" | "hr_monitor" arm to parse_device_type in bridge.rs (not Goose) - Handle HrMonitor in openwhoop_reference.rs whoop_generation_from_device_type (=> None) - cargo check passes with no errors
… full suite passes - Add parse_device_type_hr_monitor_uppercase: HR_MONITOR => DeviceType::HrMonitor - Add parse_device_type_hr_monitor_lowercase: hr_monitor => DeviceType::HrMonitor - Add parse_device_type_goose_no_regression: GOOSE => DeviceType::Goose - cargo test full suite: zero failures across all test files
- Silent Gen5 fallback removed; device_class: HR_MONITOR in upload payload - DeviceType::HrMonitor variant wired through protocol, store, bridge, openwhoop_reference - triggerManualUpload derives WHOOP type from activeDescriptor; adds HR monitor upload path - Rust test suite: zero failures; 3 new parse_device_type assertions - WEAR-03 satisfied
- Add internal buildUploadPayload(deviceID:deviceType:streams:) with no async/URLSession/bridge - performUpload delegates payload construction to the extracted function - GEN4 -> device_generation 4.0 (no device_class); GOOSE -> 5.0; default -> device_type + device_class HR_MONITOR - Internal visibility allows @testable import GooseSwift test access (HIGH-3)
…onomy (HIGH-3) - Create GooseSwiftTests/GooseUploadServiceTests.swift with 6 test methods - Gen4 -> device_generation 4.0 + no device_class; Gen5 -> 5.0 + no device_class - HR monitor (Polar H10) -> device_type + device_class HR_MONITOR + no device_generation - Unknown device defaults to HR_MONITOR device_class - Streams round-trip preserved exactly in payload - triggerManualUpload source-assertion: no unconditional deviceType: "GOOSE" literal - Add GooseUploadServiceTests to pbxproj (file ref + build file + PBXGroup + Sources phase) - Add HEADER_SEARCH_PATHS ($(SRCROOT)/Rust/core/include) to GooseSwiftTests Debug+Release configs - [Rule 3] Fix stored property in extension: move sevenDayStrainCache from HealthDataStore+Snapshots.swift to HealthDataStore.swift (pre-existing Swift compile error blocking @testable import GooseSwift)
- buildUploadPayload extracted; 6 GooseUploadServiceTests passing - Rule 3 fixes: sevenDayStrainCache moved to class body, HEADER_SEARCH_PATHS added to test target - HIGH-3 resolved; WEAR-03 upload taxonomy locked behind regression tests
… into upload pipeline
- Add DeviceType::HrMonitor branch in import_captured_frame_timed - Bypass parse_frame (which requires 0xAA FRAME_START) for HR monitor frames - Construct ParsedFrame with payload_hex = hex::encode(raw GATT bytes) - Set header_crc_valid and payload_crc_valid to true so upload bridge CRC-skip does not drop the row - Insert via store.insert_decoded_frame using existing DecodedFrameInput pattern - Non-HrMonitor flow is unchanged
…1/WEAR-03, CR-02) - bridge_hr_monitor_upload_stream_contains_bpm_and_rr: asserts hr stream populated with bpm and rr_intervals from 0x2A37 GATT frames - bridge_hr_monitor_upload_stream_no_rr_when_not_present: asserts rr_intervals is [] when GATT flags bit 4 is clear - bridge_hr_monitor_upload_stream_device_id_filter: asserts device_id filter returns only matching device frames (CR-02)
…-02 device_id filter (Task 2)
…st be < chrono_now())
…(gap closure WEAR-01/WEAR-03 + CR-02)
added 28 commits
June 6, 2026 12:14
…mini providers — eliminates per-frame Keychain reads causing UI stutter
- GooseAppModel: nonisolated(unsafe) → @ObservationIgnored nonisolated(unsafe) for captureFrameRowBuildQueueDepth, captureFrameRowBuildQueueHighWatermark, frameReassemblyBuffers — @observable macro generated backing storage that made nonisolated(unsafe) have no effect; @ObservationIgnored opts out of tracking so the annotation applies to the plain stored property - HealthDataStore: same fix for heartRateSeriesUpdateObserver - GeminiCoachProvider: remove @mainactor — conformance to CoachProvider crossed actor boundaries; aligns with ClaudeCoachProvider pattern (@observable only) - CoachChatModel: remove unnecessary await from sync seedAssistantPromptIfNeeded() calls (no async operations in the await expression)
WR-01: GeminiOAuthWebView.updateUIView — guard against reload on SwiftUI state updates (guard webView.url == nil); previously restarted OAuth flow on any parent state change mid-flight WR-02: GeminiCoachProvider.send() — replace force-unwrap URL(string:)! with guard let to handle invalid model ID strings safely
Eliminates the dominant startup bottleneck: contentsOfDirectory + JSONL line-count scans (potentially MBs of data) were blocking the main thread during GooseAppModel.init(). - recoverUncleanOvernightGuardSessionIfNeeded: dispatch latestRecoverableOvernightGuardSession() to rustStartupQueue, then callback to main for state mutations (applyUncleanSessionRecovery) - latestRecoverableOvernightGuardSession + 8 file-I/O helpers marked nonisolated (overnightGuardRootDirectoryURL, readJSONObject, fileSize, readStatusValues, countSuccessfulHistoricalRangePolls, countJSONLRecords, overnightGuardIntValue, overnightGuardTargetCounts) — none access actor-isolated state, only FileManager and pure computation - captureTimestampFormatter kept @mainactor; local ISO8601DateFormatter used inside the nonisolated function to avoid Sendable warning - defaultDatabasePath() cached via private static let _sharedDatabasePath (computed once at first call); eliminates repeated FileManager.createDirectory syscalls across CaptureFrameWriteQueue, GooseUploadService, OvernightSQLiteMirrorQueue, GooseAppModel+*, HealthDataStore
…Gemini, Custom) Merges plan/phase-18-coach-multi-provider into main. Phase 18 delivers the full Coach Multi-Provider feature: - CoachProvider protocol + CoachProviderRegistry (4 providers) - ChatGPTCoachProvider (OAuth, existing flow preserved) - ClaudeCoachProvider (Anthropic API key + Keychain) - GeminiCoachProvider (Google OAuth PKCE via WKWebView) - CustomEndpointCoachProvider (OpenAI-compatible base URL + API key) - CoachSettingsSheet with provider picker + per-provider config forms - Gear icon in CoachView + active-provider indicator - Swift 6 concurrency warnings resolved - Per-frame Keychain reads eliminated (isAuthenticated cached) - Overnight session discovery moved to background queue
Keychain survives app deletion, so restoreIntoDefaultsIfAvailable with restoreCompletion:true was skipping onboarding entirely on reinstall. Now restores profile data only (name, height, weight — pre-fills fields) but leaves onboardingComplete=false so onboarding shows on fresh install.
Audit result: 129 real-text strings missing pt-PT (mostly Phase 18 Coach UI). Phase 19 covers full translation + startup fixes already shipped.
…ROUP A) - 32 strings translated: API Key, Anthropic API key, Base URL, Bearer token, Calibrate, Change, Coach Settings, Configuration, Enter an API key first, Filters, Generation stopped, Key saved, Model ID, Must start with https://, No key saved, Not signed in, Provider, Remove API Key, Remove Key, Save API Key, Save Endpoint, Sign Out?, Sign in with ChatGPT/Google, Sign-in failed, Signed in, Signing in..., Keychain deletion message, provider re-auth message, https://hostname:8770 (technical placeholder) - Brand names intentionally excluded per D-02 (Claude, GPT, Gemini models) - JSON validated
…GROUPs B-E) - GROUP B (41 strings): Add Sleep, Cardio Load descriptions, Energy Bank, Heart rate unavailable, Sleep Insights, Workout Details, Stages, Wake, Period, Primary Sleep, Refresh Health/Score, Target sleep, avg, vs avg, etc. - GROUP C (27 strings): Alarm config (Choose vibration, Set Alarm, Disable WHOOP Alarms), device controls (Controls Locked, Sync from band, Save to band), general UI (Command Evidence, Data source, Route, Remove, etc.) - GROUP D (11 strings): Format specifiers with text - beats per minute, records acked, active, ZONE %lld -> ZONA %lld, Avg -> Med, load, etc. - GROUP E (5 strings): Time display - NOW -> AGORA, 30 MIN AGO -> HA 30 MIN, plus unit strings kept identical (0 min, 0h, 7h 39m) - Python scan confirms 0 non-trivial strings missing pt-PT - JSON validated
- 119 non-trivial strings translated across 5 groups (A-E) - Coach/provider config, Health/Sleep/Cardio, UI/alarm, format specifiers - Python scan: 0 non-trivial strings missing pt-PT - xcodebuild BUILD SUCCEEDED
…ROUP A) - 32 strings translated: API Key, Anthropic API key, Base URL, Bearer token, Calibrate, Change, Coach Settings, Configuration, Enter an API key first, Filters, Generation stopped, Key saved, Model ID, Must start with https://, No key saved, Not signed in, Provider, Remove API Key, Remove Key, Save API Key, Save Endpoint, Sign Out?, Sign in with ChatGPT/Google, Sign-in failed, Signed in, Signing in..., Keychain deletion message, provider re-auth message, https://hostname:8770 (technical placeholder) - Brand names intentionally excluded per D-02 (Claude, GPT, Gemini models) - JSON validated
…GROUPs B-E) - GROUP B (41 strings): Add Sleep, Cardio Load descriptions, Energy Bank, Heart rate unavailable, Sleep Insights, Workout Details, Stages, Wake, Period, Primary Sleep, Refresh Health/Score, Target sleep, avg, vs avg, etc. - GROUP C (27 strings): Alarm config (Choose vibration, Set Alarm, Disable WHOOP Alarms), device controls (Controls Locked, Sync from band, Save to band), general UI (Command Evidence, Data source, Route, Remove, etc.) - GROUP D (11 strings): Format specifiers with text - beats per minute, records acked, active, ZONE %lld -> ZONA %lld, Avg -> Med, load, etc. - GROUP E (5 strings): Time display - NOW -> AGORA, 30 MIN AGO -> HA 30 MIN, plus unit strings kept identical (0 min, 0h, 7h 39m) - Python scan confirms 0 non-trivial strings missing pt-PT - JSON validated
…fix UX-01 skip button key - Claude Haiku/Sonnet/Opus 4.x model preset names (kept as proper nouns in pt-PT) - Gemini 2.5 Flash/Pro model preset names (kept as proper nouns) - Google Client ID → "ID de cliente Google" - GPT-5.5 High/Medium/Low → "GPT-5.5 Alto/Médio/Baixo" - OnboardingView.swift: change skip button key from hardcoded "Saltar configuração" to "Skip setup" with pt-PT translation in xcstrings — now translatable for all locales
- milestones/v4.0-ROADMAP.md — full phase archive (16-19) - milestones/v4.0-REQUIREMENTS.md — requirements with final status - milestones/v4.0-MILESTONE-AUDIT.md — audit report (tech_debt, 13/13 integration) - MILESTONES.md — v3.0 and v4.0 entries added - PROJECT.md — full evolution review; v4.0 requirements moved to Validated - STATE.md — deferred items documented - ROADMAP.md — v4.0 collapsed to details block; phase details removed; progress table updated
Comprehensive fork delivering: - Self-hosted FastAPI+TimescaleDB server with iOS auto-upload - WHOOP 4.0 (Gen4) full iOS support - Standard HR monitor BLE pipeline (0x180D/0x2A37) - BLE stability (reconnect backoff, FFI safety, storage cap) - HR monitor scan UI + independent capture sessions - WHOOP 4.0 RTC clock sync - Recovery V2 dashboard with bridge-backed metrics - Full @observable migration (GooseAppModel + HealthDataStore + GooseBLEClient) - Coach multi-provider (ChatGPT, Claude, Gemini, Custom endpoint) - Complete pt-PT European Portuguese localisation (650+ strings) Upstream PRs integrated: #1, #3, #4, #5, #6, #7, #10, #12, #13, #15, partial #19 Note: runtime streaming tests (Claude, Gemini, Custom endpoint) and device verification tests are deferred — require live API credentials.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR represents the cumulative additions made in
tigercraft4/gooseacross four milestones (v1.0–v4.0). It is submitted as a reference/contribution PR — the changes are large and span multiple areas, so they are described below by milestone to make review practical.What Was Built
v1.0 — Self-Hosted Server + Upstream PR Integration
server/) — Docker Compose, named volumes,POST /v1/ingest-decodedendpoint, Bearer token auth, health checkGooseUploadService.swift,UploadStatusStore.swift, More tab upload status panel (last upload time, pending batches, health check indicator)v2.0 — WHOOP 4.0 (Gen4) + HR Monitor Pipeline
WearableDescriptor.whoopGen4, generation detection in connection flow,device_generation: "4.0"in upload payload, Gen4 command guards inGooseBLETypesRust/core/src/bridge.rsandroid module,cfg-gated android shim,cargo-ndkcross-compile in CI, ADR documentedGooseBLEClient+HRMonitor.swift,heart_rate_gatt_protocol.rs(0x180D/0x2A37), HR/RR stream in upload payload,DeviceType::HrMonitorRust variantv3.0 — BLE Stability, HR Monitor UX, Recovery V2, Localisation
catch_unwind+panic = "unwind"in Rust FFI layer — panics return JSON error instead of crashing the appdevice_idpropagated intocapture_sessionstableHRMonitorViewwith live scan list (device name + RSSI), connect sheet, connected panel; wired into More tab Device section@Publishedmutations inGooseBLEClient+Commands.swiftand+Parsing.swiftdispatched to main thread; eliminates "Publishing changes from background threads" runtime warnings.hrMonitorcapture mode;startHRMonitorCapture/stopHRMonitorCapturenot gated on WHOOP session; auto-start/stop onhrConnectionStatechangeLocalizable.xcstringsString Catalog; 650+ static strings; dynamic status strings viaLocalizedStatusStrings.swift(@published display extensions)rmssd_segment_aware()— RMSSD computed within BLE capture windows only, never across window boundaries; physiological band filter (300–2000 ms); Malik 20% ectopic filterhrvRmssdMs→hkHRVSDNNMs(Apple Watch reports SDNN, not RMSSD)/1.2population approximation; normalise baselines directly in SDNNv4.0 — Security, @observable Migration, Coach Multi-Provider, Localisation Completion
Deep link security (upstream PR Block state-changing debug deep links #15 by kobemartin — already integrated):
allowsRemoteInvocation: Boolproperty onGooseBLEDebugResearchCommandGooseAppModel+Lifecycle.swiftblocks state-changing commands when invoked viagooseswift://URL schemerisk == "read"or"keyed read") continue to work remotelyremoteURLExamplereturns "Remote invocation disabled" for blocked commandsFull
@Observablemigration:GooseAppModel—ObservableObject→@Observable; all 68@Publishedremoved; views use@Environment(GooseAppModel.self)instead of@EnvironmentObjectHealthDataStore— same migration;@ObservedObject→@Stateat call sitesGooseBLEClient—@Observablewith NSObject conformance preserved forCBCentralManagerDelegate;@Bindablelocal var pattern for binding in View bodiesMoreDataStore— CombineMergeManypipeline replaced with threeonChangemodifiersNavigationRequestObserver tried to update multiple times per framewarning eliminatedCoach multi-provider:
CoachProviderprotocol:id,displayName,isAuthenticated,availablePresets,send(messages:systemPrompt:) async throws -> AsyncStream<String>,signOut()CoachProviderRegistry— manages all four providers; persistsactiveProviderIdin UserDefaultsChatGPTCoachProvider— wraps existingCodexSelfContainedAuthClient; existing Keychain token atchatgpt-authis reused without re-authClaudeCoachProvider— Anthropic Messages API SSE streaming; API key in Keychain; three model presets (Haiku 4.5, Sonnet 4.6, Opus 4.8)CustomEndpointCoachProvider— OpenAI-compatible Chat Completions SSE; HTTPS URL validation; base URL + model in UserDefaults, API key in KeychainGeminiCoachProvider— Google OAuth 2.0 PKCE via WKWebView (no SDK);streamGenerateContentSSE; user-supplied client_idCoachSettingsSheet— provider picker UI in More/Coach settings; per-provider config sheets; gear icon inCoachViewpt-PT localisation completion:
"Google Client ID"→"ID de cliente Google", GPT-5.5 tiers → Alto/Médio/BaixoonboardingCompleteno longer restored from Keychain on reinstall — onboarding shows fresh, profile fields pre-filledrecoverUncleanOvernightGuardSessionIfNeeded()dispatched torustStartupQueue.asyncButton(String(localized: "Skip setup"))with pt-PT translation in xcstringsFiles Changed
Upstream PRs Integrated (from b-nnett/goose)
⚠ Untested Items (Require Live Credentials)
These items are implemented but could not be verified by static code inspection alone. They require running on a real device or simulator with live API credentials:
COACH-06 — ChatGPT Keychain migration: Cold-launch with existing
chatgpt-authtoken in Keychain → verify ChatGPT is active provider without re-auth. Code path is unchanged; assumed transparent.Claude streaming (live): Open Coach settings → select Claude → enter valid Anthropic API key → save → send message → verify streaming reply via
ClaudeCoachProvider.Custom endpoint streaming (live): Open Coach settings → select Custom → enter valid HTTPS base URL + API key + model ID → save → send message → verify streaming reply.
Gemini OAuth + streaming (live): Open Coach settings → select Gemini → enter Google OAuth Client ID → tap "Sign in with Google" → complete PKCE flow in WKWebView → verify streaming reply via
streamGenerateContent.Provider switching: Switch between two authenticated providers mid-session → verify correct backend responds, no credential leakage.
pt-PT simulator test: Switch iOS Simulator language to Portuguese (Portugal) → launch app → verify all Coach settings, provider config, and health dashboard strings appear in Portuguese.
PERF-03 runtime: Launch app, connect WHOOP, start capture → confirm "NavigationRequestObserver tried to update multiple times per frame" warning no longer appears in Xcode console. Structurally eliminated — runtime signal requires live device.
Test Plan
cargo test -p goose-core— all Rust unit + integration tests passxcodebuild build— Swift builds clean for simulator targetGooseSwiftTestssuite — all Swift unit tests pass (CoachProvider conformance, Keychain round-trips, upload service)