mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
* Revert "revert: roll source tree back to v1.4.0 to restore production"
This reverts commit e444a90b10.
* fix(v1.4.2): summaries.WEIGHT + medications array guards + healthcheck IPv4
Three production bugs surfaced after the v1.4.0 deploy stabilized:
1. `TypeError: undefined is not an object (evaluating 'E?.summaries.WEIGHT')`
on /dashboard and /insights for users without weight readings —
the optional chain stopped at `data` when it needed to also cover
`data.summaries`. Fixed: `data?.summaries?.WEIGHT` (one level deeper).
2. `TypeError: y?.filter is not a function` on /medications — the
medications query value was non-array (likely a stale
service-worker response from an older API shape). Replaced with
`Array.isArray(medications) ? medications : []` so the page is
immune to any future shape drift.
3. Healthcheck-induced 503 outage (already in 1.4.1 — included here
so 1.4.2 is the single coherent release). The docker-compose
healthcheck used `localhost` which busybox-wget resolves to IPv6
::1; Next.js standalone listens on IPv4 0.0.0.0 only. Pinned
to 127.0.0.1.
This release also includes everything from v1.4.1 (which never
published a clean GHCR tag): the per-section admin extraction, the
testcontainers integration suite, the Playwright + axe-core E2E
foundation, the moodLog SSRF fix, the central error-redaction in
WideEventBuilder + Glitchtip, the medical-citation drift fixes
(ESH 2023, Saint-Maurice attribution, steps source), and the docs
sync.
Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
import { z } from "zod/v4";
|
|
import { isPublicUrl } from "@/lib/validations/notifications";
|
|
|
|
export const moodLogCredentialsSchema = z.object({
|
|
url: z
|
|
.string()
|
|
.url()
|
|
.max(500)
|
|
// SSRF guard: stored URL must point at a public host. The sync
|
|
// worker fetches from this URL with the user's apiKey in the
|
|
// Authorization header, so a stored RFC1918 / link-local target
|
|
// would let any user pull cloud-metadata or local-network data
|
|
// back through their account.
|
|
.refine((u) => isPublicUrl(u), {
|
|
message: "URL must point at a public host (no RFC1918 / link-local)",
|
|
}),
|
|
apiKey: z.string().min(1).max(200),
|
|
});
|
|
|
|
export const moodLogWebhookPayloadSchema = z.object({
|
|
event: z.enum(["mood.created", "mood.updated", "mood.deleted"]),
|
|
timestamp: z.string().datetime(),
|
|
entry: z.object({
|
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
time: z.string().datetime(),
|
|
mood: z.enum(["SUPER_GUT", "GUT", "OKAY", "SCHLECHT", "LAUSIG"]),
|
|
score: z.number().int().min(1).max(5),
|
|
tags: z.array(z.string()).max(50).optional(),
|
|
loggedVia: z.enum(["WEB", "TELEGRAM", "DAYLIO"]).optional(),
|
|
}),
|
|
});
|
|
|
|
export const moodLogSyncResponseSchema = z.object({
|
|
version: z.string(),
|
|
entries: z.array(
|
|
z.object({
|
|
date: z.string(),
|
|
time: z.string(),
|
|
mood: z.string(),
|
|
score: z.number(),
|
|
tags: z.array(z.string()).optional(),
|
|
loggedVia: z.string().optional(),
|
|
}),
|
|
),
|
|
});
|
|
|
|
export type MoodLogCredentials = z.infer<typeof moodLogCredentialsSchema>;
|
|
export type MoodLogWebhookPayload = z.infer<typeof moodLogWebhookPayloadSchema>;
|
|
export type MoodLogSyncResponse = z.infer<typeof moodLogSyncResponseSchema>;
|
|
|
|
// --- CRUD schemas for mood entries ---
|
|
|
|
export const moodLevelEnum = z.enum([
|
|
"SUPER_GUT",
|
|
"GUT",
|
|
"OKAY",
|
|
"SCHLECHT",
|
|
"LAUSIG",
|
|
]);
|
|
|
|
export const moodSourceEnum = z.enum([
|
|
"MANUAL",
|
|
"MOODLOG",
|
|
"WEB",
|
|
"TELEGRAM",
|
|
"DAYLIO",
|
|
]);
|
|
|
|
const MOOD_SCORE_MAP: Record<string, number> = {
|
|
SUPER_GUT: 5,
|
|
GUT: 4,
|
|
OKAY: 3,
|
|
SCHLECHT: 2,
|
|
LAUSIG: 1,
|
|
};
|
|
|
|
export function getScoreForMood(mood: string): number {
|
|
return MOOD_SCORE_MAP[mood] ?? 3;
|
|
}
|
|
|
|
export const createMoodEntrySchema = z.object({
|
|
mood: moodLevelEnum,
|
|
tags: z.array(z.string().max(50)).max(20).optional(),
|
|
moodLoggedAt: z.iso.datetime({ offset: true }).transform((s) => new Date(s)),
|
|
source: moodSourceEnum.optional().default("MANUAL"),
|
|
});
|
|
|
|
export const updateMoodEntrySchema = z.object({
|
|
mood: moodLevelEnum.optional(),
|
|
tags: z.array(z.string().max(50)).max(20).nullable().optional(),
|
|
moodLoggedAt: z.iso
|
|
.datetime({ offset: true })
|
|
.transform((s) => new Date(s))
|
|
.optional(),
|
|
});
|
|
|
|
export const listMoodEntriesSchema = z.object({
|
|
mood: moodLevelEnum.optional(),
|
|
from: z
|
|
.string()
|
|
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
.optional(),
|
|
to: z
|
|
.string()
|
|
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
.optional(),
|
|
limit: z.coerce.number().int().min(1).max(500).optional().default(100),
|
|
offset: z.coerce.number().int().min(0).optional().default(0),
|
|
sortBy: z
|
|
.enum(["date", "mood", "score", "moodLoggedAt", "source"])
|
|
.optional()
|
|
.default("moodLoggedAt"),
|
|
sortDir: z.enum(["asc", "desc"]).optional().default("desc"),
|
|
});
|
|
|
|
export type CreateMoodEntryInput = z.infer<typeof createMoodEntrySchema>;
|
|
export type UpdateMoodEntryInput = z.infer<typeof updateMoodEntrySchema>;
|
|
export type ListMoodEntriesInput = z.infer<typeof listMoodEntriesSchema>;
|