From 5e721e0791c68df474bdcdfee82a0a9192ee4e2e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Sun, 26 Apr 2026 22:34:20 +0200
Subject: [PATCH] fix(onboarding): surface errors and supply default schedule
(#87)
Issue #87 (kevinpapst): medications added during onboarding were
never visible afterwards. Root cause: the createMedication schema
requires schedules.min(1) ("Mindestens ein Zeitfenster"), but the
onboarding wizard sent schedules:[]. The 422 was silently swallowed
and the user was redirected to the dashboard as if everything had
worked.
This patch is the minimal-impact fix. The proper fix lives in Phase
3.5 of the audit plan, which replaces the wizard entirely with a
dashboard-first empty-state flow. Until then:
- finishOnboarding() wraps each call in a try/catch, checks
response.ok explicitly, and surfaces a sonner toast on failure.
setSaving(false) on error so the user can retry instead of being
stuck on a spinner.
- Each medication is sent with a default schedule (08:00-09:00) so
it is actually persisted. A new medScheduleHint i18n key is shown
under the meds list while the user is in the wizard, so the
default schedule is not surprising.
- Errors are translated per stage (errorProfile, errorMedication
with {name}, errorTargets) plus an errorGeneric fallback. en.json
and de.json kept symmetric.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
messages/de.json | 7 +-
messages/en.json | 7 +-
src/app/onboarding/page.tsx | 138 +++++++++++++++++++++++-------------
3 files changed, 99 insertions(+), 53 deletions(-)
diff --git a/messages/de.json b/messages/de.json
index c471b7159..4791cd810 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -819,7 +819,12 @@
"targetsDescription": "Setze optionale Zielwerte für Blutdruck und Gewicht.",
"targetSys": "Systolisch (mmHg)",
"targetDia": "Diastolisch (mmHg)",
- "targetWeight": "Zielgewicht (kg)"
+ "targetWeight": "Zielgewicht (kg)",
+ "errorProfile": "Dein Profil konnte nicht gespeichert werden. Bitte erneut versuchen.",
+ "errorMedication": "Das Medikament „{name}\" konnte nicht angelegt werden. Bitte erneut versuchen oder später unter „Medikamente\" hinzufügen.",
+ "errorTargets": "Deine Zielwerte konnten nicht gespeichert werden. Bitte erneut versuchen.",
+ "errorGeneric": "Beim Abschluss des Setups ist etwas schiefgelaufen. Bitte erneut versuchen.",
+ "medScheduleHint": "Wir legen ein Standard-Erinnerungsfenster an (08:00–09:00 täglich). Du kannst es jederzeit unter „Medikamente\" anpassen."
},
"settings": {
"title": "Einstellungen",
diff --git a/messages/en.json b/messages/en.json
index b1d992b0d..8ff2f5b95 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -819,7 +819,12 @@
"targetsDescription": "Set optional target values for blood pressure and weight.",
"targetSys": "Systolic (mmHg)",
"targetDia": "Diastolic (mmHg)",
- "targetWeight": "Target weight (kg)"
+ "targetWeight": "Target weight (kg)",
+ "errorProfile": "Could not save your profile. Please try again.",
+ "errorMedication": "Could not add the medication \"{name}\". Please try again or add it later in Medications.",
+ "errorTargets": "Could not save your target values. Please try again.",
+ "errorGeneric": "Something went wrong while finishing setup. Please try again.",
+ "medScheduleHint": "We'll add a default reminder window (08:00–09:00 daily). You can adjust it anytime in Medications."
},
"settings": {
"title": "Settings",
diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx
index c7871d3a5..32ee666a3 100644
--- a/src/app/onboarding/page.tsx
+++ b/src/app/onboarding/page.tsx
@@ -3,6 +3,7 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
+import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -39,45 +40,63 @@ export default function OnboardingPage() {
async function finishOnboarding() {
setSaving(true);
- const body: Record = {};
- if (heightCm) body.heightCm = parseFloat(heightCm);
- if (dateOfBirth) body.dateOfBirth = dateOfBirth;
- if (gender) body.gender = gender;
+ try {
+ const body: Record = {};
+ if (heightCm) body.heightCm = parseFloat(heightCm);
+ if (dateOfBirth) body.dateOfBirth = dateOfBirth;
+ if (gender) body.gender = gender;
- await fetch("/api/onboarding/complete", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body),
- });
-
- // Create medications if any
- for (const med of meds) {
- await fetch("/api/medications", {
+ const profileRes = await fetch("/api/onboarding/complete", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- name: med.name,
- dose: med.dose,
- schedules: [],
- }),
+ body: JSON.stringify(body),
});
- }
+ if (!profileRes.ok) {
+ throw new Error(t("onboarding.errorProfile"));
+ }
- // Save targets if set
- if (targetSys || targetDia || targetWeight) {
- const targets: Record = {};
- if (targetSys) targets.bpSysTarget = parseInt(targetSys);
- if (targetDia) targets.bpDiaTarget = parseInt(targetDia);
- if (targetWeight) targets.weightTarget = parseFloat(targetWeight);
- await fetch("/api/insights/targets", {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(targets),
- });
- }
+ // Create medications. Server validation requires at least one schedule,
+ // so we attach a sensible default — the user is informed via medScheduleHint
+ // and can refine it in Medications later. Phase 3.5 of the audit replaces
+ // this entire wizard with an empty-state-driven flow.
+ for (const med of meds) {
+ const medRes = await fetch("/api/medications", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ name: med.name,
+ dose: med.dose,
+ schedules: [{ windowStart: "08:00", windowEnd: "09:00" }],
+ }),
+ });
+ if (!medRes.ok) {
+ throw new Error(t("onboarding.errorMedication", { name: med.name }));
+ }
+ }
- await queryClient.invalidateQueries({ queryKey: ["auth"] });
- router.replace("/");
+ if (targetSys || targetDia || targetWeight) {
+ const targets: Record = {};
+ if (targetSys) targets.bpSysTarget = parseInt(targetSys);
+ if (targetDia) targets.bpDiaTarget = parseInt(targetDia);
+ if (targetWeight) targets.weightTarget = parseFloat(targetWeight);
+ const targetsRes = await fetch("/api/insights/targets", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(targets),
+ });
+ if (!targetsRes.ok) {
+ throw new Error(t("onboarding.errorTargets"));
+ }
+ }
+
+ await queryClient.invalidateQueries({ queryKey: ["auth"] });
+ router.replace("/");
+ } catch (err) {
+ const message =
+ err instanceof Error ? err.message : t("onboarding.errorGeneric");
+ toast.error(message);
+ setSaving(false);
+ }
}
function addMedication() {
@@ -191,27 +210,40 @@ export default function OnboardingPage() {
placeholder={t("onboarding.medDosePlaceholder")}
className="w-24"
/>
-