diff --git a/CLAUDE.md b/CLAUDE.md
index 7e24911..1a94d6a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -99,11 +99,12 @@ Files saved to a user-accessible location (iOS Files app / Android shared storag
Scaffold is done (all screens, navigation, store, services, GPX, DB, upload).
Items below are what remains before v1 is shippable.
-### 1 — Quick fixes (each < 30 min)
+### 1 — Quick fixes ✅
-- [ ] **Keep-awake toggle** — `useKeepAwake()` in `RecordingScreen` is unconditional; wire it to `keepAwake` from the store so the toggle actually works
-- [ ] **Sensor button on Recording screen** — no entry point to the SensorPairing modal; add a BLE icon button in the header or stats area that calls `nav.navigate('SensorPairing')`
-- [ ] **GPS pause/resume** — `pause()` / `resume()` only change store state; also call `stopGpsRecording()` / `startGpsRecording()` so the background task actually pauses
+- [x] **Keep-awake toggle** — wired to `keepAwake` store state via `activateKeepAwakeAsync` / `deactivateKeepAwake`; toggle button visible in controls bar
+- [x] **Sensor button on Recording screen** — "⚡ Sensors" button overlaid on map area navigates to SensorPairing modal
+- [x] **GPS pause/resume** — `handlePause` calls `stopGpsRecording()`, `handleResume` calls `startGpsRecording()`
+- [x] **Track view** — `TrackView` component renders recorded lat/lon points as a scaled SVG polyline on a dark background (no tile server needed); replaces the grey placeholder
### 2 — BLE (half day)
@@ -120,8 +121,11 @@ Items below are what remains before v1 is shippable.
- [ ] On first launch, detect if the app is affected by battery optimization (`expo-intent-launcher`) and show a one-time prompt directing the user to whitelist bincio-rec; persist dismissal in AsyncStorage so it only shows once
-### 5 — Map (1–2 days)
+### 5 — Map (optional upgrade)
-- [ ] **Initialize MapLibre** — call `MapLibreGL.setAccessToken(null)` at app start (or configure a tile provider in Settings); add a tile source (e.g. OpenFreeMap / self-hosted)
-- [ ] **Replace placeholder** — swap the grey `` in `RecordingScreen` with a `` + `` that follows current location
-- [ ] **Live track polyline** — subscribe to `trackPoints` from the store and render a `` + `` that updates as points arrive
+The track view from section 1 already shows the GPX polyline scaled to fit the screen.
+MapLibre can be added later for a real basemap (streets/terrain), but is not required for v1.
+
+- [ ] **MapLibre basemap** — replace `TrackView` SVG with `` + a tile source (e.g. OpenFreeMap); call `MapLibreGL.setAccessToken(null)` for raster-free usage
+- [ ] **Camera follow** — add `` that follows `trackPoints[last]` during recording
+- [ ] **Line layer** — replace SVG polyline with `` + ``
diff --git a/src/screens/RecordingScreen.tsx b/src/screens/RecordingScreen.tsx
index bb62e6e..5e99d5a 100644
--- a/src/screens/RecordingScreen.tsx
+++ b/src/screens/RecordingScreen.tsx
@@ -1,27 +1,35 @@
import React, { useEffect, useRef, useState } from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
+import { View, Text, StyleSheet, TouchableOpacity, Alert, LayoutChangeEvent } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
-import { useKeepAwake } from 'expo-keep-awake';
+import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
+import Svg, { Polyline } from 'react-native-svg';
import { useRecordingStore } from '../store/recording';
import { startGpsRecording, stopGpsRecording, requestLocationPermissions } from '../services/gps';
-import { RootStackParamList } from '../types';
+import { RootStackParamList, TrackPoint } from '../types';
type Nav = NativeStackNavigationProp;
export function RecordingScreen() {
const nav = useNavigation
- {/* Map placeholder — MapLibre component goes here */}
-
- Map
+ {
+ const { width, height } = e.nativeEvent.layout;
+ setMapSize({ width, height });
+ }}
+ >
+ {mapSize.width > 0 && (
+
+ )}
+ nav.navigate('SensorPairing')}>
+ ⚡ Sensors
+
+ setKeepAwake(!keepAwake)}
+ >
+ {keepAwake ? '☀️ Awake' : '💤 Sleep'}
+
+
{status === 'idle' && (
Start
@@ -71,7 +106,7 @@ export function RecordingScreen() {
)}
{status === 'recording' && (
<>
-
+
Pause
@@ -81,7 +116,7 @@ export function RecordingScreen() {
)}
{status === 'paused' && (
<>
-
+
Resume
@@ -94,6 +129,45 @@ export function RecordingScreen() {
);
}
+const PADDING = 16;
+
+function TrackView({ trackPoints, width, height }: { trackPoints: TrackPoint[]; width: number; height: number }) {
+ if (trackPoints.length < 2) {
+ return (
+
+
+ {trackPoints.length === 0 ? 'Start recording to see your track' : '…'}
+
+
+ );
+ }
+
+ const lats = trackPoints.map((p) => p.lat);
+ const lons = trackPoints.map((p) => p.lon);
+ const minLat = Math.min(...lats), maxLat = Math.max(...lats);
+ const minLon = Math.min(...lons), maxLon = Math.max(...lons);
+ const latRange = maxLat - minLat || 0.0001;
+ const lonRange = maxLon - minLon || 0.0001;
+
+ const w = width - PADDING * 2;
+ const h = height - PADDING * 2;
+
+ // preserve aspect ratio
+ const scale = Math.min(w / lonRange, h / latRange);
+ const offX = (w - lonRange * scale) / 2 + PADDING;
+ const offY = (h - latRange * scale) / 2 + PADDING;
+
+ const points = trackPoints
+ .map((p) => `${offX + (p.lon - minLon) * scale},${offY + (maxLat - p.lat) * scale}`)
+ .join(' ');
+
+ return (
+
+ );
+}
+
function StatBox({ label, value }: { label: string; value: string }) {
return (
@@ -109,9 +183,14 @@ const styles = StyleSheet.create({
statBox: { width: '25%', padding: 8, alignItems: 'center' },
statLabel: { color: '#888', fontSize: 11, textTransform: 'uppercase' },
statValue: { color: '#fff', fontSize: 18, fontWeight: '600', marginTop: 2 },
- mapPlaceholder: { flex: 1, backgroundColor: '#1a1a2e', alignItems: 'center', justifyContent: 'center' },
- mapPlaceholderText: { color: '#444', fontSize: 16 },
- controls: { flexDirection: 'row', justifyContent: 'center', gap: 16, padding: 24 },
+ mapArea: { flex: 1, backgroundColor: '#0d0d1a', overflow: 'hidden' },
+ mapHint: { color: '#333', fontSize: 13, textAlign: 'center', marginTop: 40 },
+ sensorBtn: { position: 'absolute', top: 10, right: 10, backgroundColor: '#1e1e2e', borderRadius: 8, paddingVertical: 6, paddingHorizontal: 12 },
+ sensorBtnText: { color: '#3b82f6', fontSize: 13, fontWeight: '600' },
+ controls: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 12, padding: 20 },
+ awakeBtn: { backgroundColor: '#1e1e1e', borderRadius: 20, paddingVertical: 8, paddingHorizontal: 14 },
+ awakeBtnOn: { backgroundColor: '#2a2a1a' },
+ awakeBtnText: { color: '#aaa', fontSize: 13 },
btn: { paddingVertical: 16, paddingHorizontal: 32, borderRadius: 50 },
btnStart: { backgroundColor: '#22c55e' },
btnPause: { backgroundColor: '#f59e0b' },