health: smart-scale columns, deduped body measurement imports, quick-add form
All checks were successful
Deploy production / deploy (push) Successful in 2m32s

This commit is contained in:
2026-10-24 14:15:59 +00:00
parent b51fef14fd
commit 07d2c70973
9 changed files with 17305 additions and 14 deletions

View File

@@ -90,12 +90,14 @@ export const actions: Actions = {
for (const field of healthTables[table].fields) row[field.key] = form.get(field.key) ?? '';
const result = cleanRow(table, row);
if (!result.ok) return fail(400, { message: result.error, table });
let unchanged: boolean;
try {
await writeRows(db, { [table]: [result.row] });
const counts = await writeRows(db, { [table]: [result.row] });
unchanged = counts[table]?.unchanged === 1;
} catch (e) {
return fail(400, { message: e instanceof Error ? e.message : 'Could not save', table });
}
return { saved: table };
return { saved: table, unchanged };
},
delete: async (event) => {
requireOwner(event);

View File

@@ -4,6 +4,7 @@
import {
healthTables,
healthTableNames,
scaleFields,
type Field,
type HealthTable
} from '#lib/health/tables.js';
@@ -96,6 +97,38 @@
dialogOpen = true;
}
// Quick-add for a scale reading: same ?/save action and dedupe as imports.
const nowLocal = () => localTime.format(new Date()).replace(' ', 'T');
let quickNow = $state(nowLocal());
let quickMessage = $state<{ text: string; error: boolean } | null>(null);
const quickSubmit: SubmitFunction =
() =>
async ({ result, update }) => {
if (result.type === 'success') {
quickNow = nowLocal();
await update();
quickMessage = {
text: result.data?.unchanged ? 'Already logged, nothing changed.' : 'Saved.',
error: false
};
} else {
await update({ reset: false });
quickMessage = {
text: String((result.type === 'failure' && result.data?.message) || 'Could not save'),
error: true
};
}
};
const scaleSummary = (m: (typeof data.measurements)[number]) =>
[
m.muscleMassKg !== null ? `muscle ${num(m.muscleMassKg, 2)} kg` : '',
m.visceralFat !== null ? `visceral ${num(m.visceralFat)}` : '',
m.bodyWaterPct !== null ? `water ${num(m.bodyWaterPct)}%` : '',
m.bmi !== null ? `BMI ${num(m.bmi)}` : ''
]
.filter(Boolean)
.join(' · ');
const submit: SubmitFunction =
() =>
async ({ result, update }) => {
@@ -156,6 +189,80 @@
<div class="grid gap-4 lg:grid-cols-3">
<section class="panel lg:col-span-2">
{@render sectionHeader('Body', 'body_measurements')}
<form method="POST" action="?/save" use:enhance={quickSubmit} class="quick mb-4 grid gap-2">
<input type="hidden" name="table" value="body_measurements" />
<div class="flex flex-wrap items-end gap-2">
<label class="grid gap-1 text-xs text-[var(--site-muted)]">
When
<Input
type="datetime-local"
name="measured_at"
value={quickNow}
required
class="w-52"
/>
</label>
<label class="grid gap-1 text-xs text-[var(--site-muted)]">
Weight (kg)
<Input
type="number"
name="weight_kg"
min={20}
max={400}
step={0.01}
inputmode="decimal"
class="w-24"
/>
</label>
<label class="grid gap-1 text-xs text-[var(--site-muted)]">
Body fat (%)
<Input
type="number"
name="body_fat_pct"
min={1}
max={75}
step={0.1}
inputmode="decimal"
class="w-24"
/>
</label>
<label class="grid gap-1 text-xs text-[var(--site-muted)]">
Source
<Input name="source" value="Renpho" maxlength={100} class="w-28" />
</label>
<Button type="submit">Add</Button>
</div>
<details class="text-sm">
<summary class="cursor-pointer text-[var(--site-muted)]">More readings & notes</summary>
<div class="mt-2 grid grid-cols-2 gap-2 sm:grid-cols-4">
{#each scaleFields as field (field.key)}
<label class="grid gap-1 text-xs text-[var(--site-muted)]">
{field.label}
<Input
type="number"
name={field.key}
min={field.min}
max={field.max}
step={field.type === 'integer' ? 1 : field.step}
inputmode="decimal"
/>
</label>
{/each}
</div>
<label class="mt-2 grid gap-1 text-xs text-[var(--site-muted)]">
Notes
<Textarea name="notes" rows={2} maxlength={4000} />
</label>
</details>
{#if quickMessage}
<p
class={['text-sm', quickMessage.error ? 'text-red-500' : 'text-[var(--site-muted)]']}
role={quickMessage.error ? 'alert' : 'status'}
>
{quickMessage.text}
</p>
{/if}
</form>
{#if latest}
<div class="mb-3 flex flex-wrap gap-6">
<p>
@@ -169,6 +276,9 @@
{#if latest.bodyFatPct !== null}<p>
<span class="stat">{num(latest.bodyFatPct)}</span>% fat
</p>{/if}
{#if latest.muscleMassKg !== null}<p>
<span class="stat">{num(latest.muscleMassKg, 1)}</span> kg muscle
</p>{/if}
</div>
{#if data.measurements.length > 1}
<BodyChart points={data.measurements} />
@@ -186,6 +296,8 @@
>{dayLabel(m.date)} · {num(m.weightKg, 2)} kg · {num(m.waistCm)} cm{m.bodyFatPct !==
null
? ` · ${num(m.bodyFatPct)}%`
: ''}{scaleSummary(m) ? ` · ${scaleSummary(m)}` : ''}{m.source
? ` · ${m.source}`
: ''}{m.notes ? ` · ${m.notes}` : ''}</span
>
{@render rowActions('body_measurements', m)}