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:
94
src/lib/shape.ts
Normal file
94
src/lib/shape.ts
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user