health: drop hair_progress; owner = Stan's account email only
All checks were successful
Deploy production / deploy (push) Successful in 2m22s
All checks were successful
Deploy production / deploy (push) Successful in 2m22s
Regenerated the (not yet deployed) health migration without hair_progress.
This commit is contained in:
@@ -28,17 +28,6 @@ CREATE TABLE "health"."body_measurements" (
|
|||||||
CONSTRAINT "body_measurements_date_check" CHECK ("date" >= DATE '1900-01-01')
|
CONSTRAINT "body_measurements_date_check" CHECK ("date" >= DATE '1900-01-01')
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE "health"."hair_progress" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
"date" date NOT NULL,
|
|
||||||
"photo_ref" text,
|
|
||||||
"notes" text,
|
|
||||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
||||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "hair_progress_photo_ref_check" CHECK ("photo_ref" IS NULL OR ("photo_ref" !~* '^[a-z][a-z0-9+.-]*://' AND "photo_ref" !~ '^//')),
|
|
||||||
CONSTRAINT "hair_progress_date_check" CHECK ("date" >= DATE '1900-01-01')
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "health"."medications" (
|
CREATE TABLE "health"."medications" (
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
"name" text NOT NULL,
|
"name" text NOT NULL,
|
||||||
@@ -122,19 +111,18 @@ CREATE TABLE "health"."workouts" (
|
|||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE INDEX "appointments_date_idx" ON "health"."appointments" ("date");--> statement-breakpoint
|
CREATE INDEX "appointments_date_idx" ON "health"."appointments" ("date");--> statement-breakpoint
|
||||||
CREATE INDEX "body_measurements_date_idx" ON "health"."body_measurements" ("date");--> statement-breakpoint
|
CREATE INDEX "body_measurements_date_idx" ON "health"."body_measurements" ("date");--> statement-breakpoint
|
||||||
CREATE INDEX "hair_progress_date_idx" ON "health"."hair_progress" ("date");--> statement-breakpoint
|
|
||||||
CREATE INDEX "medications_start_date_idx" ON "health"."medications" ("start_date");--> statement-breakpoint
|
CREATE INDEX "medications_start_date_idx" ON "health"."medications" ("start_date");--> statement-breakpoint
|
||||||
CREATE INDEX "medications_stop_date_idx" ON "health"."medications" ("stop_date");--> statement-breakpoint
|
CREATE INDEX "medications_stop_date_idx" ON "health"."medications" ("stop_date");--> statement-breakpoint
|
||||||
CREATE INDEX "vaccinations_date_idx" ON "health"."vaccinations" ("date");--> statement-breakpoint
|
CREATE INDEX "vaccinations_date_idx" ON "health"."vaccinations" ("date");--> statement-breakpoint
|
||||||
CREATE INDEX "workouts_date_idx" ON "health"."workouts" ("date");--> statement-breakpoint
|
CREATE INDEX "workouts_date_idx" ON "health"."workouts" ("date");--> statement-breakpoint
|
||||||
CREATE INDEX "workouts_session_idx" ON "health"."workouts" ("session_id");--> statement-breakpoint
|
CREATE INDEX "workouts_session_idx" ON "health"."workouts" ("session_id");
|
||||||
-- Hand-written below this line (not generated by drizzle-kit).
|
-- Hand-written below this line (not generated by drizzle-kit).
|
||||||
-- updated_at triggers, same as every other core table (07_triggers_meta_grants.sql).
|
-- updated_at triggers, same as every other core table (07_triggers_meta_grants.sql).
|
||||||
DO $$
|
DO $$
|
||||||
DECLARE t text;
|
DECLARE t text;
|
||||||
BEGIN
|
BEGIN
|
||||||
IF to_regprocedure('meta.touch_updated_at()') IS NOT NULL THEN
|
IF to_regprocedure('meta.touch_updated_at()') IS NOT NULL THEN
|
||||||
FOREACH t IN ARRAY ARRAY['vaccinations','body_measurements','workouts','nutrition_daily','sleep','medications','hair_progress','appointments'] LOOP
|
FOREACH t IN ARRAY ARRAY['vaccinations','body_measurements','workouts','nutrition_daily','sleep','medications','appointments'] LOOP
|
||||||
EXECUTE format('CREATE TRIGGER touch_updated_at BEFORE UPDATE ON health.%I FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at()', t);
|
EXECUTE format('CREATE TRIGGER touch_updated_at BEFORE UPDATE ON health.%I FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at()', t);
|
||||||
END LOOP;
|
END LOOP;
|
||||||
END IF;
|
END IF;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "8",
|
"version": "8",
|
||||||
"dialect": "postgres",
|
"dialect": "postgres",
|
||||||
"id": "a9c8c2d8-9454-4842-bd66-3830867023ed",
|
"id": "b07ff36e-fc9d-4dd4-a267-448a6e807617",
|
||||||
"prevIds": [
|
"prevIds": [
|
||||||
"f166e058-a62b-4994-b4ba-12ee871a8bb0"
|
"f166e058-a62b-4994-b4ba-12ee871a8bb0"
|
||||||
],
|
],
|
||||||
@@ -423,12 +423,6 @@
|
|||||||
"entityType": "tables",
|
"entityType": "tables",
|
||||||
"schema": "health"
|
"schema": "health"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"isRlsEnabled": false,
|
|
||||||
"name": "hair_progress",
|
|
||||||
"entityType": "tables",
|
|
||||||
"schema": "health"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"isRlsEnabled": false,
|
"isRlsEnabled": false,
|
||||||
"name": "medications",
|
"name": "medications",
|
||||||
@@ -7463,84 +7457,6 @@
|
|||||||
"schema": "health",
|
"schema": "health",
|
||||||
"table": "body_measurements"
|
"table": "body_measurements"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "uuid",
|
|
||||||
"typeSchema": null,
|
|
||||||
"notNull": true,
|
|
||||||
"dimensions": 0,
|
|
||||||
"default": "gen_random_uuid()",
|
|
||||||
"generated": null,
|
|
||||||
"identity": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"schema": "health",
|
|
||||||
"table": "hair_progress"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "date",
|
|
||||||
"typeSchema": null,
|
|
||||||
"notNull": true,
|
|
||||||
"dimensions": 0,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"identity": null,
|
|
||||||
"name": "date",
|
|
||||||
"entityType": "columns",
|
|
||||||
"schema": "health",
|
|
||||||
"table": "hair_progress"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"typeSchema": null,
|
|
||||||
"notNull": false,
|
|
||||||
"dimensions": 0,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"identity": null,
|
|
||||||
"name": "photo_ref",
|
|
||||||
"entityType": "columns",
|
|
||||||
"schema": "health",
|
|
||||||
"table": "hair_progress"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"typeSchema": null,
|
|
||||||
"notNull": false,
|
|
||||||
"dimensions": 0,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"identity": null,
|
|
||||||
"name": "notes",
|
|
||||||
"entityType": "columns",
|
|
||||||
"schema": "health",
|
|
||||||
"table": "hair_progress"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "timestamp with time zone",
|
|
||||||
"typeSchema": null,
|
|
||||||
"notNull": true,
|
|
||||||
"dimensions": 0,
|
|
||||||
"default": "now()",
|
|
||||||
"generated": null,
|
|
||||||
"identity": null,
|
|
||||||
"name": "created_at",
|
|
||||||
"entityType": "columns",
|
|
||||||
"schema": "health",
|
|
||||||
"table": "hair_progress"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "timestamp with time zone",
|
|
||||||
"typeSchema": null,
|
|
||||||
"notNull": true,
|
|
||||||
"dimensions": 0,
|
|
||||||
"default": "now()",
|
|
||||||
"generated": null,
|
|
||||||
"identity": null,
|
|
||||||
"name": "updated_at",
|
|
||||||
"entityType": "columns",
|
|
||||||
"schema": "health",
|
|
||||||
"table": "hair_progress"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "uuid",
|
"type": "uuid",
|
||||||
"typeSchema": null,
|
"typeSchema": null,
|
||||||
@@ -9114,27 +9030,6 @@
|
|||||||
"schema": "health",
|
"schema": "health",
|
||||||
"table": "body_measurements"
|
"table": "body_measurements"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"nameExplicit": true,
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "date",
|
|
||||||
"isExpression": false,
|
|
||||||
"asc": true,
|
|
||||||
"nullsFirst": false,
|
|
||||||
"opclass": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"with": "",
|
|
||||||
"method": "btree",
|
|
||||||
"concurrently": false,
|
|
||||||
"name": "hair_progress_date_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"schema": "health",
|
|
||||||
"table": "hair_progress"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"nameExplicit": true,
|
"nameExplicit": true,
|
||||||
"columns": [
|
"columns": [
|
||||||
@@ -10888,16 +10783,6 @@
|
|||||||
"table": "body_measurements",
|
"table": "body_measurements",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "hair_progress_pkey",
|
|
||||||
"schema": "health",
|
|
||||||
"table": "hair_progress",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": [
|
||||||
"id"
|
"id"
|
||||||
@@ -11560,20 +11445,6 @@
|
|||||||
"schema": "health",
|
"schema": "health",
|
||||||
"table": "body_measurements"
|
"table": "body_measurements"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"value": "\"photo_ref\" IS NULL OR (\"photo_ref\" !~* '^[a-z][a-z0-9+.-]*://' AND \"photo_ref\" !~ '^//')",
|
|
||||||
"name": "hair_progress_photo_ref_check",
|
|
||||||
"entityType": "checks",
|
|
||||||
"schema": "health",
|
|
||||||
"table": "hair_progress"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "\"date\" >= DATE '1900-01-01'",
|
|
||||||
"name": "hair_progress_date_check",
|
|
||||||
"entityType": "checks",
|
|
||||||
"schema": "health",
|
|
||||||
"table": "hair_progress"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"value": "length(btrim(\"name\")) > 0",
|
"value": "length(btrim(\"name\")) > 0",
|
||||||
"name": "medications_name_check",
|
"name": "medications_name_check",
|
||||||
@@ -111,20 +111,7 @@ export const healthTables = {
|
|||||||
notes
|
notes
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
hair_progress: {
|
|
||||||
label: 'Hair progress',
|
|
||||||
fields: [
|
|
||||||
date,
|
|
||||||
{
|
|
||||||
key: 'photo_ref',
|
|
||||||
label: 'Photo key (private storage, not a URL)',
|
|
||||||
type: 'text',
|
|
||||||
maxLength: 500,
|
|
||||||
placeholder: 'health/hair/2026-10-24-top.jpg'
|
|
||||||
},
|
|
||||||
notes
|
|
||||||
]
|
|
||||||
},
|
|
||||||
appointments: {
|
appointments: {
|
||||||
label: 'Appointments & conditions',
|
label: 'Appointments & conditions',
|
||||||
fields: [
|
fields: [
|
||||||
|
|||||||
@@ -158,26 +158,6 @@ export const healthMedications = healthSchema.table(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const healthHairProgress = healthSchema.table(
|
|
||||||
'hair_progress',
|
|
||||||
{
|
|
||||||
id: pk(),
|
|
||||||
date: day().notNull(),
|
|
||||||
// Key/path into private storage (never a public URL), e.g. "health/hair/2026-10-24-top.jpg".
|
|
||||||
photoRef: text('photo_ref'),
|
|
||||||
notes: text('notes'),
|
|
||||||
...timestamps
|
|
||||||
},
|
|
||||||
(t) => [
|
|
||||||
index('hair_progress_date_idx').on(t.date),
|
|
||||||
check(
|
|
||||||
'hair_progress_photo_ref_check',
|
|
||||||
sql`${t.photoRef} IS NULL OR (${t.photoRef} !~* '^[a-z][a-z0-9+.-]*://' AND ${t.photoRef} !~ '^//')`
|
|
||||||
),
|
|
||||||
check('hair_progress_date_check', sane('date'))
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
export const healthAppointments = healthSchema.table(
|
export const healthAppointments = healthSchema.table(
|
||||||
'appointments',
|
'appointments',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ describe('health import', () => {
|
|||||||
expect(cleanRow('workouts', { date: '2026-01-02', exercise: 'squat', bogus: 1 }).ok).toBe(
|
expect(cleanRow('workouts', { date: '2026-01-02', exercise: 'squat', bogus: 1 }).ok).toBe(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
expect(cleanRow('hair_progress', { date: '2026-01-02', photo_ref: 'https://x/y.jpg' }).ok).toBe(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
expect(
|
expect(
|
||||||
cleanRow('vaccinations', { vaccine: 'X', date: '2026-01-02', country: 'nl' })
|
cleanRow('vaccinations', { vaccine: 'X', date: '2026-01-02', country: 'nl' })
|
||||||
).toMatchObject({ ok: true, row: { values: { country: 'NL' } } });
|
).toMatchObject({ ok: true, row: { values: { country: 'NL' } } });
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type { PgAsyncDatabase } from 'drizzle-orm/pg-core';
|
|||||||
import {
|
import {
|
||||||
healthAppointments,
|
healthAppointments,
|
||||||
healthBodyMeasurements,
|
healthBodyMeasurements,
|
||||||
healthHairProgress,
|
|
||||||
healthMedications,
|
healthMedications,
|
||||||
healthNutritionDaily,
|
healthNutritionDaily,
|
||||||
healthSleep,
|
healthSleep,
|
||||||
@@ -22,7 +21,6 @@ export const drizzleTables = {
|
|||||||
nutrition_daily: healthNutritionDaily,
|
nutrition_daily: healthNutritionDaily,
|
||||||
sleep: healthSleep,
|
sleep: healthSleep,
|
||||||
medications: healthMedications,
|
medications: healthMedications,
|
||||||
hair_progress: healthHairProgress,
|
|
||||||
appointments: healthAppointments
|
appointments: healthAppointments
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -184,12 +182,6 @@ export function cleanRow(table: HealthTable, input: Row): RowResult {
|
|||||||
return { ok: false, error: 'stop_date is before start_date' };
|
return { ok: false, error: 'stop_date is before start_date' };
|
||||||
}
|
}
|
||||||
if (table === 'appointments') v.type ??= 'appointment';
|
if (table === 'appointments') v.type ??= 'appointment';
|
||||||
if (
|
|
||||||
table === 'hair_progress' &&
|
|
||||||
typeof v.photo_ref === 'string' &&
|
|
||||||
/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(v.photo_ref)
|
|
||||||
)
|
|
||||||
return { ok: false, error: 'photo_ref must be a private storage key/path, not a URL' };
|
|
||||||
if (table === 'sleep') {
|
if (table === 'sleep') {
|
||||||
const day = String(v.date);
|
const day = String(v.date);
|
||||||
const wakeRaw = String(input.wake_time).trim();
|
const wakeRaw = String(input.wake_time).trim();
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ const event = (user: Partial<NonNullable<App.Locals['user']>> | null) =>
|
|||||||
({ locals: { user, session: null } }) as unknown as { locals: App.Locals };
|
({ locals: { user, session: null } }) as unknown as { locals: App.Locals };
|
||||||
|
|
||||||
describe('owner gate', () => {
|
describe('owner gate', () => {
|
||||||
it('needs admin role and the owner email', () => {
|
it('is only the owner account', () => {
|
||||||
expect(isOwner(event(null))).toBe(false);
|
expect(isOwner(event(null))).toBe(false);
|
||||||
expect(isOwner(event({ role: 'admin', email: 'someone@example.com' }))).toBe(false);
|
expect(isOwner(event({ role: 'admin', email: 'someone@example.com' }))).toBe(false);
|
||||||
expect(isOwner(event({ role: 'user', email: 'stan@stanrunge.dev' }))).toBe(false);
|
expect(isOwner(event({ role: 'user', email: 'Stan@StanRunge.dev' }))).toBe(true);
|
||||||
expect(isOwner(event({ role: 'admin', email: 'Stan@StanRunge.dev' }))).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('matches owner-only paths', () => {
|
it('matches owner-only paths', () => {
|
||||||
expect(isOwnerOnlyPath('/trackers/health')).toBe(true);
|
expect(isOwnerOnlyPath('/trackers/health')).toBe(true);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
// The site has one owner (Stan). Owner-only data, such as the health tracker, needs both
|
// The site has exactly one owner: Stan's account (prod auth.user id tfn6OZDOpiQvMARIH65HQ2XJuaVvmGEe).
|
||||||
// the admin role and the owner's account email, so granting someone else `admin` (e.g. to
|
// Owner-only data, such as the health tracker, is gated on that account's email, not on
|
||||||
// moderate Animaldex) never exposes it. Override with OWNER_EMAIL if the address changes.
|
// a role, so other users (now or later, e.g. on a public tracking project) never see it.
|
||||||
const OWNER_EMAIL = (process.env.OWNER_EMAIL || 'stan@stanrunge.dev').toLowerCase();
|
// Override with OWNER_EMAIL if the address ever changes.
|
||||||
|
const OWNER_EMAIL = (process.env.OWNER_EMAIL || 'stan@stanrunge.dev').trim().toLowerCase();
|
||||||
|
|
||||||
type OwnerEvent = { locals: App.Locals };
|
type OwnerEvent = { locals: App.Locals };
|
||||||
|
|
||||||
export function isOwner(event: OwnerEvent): boolean {
|
export function isOwner(event: OwnerEvent): boolean {
|
||||||
const user = event.locals.user;
|
return event.locals.user?.email?.trim().toLowerCase() === OWNER_EMAIL;
|
||||||
return !!user && user.role === 'admin' && user.email?.toLowerCase() === OWNER_EMAIL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Owner-only routes answer 404 to everyone else, so they don't reveal that they exist. */
|
/** Owner-only routes answer 404 to everyone else, so they don't reveal that they exist. */
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { db } from '#lib/server/db/index.js';
|
|||||||
import {
|
import {
|
||||||
healthAppointments,
|
healthAppointments,
|
||||||
healthBodyMeasurements,
|
healthBodyMeasurements,
|
||||||
healthHairProgress,
|
|
||||||
healthMedications,
|
healthMedications,
|
||||||
healthNutritionDaily,
|
healthNutritionDaily,
|
||||||
healthSleep,
|
healthSleep,
|
||||||
@@ -30,54 +29,44 @@ function daysAgo(day: string, n: number): string {
|
|||||||
export const load: PageServerLoad = async (event) => {
|
export const load: PageServerLoad = async (event) => {
|
||||||
requireOwner(event);
|
requireOwner(event);
|
||||||
const now = today();
|
const now = today();
|
||||||
const [
|
const [measurements, workouts, nutrition, sleep, medications, vaccinations, upcoming, past] =
|
||||||
measurements,
|
await Promise.all([
|
||||||
workouts,
|
db
|
||||||
nutrition,
|
.select()
|
||||||
sleep,
|
.from(healthBodyMeasurements)
|
||||||
medications,
|
.orderBy(asc(healthBodyMeasurements.date), asc(healthBodyMeasurements.createdAt)),
|
||||||
vaccinations,
|
db
|
||||||
upcoming,
|
.select()
|
||||||
past,
|
.from(healthWorkouts)
|
||||||
hair
|
.orderBy(desc(healthWorkouts.date), asc(healthWorkouts.createdAt))
|
||||||
] = await Promise.all([
|
.limit(30),
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(healthBodyMeasurements)
|
.from(healthNutritionDaily)
|
||||||
.orderBy(asc(healthBodyMeasurements.date), asc(healthBodyMeasurements.createdAt)),
|
.where(gte(healthNutritionDaily.date, daysAgo(now, 6)))
|
||||||
db
|
.orderBy(desc(healthNutritionDaily.date)),
|
||||||
.select()
|
db
|
||||||
.from(healthWorkouts)
|
.select()
|
||||||
.orderBy(desc(healthWorkouts.date), asc(healthWorkouts.createdAt))
|
.from(healthSleep)
|
||||||
.limit(30),
|
.where(gte(healthSleep.date, daysAgo(now, 6)))
|
||||||
db
|
.orderBy(desc(healthSleep.date)),
|
||||||
.select()
|
db.select().from(healthMedications).orderBy(asc(healthMedications.name)),
|
||||||
.from(healthNutritionDaily)
|
db
|
||||||
.where(gte(healthNutritionDaily.date, daysAgo(now, 6)))
|
.select()
|
||||||
.orderBy(desc(healthNutritionDaily.date)),
|
.from(healthVaccinations)
|
||||||
db
|
.orderBy(desc(healthVaccinations.date), asc(healthVaccinations.vaccine)),
|
||||||
.select()
|
db
|
||||||
.from(healthSleep)
|
.select()
|
||||||
.where(gte(healthSleep.date, daysAgo(now, 6)))
|
.from(healthAppointments)
|
||||||
.orderBy(desc(healthSleep.date)),
|
.where(gte(healthAppointments.date, now))
|
||||||
db.select().from(healthMedications).orderBy(asc(healthMedications.name)),
|
.orderBy(asc(healthAppointments.date)),
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(healthVaccinations)
|
.from(healthAppointments)
|
||||||
.orderBy(desc(healthVaccinations.date), asc(healthVaccinations.vaccine)),
|
.where(lt(healthAppointments.date, now))
|
||||||
db
|
.orderBy(desc(healthAppointments.date))
|
||||||
.select()
|
.limit(10)
|
||||||
.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 {
|
return {
|
||||||
today: now,
|
today: now,
|
||||||
measurements,
|
measurements,
|
||||||
@@ -87,7 +76,6 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
medications,
|
medications,
|
||||||
vaccinations,
|
vaccinations,
|
||||||
appointments: { upcoming, past },
|
appointments: { upcoming, past },
|
||||||
hair,
|
|
||||||
seo: { title: 'Health', description: 'Private health log.', index: false }
|
seo: { title: 'Health', description: 'Private health log.', index: false }
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -340,26 +340,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
{@render sectionHeader('Hair progress', 'hair_progress')}
|
|
||||||
{#if data.hair.length}
|
|
||||||
<ul class="divide-y divide-[var(--site-border)] text-sm">
|
|
||||||
{#each data.hair as h (h.id)}
|
|
||||||
<li class="flex items-center justify-between gap-2 py-1.5">
|
|
||||||
<span
|
|
||||||
>{dayLabel(h.date)}{h.photoRef ? ` · ${h.photoRef}` : ''}{h.notes
|
|
||||||
? ` · ${h.notes}`
|
|
||||||
: ''}</span
|
|
||||||
>
|
|
||||||
{@render rowActions('hair_progress', h)}
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{:else}
|
|
||||||
{@render empty('No hair check-ins yet.')}
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel lg:col-span-3">
|
<section class="panel lg:col-span-3">
|
||||||
{@render sectionHeader('Vaccinations', 'vaccinations')}
|
{@render sectionHeader('Vaccinations', 'vaccinations')}
|
||||||
{#if data.vaccinations.length}
|
{#if data.vaccinations.length}
|
||||||
|
|||||||
Reference in New Issue
Block a user