mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
chore(release): v1.4.38
Robustness sweep, perf hotspots, full localization, coach gate cascade closure. - /api/dashboard/summary cold-mount 4.6 s → ~500 ms via DAY-bucket sparkline + 60 s response cache. - Coach API gate inventory test surfaced and closed two orphan routes (chat[id] GET+DELETE, chat-message feedback POST). - Cross-tz fast-path runtime guard falls back to live for users more than ±3 h from UTC; proper bucket minting stays v1.5. - es / fr / it / pl coverage 27 % → 63 % (~3 400 new strings) plus three medication-cluster placeholder restorations. - IANA timezone validation closes the User.timezone self-DoS regression the new dashboard SQL would have surfaced. - Unit suite 4520 → 4524. See CHANGELOG.md for the full set.
This commit is contained in:
1 parent
fe64f66c7f
commit
a550031a84
66 files changed
+7910
-3717
No files matched your search
@@ -165,6 +165,17 @@ export function MedicationIntakeQuickAdd({
|
||||
const json = await res.json();
|
||||
return json.data as MedicationOption[];
|
||||
},
|
||||
// v1.4.38 — share the parent dashboard's medications cache.
|
||||
// `queryKeys.medications()` resolves to `["medications"]` — the
|
||||
// same key the dashboard's onboarding checklist subscriber holds.
|
||||
// Without a staleTime the sheet-mount triggers a fresh
|
||||
// `/api/medications` GET on every open even when the parent cache
|
||||
// already has current data. Match the dashboard's 60s window and
|
||||
// skip the refetch-on-mount so opening the sheet inside the TTL
|
||||
// reads straight from the React-Query cache.
|
||||
staleTime: 60_000,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const medications = useMemo(
|
||||
@@ -297,9 +308,18 @@ export function MedicationIntakeQuickAdd({
|
||||
);
|
||||
|
||||
// Empty-state branch: the user has no active medications. Render a
|
||||
// hint + CTA into the body so the user has a single tap into the
|
||||
// medication-creation surface. The form itself is suppressed so we
|
||||
// don't ship an unusable Save button.
|
||||
// hint into the body and promote the "Medikament anlegen" CTA into
|
||||
// the sheet footer slot so the primary action lives where the user
|
||||
// expects it (matches the populated form's Save-button slot). The
|
||||
// form itself is suppressed so we don't ship an unusable Save
|
||||
// button.
|
||||
//
|
||||
// v1.4.38 W-D P2-1 — symmetry fix. The previous shape rendered the
|
||||
// CTA in the body and a footer with only the Close button, which
|
||||
// read as the dialog losing a save button. Now the body carries the
|
||||
// hint and the footer carries `<Cancel> <Link asChild>Medikament
|
||||
// anlegen</Link>` so the footer-slot promise holds across both
|
||||
// branches.
|
||||
if (!medicationsLoading && medications.length === 0) {
|
||||
const emptyFooter = (
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
@@ -310,9 +330,14 @@ export function MedicationIntakeQuickAdd({
|
||||
onClick={onCancel}
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
{t("common.close")}
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
)}
|
||||
<Button asChild className="min-h-11 sm:min-h-9">
|
||||
<Link href="/medications">
|
||||
{t("dashboard.medicationIntakeQuickAdd.emptyCta")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
@@ -332,16 +357,6 @@ export function MedicationIntakeQuickAdd({
|
||||
{t("dashboard.medicationIntakeQuickAdd.emptyDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
<Link href="/medications">
|
||||
{t("dashboard.medicationIntakeQuickAdd.emptyCta")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{footerSlot ? createPortal(emptyFooter, footerSlot) : emptyFooter}
|
||||
</>
|
||||
|
||||
@@ -105,7 +105,15 @@ export function InsightStatusCard({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="animate-insight-in border-l-dracula-purple border-l-2">
|
||||
// v1.4.38 W-D P2-2 — `aria-live="polite"` so screen readers
|
||||
// announce the assessment when it swaps into the same slot the
|
||||
// structured skeleton occupied. The skeleton card already carries
|
||||
// `aria-busy="true" aria-live="polite"`; mirroring the live region
|
||||
// on the success card closes the load→loaded transition signal.
|
||||
<Card
|
||||
aria-live="polite"
|
||||
className="animate-insight-in border-l-dracula-purple border-l-2"
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -525,6 +525,11 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
const isExpanded = isGrouped
|
||||
? expandedDayKeys.has(m.dayKey as string)
|
||||
: false;
|
||||
// v1.4.38 W-D P1-1 — stable drill-down id so the
|
||||
// disclosure chevron can thread aria-controls to the
|
||||
// expanded panel. dayKey is unique per row when
|
||||
// grouped; fall back to m.id otherwise.
|
||||
const drilldownId = `drilldown-desktop-${m.dayKey ?? m.id}`;
|
||||
return (
|
||||
<Fragment key={m.id}>
|
||||
<TableRow>
|
||||
@@ -585,6 +590,7 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
toggleExpand(m.dayKey as string)
|
||||
}
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={drilldownId}
|
||||
aria-label={
|
||||
isExpanded
|
||||
? t("measurements.collapseDay")
|
||||
@@ -617,7 +623,7 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isGrouped && isExpanded && (
|
||||
<TableRow>
|
||||
<TableRow id={drilldownId}>
|
||||
<TableCell colSpan={6} className="p-0">
|
||||
<DayDrillDown
|
||||
type={m.type}
|
||||
@@ -644,6 +650,8 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
const isExpanded = isGrouped
|
||||
? expandedDayKeys.has(m.dayKey as string)
|
||||
: false;
|
||||
// v1.4.38 W-D P1-1 — see desktop counterpart.
|
||||
const drilldownId = `drilldown-mobile-${m.dayKey ?? m.id}`;
|
||||
return (
|
||||
<div
|
||||
key={m.id}
|
||||
@@ -712,6 +720,7 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
data-testid="measurement-day-expand"
|
||||
onClick={() => toggleExpand(m.dayKey as string)}
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={drilldownId}
|
||||
aria-label={
|
||||
isExpanded
|
||||
? t("measurements.collapseDay")
|
||||
@@ -742,12 +751,14 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
</div>
|
||||
</div>
|
||||
{isGrouped && isExpanded && (
|
||||
<DayDrillDown
|
||||
type={m.type}
|
||||
dayKey={m.dayKey as string}
|
||||
unit={m.unit}
|
||||
layout="mobile"
|
||||
/>
|
||||
<div id={drilldownId}>
|
||||
<DayDrillDown
|
||||
type={m.type}
|
||||
dayKey={m.dayKey as string}
|
||||
unit={m.unit}
|
||||
layout="mobile"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,10 @@ import { useEffect, useReducer, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
Check,
|
||||
CircleCheck,
|
||||
Flame,
|
||||
History,
|
||||
Loader2,
|
||||
@@ -395,13 +398,28 @@ export function Glp1MedicationCard({
|
||||
<p className="text-sm">
|
||||
<span
|
||||
className={
|
||||
currentWindowStatus.status === "in_window"
|
||||
? "text-success font-medium"
|
||||
"inline-flex items-center gap-1 font-medium " +
|
||||
(currentWindowStatus.status === "in_window"
|
||||
? "text-success"
|
||||
: currentWindowStatus.status === "late"
|
||||
? "text-dracula-yellow font-medium"
|
||||
: "text-warning font-medium"
|
||||
? "text-dracula-yellow"
|
||||
: "text-warning")
|
||||
}
|
||||
>
|
||||
{/* v1.4.38 W-D P2-3 — pair the colour with a Lucide
|
||||
glyph so colour-blind users (red-green) can
|
||||
disambiguate take-now from late from very-late.
|
||||
WCAG 1.4.1 (Use of Color). */}
|
||||
{currentWindowStatus.status === "in_window" ? (
|
||||
<CircleCheck className="size-3.5 shrink-0" aria-hidden="true" />
|
||||
) : currentWindowStatus.status === "late" ? (
|
||||
<AlertCircle className="size-3.5 shrink-0" aria-hidden="true" />
|
||||
) : (
|
||||
<AlertTriangle
|
||||
className="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{currentWindowStatus.status === "in_window"
|
||||
? t("medications.takeNow")
|
||||
: currentWindowStatus.status === "late"
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
toBerlinDate,
|
||||
} from "@/lib/medications/window-status";
|
||||
import {
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
Check,
|
||||
CircleCheck,
|
||||
SkipForward,
|
||||
Flame,
|
||||
Pencil,
|
||||
@@ -294,13 +297,28 @@ export function MedicationCard({ medication, onEdit }: MedicationCardProps) {
|
||||
<p className="text-sm">
|
||||
<span
|
||||
className={
|
||||
currentWindowStatus.status === "in_window"
|
||||
? "text-success font-medium"
|
||||
"inline-flex items-center gap-1 font-medium " +
|
||||
(currentWindowStatus.status === "in_window"
|
||||
? "text-success"
|
||||
: currentWindowStatus.status === "late"
|
||||
? "text-dracula-yellow font-medium"
|
||||
: "text-warning font-medium"
|
||||
? "text-dracula-yellow"
|
||||
: "text-warning")
|
||||
}
|
||||
>
|
||||
{/* v1.4.38 W-D P2-3 — pair the colour with a Lucide
|
||||
glyph so colour-blind users (red-green) can
|
||||
disambiguate take-now from late from very-late.
|
||||
WCAG 1.4.1 (Use of Color). */}
|
||||
{currentWindowStatus.status === "in_window" ? (
|
||||
<CircleCheck className="size-3.5 shrink-0" aria-hidden="true" />
|
||||
) : currentWindowStatus.status === "late" ? (
|
||||
<AlertCircle className="size-3.5 shrink-0" aria-hidden="true" />
|
||||
) : (
|
||||
<AlertTriangle
|
||||
className="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{currentWindowStatus.status === "in_window"
|
||||
? t("medications.takeNow")
|
||||
: currentWindowStatus.status === "late"
|
||||
|
||||
@@ -365,7 +365,11 @@ export function GettingStartedChecklist() {
|
||||
// few primary buttons rendered above the fold on the
|
||||
// onboarded-user dashboard so the regression was visible to
|
||||
// the touch-target spec.
|
||||
className="text-muted-foreground hover:text-foreground min-h-11 sm:min-h-10"
|
||||
// v1.4.38 W-D P3-3 — align the desktop floor with the
|
||||
// dashboard +Hinzufügen pattern (`sm:min-h-9`); the
|
||||
// `sm:min-h-10` here was a no-op against the Button default
|
||||
// of `h-10`.
|
||||
className="text-muted-foreground hover:text-foreground min-h-11 sm:min-h-9"
|
||||
onClick={() => setDismissedAll(true)}
|
||||
>
|
||||
{t("gettingStarted.dismissAll")}
|
||||
|
||||
@@ -163,7 +163,11 @@ export function ArztberichtHeroCard() {
|
||||
// `min-h-11` (44 px) is the WCAG 2.5.5 touch-target floor;
|
||||
// we override the default `<Button>` height on mobile and
|
||||
// fall back to the compact `sm:h-10` on pointer devices.
|
||||
className="h-11 px-5 text-sm font-medium sm:h-10"
|
||||
// v1.4.38 W-D P3-2 — pair the explicit `h-11` with a
|
||||
// `min-h-11` floor (and a `sm:min-h-9` desktop floor) so a
|
||||
// future className override can't accidentally re-lift the
|
||||
// mobile minimum below the touch-target contract.
|
||||
className="h-11 min-h-11 px-5 text-sm font-medium sm:h-10 sm:min-h-9"
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={busy}
|
||||
aria-describedby={valueStatementId}
|
||||
|
||||
@@ -42,14 +42,18 @@ function SelectTrigger({
|
||||
// desktop input rhythm. `size=sm` (filter chips) intentionally
|
||||
// keeps the 32 px footprint.
|
||||
// v1.4.37 W4a item 4 — split the trigger's horizontal padding
|
||||
// (`pl-3 pr-2.5`) and push the chevron icon 4 px further right
|
||||
// via `[&_svg:last-child]:mr-1` so the trailing glyph parks at
|
||||
// the same visual gutter as the browser-native date-input
|
||||
// calendar icon (~16-20 px on Chromium). The old uniform
|
||||
// `px-3` parked the chevron 12 px from the edge, which read
|
||||
// visibly tighter than the date-picker glyph when both
|
||||
// controls shared a form row.
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent pl-3 pr-2.5 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-11 data-[size=default]:sm:h-10 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:last-child]:mr-1",
|
||||
// (`pl-3 pr-2`) so the trailing chevron parks at the same
|
||||
// visual gutter as the browser-native date-input calendar
|
||||
// icon. The old uniform `px-3` parked the chevron 12 px from
|
||||
// the edge, which read visibly tighter than the date-picker
|
||||
// glyph when both controls shared a form row.
|
||||
// v1.4.38 W-D P1-6 — drop the `[&_svg:last-child]:mr-1`
|
||||
// double-shift and tighten `pr-2.5` to `pr-2`. The previous
|
||||
// 14 px combined gutter (10 + 4) hit Chromium-Material native
|
||||
// date-inputs cleanly but lagged ~4 px against Safari and
|
||||
// Chromium-legacy themes; `pr-2` (8 px) lands the chevron
|
||||
// inside the gutter the non-Material engines actually paint.
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent pl-3 pr-2 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-11 data-[size=default]:sm:h-10 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in new issue
Block a user