mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
release: v1.4.2 — resurrect v1.4.1 work + bug-fix triple (#152)
* 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>
This commit is contained in:
1 parent
956d29032f
commit
643e6ec3a0
59 files changed
+7318
-3844
No files matched your search
+14
-2639
File diff suppressed because it is too large.
Load diff
@@ -372,7 +372,13 @@ export const GET = apiHandler(async () => {
|
||||
unit: "steps",
|
||||
range: stepsRange,
|
||||
classification: stepsClassification,
|
||||
source: "WHO",
|
||||
// WHO publishes activity *time* (150–300 min/wk moderate),
|
||||
// not a step quota. The closest peer-reviewed dose-response
|
||||
// for the 8 000–15 000 band is Saint-Maurice JAMA 2020. The
|
||||
// AI prompts at src/lib/insights/prompts/{base-system,
|
||||
// general-status}.ts already enforce this attribution; this
|
||||
// surface label was the last "WHO" mislabel in the codebase.
|
||||
source: "Saint-Maurice JAMA 2020",
|
||||
});
|
||||
|
||||
// 8. Medication Compliance (average across active medications)
|
||||
|
||||
@@ -632,7 +632,7 @@ export default function InsightsPage() {
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
const w = analytics?.summaries.WEIGHT;
|
||||
const w = analytics?.summaries?.WEIGHT;
|
||||
const sys = analytics?.summaries.BLOOD_PRESSURE_SYS;
|
||||
const dia = analytics?.summaries.BLOOD_PRESSURE_DIA;
|
||||
const p = analytics?.summaries.PULSE;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -130,10 +130,11 @@ export default function MedicationsPage() {
|
||||
const byName = (a: Medication, b: Medication) =>
|
||||
a.name.localeCompare(b.name, "de", { sensitivity: "base" });
|
||||
|
||||
const activeMeds = (medications?.filter((m) => m.active) ?? []).sort(byName);
|
||||
const inactiveMeds = (medications?.filter((m) => !m.active) ?? []).sort(
|
||||
byName,
|
||||
);
|
||||
// Defensive against stale service-worker responses or any future API
|
||||
// shape change: only filter when we actually have an array.
|
||||
const medsArray = Array.isArray(medications) ? medications : [];
|
||||
const activeMeds = medsArray.filter((m) => m.active).sort(byName);
|
||||
const inactiveMeds = medsArray.filter((m) => !m.active).sort(byName);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -485,20 +486,52 @@ function ApiEndpointDialog({
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
type ExampleType = "curl" | "wget" | "fetch" | "powershell";
|
||||
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [activeTokenCount, setActiveTokenCount] = useState(0);
|
||||
const [loadingStatus, setLoadingStatus] = useState(false);
|
||||
const apiEndpointKey = ["medication-api-endpoint", medication?.id];
|
||||
|
||||
type ApiEndpointStatus = { enabled: boolean; activeTokenCount: number };
|
||||
|
||||
const {
|
||||
data: status,
|
||||
isFetching: loadingStatus,
|
||||
refetch: refetchStatus,
|
||||
error: statusError,
|
||||
} = useQuery<ApiEndpointStatus>({
|
||||
queryKey: apiEndpointKey,
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`/api/medications/${medication!.id}/api-endpoint`,
|
||||
);
|
||||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(json.error || t("medications.statusLoadFailed"));
|
||||
}
|
||||
return {
|
||||
enabled: json.data.enabled === true,
|
||||
activeTokenCount: json.data.activeTokenCount ?? 0,
|
||||
};
|
||||
},
|
||||
enabled: !!medication,
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
});
|
||||
|
||||
const enabled = status?.enabled ?? false;
|
||||
const activeTokenCount = status?.activeTokenCount ?? 0;
|
||||
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [toggling, setToggling] = useState(false);
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
const [exampleType, setExampleType] = useState<ExampleType>("curl");
|
||||
|
||||
const displayMsg =
|
||||
msg ?? (statusError instanceof Error ? statusError.message : null);
|
||||
|
||||
function handleClose() {
|
||||
setEnabled(false);
|
||||
setActiveTokenCount(0);
|
||||
queryClient.removeQueries({ queryKey: apiEndpointKey });
|
||||
setToken(null);
|
||||
setMsg(null);
|
||||
setCopied(null);
|
||||
@@ -506,27 +539,6 @@ function ApiEndpointDialog({
|
||||
onClose();
|
||||
}
|
||||
|
||||
const loadStatus = useCallback(async () => {
|
||||
if (!medication) return;
|
||||
setLoadingStatus(true);
|
||||
setMsg(null);
|
||||
try {
|
||||
const res = await fetch(`/api/medications/${medication.id}/api-endpoint`);
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setEnabled(json.data.enabled === true);
|
||||
setActiveTokenCount(json.data.activeTokenCount ?? 0);
|
||||
} else {
|
||||
const json = await res.json();
|
||||
setMsg(json.error || t("medications.statusLoadFailed"));
|
||||
}
|
||||
} catch {
|
||||
setMsg(t("medications.statusLoadFailed"));
|
||||
} finally {
|
||||
setLoadingStatus(false);
|
||||
}
|
||||
}, [medication, t]);
|
||||
|
||||
async function toggleEndpoint(nextEnabled: boolean) {
|
||||
if (!medication) return;
|
||||
setToggling(true);
|
||||
@@ -547,15 +559,16 @@ function ApiEndpointDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
setEnabled(json.data.enabled === true);
|
||||
if (typeof json.data.activeTokenCount === "number") {
|
||||
setActiveTokenCount(json.data.activeTokenCount);
|
||||
} else if (
|
||||
!json.data.enabled &&
|
||||
typeof json.data.revokedTokenCount === "number"
|
||||
) {
|
||||
setActiveTokenCount(0);
|
||||
}
|
||||
const nextStatus: ApiEndpointStatus = {
|
||||
enabled: json.data.enabled === true,
|
||||
activeTokenCount:
|
||||
typeof json.data.activeTokenCount === "number"
|
||||
? json.data.activeTokenCount
|
||||
: !json.data.enabled
|
||||
? 0
|
||||
: (status?.activeTokenCount ?? 0),
|
||||
};
|
||||
queryClient.setQueryData<ApiEndpointStatus>(apiEndpointKey, nextStatus);
|
||||
|
||||
if (json.data.token) {
|
||||
setToken(json.data.token);
|
||||
@@ -579,11 +592,6 @@ function ApiEndpointDialog({
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!medication) return;
|
||||
void loadStatus();
|
||||
}, [medication, loadStatus]);
|
||||
|
||||
const baseUrl =
|
||||
typeof window !== "undefined" ? window.location.origin : "https://...";
|
||||
const endpoint = `${baseUrl}/api/ingest/medication`;
|
||||
@@ -756,11 +764,11 @@ function ApiEndpointDialog({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{msg && (
|
||||
{displayMsg && (
|
||||
<p
|
||||
className={`text-sm ${msg === t("medications.apiEndpointActivated") || msg === t("medications.apiEndpointDeactivated") ? "text-dracula-green" : "text-destructive"}`}
|
||||
className={`text-sm ${displayMsg === t("medications.apiEndpointActivated") || displayMsg === t("medications.apiEndpointDeactivated") ? "text-dracula-green" : "text-destructive"}`}
|
||||
>
|
||||
{msg}
|
||||
{displayMsg}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -780,7 +788,7 @@ function ApiEndpointDialog({
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
void loadStatus();
|
||||
void refetchStatus();
|
||||
}}
|
||||
disabled={loadingStatus || toggling}
|
||||
>
|
||||
|
||||
+1
-1
@@ -197,7 +197,7 @@ export default function DashboardPage() {
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
|
||||
const w = data?.summaries.WEIGHT;
|
||||
const w = data?.summaries?.WEIGHT;
|
||||
const sys = data?.summaries.BLOOD_PRESSURE_SYS;
|
||||
const dia = data?.summaries.BLOOD_PRESSURE_DIA;
|
||||
const p = data?.summaries.PULSE;
|
||||
|
||||
Reference in new issue
Block a user