From 5416c8ccc9220bc36e7af3febcbdd9d86e88cf30 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 2 Apr 2026 11:28:00 -1000 Subject: fix(ui): make record track FAB visible (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add GpsPosition data class and NMEA RMC parser with tests - GpsPosition: latitude, longitude, sog (knots), cog (degrees true), timestampMs - NmeaParser.parseRmc: handles GP/GN talker IDs, void status, malformed input - SOG/COG default to 0.0 when fields absent; S/W coords are negative - 13 unit tests: GpsPositionTest (2), NmeaParserTest (11) — all GREEN Co-Authored-By: Claude Sonnet 4.6 * feat: add harmonic tide height predictions (Section 3.2 / 4.2) Implement offline harmonic tide prediction as specified in COMPONENT_DESIGN.md: - TideConstituent: name, speedDegPerHour, amplitudeMeters, phaseDeg - TidePrediction: timestampMs, heightMeters - TideStation: id, name, lat, lon, datumOffsetMeters, constituents - HarmonicTideCalculator: predictHeight(), predictRange(), findHighLow() Formula: h(t) = Z0 + Σ [ Hi × cos( ωi × (t − t0) − φi ) ] - 15 unit tests covering all calculation paths Co-Authored-By: Claude Sonnet 4.6 * feat: implement isochrone-based weather routing (Section 3.4) * feat: implement PDF logbook export (Section 4.8) - LogbookEntry data class: timestampMs, lat/lon, SOG, COG, wind, baro, depth, event/notes - LogbookFormatter: UTC time, position (deg/dec-min), 16-pt compass, row/page builders - LogbookPdfExporter: landscape A4 PDF via android.graphics.pdf.PdfDocument with column headers, alternating row shading, and table border - 20 unit tests covering all formatting helpers and data model behaviour Co-Authored-By: Claude Sonnet 4.6 * feat: offline GRIB staleness checker, ViewModel integration, and UI badge - Add GribRegion, GribFile data models and GribFileManager interface - Add InMemoryGribFileManager for testing and default use - Add GribStalenessChecker with FreshnessResult sealed class (Fresh/Stale/NoData) - Integrate weatherStaleness StateFlow into MainViewModel (checked after loadWeather) - Add yellow staleness banner TextView to fragment_map.xml - Wire staleness banner in MapFragment (shown on Stale, hidden on Fresh/NoData) - Add GribStalenessCheckerTest (4 TDD tests) Co-Authored-By: Claude Sonnet 4.6 * feat: satellite GRIB download with bandwidth optimisation (§9.1) Implements weather data download over Iridium satellite links: - GribParameter enum with SATELLITE_MINIMAL set (wind + pressure only) - SatelliteDownloadRequest data class (region, params, forecast hours, resolution) - SatelliteGribDownloader: size/time estimation, abort-on-oversized, pluggable fetcher - 8 unit tests covering estimation scaling, minimal param set, and download outcomes Co-Authored-By: Claude Sonnet 4.6 * feat: add AnchorWatchHandler UI with Depth/Rode Out inputs and suggested radius - Add AnchorWatchState with calculateRecommendedWatchCircleRadius, which uses ScopeCalculator.watchCircleRadius (Pythagorean scope formula) and falls back to rode length when geometry is invalid - Add AnchorWatchHandler Fragment with EditText inputs for Depth (m) and Rode Out (m); updates suggested watch circle radius live via TextWatcher - Add fragment_anchor_watch.xml layout - Wire AnchorWatchHandler into bottom navigation (MainActivity + menu) - Add AnchorWatchStateTest covering valid geometry, short-rode fallback Co-Authored-By: Claude Sonnet 4.6 * feat(safety): log wind and current conditions at MOB activation (Section 4.6) Per COMPONENT_DESIGN.md Section 4.6, the MOB navigation view must display wind and current conditions at the time of the event. - MobEvent: add nullable windSpeedKt, windDirectionDeg, currentSpeedKt, currentDirectionDeg fields captured at the exact moment of activation - MobAlarmManager.activate(): accept optional wind/current params and forward them into MobEvent (defaults to null for backward compatibility) - LocationService (new): aggregates live SensorData (resolves true wind via TrueWindCalculator) and marine-forecast current conditions; snapshot() provides a point-in-time EnvironmentalSnapshot for safety-critical logging - MobAlarmManagerTest: add tests for wind/current storage and null defaults - LocationServiceTest (new): covers snapshot, true-wind resolution, current-condition updates, and the latestSensor flow Co-Authored-By: Claude Sonnet 4.6 * feat(gps): implement NMEA/Android GPS sensor fusion in LocationService Adds priority-based selection between NMEA GPS (dedicated marine GPS, higher priority) and Android system GPS (fallback) within LocationService. Selection policy: 1. Prefer NMEA when its most recent fix is fresh (≤ nmeaStalenessThresholdMs, default 5 s) 2. Fall back to Android GPS when NMEA is stale 3. Use stale NMEA only when Android has never reported a fix 4. bestPosition is null until at least one source reports New public API: - GpsSource enum (NONE, NMEA, ANDROID) - LocationService.updateNmeaGps(GpsPosition) - LocationService.updateAndroidGps(GpsPosition) - LocationService.bestPosition: StateFlow - LocationService.activeGpsSource: StateFlow - Injectable clockMs parameter for deterministic unit tests Adds 7 unit tests covering: no-data state, fresh NMEA priority, stale NMEA fallback, only-NMEA/only-Android scenarios, exact-threshold edge case, and NMEA recovery after Android takeover. Co-Authored-By: Claude Sonnet 4.6 * feat(gps): add fix-quality (accuracy) tier to GPS sensor fusion Extend LocationService's source-selection policy with a quality-aware "marginal staleness" zone between the primary and a new extended staleness threshold (default 10 s): 1. Fresh NMEA (≤ primary threshold, 5 s) → always prefer NMEA 2. Marginally stale NMEA (5–10 s) → prefer NMEA only when GpsPosition.accuracyMeters is strictly better than Android's; fall back to Android conservatively when accuracy data is absent 3. Very stale NMEA (> 10 s) → always prefer Android 4. Only one source available → use it regardless of age Changes: - GpsPosition: add nullable accuracyMeters field (default null, no breaking change to existing callers) - LocationService: add nmeaExtendedThresholdMs constructor parameter; recomputeBestPosition() now implements three-tier logic; extract GpsPosition.hasStrictlyBetterAccuracyThan() helper - LocationServiceTest: expose nmeaExtendedThresholdMs in fusionService helper; add posWithAccuracy helper; add 4 new test cases covering accuracy-based NMEA preference, worse-accuracy fallback, no-accuracy conservative fallback, and very-stale unconditional fallback Co-Authored-By: Claude Sonnet 4.6 * chore: add .gitignore, pull-crash-logs script, updated agent permissions - .gitignore: exclude agent artifacts (.claudomator-env, .agent-home/, crash-logs/) - scripts/pull-crash-logs: download Crashlytics crash reports via Firebase CLI - .claude/settings.local.json: add Android SDK/emulator/adb permission rules Co-Authored-By: Claude Sonnet 4.6 * chore: update CI workflow to target main branch Co-Authored-By: Claude Sonnet 4.6 * fix: resolve all Kotlin compilation errors blocking CI build - Migrate kapt → KSP (Kotlin 2.0 + kapt is broken; KSP is the supported path) - Fix duplicate onResume() override in MainActivity - Fix wrong package imports: com.example.androidapp.data.model → org.terst.nav.data.model across GribFileManager, GribStalenessChecker, SatelliteGribDownloader, LogbookFormatter, LogbookPdfExporter, IsochroneRouter, AnchorWatchHandler - Create missing SensorData, ApparentWind, TrueWindData, TrueWindCalculator classes - Inline missing ScopeCalculator formula (Pythagorean) in AnchorWatchState Co-Authored-By: Claude Sonnet 4.6 * feat(track): implement GPS track recording with map overlay - TrackRepository + TrackPoint wired into MainViewModel: isRecording/trackPoints StateFlows, startTrack/stopTrack/addGpsPoint - MapHandler.updateTrackLayer(): lazily initialises a red LineLayer and updates GeoJSON polyline from List - fab_record_track FAB in activity_main.xml (top|end of bottom nav); icon toggles between ic_track_record and ic_close while recording - MainActivity feeds every GPS fix into viewModel.addGpsPoint() and observes trackPoints to redraw the polyline in real time - ic_track_record.xml vector drawable (red record dot) - 8 TrackRepositoryTest tests all GREEN Co-Authored-By: Claude Sonnet 4.6 * test(ci): share APKs between jobs and expand smoke tests CI — build job now uploads both APKs as the 'test-apks' artifact. smoke-test job downloads them and passes -x assembleDebug -x assembleDebugAndroidTest to skip recompilation (~4 min saved). Test results uploaded as 'smoke-test-results' artifact on every run. Smoke tests expanded from 1 → 11 tests covering: - MainActivity launches without crash - All 4 bottom-nav tabs are displayed - Safety tab: Safety Dashboard, ACTIVATE MOB, ANCHOR WATCH visible - Log tab: voice-log mic FAB visible - Instruments tab: bottom sheet displayed - Map tab: returns from overlay, mapView visible - MOB FAB: always visible, visible on Safety tab - Record Track FAB: displayed, toggles to Stop Recording, toggles back MainActivity: moved isRecording observer to initializeUI() so the FAB content description updates without requiring GPS permission (needed for emulator tests that run without location permission). Co-Authored-By: Claude Sonnet 4.6 * refactor: address simplify review findings TrackRepository.addPoint() now returns Boolean (true if point was added). MainViewModel.addGpsPoint() only updates _trackPoints StateFlow when a point is actually appended — eliminates ~3,600 no-op flow emissions per hour when not recording. MainActivity: loadedStyle promoted from nullable field to MutableStateFlow; trackPoints observer uses filterNotNull + combine so no track points are silently dropped if the style loads after the first GPS fix. Smoke tests: replaced 11× ActivityScenario.launch().use{} with a single @get:Rule ActivityScenarioRule — same isolation, less boilerplate. CI: removed redundant app-debug artifact upload (app-debug.apk is already included inside the test-apks artifact). Removed stale/placeholder comments from MainActivity. Co-Authored-By: Claude Sonnet 4.6 * fix(ui): anchor record track FAB to instrument sheet to prevent it being hidden The FAB was anchored to bottom_navigation, placing it in the peek zone of the instrument bottom sheet (120dp) and making it invisible to users. Re-anchoring to instrument_bottom_sheet keeps the button visible and makes it track naturally with sheet drag gestures. Co-Authored-By: Claude Sonnet 4.6 * fix(ui): anchor MOB FAB to instrument sheet to match record-track FAB The fab_mob was still anchored to bottom_navigation using the same top|start pattern that caused fab_record_track to be hidden behind the instrument sheet's 16dp elevation. Apply the same fix so the safety-critical MOB button is not occluded. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claudomator Agent Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Agent Co-authored-by: Claude Agent --- scripts/pull-crash-logs | 227 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100755 scripts/pull-crash-logs (limited to 'scripts/pull-crash-logs') diff --git a/scripts/pull-crash-logs b/scripts/pull-crash-logs new file mode 100755 index 0000000..7458e18 --- /dev/null +++ b/scripts/pull-crash-logs @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +# pull-crash-logs — Download recent Crashlytics crash reports via Firebase CLI +# +# Usage: ./scripts/pull-crash-logs [--limit N] [--app-id APP_ID] [--out DIR] +# Example: ./scripts/pull-crash-logs --limit 20 --out /tmp/crashes +# +# Requirements: +# - firebase-tools installed and authenticated (firebase login) +# - FIREBASE_PROJECT set in env, or edit PROJECT_ID below +# - firebase-admin or curl + gcloud token for REST fallback + +set -euo pipefail + +# ── Config ────────────────────────────────────────────────────────────────── +PROJECT_ID="${FIREBASE_PROJECT:-nav-test-c2872}" +LIMIT=10 +OUT_DIR="$(pwd)/crash-logs" + +# ── Arg parsing ───────────────────────────────────────────────────────────── +while [[ $# -gt 0 ]]; do + case "$1" in + --limit) LIMIT="$2"; shift 2 ;; + --app-id) APP_ID="$2"; shift 2 ;; + --out) OUT_DIR="$2"; shift 2 ;; + *) echo "Unknown arg: $1" >&2; exit 1 ;; + esac +done + +mkdir -p "$OUT_DIR" + +echo "=== Firebase Crashlytics crash log pull ===" +echo " Project : $PROJECT_ID" +echo " Limit : $LIMIT" +echo " Output : $OUT_DIR" +echo "" + +# ── Check tooling ──────────────────────────────────────────────────────────── +require() { command -v "$1" &>/dev/null || { echo "ERROR: '$1' not found. Install it first." >&2; exit 1; }; } + +# Prefer firebase CLI → fall back to REST via gcloud token +if command -v firebase &>/dev/null; then + MODE="firebase-cli" +elif command -v gcloud &>/dev/null; then + MODE="rest" +else + echo "ERROR: neither firebase-tools nor gcloud found." >&2 + echo " Install firebase-tools: npm install -g firebase-tools" >&2 + echo " Or gcloud SDK: https://cloud.google.com/sdk/docs/install" >&2 + exit 1 +fi + +echo "Mode: $MODE" +echo "" + +# ── Pull via firebase CLI ──────────────────────────────────────────────────── +if [[ "$MODE" == "firebase-cli" ]]; then + # List Android apps in project to find app ID if not provided + if [[ -z "${APP_ID:-}" ]]; then + echo "Fetching app list..." + APP_ID=$(firebase apps:list --project "$PROJECT_ID" --json 2>/dev/null \ + | python3 -c " +import json, sys +apps = json.load(sys.stdin) +android = [a for a in apps.get('result', []) if a.get('platform') == 'ANDROID'] +if not android: + print('', end='') +else: + print(android[0]['appId'], end='') +" 2>/dev/null || true) + + if [[ -z "$APP_ID" ]]; then + echo "WARNING: Could not auto-detect app ID. Set --app-id manually." >&2 + echo " Run: firebase apps:list --project $PROJECT_ID" >&2 + else + echo " Auto-detected app ID: $APP_ID" + fi + fi + + # Crashlytics data via firebase crashlytics:symbols or REST + # firebase CLI doesn't expose crash issue listing directly; + # use the REST API path below with a gcloud-obtained token. + echo "" + echo "NOTE: firebase CLI does not expose crash issue listing." + echo "Switching to REST API (requires gcloud auth)..." + MODE="rest" +fi + +# ── Pull via Crashlytics REST API ──────────────────────────────────────────── +if [[ "$MODE" == "rest" ]]; then + require curl + require python3 + + # Try gcloud first, then fall back to firebase-tools stored token + TOKEN=$(gcloud auth print-access-token 2>/dev/null) || TOKEN="" + + if [[ -z "$TOKEN" ]]; then + FIREBASE_CONFIG="$HOME/.config/configstore/firebase-tools.json" + if [[ -f "$FIREBASE_CONFIG" ]]; then + TOKEN=$(python3 -c " +import json, sys +with open('$FIREBASE_CONFIG') as f: + d = json.load(f) +tokens = d.get('tokens', {}) +print(tokens.get('access_token', ''), end='') +" 2>/dev/null) + fi + fi + + if [[ -z "$TOKEN" ]]; then + echo "ERROR: Not authenticated. Run: firebase login --no-localhost" >&2 + exit 1 + fi + + if [[ -z "${APP_ID:-}" ]]; then + echo "Fetching app list from Firebase Management API..." + APPS_JSON=$(curl -s \ + -H "Authorization: Bearer $TOKEN" \ + "https://firebase.googleapis.com/v1beta1/projects/${PROJECT_ID}/androidApps") + + APP_ID=$(echo "$APPS_JSON" | python3 -c " +import json, sys +data = json.load(sys.stdin) +apps = data.get('apps', []) +if not apps: + print('', end='') +else: + print(apps[0]['appId'], end='') +" 2>/dev/null || true) + + if [[ -z "$APP_ID" ]]; then + echo "ERROR: No Android apps found in project $PROJECT_ID" >&2 + echo "Response: $APPS_JSON" >&2 + exit 1 + fi + echo " Auto-detected app ID: $APP_ID" + fi + + # Crashlytics issue list via v1alpha1 API + ISSUES_URL="https://firebasecrashlytics.googleapis.com/v1alpha1/projects/${PROJECT_ID}/apps/${APP_ID}/issues?pageSize=${LIMIT}&orderBy=eventCount+desc" + + echo "" + echo "Fetching top $LIMIT crash issues..." + ISSUES_FILE="$OUT_DIR/issues.json" + + curl -s \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + "$ISSUES_URL" \ + -o "$ISSUES_FILE" + + ISSUE_COUNT=$(python3 -c " +import json, sys +with open('$ISSUES_FILE') as f: + data = json.load(f) +issues = data.get('issues', []) +print(len(issues)) +" 2>/dev/null || echo "0") + + echo " Found $ISSUE_COUNT issues. Saved to $ISSUES_FILE" + echo "" + + # Pretty-print summary + python3 -c " +import json, sys + +with open('$ISSUES_FILE') as f: + data = json.load(f) + +issues = data.get('issues', []) +if not issues: + print('No crash issues found (or API returned an error).') + print('Raw response:') + print(json.dumps(data, indent=2)) + sys.exit(0) + +print(f'{'ID':<30} {'EVENTS':>8} {'USERS':>7} TITLE') +print('-' * 90) +for issue in issues: + issue_id = issue.get('issueId', 'N/A')[:30] + title = issue.get('title', 'N/A')[:50] + event_cnt = issue.get('eventCount', '?') + user_cnt = issue.get('impactedUsersCount', '?') + print(f'{issue_id:<30} {str(event_cnt):>8} {str(user_cnt):>7} {title}') +" + + # Pull individual events for each issue + echo "" + echo "Fetching events per issue..." + python3 -c " +import json, sys, subprocess, os + +with open('$ISSUES_FILE') as f: + data = json.load(f) + +issues = data.get('issues', []) +token = '$TOKEN' +project = '$PROJECT_ID' +app_id = '$APP_ID' +out_dir = '$OUT_DIR' + +for issue in issues[:5]: # cap at 5 issues for detail pull + issue_id = issue.get('issueId', '') + if not issue_id: + continue + + url = (f'https://firebasecrashlytics.googleapis.com/v1alpha1/' + f'projects/{project}/apps/{app_id}/issues/{issue_id}/events?pageSize=5') + + out_file = os.path.join(out_dir, f'events_{issue_id[:20]}.json') + result = subprocess.run( + ['curl', '-s', '-H', f'Authorization: Bearer {token}', url], + capture_output=True, text=True + ) + with open(out_file, 'w') as f: + f.write(result.stdout) + + try: + events = json.loads(result.stdout).get('events', []) + print(f' {issue_id[:30]}: {len(events)} events → {out_file}') + except Exception: + print(f' {issue_id[:30]}: parse error → {out_file}') +" +fi + +echo "" +echo "Done. Files written to: $OUT_DIR" +ls -lh "$OUT_DIR" -- cgit v1.2.3