Titel-Mockup getauscht, Projektstand geht beim Laden nicht mehr verloren

Der Ladepfad hat jeden Stand ab Version 3 verworfen: STATE_V stand auf 3,
die Pruefung liess aber nur 1 und 2 durch. Damit lief das Blatt mit der
Voreinstellung an und schrieb sie ueber die vorhandene Datei. Die Pruefung
gilt jetzt von 1 bis STATE_V.

Damit so etwas nicht mehr unbemerkt bleibt: liegt eine Standdatei vor, die
sich nicht anwenden laesst, wird nichts mehr zurueckgeschrieben, weder in
die Datei noch in den localStorage.

Merkzettel gehaertet. "Stand merken" liest die Datei gegen und meldet erst
dann Erfolg. "Zurueck zum Stand" prueft Version und Slide-Anzahl, bevor es
den laufenden Stand ersetzt, legt ihn vorher als <projekt>-vorher ab und
laedt erst neu, wenn die Datei nachweislich steht.

Auf der Titel-Slide steht das Geraet jetzt auf dem Fels-Mockup. Die
Wortmarke steckt im Bild, deshalb faellt die separate wabe-logo dort weg.
Der Rahmen ist auf 880px breiter, weil das Bild links und rechts rund ein
Viertel freien Rand mitbringt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-07 00:04:34 +02:00
1 parent 25ffc30c4e
commit 344f58dde0
2 files changed
+100 -24

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+100 -24
View File
@@ -656,7 +656,11 @@
Display, keine Projektion per matrix3d, kein zweiter Screenshot dahinter. Display, keine Projektion per matrix3d, kein zweiter Screenshot dahinter.
Damit fallen auch die gemessenen Vierecke und Eckradien weg, die das Damit fallen auch die gemessenen Vierecke und Eckradien weg, die das
vorherige Szenen-Mockup gebraucht hat. */ vorherige Szenen-Mockup gebraucht hat. */
.szene { position: relative; width: 720px; } /* Breiter als das vorherige Mockup, weil dieses Bild links und rechts rund
ein Viertel freien Rand mitbringt: bei 880px ist das Geraet selbst wieder
etwa so gross wie vorher, und 880 bleibt innerhalb der 888px, die zwischen
den Innenraendern der Slide frei sind. */
.szene { position: relative; width: 880px; }
.szene-dev { width: 100%; height: auto; display: block; } .szene-dev { width: 100%; height: auto; display: block; }
.laptop-missing { .laptop-missing {
@@ -1061,7 +1065,7 @@
"logo-full": { label: "Logo", size: 560, prop: "width" }, "logo-full": { label: "Logo", size: 560, prop: "width" },
"screen-logo": { label: "Logo im Screen", size: 430, prop: "width" }, "screen-logo": { label: "Logo im Screen", size: 430, prop: "width" },
"laptop": { label: "Laptop", size: 960, prop: "width" }, "laptop": { label: "Laptop", size: 960, prop: "width" },
"szene": { label: "Laptop · Board", size: 720, prop: "width" }, "szene": { label: "Laptop · Board", size: 880, prop: "width" },
"mock": { label: "Bild", size: null, prop: "width" }, "mock": { label: "Bild", size: null, prop: "width" },
"mock-2": { label: "Zweites Bild", size: null, prop: "width" }, "mock-2": { label: "Zweites Bild", size: null, prop: "width" },
"note": { label: "Notizzettel", size: null, prop: "width" }, "note": { label: "Notizzettel", size: null, prop: "width" },
@@ -1148,7 +1152,22 @@
state/<projekt>.json. Nur die Datei lässt sich pushen, also ist sie die state/<projekt>.json. Nur die Datei lässt sich pushen, also ist sie die
Wahrheit; der localStorage ist der Rückfall, wenn der Server nicht läuft. */ Wahrheit; der localStorage ist der Rückfall, wenn der Server nicht läuft. */
let pushTimer = null; let pushTimer = null;
/* Ein vorhandener Stand, der sich nicht lesen laesst, ist ein Grund zum
Anhalten und nicht zum Weiterarbeiten: sonst laeuft das Blatt mit der
Voreinstellung an und schreibt die beim ersten Klick ueber genau die
Datei, die gerade nicht verstanden wurde. Solange die Sperre steht, geht
nichts mehr raus, weder in die Datei noch in den localStorage. */
let stateLocked = false;
function lockState(grund) {
stateLocked = true;
clearTimeout(pushTimer); pushTimer = null;
setStatus("Stand nicht geladen (" + grund + "). Es wird nichts gespeichert, "
+ "damit die vorhandene Datei nicht ueberschrieben wird.", "err");
}
function saveState() { function saveState() {
if (stateLocked) return;
const state = currentState(); const state = currentState();
try { localStorage.setItem(STORE, JSON.stringify(state)); } try { localStorage.setItem(STORE, JSON.stringify(state)); }
catch (err) { console.warn("Speichern fehlgeschlagen", err); } catch (err) { console.warn("Speichern fehlgeschlagen", err); }
@@ -1159,6 +1178,7 @@
async function pushState(state) { async function pushState(state) {
pushTimer = null; pushTimer = null;
if (stateLocked) return;
try { try {
const res = await fetch("/state?project=" + PROJECT, { const res = await fetch("/state?project=" + PROJECT, {
method: "POST", method: "POST",
@@ -1171,24 +1191,34 @@
// Ein Reload darf keine Änderung verschlucken, die noch im Timer hängt. // Ein Reload darf keine Änderung verschlucken, die noch im Timer hängt.
addEventListener("beforeunload", () => { addEventListener("beforeunload", () => {
if (!pushTimer) return; if (!pushTimer || stateLocked) return;
clearTimeout(pushTimer); clearTimeout(pushTimer);
navigator.sendBeacon("/state?project=" + PROJECT, navigator.sendBeacon("/state?project=" + PROJECT,
new Blob([JSON.stringify(currentState())], { type: "application/json" })); new Blob([JSON.stringify(currentState())], { type: "application/json" }));
}); });
async function loadState() { async function loadState() {
let d = null, fromFile = false; let d = null, fromFile = false, fileSeen = false;
// Erst die Datei aus dem Repo, dann der Browser. Wer das Projekt frisch // Erst die Datei aus dem Repo, dann der Browser. Wer das Projekt frisch
// gezogen hat, soll den gepushten Stand sehen und nicht seinen eigenen alten. // gezogen hat, soll den gepushten Stand sehen und nicht seinen eigenen alten.
try { try {
const res = await fetch(STATE_URL, { cache: "no-store" }); const res = await fetch(STATE_URL, { cache: "no-store" });
if (res.ok) { d = await res.json(); fromFile = true; } if (res.ok) { fileSeen = true; d = await res.json(); fromFile = true; }
} catch (_) {} } catch (_) {}
// Datei ist da, laesst sich aber nicht lesen: anhalten statt ueberschreiben.
if (fileSeen && !d) { lockState("Datei unlesbar"); return false; }
if (!d) { if (!d) {
try { d = JSON.parse(localStorage.getItem(STORE) || "null"); } catch (_) {} try { d = JSON.parse(localStorage.getItem(STORE) || "null"); } catch (_) {}
} }
if (!d || !(d.v === 1 || d.v === 2)) return false; if (!d) return false;
// Jeder Stand von 1 bis zum aktuellen ist gueltig; aeltere hebt migrateState
// an. Frueher standen hier nur 1 und 2 fest verdrahtet, dadurch wurde jeder
// frisch gespeicherte Stand beim Laden verworfen und von der Voreinstellung
// ueberschrieben.
if (!(d.v >= 1 && d.v <= STATE_V)) {
if (fromFile) { lockState("Stand " + d.v + ", dieses Blatt kennt bis " + STATE_V); }
return false;
}
const migrated = migrateState(d); const migrated = migrateState(d);
// Stand aus dem Browser, aber noch keine Datei: einmalig hinueberschreiben, // Stand aus dem Browser, aber noch keine Datei: einmalig hinueberschreiben,
// sonst bliebe ein ueber Wochen gewachsener Stand ungeteilt liegen. // sonst bliebe ein ueber Wochen gewachsener Stand ungeteilt liegen.
@@ -1381,13 +1411,15 @@
} }
const logo = variant === "dunkel" ? assets["./assets/brand/logo-white.svg"] : assets["./assets/brand/logo.svg"]; const logo = variant === "dunkel" ? assets["./assets/brand/logo-white.svg"] : assets["./assets/brand/logo.svg"];
if (layoutState[i] === "szene") { if (layoutState[i] === "szene") {
const dev = assets["./assets/mockups/macbook-board.png"] // Die Wortmarke steckt in diesem Mockup schon im Bild. Ein zweites Logo
? '<div class="szene"><img class="szene-dev" alt="" src="' + // auf der Slide waere eine Dopplung, deshalb faellt hier die wabe-logo
assets["./assets/mockups/macbook-board.png"] + '"></div>' // weg und die Zeile oben traegt nur die Tagline.
: '<div class="laptop-missing">Mockup fehlt.<br>Das freigestellte PNG als <b>assets/mockups/macbook-board.png</b> ablegen.</div>'; const dev = assets["./assets/mockups/macbook-fels.png"]
? '<div class="szene"><img class="szene-dev" alt="HiveGrid" src="' +
assets["./assets/mockups/macbook-fels.png"] + '"></div>'
: '<div class="laptop-missing">Mockup fehlt.<br>Das freigestellte PNG als <b>assets/mockups/macbook-fels.png</b> ablegen.</div>';
return '<div class="title-top">' + return '<div class="title-top">' +
'<p class="wabe-tagline">Vom Handwerker<br>für <span class="a">Handwerker</span></p>' + '<p class="wabe-tagline">Vom Handwerker<br>für <span class="a">Handwerker</span></p>' +
'<img class="wabe-logo" alt="HiveGrid" src="' + logo + '">' +
'</div>' + dev + '</div>' + dev +
'<p class="wabe-claim">' + s.sub + '</p>'; '<p class="wabe-claim">' + s.sub + '</p>';
} }
@@ -1641,8 +1673,8 @@
.concat(SLIDES.filter(s => s.media2).map(s => s.media2.img)); .concat(SLIDES.filter(s => s.media2).map(s => s.media2.img));
paths.push("./assets/screens/pipelines.png", "./assets/screens/chat-tablet.png"); paths.push("./assets/screens/pipelines.png", "./assets/screens/chat-tablet.png");
// Optional: the layout falls back to a hint if the file is not there yet. // Optional: the layout falls back to a hint if the file is not there yet.
const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-board.png"]); const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]);
paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-board.png"); paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png");
for (const path of [...new Set(paths)]) { for (const path of [...new Set(paths)]) {
const res = await fetch(path, { cache: "no-store" }).catch(() => null); const res = await fetch(path, { cache: "no-store" }).catch(() => null);
if (!res || !res.ok) { if (!res || !res.ok) {
@@ -2394,26 +2426,56 @@
const snapSave = document.getElementById("snap-save"); const snapSave = document.getElementById("snap-save");
const snapLoad = document.getElementById("snap-load"); const snapLoad = document.getElementById("snap-load");
// Was aus der Merkdatei kommt, wird geprueft, bevor es den laufenden Stand
// ersetzt: Slide-Anzahl und Version muessen zu diesem Blatt passen.
function snapUsable(d) {
return !!d && typeof d.v === "number" && d.v >= 1 && d.v <= STATE_V
&& Array.isArray(d.edits) && d.edits.length === SLIDES.length
&& Array.isArray(d.layoutState) && d.layoutState.length === SLIDES.length
&& Array.isArray(d.bgState) && d.bgState.length === SLIDES.length;
}
// Immer frisch: der Merkzettel wird waehrend der Arbeit ueberschrieben, ein
// gecachter Treffer wuerde den Stand von vorhin zurueckholen.
function snapUrl() {
return "./state/" + SNAP_PROJECT + ".json?t=" + Date.now();
}
async function readSnap() {
const res = await fetch(snapUrl(), { cache: "no-store" });
if (!res.ok) throw new Error("kein gemerkter Stand");
return await res.json();
}
async function snapExists() { async function snapExists() {
try { try { return snapUsable(await readSnap()); }
const res = await fetch("./state/" + SNAP_PROJECT + ".json", { cache: "no-store" }); catch (_) { return false; }
return res.ok;
} catch (_) { return false; }
} }
snapExists().then(ok => { snapLoad.disabled = !ok; }); snapExists().then(ok => { snapLoad.disabled = !ok; });
snapSave.addEventListener("click", async () => { snapSave.addEventListener("click", async () => {
snapSave.disabled = true; snapSave.disabled = true;
try { try {
const mine = currentState();
const res = await fetch("/state?project=" + SNAP_PROJECT, { const res = await fetch("/state?project=" + SNAP_PROJECT, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(currentState()), body: JSON.stringify(mine),
}); });
if (!res.ok) throw new Error("HTTP " + res.status); if (!res.ok) throw new Error("HTTP " + res.status);
// Gegengelesen: gemerkt ist erst, was auch wirklich in der Datei steht.
// Ein "ok" vom Server allein hat hier schon einmal zu wenig bewiesen.
const back = await readSnap();
if (!snapUsable(back)) throw new Error("Datei unbrauchbar zurueckgelesen");
if (JSON.stringify(back.edits) !== JSON.stringify(mine.edits)
|| JSON.stringify(back.bgState) !== JSON.stringify(mine.bgState)
|| JSON.stringify(back.layoutState) !== JSON.stringify(mine.layoutState)) {
throw new Error("Datei weicht vom Stand ab");
}
snapLoad.disabled = false; snapLoad.disabled = false;
setStatus("Stand gemerkt", "ok"); setStatus("Stand gemerkt und gegengelesen", "ok");
} catch (err) { } catch (err) {
snapLoad.disabled = !(await snapExists());
setStatus("Merken fehlgeschlagen: " + err.message, "err"); setStatus("Merken fehlgeschlagen: " + err.message, "err");
} finally { snapSave.disabled = false; } } finally { snapSave.disabled = false; }
}); });
@@ -2421,21 +2483,35 @@
snapLoad.addEventListener("click", async () => { snapLoad.addEventListener("click", async () => {
snapLoad.disabled = true; snapLoad.disabled = true;
try { try {
const res = await fetch("./state/" + SNAP_PROJECT + ".json", { cache: "no-store" }); const d = await readSnap();
if (!res.ok) throw new Error("kein gemerkter Stand"); // Erst pruefen, dann ersetzen. Ein Merkzettel, der nicht passt, darf
const d = await res.json(); // nicht den Stand kosten, der gerade auf dem Schirm steht.
// Erst den laufenden Timer entschaerfen, sonst schreibt er beim Entladen if (!snapUsable(d)) throw new Error("gemerkter Stand passt nicht zu diesem Blatt");
// den alten Stand ueber den gerade zurueckgeholten. // Rueckfahrkarte: der Stand von jetzt wird als <projekt>-vorher abgelegt,
// bevor er ueberschrieben wird. Ein Fehlklick ist damit umkehrbar.
await fetch("/state?project=" + PROJECT + "-vorher", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(currentState()),
}).catch(() => {});
// Den laufenden Timer entschaerfen und dichtmachen, sonst schreibt der
// Reload den alten Stand ueber den gerade zurueckgeholten.
clearTimeout(pushTimer); pushTimer = null; clearTimeout(pushTimer); pushTimer = null;
stateLocked = true;
const put = await fetch("/state?project=" + PROJECT, { const put = await fetch("/state?project=" + PROJECT, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(d), body: JSON.stringify(d),
}); });
if (!put.ok) throw new Error("HTTP " + put.status); if (!put.ok) throw new Error("HTTP " + put.status);
// Gegenlesen, bevor neu geladen wird: erst wenn die Datei steht, ist der
// Reload sicher. Sonst startet das Blatt ohne Stand.
const back = await fetch(STATE_URL + "?t=" + Date.now(), { cache: "no-store" });
if (!back.ok || !snapUsable(await back.json())) throw new Error("Stand nicht geschrieben");
try { localStorage.setItem(STORE, JSON.stringify(d)); } catch (_) {} try { localStorage.setItem(STORE, JSON.stringify(d)); } catch (_) {}
location.reload(); location.reload();
} catch (err) { } catch (err) {
stateLocked = false;
snapLoad.disabled = false; snapLoad.disabled = false;
setStatus("Zurueckholen fehlgeschlagen: " + err.message, "err"); setStatus("Zurueckholen fehlgeschlagen: " + err.message, "err");
} }