mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
Add a CoachPlan model: durable, user-scoped if-then implementation plans the Coach proposes and the user confirms. The background memory-refresh pass extracts plans the user agreed to and writes them as "proposed"; only the user-facing PATCH on /api/coach/plans/[id] activates one. The free-text cue / action / target are encrypted at rest with the shared Bytes codec; metric, status and dates stay plain so the injection picker ranks without a per-row decrypt. Active plans fold into the Coach snapshot's memory block and survive the prompt-budget drop alongside facts. List, confirm and soft-delete routes are Coach-gated and narrow the owner from the session. Key the dashboard-snapshot query cell by locale so a locale switch reads freshly localised tile copy on its own cell instead of serving the prior locale's prose until the slot goes stale. The zero-arg key stays byte-identical, so every prefix-based invalidation keeps reaching the locale-keyed cells; the offline-banner and preloader match on the prefix. Migration 0194 (additive CREATE TABLE). Encryption-rotation registry and script cover the three new Bytes columns.
94 lines
3.7 KiB
TypeScript
94 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { WifiOff } from "lucide-react";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import { queryKeys } from "@/lib/query-keys";
|
|
|
|
/**
|
|
* v1.4.43 QoL (M5) — slim banner that surfaces "you're offline" while
|
|
* `navigator.onLine` reads false. The PWA service worker already
|
|
* serves the read paths from cache (`public/sw.js`), but pre-fix a
|
|
* user toggling airplane mode mid-form-fill saw blank tiles and no
|
|
* explanation. This banner closes the explanation gap.
|
|
*
|
|
* Mount strategy: rendered once at the top of `<AuthShell>` so every
|
|
* authenticated route inherits it. The component is mount-time SSR-
|
|
* safe — it never paints during the initial server render (the
|
|
* `online`/`offline` window events only fire client-side); a `useEffect`
|
|
* subscribes to the events once and toggles the visible state from
|
|
* there. Honours `prefers-reduced-motion` via the absence of any
|
|
* animation.
|
|
*
|
|
* Copy lives in `messages/*.json` under `offlineBanner.message`; all
|
|
* six locales ship today.
|
|
*/
|
|
export function OfflineBanner() {
|
|
const { t } = useTranslations();
|
|
const queryClient = useQueryClient();
|
|
// Initialise to `true` so the banner stays hidden during SSR + the
|
|
// first client render. The effect below flips it true/false after
|
|
// mount based on `navigator.onLine`. (We do NOT read navigator on
|
|
// the very first render to keep the SSR markup hydration-stable.)
|
|
const [isOnline, setIsOnline] = useState(true);
|
|
|
|
useEffect(() => {
|
|
function handleOnline() {
|
|
setIsOnline(true);
|
|
}
|
|
function handleOffline() {
|
|
setIsOnline(false);
|
|
}
|
|
// Subscribe to the live transition events; both handlers route
|
|
// through the React setState pathway via the listener callback
|
|
// rather than a synchronous in-effect setState so the strict
|
|
// `react-hooks/set-state-in-effect` rule stays happy.
|
|
window.addEventListener("online", handleOnline);
|
|
window.addEventListener("offline", handleOffline);
|
|
// Sync the initial state from `navigator.onLine` — deferred to a
|
|
// microtask so React sees this as an external-system bridge
|
|
// rather than a render-cascade setState. The legacy in-effect
|
|
// setState path tripped the strict lint rule, the queueMicrotask
|
|
// shape is the documented escape hatch.
|
|
queueMicrotask(() => {
|
|
if (typeof navigator !== "undefined" && navigator.onLine === false) {
|
|
setIsOnline(false);
|
|
}
|
|
});
|
|
return () => {
|
|
window.removeEventListener("online", handleOnline);
|
|
window.removeEventListener("offline", handleOffline);
|
|
};
|
|
}, []);
|
|
|
|
if (isOnline) return null;
|
|
|
|
// v1.18.6 — when an offline install is showing last-synced data (the SW's
|
|
// allowlisted read cache + the IndexedDB query persister hydrated the
|
|
// dashboard snapshot), say so honestly rather than implying a blank screen.
|
|
// v1.21.3 (b) — the snapshot cell is now locale-keyed, so match on the
|
|
// `["dashboard","snapshot"]` PREFIX (`getQueriesData`) rather than the
|
|
// exact zero-arg slot — any hydrated locale's snapshot proves cached data
|
|
// is present.
|
|
const hasCachedData = queryClient
|
|
.getQueriesData({ queryKey: queryKeys.dashboardSnapshot() })
|
|
.some(([, data]) => data != null);
|
|
|
|
return (
|
|
<div
|
|
role="status"
|
|
aria-live="polite"
|
|
data-slot="offline-banner"
|
|
className="bg-warning/15 border-warning/40 text-foreground flex items-center justify-center gap-2 border-b px-3 py-2 text-xs"
|
|
>
|
|
<WifiOff className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
|
<span className="text-center">
|
|
{t("offlineBanner.message")}
|
|
{hasCachedData ? ` ${t("offlineBanner.showingCached")}` : ""}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|