mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Merge remote-tracking branch 'origin/main' into develop
This commit is contained in:
commit
96e7442bab
6 files changed
+190
-2
No files matched your search
@@ -1,5 +1,47 @@
|
||||
# Changelog
|
||||
|
||||
## [1.4.38.4] — 2026-05-18 — Self-healing stale-shell after every deploy
|
||||
|
||||
v1.4.38.3 closed the stale-shell case where the user trips
|
||||
`ChunkLoadError` mid-navigation — but the reload only fired AFTER
|
||||
the error surfaced. Users sitting on the dashboard or insights
|
||||
overview rode out the staleness until they clicked something. The
|
||||
PWA shell could silently drift across several deploys, surfacing
|
||||
nothing visible to the user until a click happened to lazy-load a
|
||||
missing chunk.
|
||||
|
||||
### Added
|
||||
|
||||
- **`<VersionPoller>` client component** mounted inside
|
||||
`<Providers>`. Polls `/api/version` every 60 s. If the live
|
||||
version moves ahead of the shell-baked version it unregisters
|
||||
every active service worker, wipes CacheStorage, and triggers
|
||||
`window.location.reload()`. `sessionStorage` gates the reload to
|
||||
once per session so a mid-deploy webhook race that briefly serves
|
||||
a stale image cannot loop the page.
|
||||
- **`NEXT_PUBLIC_APP_VERSION`** is now injected from `package.json`
|
||||
at build time (via `next.config.ts`) so the running shell knows
|
||||
which release it was built against. Available to any client
|
||||
component that needs to compare against the live server version.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Service worker `CACHE_VERSION` now keys to the release tag**
|
||||
(`v1.4.38.4`). The `activate` step already evicts any cache name
|
||||
outside the current pair, so bumping the string at release time
|
||||
guarantees the precached root HTML and the precached
|
||||
`/_next/static/*` chunks from the previous deploy are dropped on
|
||||
the next SW install. The string is now part of the release
|
||||
routine alongside the `package.json` bump.
|
||||
|
||||
### Operator notes
|
||||
|
||||
- No new migration. No env-var change.
|
||||
- Coolify auto-deploys main on tag push.
|
||||
- Users on the pre-v1.4.38.4 shell still need a single manual
|
||||
refresh to pick up the new poller; from v1.4.38.4 onwards every
|
||||
future release self-heals.
|
||||
|
||||
## [1.4.38.3] — 2026-05-17 — CI green-up, e2e drift fixes, chunk-load auto-recover
|
||||
|
||||
Cleans up the three pre-existing CI reds that had been failing on
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import type { NextConfig } from "next";
|
||||
import bundleAnalyzer from "@next/bundle-analyzer";
|
||||
|
||||
import { version as PKG_VERSION } from "./package.json";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
poweredByHeader: false,
|
||||
// v1.4.38.4 — expose the package.json version to the client bundle
|
||||
// so the `<VersionPoller>` can compare the shell-baked version
|
||||
// against the live `/api/version` response and trigger a self-heal
|
||||
// (SW unregister + cache wipe + hard reload) when the server moves
|
||||
// ahead of the running shell after a deploy. Without this the user
|
||||
// had to discover "pull-to-refresh" themselves after every release.
|
||||
env: {
|
||||
NEXT_PUBLIC_APP_VERSION: PKG_VERSION,
|
||||
},
|
||||
// v1.4.33 IW2 — strip `console.*` calls from the production bundle
|
||||
// (keep `console.error` + `console.warn` so the GlitchTip reporter
|
||||
// and prod-side debug rails still surface). The Lighthouse audit
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.4.38.3",
|
||||
"version": "1.4.38.4",
|
||||
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"homepage": "https://healthlog.dev",
|
||||
|
||||
+11
-1
@@ -3,7 +3,17 @@
|
||||
* Handles Web Push notifications and offline caching.
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = "v1";
|
||||
// v1.4.38.4 — `CACHE_VERSION` now tracks the app release tag.
|
||||
// Bumped on every release so the `activate` step evicts every
|
||||
// pre-release cache entry. Without this the precached root HTML and
|
||||
// the cached `/_next/static/*` chunks survived across deploys,
|
||||
// stale-shell-served the old chunk graph, and the running app then
|
||||
// hit `ChunkLoadError` on the first lazy navigation. Pair with the
|
||||
// `<VersionPoller>` client component (mounted in `<Providers>`) that
|
||||
// polls `/api/version`, compares against `NEXT_PUBLIC_APP_VERSION`,
|
||||
// and triggers an SW-unregister + cache-wipe + hard reload when the
|
||||
// server moves ahead of the running shell.
|
||||
const CACHE_VERSION = "v1.4.38.4";
|
||||
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
|
||||
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
|
||||
const MAX_STATIC_ENTRIES = 150;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { I18nProvider } from "@/lib/i18n/context";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { AppSettingsProvider } from "@/components/app-settings-provider";
|
||||
import { VersionPoller } from "@/components/version-poller";
|
||||
|
||||
// ── Theme Context ────────────────────────────────────
|
||||
|
||||
@@ -130,6 +131,7 @@ export function Providers({
|
||||
<AppSettingsProvider>
|
||||
{children}
|
||||
<Toaster position="bottom-right" richColors />
|
||||
<VersionPoller />
|
||||
</AppSettingsProvider>
|
||||
</I18nProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* v1.4.38.4 — runtime self-heal for the stale-shell post-deploy
|
||||
* paper-cut.
|
||||
*
|
||||
* After a release the cached SPA shell still references the old
|
||||
* chunk filenames; the running React tree lazy-loads them, the new
|
||||
* server 404s them, and the user lands on `ChunkLoadError`. The
|
||||
* v1.4.38.3 `AppError` boundary added a one-shot
|
||||
* `window.location.reload()` for that case, but it only fires AFTER
|
||||
* the user trips the error — typically in the middle of a
|
||||
* navigation flow.
|
||||
*
|
||||
* This component runs the check pro-actively. Every 60 s it fetches
|
||||
* `/api/version` and compares the live version string against
|
||||
* `NEXT_PUBLIC_APP_VERSION` (injected from `package.json` at build
|
||||
* time by `next.config.ts`). When the live version moves ahead of
|
||||
* the running shell:
|
||||
*
|
||||
* 1. Unregister every active service worker so the next page load
|
||||
* doesn't reinstall the old SW.
|
||||
* 2. Delete every CacheStorage entry so the precached root HTML
|
||||
* and the `/_next/static/*` chunks from the previous deploy
|
||||
* can't be served back.
|
||||
* 3. `window.location.reload()` — fetches the fresh shell + new
|
||||
* chunk graph.
|
||||
*
|
||||
* `sessionStorage` gates the reload to once per session so a misset
|
||||
* version (server briefly serves a stale image, e.g. mid-deploy
|
||||
* webhook race) cannot loop.
|
||||
*/
|
||||
|
||||
const POLL_INTERVAL_MS = 60_000;
|
||||
const SESSION_GUARD_KEY = "healthlog:version-reload-attempted";
|
||||
const SHELL_VERSION = process.env.NEXT_PUBLIC_APP_VERSION ?? "";
|
||||
|
||||
type VersionEnvelope = {
|
||||
data?: { version?: string };
|
||||
};
|
||||
|
||||
async function fetchLiveVersion(signal: AbortSignal): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch("/api/version", {
|
||||
cache: "no-store",
|
||||
signal,
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const json = (await res.json()) as VersionEnvelope;
|
||||
return json.data?.version ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function evictAndReload(): Promise<void> {
|
||||
try {
|
||||
sessionStorage.setItem(SESSION_GUARD_KEY, String(Date.now()));
|
||||
} catch {
|
||||
// sessionStorage can throw under strict-privacy modes; fall
|
||||
// through to the reload anyway — worst case the next deploy
|
||||
// re-triggers the same heal flow.
|
||||
}
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
try {
|
||||
const regs = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(regs.map((r) => r.unregister()));
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof caches !== "undefined") {
|
||||
try {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map((k) => caches.delete(k)));
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export function VersionPoller(): null {
|
||||
useEffect(() => {
|
||||
if (!SHELL_VERSION) return;
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
if (sessionStorage.getItem(SESSION_GUARD_KEY)) return;
|
||||
} catch {
|
||||
/* fall through — we'll try the reload anyway if mismatch hits */
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
async function checkOnce(): Promise<void> {
|
||||
const live = await fetchLiveVersion(controller.signal);
|
||||
if (!live) return;
|
||||
if (live === SHELL_VERSION) return;
|
||||
await evictAndReload();
|
||||
}
|
||||
|
||||
// First check 5 s after mount — gives the app a chance to settle
|
||||
// before we touch caches. Then on the recurring interval.
|
||||
const initial = window.setTimeout(() => void checkOnce(), 5_000);
|
||||
const interval = window.setInterval(
|
||||
() => void checkOnce(),
|
||||
POLL_INTERVAL_MS,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(initial);
|
||||
window.clearInterval(interval);
|
||||
controller.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user