Add private health tracker (owner-only)
- health schema: vaccinations, body_measurements, workouts, nutrition_daily, sleep (generated hours), medications, hair_progress, appointments - migration with updated_at triggers; grants to personal_app only - /trackers/health overview with add/edit/delete, owner-gated (404 otherwise), noindex + private no-store, hidden from listings for non-owners - owner-only JSON/CSV import endpoint and scripts/health-import.ts
This commit is contained in:
122
src/routes/trackers/health/+page.server.ts
Normal file
122
src/routes/trackers/health/+page.server.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { db } from '#lib/server/db/index.js';
|
||||
import {
|
||||
healthAppointments,
|
||||
healthBodyMeasurements,
|
||||
healthHairProgress,
|
||||
healthMedications,
|
||||
healthNutritionDaily,
|
||||
healthSleep,
|
||||
healthVaccinations,
|
||||
healthWorkouts
|
||||
} from '#lib/server/db/schema/index.js';
|
||||
import { requireOwner } from '#lib/server/owner.js';
|
||||
import { cleanRow, drizzleTables, TIME_ZONE, writeRows } from '#lib/server/health-import.js';
|
||||
import { healthTables, isHealthTable } from '#lib/health/tables.js';
|
||||
import { asc, desc, eq, gte, lt } from 'drizzle-orm';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
function today(): string {
|
||||
return new Intl.DateTimeFormat('en-CA', { timeZone: TIME_ZONE }).format(new Date());
|
||||
}
|
||||
function daysAgo(day: string, n: number): string {
|
||||
const d = new Date(`${day}T12:00:00Z`);
|
||||
d.setUTCDate(d.getUTCDate() - n);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
requireOwner(event);
|
||||
const now = today();
|
||||
const [
|
||||
measurements,
|
||||
workouts,
|
||||
nutrition,
|
||||
sleep,
|
||||
medications,
|
||||
vaccinations,
|
||||
upcoming,
|
||||
past,
|
||||
hair
|
||||
] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(healthBodyMeasurements)
|
||||
.orderBy(asc(healthBodyMeasurements.date), asc(healthBodyMeasurements.createdAt)),
|
||||
db
|
||||
.select()
|
||||
.from(healthWorkouts)
|
||||
.orderBy(desc(healthWorkouts.date), asc(healthWorkouts.createdAt))
|
||||
.limit(30),
|
||||
db
|
||||
.select()
|
||||
.from(healthNutritionDaily)
|
||||
.where(gte(healthNutritionDaily.date, daysAgo(now, 6)))
|
||||
.orderBy(desc(healthNutritionDaily.date)),
|
||||
db
|
||||
.select()
|
||||
.from(healthSleep)
|
||||
.where(gte(healthSleep.date, daysAgo(now, 6)))
|
||||
.orderBy(desc(healthSleep.date)),
|
||||
db.select().from(healthMedications).orderBy(asc(healthMedications.name)),
|
||||
db
|
||||
.select()
|
||||
.from(healthVaccinations)
|
||||
.orderBy(desc(healthVaccinations.date), asc(healthVaccinations.vaccine)),
|
||||
db
|
||||
.select()
|
||||
.from(healthAppointments)
|
||||
.where(gte(healthAppointments.date, now))
|
||||
.orderBy(asc(healthAppointments.date)),
|
||||
db
|
||||
.select()
|
||||
.from(healthAppointments)
|
||||
.where(lt(healthAppointments.date, now))
|
||||
.orderBy(desc(healthAppointments.date))
|
||||
.limit(10),
|
||||
db.select().from(healthHairProgress).orderBy(desc(healthHairProgress.date)).limit(10)
|
||||
]);
|
||||
return {
|
||||
today: now,
|
||||
measurements,
|
||||
workouts,
|
||||
nutrition,
|
||||
sleep,
|
||||
medications,
|
||||
vaccinations,
|
||||
appointments: { upcoming, past },
|
||||
hair,
|
||||
seo: { title: 'Health', description: 'Private health log.', index: false }
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
save: async (event) => {
|
||||
requireOwner(event);
|
||||
const form = await event.request.formData();
|
||||
const table = form.get('table');
|
||||
if (!isHealthTable(table)) return fail(400, { message: 'Unknown table' });
|
||||
const row: Record<string, unknown> = { id: form.get('id') || undefined };
|
||||
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 });
|
||||
try {
|
||||
await writeRows(db, { [table]: [result.row] });
|
||||
} catch (e) {
|
||||
return fail(400, { message: e instanceof Error ? e.message : 'Could not save', table });
|
||||
}
|
||||
return { saved: table };
|
||||
},
|
||||
delete: async (event) => {
|
||||
requireOwner(event);
|
||||
const form = await event.request.formData();
|
||||
const table = form.get('table');
|
||||
const id = String(form.get('id') ?? '');
|
||||
if (!isHealthTable(table) || !UUID.test(id)) return fail(400, { message: 'Invalid entry' });
|
||||
const target = drizzleTables[table];
|
||||
await db.delete(target).where(eq(target.id, id));
|
||||
return { deleted: table };
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user