Analyse: Heatmap-Link, Sperre, leere Domains, Tab-Wettlauf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5a6fbc7328
commit
7fc5ba8528
3 files changed
+25
-13
No files matched your search
+19
-9
@@ -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')
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
Reference in new issue
Block a user