From eadb768d1ab7610db4466ceef3ed0fee090da05c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 20:54:28 +0000 Subject: [PATCH] =?UTF-8?q?Analyse:=20versp=C3=A4tete=20Statistik-Antwort?= =?UTF-8?q?=20=C3=BCberschreibt=20den=20Stand=20nicht=20mehr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadStats schrieb state.stats/range/stamp schon beim Eintreffen der Antwort, vor der current()-Prüfung der Aufrufer. Eine späte 30-Tage-Antwort überschrieb so den 7-Tage-Stand, und "Alle anzeigen" oder der Ranglisten-Umschalter malte danach mit 30-Tage-Zahlen. Jetzt liefert fetchStats nur die Antwort, adopt() übernimmt sie erst nach current(). Übersicht und Heatmap teilen sich eine laufende Anfrage je Zeitraum. Co-Authored-By: Claude Opus 5.5 --- public/analyse.js | 24 +++++++++++++++++++----- test/ui/analyse.ui.mjs | 23 ++++++++++++++++++++--- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/public/analyse.js b/public/analyse.js index e65dfd1..2a96544 100644 --- a/public/analyse.js +++ b/public/analyse.js @@ -196,12 +196,22 @@ function mountCockpit({ an, stage, project, usable, reload }) { }) stage.setAttribute('aria-labelledby', `an-tab-${tab}`) } - async function loadStats(range) { - const stats = await request(`/api/funktionen/analyse/stats?projectId=${encodeURIComponent(pid)}&range=${range}`) + // Anfrage je Zeitraum: Uebersicht und Heatmap teilen sich eine laufende Anfrage. Sie schreibt nichts in state; uebernommen + // (adopt) wird die Antwort erst nach current(mine), sonst ueberschriebe eine verspaetete den Stand des neueren Zeitraums. + const pending = {} + function fetchStats(range) { + if (!pending[range]) { + const p = request(`/api/funktionen/analyse/stats?projectId=${encodeURIComponent(pid)}&range=${range}`) + const done = () => { if (pending[range] === p) delete pending[range] } + p.then(done, done) + pending[range] = p + } + return pending[range] + } + function adopt(stats, range) { state.stats = stats state.range = range state.stamp = CLOCK.format(new Date()) - return stats } function paintOverview() { @@ -224,14 +234,16 @@ function mountCockpit({ an, stage, project, usable, reload }) { return } stage.innerHTML = skeletonHtml() + let stats try { - await loadStats(range) + stats = await fetchStats(range) } catch (err) { if (!current(mine)) return stage.innerHTML = errorHtml(err.status === 503 ? 'Auswertung gerade nicht erreichbar' : err.message) return } if (!current(mine)) return + adopt(stats, range) paintHead() paintOverview() } @@ -242,14 +254,16 @@ function mountCockpit({ an, stage, project, usable, reload }) { const box = stage.querySelector('#an-heat') if (!state.stats) { box.innerHTML = '

Heatmap

Lade…

' + let stats try { - await loadStats(30) + stats = await fetchStats(30) } catch (err) { if (!current(mine)) return stage.innerHTML = errorHtml(err.status === 503 ? 'Auswertung gerade nicht erreichbar' : err.message) return } if (!current(mine)) return + adopt(stats, 30) paintHead() } renderAnalyseHeat(box, pid, state.stats.pages || []) diff --git a/test/ui/analyse.ui.mjs b/test/ui/analyse.ui.mjs index 6044d7b..a896dbc 100644 --- a/test/ui/analyse.ui.mjs +++ b/test/ui/analyse.ui.mjs @@ -45,14 +45,15 @@ const BEFORE = { 30: { pv: 6080, visitors: 2210, contacts: 143, consentRate: 0.6, engaged: 1500, avgSec: 114, avgScroll: 60 }, 90: { pv: 17900, visitors: 6200, contacts: 470, consentRate: 0.6, engaged: 4500, avgSec: 112, avgScroll: 61 }, } +const SCALE = { 7: 0.25, 30: 1, 90: 3 } // Anteil der 30-Tage-Zahlen je Zeitraum const PAGES = [['/', 2480], ['/preise', 1310], ['/kurse', 1150], ['/routen', 960], ['/oeffnungszeiten', 640], ['/team', 410], ['/kontakt', 300], ['/impressum', 120], ['/datenschutz', 90]] function makeStats(range, over = {}) { const from = addDays(TODAY, -(range - 1)) const pfrom = addDays(from, -range) return { range, from, to: TODAY, firstDay: addDays(pfrom, -40), totals: TOTALS[range], series: daysOf(from, range, 231, Math.min(14, range - 1)), - pages: PAGES.map(([key, count]) => ({ key, count })), - refs: [{ key: 'google.com', count: 880 }, { key: '', count: 700 }, { key: 'instagram.com', count: 400 }, { key: 'l.facebook.com', count: 290 }], + pages: PAGES.map(([key, count]) => ({ key, count: Math.round(count * SCALE[range]) })), + refs: [{ key: 'google.com', count: 880 }, { key: '', count: 700 }, { key: 'instagram.com', count: 400 }, { key: 'l.facebook.com', count: 290 }].map((r) => ({ ...r, count: Math.round(r.count * SCALE[range]) })), countries: [{ key: 'DE', count: 6000 }, { key: 'AT', count: 600 }, { key: 'CH', count: 330 }], devices: [{ key: 'm', count: 4590 }, { key: 'd', count: 2030 }, { key: 't', count: 310 }], contacts: [{ key: 'tel', count: 96 }, { key: 'mail', count: 41 }, { key: 'wa', count: 38 }, { key: 'out', count: 13 }], @@ -267,7 +268,23 @@ try { await page.click('.an-seg [data-range="7"]') await page.waitForFunction(() => document.querySelector('.an-lead')?.textContent.includes('7 Tagen')) await page.waitForTimeout(1900) - check(L('verzögerte erste Antwort (range=30, 1,5 s) überschreibt die spätere (range=7) nicht'), (await text('.an-lead')).includes('7 Tagen') && !(await text('.an-lead')).includes('30 Tagen') && statsRequests.join() === '30,7' && (await page.getAttribute('.an-seg .on', 'data-range')) === '7') + const lateOk = (await text('.an-lead')).includes('7 Tagen') && !(await text('.an-lead')).includes('30 Tagen') && statsRequests.join() === '30,7' && (await page.getAttribute('.an-seg .on', 'data-range')) === '7' + // auch der Zustand dahinter bleibt der 7-Tage-Stand: "Alle anzeigen" und der Umschalter malen mit 7-Tage-Zahlen (Startseite 620, Google 220) + const firstRow = (card) => text(`[data-rank="${card}"] .an-lr`) + const before7 = await firstRow('seen') + await page.click('[data-rank="seen"] [data-rank-more]') + const more = await firstRow('seen') + await page.click('[data-rank="seen"] [data-rank-tab="refs"]') + const refs = await firstRow('seen') + check(L('verzögerte erste Antwort (range=30, 1,5 s) überschreibt die spätere (range=7) nicht'), lateOk && before7.includes('620') && more.includes('620') && !more.includes('2.480') && refs.includes('220') && !refs.includes('880') && (await count('[data-rank="seen"] .an-lr')) === 4) + delayFirst30 = 0 + + // Heatmap-Reiter während der ersten Übersicht-Anfrage: nutzt sie mit, statt dieselbe Anfrage noch einmal zu stellen + delayFirst30 = 1 + await open('full', '.an-seg') + await page.click('.an-tabbtn[data-tab="heatmap"]') + await page.waitForFunction(() => document.querySelectorAll('.hs-dev iframe').length === 2) + check(L('Heatmap-Reiter nutzt die laufende 30-Tage-Anfrage mit'), statsRequests.join() === '30') delayFirst30 = 0 // Website wechseln: Reiter bleibt