Skip to content

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
b-nnett:mainfrom
tigercraft4:pr/fork-v1-to-v4
Closed

feat: fork additions — self-hosted server, Coach multi-provider, @Observable migration, pt-PT localisation (v1.0–v4.0)#28
tigercraft4 wants to merge 430 commits into
b-nnett:mainfrom
tigercraft4:pr/fork-v1-to-v4

Conversation

@tigercraft4

Copy link
Copy Markdown

Summary

This PR represents the cumulative additions made in tigercraft4/goose across 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

v2.0 — WHOOP 4.0 (Gen4) + HR Monitor Pipeline

  • WHOOP 4.0 (Gen4) supportWearableDescriptor.whoopGen4, generation detection in connection flow, device_generation: "4.0" in upload payload, Gen4 command guards in GooseBLETypes
  • Android JNI foundationsRust/core/src/bridge.rs android module, cfg-gated android shim, cargo-ndk cross-compile in CI, ADR documented
  • Standard HR monitor BLE pipelineGooseBLEClient+HRMonitor.swift, heart_rate_gatt_protocol.rs (0x180D/0x2A37), HR/RR stream in upload payload, DeviceType::HrMonitor Rust variant

v3.0 — BLE Stability, HR Monitor UX, Recovery V2, Localisation

  • BLE stability (from upstream PR feat: add exponential backoff and attempt limits to BLE reconnection #18 + Add WHOOP 4.0 (Gen4) BLE support + stability/perf fixes #19 concepts):
    • Exponential reconnect backoff (1 s base, doubles, 60 s cap, 10-attempt circuit breaker) for both WHOOP and HR monitor
    • Manual retry + abort UI in ConnectionView
    • catch_unwind + panic = "unwind" in Rust FFI layer — panics return JSON error instead of crashing the app
    • 24 MB storage retention cap for raw evidence payloads
    • Per-row device_id propagated into capture_sessions table
  • HR monitor scan/connect UIHRMonitorView with live scan list (device name + RSSI), connect sheet, connected panel; wired into More tab Device section
  • BLE main-thread publishing fix — all @Published mutations in GooseBLEClient+Commands.swift and +Parsing.swift dispatched to main thread; eliminates "Publishing changes from background threads" runtime warnings
  • HR monitor independent capture.hrMonitor capture mode; startHRMonitorCapture/stopHRMonitorCapture not gated on WHOOP session; auto-start/stop on hrConnectionState change
  • WHOOP 4.0 RTC clock sync — reads device clock via BLE, compares to iPhone time, writes corrected time silently on drift
  • Recovery V2 dashboard — hero recovery score, HRV, RHR from Rust bridge; 7-day trend chart
  • pt-PT localisationLocalizable.xcstrings String Catalog; 650+ static strings; dynamic status strings via LocalizedStatusStrings.swift (@published display extensions)
  • Recovery formula SDNN accuracy (from upstream PR feat: Apple Health fallback for sleep, recovery, strain, and vitals #5 review by OKKHALIL3):
    • rmssd_segment_aware() — RMSSD computed within BLE capture windows only, never across window boundaries; physiological band filter (300–2000 ms); Malik 20% ectopic filter
    • Rename hrvRmssdMshkHRVSDNNMs (Apple Watch reports SDNN, not RMSSD)
    • Remove /1.2 population approximation; normalise baselines directly in SDNN

v4.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: Bool property on GooseBLEDebugResearchCommand
    • Guard in GooseAppModel+Lifecycle.swift blocks state-changing commands when invoked via gooseswift:// URL scheme
    • Read-only commands (risk == "read" or "keyed read") continue to work remotely
    • remoteURLExample returns "Remote invocation disabled" for blocked commands
  • Full @Observable migration:

    • GooseAppModelObservableObject@Observable; all 68 @Published removed; views use @Environment(GooseAppModel.self) instead of @EnvironmentObject
    • HealthDataStore — same migration; @ObservedObject@State at call sites
    • GooseBLEClient@Observable with NSObject conformance preserved for CBCentralManagerDelegate; @Bindable local var pattern for binding in View bodies
    • MoreDataStore — Combine MergeMany pipeline replaced with three onChange modifiers
    • Result: per-property SwiftUI re-render granularity; NavigationRequestObserver tried to update multiple times per frame warning eliminated
  • Coach multi-provider:

    • CoachProvider protocol: id, displayName, isAuthenticated, availablePresets, send(messages:systemPrompt:) async throws -> AsyncStream<String>, signOut()
    • CoachProviderRegistry — manages all four providers; persists activeProviderId in UserDefaults
    • ChatGPTCoachProvider — wraps existing CodexSelfContainedAuthClient; existing Keychain token at chatgpt-auth is reused without re-auth
    • ClaudeCoachProvider — 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 Keychain
    • GeminiCoachProvider — Google OAuth 2.0 PKCE via WKWebView (no SDK); streamGenerateContent SSE; user-supplied client_id
    • CoachSettingsSheet — provider picker UI in More/Coach settings; per-provider config sheets; gear icon in CoachView
  • pt-PT localisation completion:

    • 119 new pt-PT strings covering all v4.0 UI additions (Coach settings, provider config, API key fields, model preset labels)
    • Additional 9 strings fixed post-audit: Claude/Gemini model preset names (proper nouns), "Google Client ID""ID de cliente Google", GPT-5.5 tiers → Alto/Médio/Baixo
    • Onboarding Keychain fix: onboardingComplete no longer restored from Keychain on reinstall — onboarding shows fresh, profile fields pre-filled
    • Startup non-blocking: recoverUncleanOvernightGuardSessionIfNeeded() dispatched to rustStartupQueue.async
    • Onboarding skip button: Button(String(localized: "Skip setup")) with pt-PT translation in xcstrings

Files Changed

Area Files
Swift (GooseSwift/) 90 files
Rust (Rust/core/) 49 files
Server (server/) 120 files
GitHub Actions / CI 6 workflows
Total 298 files, +46,576 / -2,103 lines

Upstream PRs Integrated (from b-nnett/goose)

PR Author Description Status
#1 Fix timeout/duration ✅ Integrated
#3 FFI documentation ✅ Integrated
#4 Scroll performance ✅ Integrated
#5 Apple Health import ✅ Integrated
#6 Rust CI workflow ✅ Integrated
#7 list_methods RPC ✅ Integrated
#10 CI + bug fixes ✅ Integrated
#12 FFI threading safety ✅ Integrated
#13 Windows compatibility ✅ Integrated
#15 kobemartin Block state-changing deep links ✅ Integrated
#19 po-sc rmssd_segment_aware (partial) ⚠ Partial — concepts used

⚠ 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:

  1. COACH-06 — ChatGPT Keychain migration: Cold-launch with existing chatgpt-auth token in Keychain → verify ChatGPT is active provider without re-auth. Code path is unchanged; assumed transparent.

  2. Claude streaming (live): Open Coach settings → select Claude → enter valid Anthropic API key → save → send message → verify streaming reply via ClaudeCoachProvider.

  3. Custom endpoint streaming (live): Open Coach settings → select Custom → enter valid HTTPS base URL + API key + model ID → save → send message → verify streaming reply.

  4. 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.

  5. Provider switching: Switch between two authenticated providers mid-session → verify correct backend responds, no credential leakage.

  6. 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.

  7. 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 pass
  • xcodebuild build — Swift builds clean for simulator target
  • GooseSwiftTests suite — all Swift unit tests pass (CoachProvider conformance, Keychain round-trips, upload service)
  • Live Claude streaming (requires Anthropic API key)
  • Live Custom endpoint streaming (requires external HTTPS endpoint)
  • Live Gemini OAuth (requires Google Cloud OAuth client)
  • pt-PT simulator language switch (requires simulator run)
  • WHOOP/HR monitor device tests (requires physical device)

tigercraft4 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
- 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)
tigercraft4 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
- ae7d3c1: GROUP A Coach/provider strings (cherry-picked into worktree)
- 9146b53: GROUPs B-E Health/UI strings (cherry-picked into worktree)
…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.
@tigercraft4 tigercraft4 closed this Jun 8, 2026
@tigercraft4 tigercraft4 deleted the pr/fork-v1-to-v4 branch June 8, 2026 18:23
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.

1 participant