fix: Update-Hinweis vergleicht gegen den laufenden Build (D45)
Der Hinweis erschien oft für genau die Fassung, die gerade geladen war. Zwei Fehler mit derselben Wurzel — die Prüfung verglich nie mit dem laufenden Stand: - Relais statt Vergleich: der abgerufene Inhalts-Hash wurde gegen den ZULETZT ABGERUFENEN gehalten (localStorage), nicht gegen den laufenden. Beim Deploy antworten CDN-Knoten unterschiedlich; jeder Wechsel schlug an, und der Schlüssel wird von allen Tabs geteilt. - Klebendes Flag: `werkbaum-update-available` entfernte nur „Jetzt laden". Nach F5 oder „Später" blieb es stehen, und der Load-Check zeigte das Banner bei jedem weiteren Laden erneut. Verglichen wird jetzt der Commit aus dem Footer-Versionslink der laufenden Seite gegen den der abgerufenen HTML (D16 spritzt ihn in beiden Deploy-Wegen ein). Kein localStorage dazwischen; beim Laden wird nichts gemeldet, die Prüfung zwei Sekunden später holt es nach. Eine spätere „aktuell"-Meldung sammelt Banner und Footer-Symbol wieder ein. Rückfall auf den Inhalts-Hash für Builds ohne Marker (Dev-Server), Vergleichsstand dort der erste Abruf der Seiten-Sitzung. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
579dfbf789
commit
f1ac57c931
@@ -2824,3 +2824,88 @@ hervor, dieser Markensatz ist also nicht schreibbar. Das Bild stimmt trotzdem
|
||||
einem Plan ohne `!!!` werden die Marken geschrieben: `- [ ] > Mittel (M)`,
|
||||
`- [ ] > Klein (S)` und `- [ ] > Teil 3 (M)`, während `(L)`, `(XL)` und der
|
||||
Knoten ohne Größe offen bleiben.
|
||||
|
||||
## D45 — Update-Prüfung vergleicht gegen den laufenden Build, nicht gegen einen gemerkten Abruf
|
||||
Gemeldet: „Oft wird der Hinweis, dass eine neue Version vorliege, oben
|
||||
angezeigt, obwohl genau die bereits geladen wurde“ — mit dem Verdacht auf eine
|
||||
Race-Condition und der Vermutung, dass es in der Vorschau-Instanz deshalb
|
||||
immer wieder auftritt. Beides trifft zu, und es sind **zwei** Fehler, die
|
||||
dieselbe Wurzel haben: Die Prüfung verglich nie mit dem Stand, der gerade
|
||||
läuft.
|
||||
|
||||
**Wurzel: ein Relais statt eines Vergleichs.** `checkForUpdates()` holte die
|
||||
Seite und verglich ihren Inhalts-Hash mit `werkbaum-html-hash` — dem Hash des
|
||||
**zuletzt abgerufenen** Stands. Über den läuft die Aussage „neu“ also
|
||||
indirekt: Sie sagt „der Server liefert etwas anderes als beim letzten Abruf“,
|
||||
nicht „der Server liefert etwas anderes als das, was du vor dir hast“. Das
|
||||
sind verschiedene Aussagen, sobald etwas zwischen Laden und Abruf dazwischen
|
||||
kommt — und genau das tut ein CDN. Der Kommentar im Code hielt schon fest,
|
||||
dass GitHub Pages je Cache-Knoten abweichende ETags liefert; dasselbe gilt
|
||||
zeitlich für den **Inhalt**: Während ein Deploy durchläuft, antworten Knoten
|
||||
unterschiedlich, aufeinanderfolgende Abrufe wechseln zwischen alt und neu. Der
|
||||
Hash im localStorage wurde dabei bei **jedem** Abruf nachgeführt, jeder Wechsel
|
||||
schlug also erneut an — auch wenn der laufende Tab längst den neuen Stand
|
||||
hatte. Dazu kommt, dass sich alle Tabs denselben Schlüssel teilen: Zwei
|
||||
geöffnete Tabs schreiben abwechselnd ihren Stand hinein und melden einander
|
||||
Updates.
|
||||
|
||||
**Zweiter Fehler, der eigentliche Dauerbrenner: das Flag blieb kleben.**
|
||||
`werkbaum-update-available` wurde bei einem Fund gesetzt und **nur** vom Knopf
|
||||
„Jetzt laden“ wieder entfernt. Wer statt dessen F5 drückte — oder „Später“, das
|
||||
lediglich das Banner-Element entfernte —, behielt das Flag; und beim Laden
|
||||
stand:
|
||||
|
||||
```js
|
||||
if(!document.hidden && localStorage.getItem('werkbaum-update-available')){
|
||||
checkAndShowUpdateNotification();
|
||||
}
|
||||
```
|
||||
|
||||
Also erschien das Banner ausgerechnet auf der Fassung, die es gerade eingespielt
|
||||
hatte, und danach bei **jedem** weiteren Laden, bis irgendwann jemand den
|
||||
richtigen Knopf traf. Nachgestellt: Flag setzen, neu laden → Banner; „Später“ →
|
||||
Flag steht weiter; neu laden → Banner. Endlos.
|
||||
|
||||
**Entscheidung: die laufende Seite ist der Vergleichsmaßstab, und sie kennt
|
||||
sich selbst.** Beide Deploy-Wege spritzen den Commit in den
|
||||
Footer-Versionslink (D16, `<a class="ver" href="…/commit/<sha>">`) — die
|
||||
laufende Seite trägt ihre Identität also im DOM, die abgerufene im HTML-Text.
|
||||
Zwei Werte, die im selben Moment vorliegen; dazwischen kein Speicher, der
|
||||
altern könnte. Damit sind alle drei Ursachen weg: kein Relais (CDN-Flattern
|
||||
meldet nichts mehr, solange der gelieferte Commit der laufende ist), keine
|
||||
Kopplung zwischen Tabs, und nichts, was ein Neuladen überdauert. Nebengewinn:
|
||||
Schon die **erste** Prüfung nach dem Laden ist aussagekräftig — der alte Weg
|
||||
konnte beim ersten Mal grundsätzlich nichts sagen, weil er erst einen
|
||||
Vergleichsstand anlegen musste.
|
||||
|
||||
**Der Zustand lebt nur noch im Speicher.** `werkbaum-update-available` und
|
||||
`werkbaum-html-hash` werden nicht mehr geschrieben (der Reset räumt sie noch
|
||||
weg, falls sie aus einer früheren Fassung herumliegen). Beim Laden wird
|
||||
grundsätzlich **nichts** gemeldet: Was der Browser gerade geholt hat, *ist* der
|
||||
aktuelle Stand, bis eine Prüfung etwas anderes zeigt — und die läuft zwei
|
||||
Sekunden später ohnehin. Damit kann die Meldung nicht mehr klemmen: Gilt sie
|
||||
noch, kommt sie sofort wieder; gilt sie nicht, bleibt sie weg. „Später“ darf
|
||||
deshalb weiterhin nur das Element entfernen.
|
||||
|
||||
**Die Meldung wird auch wieder eingesammelt.** Sagt eine spätere Prüfung
|
||||
„aktuell“, während das Banner steht, verschwinden Banner und Footer-Symbol.
|
||||
Das deckt den Rollback ab und den Fall, dass ein einzelner Abruf doch einmal
|
||||
gegen einen veralteten Knoten lief.
|
||||
|
||||
**Rückfall für Builds ohne Marker** (Dev-Server, `file://`, lokales
|
||||
`npm run preview`): dort steht im Footer der Platzhalter `…/commit/main`, also
|
||||
keine Commit-Kennung. Dann wird weiter der Inhalts-Hash verglichen — aber gegen
|
||||
den **ersten Abruf dieser Seiten-Sitzung**, der als Vergleichsstand stehen
|
||||
bleibt, statt gegen einen fortlaufend nachgeführten Wert im localStorage. Der
|
||||
Preis ist ein blindes Fenster von zwei Sekunden zwischen Laden und Erstprüfung;
|
||||
auf dem Dev-Server ist das gleichgültig, weil dort HMR arbeitet.
|
||||
|
||||
**Nachgemessen** (Dev-Server, Marker zum Prüfen von Hand eingespritzt):
|
||||
|
||||
| Fall | vorher | nachher |
|
||||
|---|---|---|
|
||||
| Altes Flag im localStorage, neu laden | Banner, bei jedem Laden erneut | kein Banner, drei Takte „✓ Alles aktuell“ |
|
||||
| Gespeicherter Hash ≠ Auslieferung, Seite unverändert | „✅ NEUE VERSION ERKANNT!“ | „✓ Alles aktuell“ |
|
||||
| Server liefert anderen Commit | — | „✅ Neuer Build 2222222“, Banner + Footer-Symbol |
|
||||
| Danach wieder derselbe Commit | Banner blieb stehen | Banner und Symbol verschwinden |
|
||||
| „Später“, dann F5 | Banner sofort wieder da | weg und bleibt weg |
|
||||
|
||||
@@ -491,3 +491,16 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
||||
Diagramm maximiert. Layout-CSS hängt an `body.mobile`, nicht an einer eigenen
|
||||
`@media`-Regel — beide Seiten müssen denselben 640-px-Schwellwert nutzen
|
||||
(SPEC §9, D17).
|
||||
- **Update-Prüfung (D45):** verglichen wird der Commit aus dem
|
||||
Footer-Versionslink der **laufenden** Seite (`runningBuildId()`, DOM) gegen
|
||||
den aus der abgerufenen HTML (`buildIdFromHtml()`). Kein localStorage
|
||||
dazwischen — `werkbaum-update-available`/`werkbaum-html-hash` werden **nicht
|
||||
mehr geschrieben** (der Reset räumt sie nur noch weg). Wer hier etwas
|
||||
„merken" will: Genau das war der Fehler — ein gemerktes „Update verfügbar"
|
||||
überlebt das Neuladen, das es einspielt, und meldet dieselbe Fassung endlos
|
||||
weiter. Beim Laden wird deshalb **nichts** gemeldet; die Prüfung nach zwei
|
||||
Sekunden holt es nach. Zum Testen: Auf dem Dev-Server steht im Footer der
|
||||
Platzhalter `…/commit/main` (keine Commit-Kennung) — dort läuft der
|
||||
Hash-Rückfall, der Marker-Pfad ist nur zu prüfen, wenn man einen echten
|
||||
`commit/<sha>` in `index.html` einspritzt (danach zurücknehmen!) und
|
||||
`window.fetch` überschreibt.
|
||||
|
||||
+72
-28
@@ -3321,7 +3321,35 @@ mountBuildBadge();
|
||||
ALLEN Builds aktiv. */
|
||||
const isProdBuild = import.meta.env.VITE_BUILD_BADGE === 'none';
|
||||
|
||||
/* Kompakter, deterministischer Hash (cyrb53) über den GESAMTEN HTML-Text.
|
||||
/* Beides bewusst NUR im Speicher, nicht im localStorage (D45): Ein gemerktes
|
||||
„Update verfügbar" überlebte das Neuladen, das es gerade eingespielt hat —
|
||||
und meldete dieselbe Fassung wieder und wieder. Nach dem Laden ist nichts
|
||||
bekannt; was noch gilt, findet die nächste Prüfung zwei Sekunden später. */
|
||||
let updateAvailable = false;
|
||||
let baselineHash = null;
|
||||
|
||||
/* Verglichen wird gegen den LAUFENDEN Build, nicht gegen einen gemerkten Abruf.
|
||||
Beide Deploy-Wege spritzen den Commit in den Footer-Versionslink (D16); die
|
||||
laufende Seite trägt ihn also selbst, und die abgerufene Seite auch. Zwei
|
||||
Werte, die im selben Moment vorliegen — kein localStorage dazwischen, damit
|
||||
nichts hängenbleiben und nichts zwischen Tabs durcheinandergeraten kann.
|
||||
Siehe D45. */
|
||||
function buildIdFromHtml(html){
|
||||
const m = html.match(/<a class="ver"[^>]*href="[^"]*\/commit\/([0-9a-f]{7,40})/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
let runningBuild;
|
||||
function runningBuildId(){
|
||||
if(runningBuild === undefined){
|
||||
const el = document.querySelector('.site-footer .ver');
|
||||
const m = el && (el.getAttribute('href') || '').match(/\/commit\/([0-9a-f]{7,40})\b/);
|
||||
runningBuild = m ? m[1] : null; /* Platzhalter „…/commit/main" ⇒ null */
|
||||
}
|
||||
return runningBuild;
|
||||
}
|
||||
|
||||
/* Rückfall für Builds ohne eingespritzten Commit (Dev-Server, `file://`):
|
||||
Kompakter, deterministischer Hash (cyrb53) über den GESAMTEN HTML-Text.
|
||||
Wichtig: Der frühere Ansatz „erste 300 + letzte 300 Zeichen" verfehlte JEDE
|
||||
Änderung — die Seite ist eine self-contained Datei (D19), der komplette
|
||||
App-Code UND die Footer-Version liegen als inline-Bundle in der MITTE, also
|
||||
@@ -3349,25 +3377,44 @@ async function checkForUpdates(){
|
||||
const resp = await fetch(bust.href, { cache: 'no-store' });
|
||||
if(!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
|
||||
/* Voller Content-Hash ist die alleinige Wahrheit. ETag/Last-Modified werden
|
||||
bewusst NICHT mehr herangezogen: GitHub Pages liefert je Cache-Knoten
|
||||
unterschiedliche ETags für identischen Inhalt und löste damit die
|
||||
irreführende Meldung „Metadaten geändert, aber Inhalt gleich" aus. */
|
||||
/* ETag/Last-Modified werden bewusst NICHT herangezogen: GitHub Pages liefert
|
||||
je Cache-Knoten unterschiedliche ETags für identischen Inhalt und löste
|
||||
damit die irreführende Meldung „Metadaten geändert, aber Inhalt gleich" aus. */
|
||||
const html = await resp.text();
|
||||
const hash = hashContent(html);
|
||||
const stored = localStorage.getItem('werkbaum-html-hash');
|
||||
const laufend = runningBuildId(), geliefert = buildIdFromHtml(html);
|
||||
let neu;
|
||||
|
||||
if(!stored){
|
||||
logUpdate('✓ Erste Prüfung – Hash gespeichert');
|
||||
} else if(hash === stored){
|
||||
logUpdate('✓ Alles aktuell');
|
||||
if(laufend && geliefert){
|
||||
neu = geliefert !== laufend;
|
||||
logUpdate(neu ? '✅ Neuer Build ' + geliefert.slice(0, 7) : '✓ Alles aktuell');
|
||||
} else {
|
||||
localStorage.setItem('werkbaum-update-available', 'true');
|
||||
logUpdate('✅ NEUE VERSION ERKANNT!');
|
||||
if(!document.hidden) checkAndShowUpdateNotification();
|
||||
/* Ohne Marker bleibt nur der Inhalts-Hash. Vergleichsstand ist der ERSTE
|
||||
Abruf dieser Seiten-Sitzung und bleibt es — er im localStorage
|
||||
nachgeführt hieße: ein einzelner Abruf gegen einen veralteten
|
||||
CDN-Knoten setzt den Stand um, und der nächste meldet fälschlich neu. */
|
||||
const hash = hashContent(html);
|
||||
if(baselineHash === null){
|
||||
baselineHash = hash;
|
||||
neu = false;
|
||||
logUpdate('✓ Erste Prüfung – Vergleichsstand gesetzt');
|
||||
} else {
|
||||
neu = hash !== baselineHash;
|
||||
logUpdate(neu ? '✅ NEUE VERSION ERKANNT!' : '✓ Alles aktuell');
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('werkbaum-html-hash', hash);
|
||||
if(neu && !updateAvailable){
|
||||
updateAvailable = true;
|
||||
if(!document.hidden) checkAndShowUpdateNotification();
|
||||
else updateFooterUpdateIcon();
|
||||
} else if(!neu && updateAvailable){
|
||||
/* Zurückgenommen (Rollback, oder der Abruf lief vorher gegen einen
|
||||
veralteten Knoten): Meldung wieder einsammeln, statt sie stehenzulassen. */
|
||||
updateAvailable = false;
|
||||
const notif = document.getElementById('updateNotification');
|
||||
if(notif) notif.remove();
|
||||
updateFooterUpdateIcon();
|
||||
}
|
||||
} catch(err) {
|
||||
const msg = err.message || err.toString();
|
||||
if(msg.includes('Failed to fetch')) {
|
||||
@@ -3457,16 +3504,12 @@ document.addEventListener('visibilitychange', () => {
|
||||
if(!document.hidden){
|
||||
checkForUpdates();
|
||||
showUpdateDebug();
|
||||
if(localStorage.getItem('werkbaum-update-available')){
|
||||
checkAndShowUpdateNotification();
|
||||
}
|
||||
if(updateAvailable) checkAndShowUpdateNotification();
|
||||
}
|
||||
});
|
||||
|
||||
/* Prüfe beim Laden, falls Update bereits verfügbar */
|
||||
if(!document.hidden && localStorage.getItem('werkbaum-update-available')){
|
||||
checkAndShowUpdateNotification();
|
||||
}
|
||||
/* Beim Laden wird NICHT gemeldet: Was die Seite gerade geladen hat, IST der
|
||||
aktuelle Stand, bis eine Prüfung etwas anderes zeigt (D45). */
|
||||
|
||||
function checkAndShowUpdateNotification(){
|
||||
const existingNotif = document.getElementById('updateNotification');
|
||||
@@ -3519,7 +3562,6 @@ function checkAndShowUpdateNotification(){
|
||||
document.body.insertBefore(notif, document.body.firstChild);
|
||||
|
||||
notif.querySelector('.updateBtn').addEventListener('click', () => {
|
||||
localStorage.removeItem('werkbaum-update-available');
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
@@ -3539,8 +3581,10 @@ function resetToDefaults(){
|
||||
|
||||
if(!confirmed) return;
|
||||
|
||||
/* Nicht-Dokument-Zustand auf Defaults (UI, Sprache, Update-Flags) — die
|
||||
Dokumentenliste (werkbaum-docs) bleibt erhalten (D22). */
|
||||
/* Nicht-Dokument-Zustand auf Defaults (UI, Sprache, Update-Log) — die
|
||||
Dokumentenliste (werkbaum-docs) bleibt erhalten (D22). Die beiden
|
||||
Update-Schlüssel schreibt niemand mehr (D45); sie werden nur noch
|
||||
aufgeräumt, falls sie aus einer früheren Fassung herumliegen. */
|
||||
['werkbaum-ui','werkbaum-lang','werkbaum-html-hash','werkbaum-update-available','werkbaum-update-log']
|
||||
.forEach(k => { try{ localStorage.removeItem(k); }catch(_){} });
|
||||
|
||||
@@ -3572,7 +3616,7 @@ function resetToDefaults(){
|
||||
/* Update-Benachrichtigung-Symbol im Footer (rechts neben Version) */
|
||||
function updateFooterUpdateIcon(){
|
||||
let icon = document.getElementById('footerUpdateIcon');
|
||||
const hasUpdate = localStorage.getItem('werkbaum-update-available');
|
||||
const hasUpdate = updateAvailable;
|
||||
|
||||
if(hasUpdate && !icon){
|
||||
const footer = document.querySelector('.site-footer');
|
||||
@@ -3609,8 +3653,8 @@ function updateFooterUpdateIcon(){
|
||||
}
|
||||
}
|
||||
|
||||
/* Icon beim Laden und bei Update-Erkennung aktualisieren */
|
||||
document.addEventListener('DOMContentLoaded', updateFooterUpdateIcon);
|
||||
/* Icon bei Update-Erkennung aktualisieren. Beim Laden gibt es nichts zu zeigen —
|
||||
der Zustand lebt nur in dieser Seiten-Sitzung (D45). */
|
||||
const originalCheckAndShowUpdateNotification = checkAndShowUpdateNotification;
|
||||
checkAndShowUpdateNotification = function(){
|
||||
originalCheckAndShowUpdateNotification.call(this);
|
||||
|
||||
Reference in New Issue
Block a user