diff --git a/public/funktionen.js b/public/funktionen.js index 082aa5d..ff130aa 100644 --- a/public/funktionen.js +++ b/public/funktionen.js @@ -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 = `` + let text = `${ANALYSE_TEXT} Ohne Cookies, auf einem Server in Deutschland.` + if (state === 'unknown') action = '

Die Daten konnten gerade nicht geladen werden.

' + else if (state === 'disabled') { text = 'Bitte melde dich bei uns'; action = '' } + else if (state === 'active') action = `` else if (state === 'off') action = `` else if (a.reason === 'kein_abo') action = '

Im Wartungs-Abo enthalten.

' else action = `

${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}

` @@ -124,7 +127,7 @@ function analyseTile(projects) {
${svg('chart')}${pill[0]}

Analyse

-

${ANALYSE_TEXT} Ohne Cookies, auf einem Server in Deutschland.

+

${text}

${count} ${action}
` @@ -147,7 +150,7 @@ export async function initFunktionen(root) { ${analyseTile(projects)} ` 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 = `

${esc(err.message)}

` }) if (a.state === 'active') renderAnalyseStats(stage, project, reload) else if (a.state === 'disabled') stage.innerHTML = '

Analyse ist abgeschaltet

Für diese Website ist die Analyse im Moment gesperrt. , dann klären wir das.

' + else if (a.state === 'unknown') stage.innerHTML = '

Die Daten konnten gerade nicht geladen werden.

' else if (!a.eligible) stage.innerHTML = `

Analyse nicht verfügbar

${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}${a.reason === 'kein_abo' ? ' ' : ''}

` else renderAnalyseActivation(stage, project, reload) } @@ -670,7 +674,9 @@ function barList(title, items, labelOf, { html = false } = {}) { const COUNTRY_ROW = (key) => `${flagEmoji(key) ? ` ` : ''}${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 = `` 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) => `
${label}${esc(value)}
` @@ -725,15 +731,19 @@ function renderAnalyseHeat(box, projectId, pages) { -
` +
+ ` 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) ? `Heatmap öffnen` : '' } catch (err) { error.textContent = err.status === 503 ? 'Auswertung gerade nicht erreichbar' : err.message error.classList.remove('hidden') diff --git a/server/services/analyse.js b/server/services/analyse.js index 9b22f33..6f6a675 100644 --- a/server/services/analyse.js +++ b/server/services/analyse.js @@ -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') { diff --git a/test/analyseRoutes.test.js b/test/analyseRoutes.test.js index 5b7ca2d..e4e52c9 100644 --- a/test/analyseRoutes.test.js +++ b/test/analyseRoutes.test.js @@ -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) })