mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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.
135 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|