summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-04-02 11:28:00 -1000
committerGitHub <noreply@github.com>2026-04-02 11:28:00 -1000
commit5416c8ccc9220bc36e7af3febcbdd9d86e88cf30 (patch)
tree46e990ae9dc43f4f7b6ea75cb4e5467d6fdb6359 /scripts
parenta9d87b600848178b03b85a06ccdfd53b11e38c38 (diff)
fix(ui): make record track FAB visible (#1)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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<GpsPosition?> - LocationService.activeGpsSource: StateFlow<GpsSource> - 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * chore: update CI workflow to target main branch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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<TrackPoint> - 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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<Style?>; 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claudomator Agent <agent@claudomator> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Agent <agent@example.com> Co-authored-by: Claude Agent <agent@claude.ai>
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/pull-crash-logs227
1 files changed, 227 insertions, 0 deletions
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 <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"