Analyse: Heatmap-Link, Sperre, leere Domains, Tab-Wettlauf

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-01 10:27:20 +00:00
1 parent 5a6fbc7328
commit 7fc5ba8528
3 files changed
+25 -13

No files matched your search

+19 -9
View File
@@ -100,7 +100,7 @@ function newsTile(projects) {
/* Analyse: Kachel je nach Zustand der Websites (comingSoon = Schalter aus) */
const analyseOf = (project) => project.analyse || { comingSoon: true }
const analyseState = (a) => (a.comingSoon ? 'soon' : a.state === 'active' ? 'active' : a.eligible ? 'off' : 'blocked')
const analyseState = (a) => (a.comingSoon ? 'soon' : a.state === 'unknown' ? 'unknown' : a.state === 'disabled' ? 'disabled' : a.state === 'active' ? 'active' : a.eligible ? 'off' : 'blocked')
const ANALYSE_TEXT = 'Sieh, wie viele Besucher deine Website hat und was sie sich ansehen.'
function pickAnalyseProject(projects) {
@@ -113,9 +113,12 @@ function analyseTile(projects) {
if (!target || a.comingSoon) return soonTile('chart', 'Analyse', ANALYSE_TEXT)
const state = analyseState(a)
const active = projects.filter((p) => analyseState(analyseOf(p)) === 'active').length
const pill = state === 'active' ? ['Aktiv', 'on'] : ['Aus', 'off']
const pill = state === 'active' ? ['Aktiv', 'on'] : state === 'disabled' ? ['Gesperrt', 'off'] : ['Aus', 'off']
let action
if (state === 'active') action = `<button type="button" class="kb-btn-primary fn-tile-cta" data-view="analyse" data-project="${esc(target.projectId)}">Auswertung ansehen</button>`
let text = `${ANALYSE_TEXT} Ohne Cookies, auf einem Server in Deutschland.`
if (state === 'unknown') action = '<p class="fn-count">Die Daten konnten gerade nicht geladen werden.</p>'
else if (state === 'disabled') { text = 'Bitte melde dich bei uns'; action = '<button type="button" class="kb-btn-primary fn-tile-cta" data-view="chat">Chat öffnen</button>' }
else if (state === 'active') action = `<button type="button" class="kb-btn-primary fn-tile-cta" data-view="analyse" data-project="${esc(target.projectId)}">Auswertung ansehen</button>`
else if (state === 'off') action = `<button type="button" class="kb-btn-primary fn-tile-cta" data-view="analyse" data-project="${esc(target.projectId)}">Einrichten</button>`
else if (a.reason === 'kein_abo') action = '<p class="fn-count">Im Wartungs-Abo enthalten. <button type="button" class="kb-link" data-view="vertraege">Zu Verträge</button></p>'
else action = `<p class="fn-count">${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}</p>`
@@ -124,7 +127,7 @@ function analyseTile(projects) {
<article class="fn-tile" data-fn-tile="analyse">
<div class="fn-tile-head">${svg('chart')}<span class="fn-pill fn-pill-${pill[1]}">${pill[0]}</span></div>
<h3>Analyse</h3>
<p>${ANALYSE_TEXT} Ohne Cookies, auf einem Server in Deutschland.</p>
<p>${text}</p>
${count}
${action}
</article>`
@@ -147,7 +150,7 @@ export async function initFunktionen(root) {
${analyseTile(projects)}
</div>`
root.querySelector('[data-fn-tile="news"] .fn-tile-cta')?.addEventListener('click', (ev) => { selection.projectId = ev.currentTarget.dataset.project })
root.querySelector('[data-fn-tile="analyse"] .fn-tile-cta')?.addEventListener('click', (ev) => { analyseSel.projectId = ev.currentTarget.dataset.project })
root.querySelector('[data-fn-tile="analyse"] .fn-tile-cta[data-project]')?.addEventListener('click', (ev) => { analyseSel.projectId = ev.currentTarget.dataset.project })
}
/* ---------------------------------------------------------------- News --- */
@@ -576,6 +579,7 @@ export async function initAnalyse(root, { projectId } = {}) {
const reload = () => initAnalyse(root).catch((err) => { root.innerHTML = `<p class="muted">${esc(err.message)}</p>` })
if (a.state === 'active') renderAnalyseStats(stage, project, reload)
else if (a.state === 'disabled') stage.innerHTML = '<div class="kb-panel"><h3>Analyse ist abgeschaltet</h3><p>Für diese Website ist die Analyse im Moment gesperrt. <button type="button" class="kb-link" data-view="chat">Schreib uns im Chat</button>, dann klären wir das.</p></div>'
else if (a.state === 'unknown') stage.innerHTML = '<div class="kb-panel"><p>Die Daten konnten gerade nicht geladen werden.</p></div>'
else if (!a.eligible) stage.innerHTML = `<div class="kb-panel"><h3>Analyse nicht verfügbar</h3><p>${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}${a.reason === 'kein_abo' ? ' <button type="button" class="kb-link" data-view="vertraege">Zu Verträge</button>' : ''}</p></div>`
else renderAnalyseActivation(stage, project, reload)
}
@@ -670,7 +674,9 @@ function barList(title, items, labelOf, { html = false } = {}) {
const COUNTRY_ROW = (key) => `${flagEmoji(key) ? `<span aria-hidden="true">${flagEmoji(key)}</span> ` : ''}${esc(countryName(key))}`
let statsRequestId = 0
async function renderAnalyseStats(stage, project, reload) {
const myRequest = ++statsRequestId
const range = [7, 30, 90].includes(analyseSel.range) ? analyseSel.range : 30
const pid = project.projectId
stage.innerHTML = `
@@ -687,11 +693,11 @@ async function renderAnalyseStats(stage, project, reload) {
try {
stats = await request(`/api/funktionen/analyse/stats?projectId=${encodeURIComponent(pid)}&range=${range}`)
} catch (err) {
if (!stage.isConnected) return
if (!stage.isConnected || myRequest !== statsRequestId) return
data.innerHTML = `<p class="error" role="alert">${esc(err.status === 503 ? 'Auswertung gerade nicht erreichbar' : err.message)}</p>`
return
}
if (!stage.isConnected) return
if (!stage.isConnected || myRequest !== statsRequestId) return
const t = stats.totals || {}
const rate = Math.round((Number(t.consentRate) || 0) * 100)
const kpi = (label, value) => `<div class="an-kpi"><span>${label}</span><b>${esc(value)}</b></div>`
@@ -725,15 +731,19 @@ function renderAnalyseHeat(box, projectId, pages) {
<label>Gerät<select id="an-heat-vp">${Object.entries(DEVICE_LABELS).map(([k, v]) => `<option value="${k}"${k === 'm' ? ' selected' : ''}>${v}</option>`).join('')}</select></label>
</div>
<p class="error hidden" id="an-heat-error" role="alert"></p>
<div class="abo-actions"><button type="button" class="abo-btn" id="an-heat-go">Auf meiner Website ansehen</button></div>`
<div class="abo-actions"><button type="button" class="abo-btn" id="an-heat-go">Auf meiner Website ansehen</button></div>
<div class="abo-actions" id="an-heat-link" aria-live="polite"></div>`
const go = box.querySelector('#an-heat-go')
const error = box.querySelector('#an-heat-error')
go.addEventListener('click', async () => {
go.disabled = true
error.classList.add('hidden')
box.querySelector('#an-heat-link').innerHTML = ''
try {
const { url } = await request('/api/funktionen/analyse/heatmap-link', { method: 'POST', body: { projectId, path: box.querySelector('#an-heat-page').value || '/', vp: box.querySelector('#an-heat-vp').value } })
if (safeUrl(url)) window.open(url, '_blank', 'noopener')
// Kein window.open nach await (Safari/iOS blockiert das): sichtbarer Link, den der Kunde selbst antippt
const link = box.querySelector('#an-heat-link')
link.innerHTML = safeUrl(url) ? `<a class="abo-btn" href="${esc(url)}" target="_blank" rel="noopener">Heatmap öffnen</a>` : ''
} catch (err) {
error.textContent = err.status === 503 ? 'Auswertung gerade nicht erreichbar' : err.message
error.classList.remove('hidden')
+1
View File
@@ -139,6 +139,7 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons
const eligibility = await analyseEligibility(customerId, projectId)
if (!eligibility.ok) throw httpError(ELIGIBILITY_STATUS[eligibility.reason], ELIGIBILITY_MESSAGES[eligibility.reason])
const { project } = eligibility
if (!analyseDomains(project).length) throw httpError(409, 'Keine gültige Domain für die Analyse')
const existing = await ensureFeatureDoc(customerId, projectId, FEATURE_KEY)
if (existing.state === 'disabled' && by === 'customer') {
+5 -4
View File
@@ -230,10 +230,11 @@ test('Härtung: ungültige Hosts, Pfadzeichen, Fehler bei einem Projekt', async
const link = (path) => post(`${A}/heatmap-link`, { projectId: 'p1', path, vp: 'd' }, { cookie })
for (const path of ['/a\\b', '/a b', '/a\nb', '/a\u0000b', '/a\tb', '/a\u007fb']) assert.equal((await link(path)).status, 400, JSON.stringify(path))
assert.equal((await link('/über-uns')).status, 200)
// keine gueltige Domain => 409
// keine gueltige Domain => Aktivieren scheitert mit 409, vor dem Sammeldienst
aw.seed('websiteProjects', { $id: 'p7', customerId: 'c1', projectName: 'Kaputt', subdomain: 'ka putt', liveDomain: 'x_y', isPublic: true })
await post(`${A}/activate`, { projectId: 'p7', consent: consent() }, { cookie })
const bad = await post(`${A}/heatmap-link`, { projectId: 'p7', path: '/', vp: 'd' }, { cookie })
const before = puts.length
const bad = await post(`${A}/activate`, { projectId: 'p7', consent: consent() }, { cookie })
assert.equal(bad.status, 409)
assert.equal(bad.body.error, 'Keine gültige Domain')
assert.equal(bad.body.error, 'Keine gültige Domain für die Analyse')
assert.equal(puts.length, before)
})