Expo app: native front end for health.rehbock.xyz

Login (HTTP Basic, token in SecureStore) + Today, Blood, Sleep, Train,
Body, Mind, DNA screens ported from the PWA at /app/. All data is
fetched live from health.rehbock.xyz; nothing is stored on device
beyond the auth token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 16:38:52 -07:00
parent 36bd13ff59
commit 2a5c956c32
38 changed files with 2060 additions and 1180 deletions

94
src/lib/shape.ts Normal file
View File

@@ -0,0 +1,94 @@
import type {
BiomarkersResponse, MarkerGroup, Night, SleepApiResponse, SleepCycleExport,
Week, Workout,
} from './types';
export function shapeSleep(api: SleepApiResponse, sc: SleepCycleExport): Night[] {
const byDate = new Map<string, Night>();
(sc.sessions || []).forEach((r) => {
const o: Record<string, any> = Object.fromEntries(sc.fields.map((f, i) => [f, r[i]]));
const date = String(o.end || '').slice(0, 10);
if (!date) return;
byDate.set(date, {
date,
total: o.inbed_s || o.asleep_s,
asleep: o.asleep_s,
deep: o.deep_s,
rem: o.rem_s,
light: o.light_s || (o.deep_s || o.rem_s ? 0 : o.asleep_s),
awake: o.awake_s,
quality: o.quality,
hrv: null,
hr: null,
src: 'Sleep Cycle',
});
});
(api.sessions || []).forEach((s) => {
const date = s.ended_at.slice(0, 10);
byDate.set(date, {
date,
total: s.total_seconds,
asleep: s.sleep_seconds,
deep: s.deep_seconds,
rem: s.rem_seconds,
light: s.light_seconds,
awake: s.awake_seconds,
quality: null,
hrv: s.avg_hrv,
hr: s.avg_hr,
src: 'Eight Sleep',
});
});
return [...byDate.values()].sort((a, b) => (a.date < b.date ? -1 : 1));
}
export function workoutVolume(w: Workout): { vol: number; sets: number } {
let vol = 0;
let sets = 0;
(w.exercises || []).forEach((ex) =>
(ex.sets || []).forEach((s) => {
sets++;
if (s.weight_kg && s.reps) vol += s.weight_kg * s.reps;
}),
);
return { vol, sets };
}
export function weekStart(d: string): string {
const dt = new Date(d);
const day = (dt.getDay() + 6) % 7;
dt.setDate(dt.getDate() - day);
return dt.toISOString().slice(0, 10);
}
export function shapeWeeks(workouts: Workout[], n: number): Week[] {
const map = new Map<string, Week>();
workouts.forEach((w) => {
const k = weekStart(w.start_time);
if (!map.has(k)) map.set(k, { start: k, vol: 0, count: 0, sets: 0 });
const e = map.get(k)!;
const { vol, sets } = workoutVolume(w);
e.vol += vol;
e.count++;
e.sets += sets;
});
return [...map.values()].sort((a, b) => (a.start < b.start ? -1 : 1)).slice(-n);
}
export function shapeBiomarkers(bm: BiomarkersResponse): MarkerGroup[] {
const groups = new Map<string, typeof bm.biomarkers>();
bm.biomarkers.forEach((b) => {
if (!groups.has(b.name)) groups.set(b.name, []);
groups.get(b.name)!.push(b);
});
const out: MarkerGroup[] = [];
groups.forEach((hist, name) => {
hist.sort((a, b) => (a.collected < b.collected ? -1 : 1));
const latest = hist[hist.length - 1];
const series = hist
.filter((h) => h.unit === latest.unit && h.value_num != null)
.map((h) => ({ d: h.collected, v: h.value_num! }));
out.push({ name, latest, series, panel: latest.panel });
});
return out.sort((a, b) => a.name.localeCompare(b.name));
}