Forty-three PRs into kickstand-web this week. Most of it falls into three buckets: safety, ride data, and the long tail of "make the app stop being embarrassing on Android and the web."
Safety
The big one is the SOS pipeline. #172 shipped manual SOS and an emergency-contacts table (owner-only RLS, degrades gracefully if the table's missing). #179 put Twilio behind it: when a rider opts into Settings → Safety → "Auto-send SOS texts," tapping SOS fires the message server-side with their live location. If Twilio fails for any reason, it falls back to the device-native Messages share path. The fallback was the part I cared about — a safety feature that depends on a third-party API being up is not a safety feature.
#181 is the design spec for automatic crash detection (Elite/Founder tier, accelerometer impact + GPS sudden-stop with a countdown into the same SOS pipeline). I wrote the reliability envelope honestly: best-effort, screen-on most reliable, not a 911 substitute. Five implementation units, pure detector unit-tested in isolation. No code yet — just the spec, approved, ready to build.
#182 was a hair-pulling crash-simulation bug. The ?simcrash=1 dev effect depended on a crash object that got recreated every render, and the recorder re-renders ~4×/s, so the cleanup tore down the 3s timeout before it could fire. Held onCrashSuspected in a ref. Dev-only, but the same instability pattern is the kind of thing that'll bite real detection logic later.
Ride telemetry
#175 and #176 are the data layer for the Ride Report. The recorder was already measuring speed, signed lean angle, and lateral g every tick — and throwing it all away, keeping only aggregates and a bare [lng,lat] path. Now it persists the full per-sample TelemetrySample stream ({t, lng, lat, speed, lean, g, alt}) so the Ride Report can replay a ride from real data instead of synthesized fakes. buildRideReport() in packages/shared turns the samples into per-metric series, segment splits, a lean-balance number, and the "Ride DNA" score. Mobile and web renderers stay thin and identical.
AI where it earns its keep
#144 moved photo moderation from a scheduled GitHub cron (heavily throttled — photos sat pending for hours) to moderate-on-upload. Each photo is Claude-vision-reviewed the moment it posts; the cron sweep stays as a backstop. This is the pattern I keep landing on for AI in production: synchronous on the hot path where latency matters to the user, batch as backstop. Not "AI-powered moderation" as a feature. Just the right substrate for the job.
#166 and #178 kept the 2x/week blog engine alive. All 50 state spotlights are published, so the cron picker was returning empty and the schedule had stalled. Added two more grounded blog types — iconic-road deep-dives and monthly rally roundups — both feeding the same drafts-only, web_search-grounded contract. No hallucinated POIs; everything anchors on an atlas slug that exists.
The rest
Twenty-something PRs of UI work I won't enumerate: a shared on-route overlay so Live Nav, live ride, and Track stop drifting apart (#167, #168); turn-by-turn directions (#165); HEIC image-transform fixes for every web <img> rendering iPhone uploads (#157–#159); virtualizing Discover and Community with FlatList (#163); ephemeral 24h stories with report-reasons + an admin review queue (#146, #147).
And #183 — the kind of bug that costs you a day. EAS standalone builds embed JS at build time, so EXPO_PUBLIC_* vars need to live in EAS env, not just the gitignored local .env. Without them, lib/supabase.ts threw on launch and the app closed instantly. Dev client worked fine because Metro injects at runtime. Classic "works in dev" failure mode.
Heavy week. Crash detection ships next.