Files
HealthLog/src/app/global-error.tsx
T
Marc-André Bombeck ac212cf6b7 fix(pwa): give the offline page a way back and reload once on any stale chunk
Without a connection a signed-in page opens the worker's offline page:
signed-in pages carry `private, no-store` from the proxy and are never
cached, so this is also what an offline launch of the installed app
shows. The page now offers a real 64 px retry control, reloads by itself
when the connection returns, follows the device's light or dark scheme
and keeps clear of the notch and home indicator. The worker's comment on
the page cache says plainly that only public pages land in it.

The single reload after a stale-shell chunk failure moves into
src/lib/pwa/chunk-reload.ts and now runs in the root error boundary too,
and it recognises a failed dynamic import as WebKit and Firefox word it.

pwa-offline.spec.ts runs in the service-worker project and proves the
offline strip, the offline page and the automatic recovery against the
real worker.
2026-10-08 17:23:47 +02:00

135 lines
4.4 KiB
TypeScript

"use client";
import { useEffect } from "react";
import { reloadOnceForChunkError } from "@/lib/pwa/chunk-reload";
/**
* Root-level error boundary. Renders when even the root layout itself fails,
* so it cannot rely on any providers (i18n, query, auth). Keep it static,
* in English, and self-contained.
*/
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
// A chunk of the root layout's client tree missing after a deploy lands
// here rather than in `app/error.tsx`; it gets the same single reload.
useEffect(() => {
reloadOnceForChunkError(error);
}, [error]);
async function handleCopy() {
// Strip query string — OAuth callback routes carry sensitive tokens.
const loc = typeof window !== "undefined" ? window.location : null;
const payload = {
message: error.message,
digest: error.digest,
name: error.name,
stack: error.stack?.split("\n").slice(0, 10).join("\n"),
urlPath: loc?.pathname ?? null,
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : null,
timestamp: new Date().toISOString(),
};
const text = JSON.stringify(payload, null, 2);
try {
await navigator.clipboard.writeText(text);
} catch {
window.prompt("Copy details", text);
}
}
return (
<html lang="en">
<body
style={{
fontFamily: "system-ui, sans-serif",
backgroundColor: "#1e1f29",
color: "#f8f8f2",
margin: 0,
// v1.4.27 MB6 — `100dvh` follows the dynamic viewport on
// iOS Safari (URL-bar collapse) so the wrapper still
// anchors centred when the browser chrome animates in or
// out. `safe-area-inset-top` keeps the headline out from
// under the notch / Dynamic Island.
minHeight: "100dvh",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "calc(env(safe-area-inset-top, 0px) + 24px) 24px 24px 24px",
}}
>
<div style={{ maxWidth: 560 }}>
{/*
v1.4.43 QoL — bilingual lockup so a German user landing
in this boundary doesn't read pure English in an already-
bad-state moment. The root layout has failed by the time
this fires, so no i18n provider is available; the static
DE/EN pairing covers the dominant audience without a
runtime cost. Copy tightened per the v1.4.43 audit (L1)
to drop the verbose secondary sentence.
*/}
<h1 style={{ fontSize: 20, marginBottom: 12 }}>
Etwas ist schiefgegangen / Something went wrong
</h1>
<p style={{ color: "#d0d0d0", marginBottom: 16, fontSize: 14 }}>
Ein kritischer Fehler ist aufgetreten. / A critical error occurred.
</p>
<pre
style={{
background: "#282a36",
padding: 12,
borderRadius: 6,
fontSize: 12,
overflow: "auto",
marginBottom: 16,
border: "1px solid #44475a",
}}
>
{error.message || "Unknown error"}
</pre>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<button
onClick={reset}
style={{
background: "#bd93f9",
color: "#282a36",
border: "none",
borderRadius: 6,
// v1.4.27 MB6 — 44 px minimum height matches the
// app-wide tap-target floor; the larger inline padding
// keeps the labels comfortably finger-tap-sized.
minHeight: 44,
padding: "10px 16px",
cursor: "pointer",
fontSize: 14,
fontWeight: 500,
}}
>
Retry
</button>
<button
onClick={handleCopy}
style={{
background: "transparent",
color: "#f8f8f2",
border: "1px solid #44475a",
borderRadius: 6,
minHeight: 44,
padding: "10px 16px",
cursor: "pointer",
fontSize: 14,
}}
>
Copy details
</button>
</div>
</div>
</body>
</html>
);
}