Website-Statistik: eigene Seite für die Analyse von webklar.com

Neue Mitarbeiterseite /statistik (für alle Mitarbeiter, Eintrag nach "Rechtliches"):
Kennzahlen mit Vergleich zur Vorperiode, Verlauf pro Tag (uPlot), Ranglisten für
Seiten, Herkunft, Länder, Geräte und Kontakt-Klicks, Banner-Zahlen und Heatmap-Link
(neuer Tab über openInNewTab). Anzeige-Logik als reine Helfer mit Tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-17 18:34:17 +00:00
1 parent f11fd0bb25
commit f9b0cbc855
12 files changed
+790 -2

No files matched your search

+148
View File
@@ -0,0 +1,148 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
// Website-Statistik (webklar.com): reine Anzeige-Helfer
const v = await loadModule('src/lib/eigeneAnalyseView.js')
test('Zahlen: Tausenderpunkt, Prozent, fehlender Wert', () => {
assert.equal(v.formatInt(1234567), '1.234.567')
assert.equal(v.formatInt(0), '0')
assert.equal(v.formatInt(null), '–')
assert.equal(v.formatInt(undefined), '–')
assert.equal(v.formatPercent(0.63), '63 %')
assert.equal(v.formatPercent(0.625), '63 %')
assert.equal(v.formatPercent(0), '0 %')
assert.equal(v.formatPercent(1), '100 %')
assert.equal(v.formatPercent(null), '–')
})
test('Dauer: Sekunden, Minuten mit zweistelligen Sekunden, null', () => {
assert.equal(v.formatDuration(42), '42 s')
assert.equal(v.formatDuration(0), '0 s')
assert.equal(v.formatDuration(65), '1 Min. 05 s')
assert.equal(v.formatDuration(60), '1 Min. 00 s')
assert.equal(v.formatDuration(59.6), '1 Min. 00 s', 'gerundet, nie "60 s"')
assert.equal(v.formatDuration(754.2), '12 Min. 34 s')
assert.equal(v.formatDuration(null), '–')
assert.equal(v.formatDuration(undefined), '–')
})
test('Aenderung in Prozent: plus, minus (U+2212), null, neu, kein Vergleich', () => {
assert.deepEqual(v.changeVsPrevious(112, 100), { text: '+12 %', dir: 'up' })
assert.deepEqual(v.changeVsPrevious(95, 100), { text: '−5 %', dir: 'down' })
assert.deepEqual(v.changeVsPrevious(100, 100), { text: '0 %', dir: 'flat' })
assert.deepEqual(v.changeVsPrevious(1001, 1000), { text: '0 %', dir: 'flat' }, 'unter 0,5 % gilt als unveraendert')
assert.deepEqual(v.changeVsPrevious(5, 0), { text: 'neu', dir: 'up' })
assert.deepEqual(v.changeVsPrevious(0, 0), { text: '0 %', dir: 'flat' })
assert.deepEqual(v.changeVsPrevious(0, 40), { text: '−100 %', dir: 'down' })
assert.deepEqual(v.changeVsPrevious(13000, 1000), { text: '+1.200 %', dir: 'up' })
assert.equal(v.changeVsPrevious(null, 10), null)
assert.equal(v.changeVsPrevious(10, null), null)
assert.equal(v.changeVsPrevious(10, undefined), null)
})
test('Aenderung der Zustimmungsquote in Prozentpunkten', () => {
assert.deepEqual(v.changeInPoints(0.63, 0.59), { text: '+4 Pkt.', dir: 'up' })
assert.deepEqual(v.changeInPoints(0.5, 0.52), { text: '−2 Pkt.', dir: 'down' })
assert.deepEqual(v.changeInPoints(0.5, 0.5), { text: '0 Pkt.', dir: 'flat' })
assert.equal(v.changeInPoints(0.5, null), null)
})
test('kpiChange: ohne vollstaendige Vorperiode kein Vergleich', () => {
const stats = {
totals: { pv: 120, visitors: 40, contacts: 3, consentRate: 0.63, avgSec: 65 },
previous: { complete: true, totals: { pv: 100, visitors: 50, contacts: 0, consentRate: 0.59, avgSec: null } },
}
assert.equal(v.kpiChange(stats, 'pv').text, '+20 %')
assert.equal(v.kpiChange(stats, 'visitors').text, '−20 %')
assert.equal(v.kpiChange(stats, 'contacts').text, 'neu')
assert.equal(v.kpiChange(stats, 'consentRate').text, '+4 Pkt.')
assert.equal(v.kpiChange(stats, 'avgSec'), null, 'Vorperiode ohne Dauer')
assert.equal(v.kpiChange({ ...stats, previous: { ...stats.previous, complete: false } }, 'pv'), null)
assert.equal(v.kpiChange({ totals: stats.totals }, 'pv'), null)
assert.equal(v.kpiChange(null, 'pv'), null)
})
test('Beschriftungen: Geraete, Kontakte, Herkunft, Seiten', () => {
assert.equal(v.deviceLabel('m'), 'Handy')
assert.equal(v.deviceLabel('t'), 'Tablet')
assert.equal(v.deviceLabel('d'), 'Computer')
assert.equal(v.deviceLabel(''), 'unbekannt')
assert.equal(v.deviceLabel('x'), 'x')
assert.equal(v.contactLabel('tel'), 'Telefon')
assert.equal(v.contactLabel('mail'), 'E-Mail')
assert.equal(v.contactLabel('wa'), 'WhatsApp')
assert.equal(v.contactLabel('out'), 'Link zu anderer Website')
assert.equal(v.contactLabel('zzz'), 'zzz')
assert.equal(v.refLabel(''), 'direkt / unbekannt')
assert.equal(v.refLabel('google.com'), 'google.com')
assert.equal(v.pageLabel('/'), '/ (Startseite)')
assert.equal(v.pageLabel('/preise'), '/preise')
})
test('Laender: deutsche Namen, unbekannt, ungueltiger Code bleibt Code', () => {
assert.equal(v.countryLabel('DE'), 'Deutschland')
assert.equal(v.countryLabel('AT'), 'Österreich')
assert.equal(v.countryLabel('CH'), 'Schweiz')
assert.equal(v.countryLabel(''), 'unbekannt')
assert.equal(v.countryLabel('T1'), 'T1', 'kein gueltiger Regionscode')
})
test('Datum: Tag, Kurzform, Zeitraum (gleiches Jahr, Jahreswechsel, ein Tag)', () => {
assert.equal(v.formatDay('2026-10-17'), '17.10.2026')
assert.equal(v.formatDay(''), '–')
assert.equal(v.formatDay('17.10.2026'), '–')
assert.equal(v.formatDayShort('2026-10-07'), '07.10.')
assert.equal(v.periodText('2026-09-10', '2026-10-16'), '10.09.–16.10.2026')
assert.equal(v.periodText('2025-12-30', '2026-01-05'), '30.12.2025–05.01.2026')
assert.equal(v.periodText('2026-10-16', '2026-10-16'), '16.10.2026')
assert.equal(v.periodText('', '2026-10-16'), '')
})
test('Verlauf: jeder Tag einmal, fehlende Tage 0, Tage vor der Messung null, ueber Monatsgrenze', () => {
const stats = {
from: '2026-09-29', to: '2026-10-03', firstDay: '2026-09-30',
series: [
{ day: '2026-09-30', pv: 4, visitors: 2 },
{ day: '2026-10-02', pv: 9, visitors: 5 },
],
}
const c = v.chartDays(stats)
assert.deepEqual(c.days, ['2026-09-29', '2026-09-30', '2026-10-01', '2026-10-02', '2026-10-03'])
assert.deepEqual(c.pv, [null, 4, 0, 9, 0])
assert.deepEqual(c.visitors, [null, 2, 0, 5, 0])
assert.deepEqual(v.chartDays({ from: '2026-10-03', to: '2026-10-01', series: [] }).days, [])
assert.deepEqual(v.chartDays({ from: '2026-10-01', to: '2026-10-02', firstDay: '', series: [] }).pv, [0, 0])
})
test('Rangliste: sortiert, Balken relativ zum groessten, Anteil nur auf Wunsch, Nullen raus', () => {
const items = [{ key: 'm', count: 30 }, { key: 'd', count: 60 }, { key: 't', count: 10 }, { key: '', count: 0 }]
const rows = v.rankRows(items, v.deviceLabel, { percent: true })
assert.deepEqual(rows.map((r) => [r.key, r.label, r.count, r.bar, r.percent]), [
['d', 'Computer', 60, 1, '60'],
['m', 'Handy', 30, 0.5, '30'],
['t', 'Tablet', 10, 10 / 60, '10'],
])
assert.equal(v.rankRows([{ key: 'a', count: 1000 }, { key: 'b', count: 1 }], String, { percent: true })[1].percent, '<1', 'unter einem halben Prozent')
assert.equal(v.rankRows(items, v.deviceLabel)[0].percent, null)
assert.deepEqual(v.rankRows(null, String), [])
})
test('Texte: Heute-Zeile mit Einzahl, Leerzustand, Hinweis auf spaeten Messbeginn', () => {
assert.equal(
v.todayText({ pv: 12, visitors: 3, contacts: 1 }),
'Heute bis jetzt: 12 Aufrufe · 3 Besucher · 1 Kontakt-Klick'
)
assert.equal(
v.todayText({ pv: 1, visitors: 1, contacts: 0 }),
'Heute bis jetzt: 1 Aufruf · 1 Besucher · 0 Kontakt-Klicks'
)
assert.equal(v.todayText({ pv: 1500, visitors: 0, contacts: 2 }), 'Heute bis jetzt: 1.500 Aufrufe · 0 Besucher · 2 Kontakt-Klicks')
assert.equal(v.hasNoData({ totals: { pv: 0 }, today: { pv: 0 } }), true)
assert.equal(v.hasNoData({ totals: { pv: 0 }, today: { pv: 2 } }), false)
assert.equal(v.hasNoData({ totals: { pv: 5 }, today: { pv: 0 } }), false)
assert.equal(v.sinceHint({ from: '2026-09-17', firstDay: '2026-10-05' }), 'Messung seit 05.10.2026')
assert.equal(v.sinceHint({ from: '2026-09-17', firstDay: '2026-09-17' }), '')
assert.equal(v.sinceHint({ from: '2026-09-17', firstDay: '' }), '')
})
+3 -2
View File
@@ -15,11 +15,12 @@ test('buildNavLinks: vier Unterpunkte unter Kunden mit Zahlen, Admin-Eintraege n
['angebote', 'Abo-Angebote', '/customers/angebote', undefined],
['speicher', 'E-Mail-Speicher', '/customers/email-speicher', 2],
])
assert.deepEqual(links.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'leads', 'legal'])
assert.deepEqual(links.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'leads', 'legal', 'statistik'])
assert.equal(links.find((l) => l.key === 'leads').paused, true)
assert.deepEqual(links.find((l) => l.key === 'statistik'), { key: 'statistik', label: 'Website-Statistik', href: '/statistik' }, 'fuer alle Mitarbeiter sichtbar')
assert.equal(kunden.children.find((c) => c.key === 'chat').hot, true)
const admin = buildNavLinks({ isAdmin: true, pulseLevel: 'warn', counts: {} })
assert.deepEqual(admin.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'leads', 'finance', 'legal', 'serverpuls', 'admin'])
assert.deepEqual(admin.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'leads', 'finance', 'legal', 'statistik', 'serverpuls', 'admin'])
assert.equal(admin.find((l) => l.key === 'serverpuls').dot, 'warn')
assert.equal(admin.find((l) => l.key === 'finance').also, '/finanzen')
assert.equal(admin.find((l) => l.key === 'customers').children[0].count, 0, 'ohne Zahl 0')