Skip to content

Commit 7bf530a

Browse files
committed
feat(emergency): automate SOS orchestration and location safety
Restore hold-SOS to Incident Tracker, run ICE+108 SMS and hospital Maps after activation, fix trauma navigation to nearest facility, add Call ICE FAB, reduce false distress alerts, and wire GPS into Sarthi and community briefs.
1 parent dc5e50e commit 7bf530a

51 files changed

Lines changed: 816 additions & 183 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎novadrive-mobile/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ Full matrix: [docs/DEVICE_SMOKE_MATRIX.md](docs/DEVICE_SMOKE_MATRIX.md)
146146

147147
```bash
148148
npm run typecheck # TypeScript (includes *.test.ts)
149-
npm test # 208 unit tests — lib/, voice, FSM, crash, GHP, Sarthi, Naari Shakti, brand, tokens, public branding, Phase 2–3
149+
npm test # 216 unit tests — lib/, voice, FSM, crash, GHP, Sarthi, Naari Shakti, brand, tokens, public branding, Phase 2–3
150150
npm run verify:docs # README "N unit tests" matches src/**/*.test.ts
151151
npm run verify:branding # no NovaDrive in public GitHub copy
152152
npm run test:coverage

‎novadrive-mobile/app/(tabs)/explore.tsx‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { NaariShaktiProtocolModal } from '../../src/components/naari/NaariShakti
1212
import { useApp } from '../../src/context/AppContext';
1313
import { useNaariShakti } from '../../src/context/NaariShaktiContext';
1414
import { EMERGENCY_SELECTION_PATH } from '../../src/lib/emergency/emergencyNavigation';
15-
import { openEmergencySmsIntent } from '../../src/lib/emergencySms';
1615
import { runQuickSos } from '../../src/lib/home/quickSos';
1716
import {
1817
isNaariShaktiEligible,
@@ -111,7 +110,6 @@ export default function HomeTabScreen() {
111110
);
112111
runQuickSos(() => {
113112
beginEmergencyFlow();
114-
void openEmergencySmsIntent('sos_hold');
115113
router.push(EMERGENCY_SELECTION_PATH as Href);
116114
});
117115
}}

‎novadrive-mobile/app/(tabs)/history.tsx‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { MaterialIcons } from '@expo/vector-icons';
1313
import { DashboardHeader } from '../../src/components/DashboardHeader';
1414
import { HudCard } from '../../src/components/HudCard';
1515
import { HudText } from '../../src/components/HudText';
16+
import { useHomeLocationWeather } from '../../src/hooks/useHomeLocationWeather';
17+
import { filterAlertsWithinKm } from '../../src/lib/home/localSafetyAlerts';
1618
import {
1719
OFFICIAL_REPORT_OPTIONS,
1820
SEED_ALERTS,
@@ -59,19 +61,30 @@ export default function CommunityTabScreen() {
5961
const [reportCategory, setReportCategory] = useState(0);
6062
const [reportText, setReportText] = useState('');
6163
const [submitting, setSubmitting] = useState(false);
64+
const { lat, lng, loading: locationLoading, permissionDenied, refresh: refreshLocation } =
65+
useHomeLocationWeather();
6266

6367
useFocusEffect(
6468
useCallback(() => {
69+
void refreshLocation();
6570
listCommunityFeedback(40).then(setFeedback);
66-
}, [])
71+
}, [refreshLocation])
6772
);
6873

6974
const alerts = useMemo(() => {
7075
const userAlerts = feedback
7176
.filter((f) => f.category === 'safety' || f.category === 'road')
7277
.map(feedbackToAlert);
73-
return [...userAlerts, ...SEED_ALERTS];
74-
}, [feedback]);
78+
const geoAlerts = SEED_ALERTS.filter(
79+
(a): a is CommunityAlert & { lat: number; lng: number } =>
80+
a.lat != null && a.lng != null
81+
);
82+
if (lat == null || lng == null) {
83+
return userAlerts;
84+
}
85+
const nearbySeed = filterAlertsWithinKm(geoAlerts, lat, lng, 5);
86+
return [...userAlerts, ...nearbySeed];
87+
}, [feedback, lat, lng]);
7588

7689
const submitOfficialReport = async () => {
7790
if (!reportText.trim()) {
@@ -138,6 +151,20 @@ export default function CommunityTabScreen() {
138151
</HudText>
139152
</View>
140153

154+
{locationLoading ? (
155+
<HudText variant="bodySm" style={styles.sectionHint}>
156+
Locating alerts near you…
157+
</HudText>
158+
) : permissionDenied || lat == null ? (
159+
<HudText variant="bodySm" style={styles.sectionHint}>
160+
Enable location to see corridor alerts within 5 km.
161+
</HudText>
162+
) : alerts.length === 0 ? (
163+
<HudText variant="bodySm" style={styles.sectionHint}>
164+
No alerts near you within 5 km.
165+
</HudText>
166+
) : null}
167+
141168
{alerts.map((alert) => (
142169
<View key={alert.id} style={styles.alertCard}>
143170
<View style={[styles.alertStripe, { backgroundColor: severityAccent(alert.severity) }]} />

‎novadrive-mobile/app/_layout.tsx‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export default function RootLayout() {
5858
headerTitleStyle: { fontFamily: 'HankenGrotesk_700Bold' },
5959
contentStyle: { backgroundColor: tokens.background },
6060
animation: 'slide_from_right',
61-
animationDuration: 280,
61+
animationDuration: 200,
6262
}}
6363
>
6464
<Stack.Screen name="index" options={{ headerShown: false }} />
@@ -73,14 +73,14 @@ export default function RootLayout() {
7373
<Stack.Screen name="journey/complete" options={{ headerShown: false }} />
7474
<Stack.Screen name="journey/feedback" options={{ headerShown: false }} />
7575
<Stack.Screen name="journey" options={{ headerShown: false }} />
76-
<Stack.Screen name="emergency/locate" options={{ headerShown: false }} />
77-
<Stack.Screen name="emergency/activation" options={{ headerShown: false }} />
78-
<Stack.Screen name="emergency/response" options={{ headerShown: false }} />
79-
<Stack.Screen name="emergency/selection" options={{ headerShown: false }} />
80-
<Stack.Screen name="emergency/triage" options={{ headerShown: false }} />
81-
<Stack.Screen name="emergency/route" options={{ headerShown: false }} />
82-
<Stack.Screen name="emergency/packet" options={{ headerShown: false }} />
83-
<Stack.Screen name="emergency/relay" options={{ headerShown: false }} />
76+
<Stack.Screen name="emergency/locate" options={{ headerShown: false, animation: 'fade', animationDuration: 180 }} />
77+
<Stack.Screen name="emergency/activation" options={{ headerShown: false, animation: 'fade', animationDuration: 180 }} />
78+
<Stack.Screen name="emergency/response" options={{ headerShown: false, animation: 'fade', animationDuration: 180 }} />
79+
<Stack.Screen name="emergency/selection" options={{ headerShown: false, animation: 'fade', animationDuration: 180 }} />
80+
<Stack.Screen name="emergency/triage" options={{ headerShown: false, animation: 'fade', animationDuration: 180 }} />
81+
<Stack.Screen name="emergency/route" options={{ headerShown: false, animation: 'fade', animationDuration: 180 }} />
82+
<Stack.Screen name="emergency/packet" options={{ headerShown: false, animation: 'fade', animationDuration: 180 }} />
83+
<Stack.Screen name="emergency/relay" options={{ headerShown: false, animation: 'fade', animationDuration: 180 }} />
8484
<Stack.Screen name="scan" options={{ headerShown: false }} />
8585
<Stack.Screen
8686
name="sarthi"

‎novadrive-mobile/app/emergency/activation.tsx‎

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ import {
2020
shouldShowManualContinue,
2121
} from '../../src/lib/emergency/activationUi';
2222
import {
23+
EMERGENCY_HOLD_ENTRY_PATH,
2324
EMERGENCY_RESPONSE_PATH,
2425
EMERGENCY_SELECTION_PATH,
26+
shouldRedirectActivationToSelection,
2527
} from '../../src/lib/emergency/emergencyNavigation';
28+
import { runEmergencyOrchestrator } from '../../src/lib/emergency/emergencyOrchestrator';
2629

2730
const EMERGENCY_TRIAGE_PATH = '/emergency/triage';
2831
import { tokens } from '../../src/theme/tokens';
@@ -41,20 +44,21 @@ const RING_RADIUS = (RING_SIZE - RING_STROKE) / 2;
4144
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
4245

4346
export default function EmergencyActivationScreen() {
44-
const { settings, updateSettings, session, resetEmergency } = useApp();
47+
const { settings, updateSettings, session, resetEmergency, profile, setLocation, selectFacility } =
48+
useApp();
4549
const [statusIndex, setStatusIndex] = useState(0);
4650
const [secondsLeft, setSecondsLeft] = useState(ACTIVATION_SPLASH_SECONDS);
4751
const [mode, setMode] = useState<ActivationMode>('auto');
4852
const [backendReady, setBackendReady] = useState(false);
4953
const hasAutoNavigatedRef = useRef(false);
54+
const orchestratorRanRef = useRef(false);
5055
const status = useMemo(() => ROTATING_STATUS[statusIndex % ROTATING_STATUS.length], [statusIndex]);
5156
const progress = activationCountdownProgress(secondsLeft, ACTIVATION_SPLASH_SECONDS);
5257
const showContinue = shouldShowManualContinue(mode, secondsLeft);
5358

5459
useEffect(() => {
55-
if (!session.incidentType) {
56-
router.replace('/emergency/selection' as Href);
57-
}
60+
if (!shouldRedirectActivationToSelection(session.incidentType)) return;
61+
router.replace(EMERGENCY_SELECTION_PATH as Href);
5862
}, [session.incidentType]);
5963

6064
useEffect(() => {
@@ -90,7 +94,30 @@ export default function EmergencyActivationScreen() {
9094
return () => clearInterval(statusTimer);
9195
}, [backendReady]);
9296

93-
const navigateToResponse = (selectedMode: ActivationMode) => {
97+
const runOrchestration = async () => {
98+
if (orchestratorRanRef.current || !session.incidentType) return;
99+
orchestratorRanRef.current = true;
100+
const result = await runEmergencyOrchestrator({
101+
profile,
102+
settings,
103+
incidentType: session.incidentType,
104+
smsKind: 'sos_hold',
105+
triage: session.triage ?? 'RED',
106+
});
107+
if (result.coords) {
108+
setLocation({
109+
lat: result.coords.lat,
110+
lng: result.coords.lng,
111+
capturedAt: new Date().toISOString(),
112+
});
113+
}
114+
if (result.nearestFacility) {
115+
selectFacility(result.nearestFacility);
116+
}
117+
};
118+
119+
const navigateToResponse = async (selectedMode: ActivationMode) => {
120+
await runOrchestration();
94121
const responseMode = selectedMode === 'manual' ? 'guided' : 'auto';
95122
router.replace(`${EMERGENCY_RESPONSE_PATH}?mode=${responseMode}` as Href);
96123
};
@@ -107,7 +134,7 @@ export default function EmergencyActivationScreen() {
107134
return;
108135
}
109136
hasAutoNavigatedRef.current = true;
110-
navigateToResponse(mode);
137+
void navigateToResponse(mode);
111138
}, [secondsLeft, mode, backendReady]);
112139

113140
const cancelFlow = () => {
@@ -122,7 +149,7 @@ export default function EmergencyActivationScreen() {
122149
<MaterialIcons name="arrow-back" size={24} color={tokens.onPrimary} />
123150
</Pressable>
124151
<HudText variant="headlineMd" style={styles.headerTitle}>
125-
TRAUMA RESPONSE
152+
ACTIVATING SOS
126153
</HudText>
127154
<Pressable onPress={() => router.replace(EMERGENCY_SELECTION_PATH as Href)}>
128155
<MaterialIcons name="emergency-share" size={22} color={tokens.onPrimary} />
@@ -212,7 +239,7 @@ export default function EmergencyActivationScreen() {
212239
router.push(EMERGENCY_TRIAGE_PATH as Href);
213240
return;
214241
}
215-
navigateToResponse(mode);
242+
void navigateToResponse(mode);
216243
}}
217244
>
218245
<HudText variant="bodyMd" style={styles.continueText}>

0 commit comments

Comments
 (0)