Analyse: verspätete Statistik-Antwort überschreibt den Stand nicht mehr

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 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 21:34:43 +00:00
1 parent a9f0711e97
commit eadb768d1a
2 files changed
+39 -8

No files matched your search

+19 -5
View File
@@ -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 = '<h3>Heatmap</h3><p class="an-empty">Lade…</p>'
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 || [])
+20 -3
View File
@@ -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