diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-04-02 11:28:00 -1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-02 11:28:00 -1000 |
| commit | 5416c8ccc9220bc36e7af3febcbdd9d86e88cf30 (patch) | |
| tree | 46e990ae9dc43f4f7b6ea75cb4e5467d6fdb6359 /scripts | |
| parent | a9d87b600848178b03b85a06ccdfd53b11e38c38 (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-x | scripts/pull-crash-logs | 227 |
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" |
