@cosmicdrift/kumiko-renderer-web 0.57.1 → 0.59.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.1",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"./styles.css": "./src/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.57.2",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.57.2",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.57.2",
|
|
22
22
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
23
23
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.8",
|
package/src/app/create-app.tsx
CHANGED
|
@@ -35,6 +35,7 @@ import { defaultPrimitives } from "../primitives";
|
|
|
35
35
|
import { ToastProvider } from "../primitives/toast";
|
|
36
36
|
import { createEventSourceLiveEvents } from "../sse/live-events";
|
|
37
37
|
import { useBrowserTokensApi } from "../tokens";
|
|
38
|
+
import { UpdateChecker } from "../version/update-checker";
|
|
38
39
|
import { createBrowserLocaleResolver } from "./browser-locale";
|
|
39
40
|
import { type ClientFeatureDefinition, stackWrappers } from "./client-plugin";
|
|
40
41
|
import { useBrowserNavApi } from "./nav";
|
|
@@ -278,6 +279,7 @@ export function createKumikoApp(options: CreateKumikoAppOptions = {}): { readonl
|
|
|
278
279
|
<TreeProvidersProvider value={treeProviders} entities={treeEntities}>
|
|
279
280
|
<ResolversProvider resolvers={resolvers}>
|
|
280
281
|
<ToastProvider>
|
|
282
|
+
<UpdateChecker />
|
|
281
283
|
{stackWrappers(providers, stackWrappers(gates, screenNode))}
|
|
282
284
|
</ToastProvider>
|
|
283
285
|
</ResolversProvider>
|
|
@@ -63,14 +63,10 @@ export function DefaultAppShell({
|
|
|
63
63
|
}: DefaultAppShellProps): ReactNode {
|
|
64
64
|
return (
|
|
65
65
|
<AppLayout
|
|
66
|
-
|
|
66
|
+
fill={fill}
|
|
67
67
|
sidebar={
|
|
68
|
-
<Sidebar
|
|
69
|
-
|
|
70
|
-
{...(sidebarActions !== undefined && { actions: sidebarActions })}
|
|
71
|
-
{...(sidebarFooter !== undefined && { footer: sidebarFooter })}
|
|
72
|
-
>
|
|
73
|
-
<NavTree schema={schema} {...(user !== undefined && { user })} />
|
|
68
|
+
<Sidebar header={brand} actions={sidebarActions} footer={sidebarFooter}>
|
|
69
|
+
<NavTree schema={schema} user={user} />
|
|
74
70
|
</Sidebar>
|
|
75
71
|
}
|
|
76
72
|
>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Update-Erkennung: die Korrektheitsgrenze ist der ID-Vergleich, nicht die
|
|
2
|
+
// Listener-Verdrahtung. shouldShowUpdate kapselt die "nie ein Fake-Banner"-
|
|
3
|
+
// Invariante — hier die vier Fälle direkt geprüft.
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { shouldShowUpdate } from "../update-checker";
|
|
7
|
+
|
|
8
|
+
describe("shouldShowUpdate", () => {
|
|
9
|
+
test("andere Server-id als geladen → Banner", () => {
|
|
10
|
+
expect(shouldShowUpdate("aaa", { id: "bbb", builtAt: "2026-06-18T00:00:00Z" })).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("gleiche id → kein Banner", () => {
|
|
14
|
+
expect(shouldShowUpdate("aaa", { id: "aaa", builtAt: "2026-06-18T00:00:00Z" })).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("kein Server-Stand (Fetch-Fehler/kaputtes JSON → null) → kein Banner", () => {
|
|
18
|
+
expect(shouldShowUpdate("aaa", null)).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("kein geladener Stand (Dev/altes Bundle) → kein Banner, auch bei Server-Drift", () => {
|
|
22
|
+
expect(shouldShowUpdate(undefined, { id: "bbb", builtAt: "2026-06-18T00:00:00Z" })).toBe(false);
|
|
23
|
+
expect(shouldShowUpdate("", { id: "bbb", builtAt: "2026-06-18T00:00:00Z" })).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Update-Awareness — default an, kein Flag. Ein offener Tab erfährt sonst
|
|
2
|
+
// nie von einem neuen Deploy (SPA bleibt geladen, index.html wird nicht neu
|
|
3
|
+
// geholt → "muss hart reloaden"). Statt Service-Worker (der ist die *Ursache*
|
|
4
|
+
// von "hängt auf alter Version") pollt diese Komponente die statische
|
|
5
|
+
// build-info.json beim Tab-Fokus und zeigt ein Reload-Banner bei Drift.
|
|
6
|
+
//
|
|
7
|
+
// Versionsquelle ist build-info.json (Hash über die Asset-URLs, vom Prod-Build
|
|
8
|
+
// geschrieben) — NICHT /api/version (live leer/404, Env-/Dockerfile-Falle).
|
|
9
|
+
// Banner-Strings über kumikoDefaultTranslations (kumiko.version.*, de/en).
|
|
10
|
+
|
|
11
|
+
import { useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
12
|
+
import { type ReactNode, useEffect, useState } from "react";
|
|
13
|
+
import { cn } from "../lib/cn";
|
|
14
|
+
|
|
15
|
+
// Build-Stand, der beim Page-Load aktiv war. Vom Prod-Build in die index.html
|
|
16
|
+
// gebacken (build-prod-bundle injectAssetTags). Fehlt im Dev und in alten
|
|
17
|
+
// Bundles → der Checker macht dann nichts (fail-safe).
|
|
18
|
+
type KumikoBuild = {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly builtAt: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
declare global {
|
|
24
|
+
interface Window {
|
|
25
|
+
__KUMIKO_BUILD__?: KumikoBuild;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Korrektheitsgrenze der Update-Erkennung: ein Banner NUR bei echtem
|
|
30
|
+
// ID-Drift. Kein geladener Stand (Dev/altes Bundle) oder kein Server-Stand
|
|
31
|
+
// (Fetch-Fehler/kaputtes JSON → null) → nie ein Fake-Banner.
|
|
32
|
+
export function shouldShowUpdate(
|
|
33
|
+
loadedId: string | undefined,
|
|
34
|
+
server: KumikoBuild | null,
|
|
35
|
+
): boolean {
|
|
36
|
+
if (!loadedId || !server) return false;
|
|
37
|
+
return server.id !== loadedId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchServerBuild(): Promise<KumikoBuild | null> {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch("/build-info.json", { cache: "no-store" });
|
|
43
|
+
if (!res.ok) return null;
|
|
44
|
+
const info = (await res.json()) as Partial<KumikoBuild>;
|
|
45
|
+
if (typeof info.id !== "string" || info.id.length === 0) return null;
|
|
46
|
+
return { id: info.id, builtAt: typeof info.builtAt === "string" ? info.builtAt : "" };
|
|
47
|
+
} catch {
|
|
48
|
+
// Netzwerkfehler / kaputtes JSON → kein Banner. Nie ein Fake-Update zeigen.
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function UpdateChecker(): ReactNode {
|
|
54
|
+
const t = useTranslation();
|
|
55
|
+
const loaded = typeof window !== "undefined" ? window.__KUMIKO_BUILD__ : undefined;
|
|
56
|
+
const [hasUpdate, setHasUpdate] = useState(false);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const loadedId = loaded?.id;
|
|
60
|
+
if (!loadedId) return; // Dev / altes Bundle → keine Awareness.
|
|
61
|
+
|
|
62
|
+
let cancelled = false;
|
|
63
|
+
const check = async (): Promise<void> => {
|
|
64
|
+
if (document.visibilityState !== "visible") return;
|
|
65
|
+
const server = await fetchServerBuild();
|
|
66
|
+
if (!cancelled && shouldShowUpdate(loadedId, server)) setHasUpdate(true);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
document.addEventListener("visibilitychange", check);
|
|
70
|
+
window.addEventListener("focus", check);
|
|
71
|
+
void check();
|
|
72
|
+
return () => {
|
|
73
|
+
cancelled = true;
|
|
74
|
+
document.removeEventListener("visibilitychange", check);
|
|
75
|
+
window.removeEventListener("focus", check);
|
|
76
|
+
};
|
|
77
|
+
}, [loaded?.id]);
|
|
78
|
+
|
|
79
|
+
if (!hasUpdate) return null;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
role="status"
|
|
84
|
+
className={cn(
|
|
85
|
+
"fixed inset-x-0 top-0 z-[200] flex items-center justify-center gap-3",
|
|
86
|
+
"border-b bg-background px-4 py-2 text-sm text-foreground shadow-md",
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
<span>{t("kumiko.version.update-available")}</span>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={() => window.location.reload()}
|
|
93
|
+
className={cn(
|
|
94
|
+
"rounded-md bg-primary px-3 py-1 text-xs font-medium text-primary-foreground",
|
|
95
|
+
"hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-ring",
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{t("kumiko.actions.reload")}
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|