Website-Analyse: AVV als Seite, www/Apex-Domains, Abo-Check unabhängig vom Schalter, Dev-Hinweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7ead59ff73
commit
714687fe3b
10 files changed
+267
-20
No files matched your search
@@ -41,7 +41,7 @@ Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bed
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, es sei denn, eine gesetzliche Pflicht verlangt etwas anderes. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es erlaubt.
|
||||
|
||||
(2) Die Weisungen ergeben sich aus diesem Vertrag, der Leistungsbeschreibung und den Einstellungen des Kunden im Kundenportal (Einschalten, Ausschalten, Akzentfarbe des Banners). Weitere Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin und darf die Ausführung aussetzen.
|
||||
(2) Die Weisungen ergeben sich aus diesem Vertrag, der Leistungsbeschreibung und den Einstellungen des Kunden im Kundenportal (Einschalten und Ausschalten; die Akzentfarbe des Banners wird beim Einschalten gewählt). Weitere Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin und darf die Ausführung aussetzen.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
(1) Die Website-Analyse misst, wie die öffentliche Website des Kunden genutzt wird. Sie ist im Wartungs-Abo ohne Aufpreis enthalten und wird vom Kunden im Kundenportal eingeschaltet.
|
||||
|
||||
(2) WEBklar fügt beim Veröffentlichen der Website ein Messskript und ein Cookie-Banner (Leiste unten, „Ablehnen“ und „Annehmen“ gleichwertig, einstellbare Akzentfarbe, Link „Cookie-Einstellungen“) ein und erzeugt eine Zusatzseite „/datenschutz-analyse.html“ mit den Datenschutzhinweisen zur Messung (Verantwortlicher: der Kunde).
|
||||
(2) WEBklar fügt beim Veröffentlichen der Website ein Messskript und ein Cookie-Banner (Leiste unten, „Ablehnen“ und „Annehmen“ gleichwertig, Akzentfarbe, die beim Einschalten gewählt wird, Link „Cookie-Einstellungen“) ein und erzeugt eine Zusatzseite „/datenschutz-analyse.html“ mit den Datenschutzhinweisen zur Messung (Verantwortlicher: der Kunde).
|
||||
|
||||
(3) Gemessen wird:
|
||||
- **ohne Einwilligung des Besuchers:** Seitenaufrufe mit Pfad (ohne Suchparameter), Domainname der verweisenden Website, Land (die IP-Adresse wird nur im Moment der Abfrage verwendet und nicht gespeichert), Geräteklasse sowie anonyme Banner-Zähler;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { bannerColor, chartModel, barRows, countryName, flagEmoji, refLabel, DEVICE_LABELS, CONTACT_LABELS } from '/analyse-view.js'
|
||||
|
||||
const PRICE = '30 €'
|
||||
const AVV_URL = '/rechtliches/avv-website-analyse' // Platzhalter: das Dokument liefert Task 13
|
||||
const AVV_URL = '/rechtliches/avv-website-analyse.html'
|
||||
const LEISTUNGSBESCHREIBUNG = '/vertragsunterlagen/funktionen-leistungsbeschreibung.html'
|
||||
const PHOTO_ACCEPT = 'image/jpeg,image/png,image/webp' // iOS wandelt HEIC dann selbst in JPEG um
|
||||
const PHOTO_MAX_BYTES = 10 * 1024 * 1024
|
||||
@@ -86,6 +86,7 @@ function newsTile(projects) {
|
||||
if (active) { pill = ['Aktiv', 'on']; cta = 'Beiträge schreiben' } else if (states.some(isSetup)) { pill = ['In Einrichtung', 'setup']; cta = 'Status ansehen' } else if (states.length && states.every((s) => s === 'disabled')) { pill = ['Abgeschaltet', 'off']; cta = 'Status ansehen' }
|
||||
const target = pickProject(projects)
|
||||
const count = projects.length > 1 && active ? `<p class="fn-count">aktiv für ${active} von ${projects.length} Websites</p>` : ''
|
||||
const devHint = state === 'active' && a.devMode ? `<p class="fn-count an-dev-hint">${DEV_HINT}</p>` : ''
|
||||
return `
|
||||
<article class="fn-tile" data-fn-tile="news">
|
||||
<div class="fn-tile-head">${svg('news')}<span class="fn-pill fn-pill-${pill[1]}">${pill[0]}</span></div>
|
||||
@@ -101,6 +102,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 === 'unknown' ? 'unknown' : a.state === 'disabled' ? 'disabled' : a.state === 'active' ? 'active' : a.eligible ? 'off' : 'blocked')
|
||||
const DEV_HINT = 'Im Dev-Modus wird nicht gemessen.'
|
||||
const ANALYSE_TEXT = 'Sieh, wie viele Besucher deine Website hat und was sie sich ansehen.'
|
||||
|
||||
function pickAnalyseProject(projects) {
|
||||
@@ -129,6 +131,7 @@ function analyseTile(projects) {
|
||||
<h3>Analyse</h3>
|
||||
<p>${text}</p>
|
||||
${count}
|
||||
${devHint}
|
||||
${action}
|
||||
</article>`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Auftragsverarbeitungsvertrag Website-Analyse – WEBklar</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #f7f6f3; color: #1d1d1f; font: 16px/1.6 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 13px; color: #6d1fd1; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #e3e0da; }
|
||||
h3 { font-size: 16.5px; margin: 22px 0 6px; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #6b6b70; font-size: 14px; }
|
||||
.box { background: #fff; border: 1px solid #e3e0da; border-radius: 14px; padding: 16px 20px; margin: 18px 0; }
|
||||
.box.widerspruch { border: 2px solid #6d1fd1; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #eee; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #6b6b70; }
|
||||
a { color: #6d1fd1; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.offen { background: #fff3cd; padding: 0 4px; border-radius: 4px; }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar · AVV Website-Analyse"; font: 8pt sans-serif; color: #888; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #888; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
}
|
||||
.box.draft { border: 2px solid #d9a400; background: #fff8e1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Kundenportal</a> · <a href="/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Auftragsverarbeitungsvertrag Website-Analyse (Art. 28 DSGVO)</h1>
|
||||
|
||||
<div class="box draft"><p><strong>Entwurf – vor Verwendung anwaltlich prüfen lassen.</strong> Dieser Text ist keine Rechtsberatung. Stellen mit „Prüfpunkt Anwalt“ sind offen.</p></div>
|
||||
|
||||
<p class="meta"><strong>Fassung 2026-10-01</strong></p>
|
||||
|
||||
<p>zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).</p>
|
||||
|
||||
<p>Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Er wird vereinbart, wenn der Kunde im Kundenportal die Funktion „Website-Analyse“ einschaltet und dabei die Zustimmung zu dieser Fassung bestätigt (Haken im Portal; das Portal speichert Fassung, Zeitpunkt und Person). Schaltet ein Mitarbeiter von WEBklar die Funktion auf Wunsch des Kunden ein, bestätigt der Mitarbeiter im System, dass der Kunde diese Fassung des Vertrags bestätigt hat. Gespeichert werden die Fassung, der Zeitpunkt und der Name des Mitarbeiters. Bei Zustimmung im Kundenportal wird die Zustimmung dem Kundenkonto zugeordnet.</p>
|
||||
|
||||
<h2>1. Gegenstand und Dauer</h2>
|
||||
|
||||
<p>(1) WEBklar betreibt für die öffentliche Website des Kunden eine Reichweitenmessung („Website-Analyse“) und verarbeitet dabei personenbezogene Daten der Besucher der Website im Auftrag des Kunden. Die Leistung ist in der Leistungsbeschreibung „Website-Analyse“ beschrieben.</p>
|
||||
|
||||
<p>(2) Der Vertrag beginnt mit dem Einschalten der Funktion und gilt, solange die Funktion eingeschaltet ist. Er endet mit dem Ausschalten, spätestens mit dem Ende des Wartungs-Abos, und gilt darüber hinaus, bis alle Daten nach Nummer 9 gelöscht sind.</p>
|
||||
|
||||
<h2>2. Art und Zweck der Verarbeitung</h2>
|
||||
|
||||
<p>(1) Art: Erheben über ein Skript, das WEBklar in die Website des Kunden einfügt, Übermitteln an den Messdienst von WEBklar, Speichern, Verdichten zu Tageswerten, Auswerten im Kundenportal (Kennzahlen, Heatmap) und Löschen.</p>
|
||||
|
||||
<p>(2) Zweck: Der Kunde soll erkennen, wie seine Website genutzt wird, um sie zu verbessern. Andere Zwecke verfolgt WEBklar mit diesen Daten nicht. Es findet kein Profiling, keine Werbung und keine Zusammenführung mit anderen Daten statt.</p>
|
||||
|
||||
<h2>3. Datenarten und Betroffene</h2>
|
||||
|
||||
<p>(1) Datenarten ohne Einwilligung des Besuchers: Seitenaufruf mit Pfad (ohne Suchparameter), Domainname der verweisenden Website, Land (abgeleitet aus der IP-Adresse; die IP-Adresse wird nur im Moment der Abfrage verwendet und nicht gespeichert), Geräteklasse (Handy, Tablet, Computer) und Größenklasse des Browserfensters (vp), anonyme Zähler zur Anzeige, Annahme und Ablehnung des Banners.</p>
|
||||
|
||||
<p>(2) Zusätzlich nach Annehmen des Banners durch den Besucher: eine täglich wechselnde pseudonyme Besucher-Kennung (nicht über mehrere Tage verknüpfbar), Verweildauer und Scrolltiefe je Seite, Klickpositionen mit vereinfachtem Elementpfad (Heatmap, keine Formulareingaben), Art des Kontakt-Klicks (Telefon, E-Mail, WhatsApp, Link auf eine andere Website mit Domainname).</p>
|
||||
|
||||
<p>(3) Nicht gespeichert werden IP-Adressen und Browser-Kennzeichen (User-Agent); sie werden nur kurzzeitig im Arbeitsspeicher verarbeitet (siehe Anlage). Nicht erhoben werden Cookies, Formularinhalte, Seiteninhalte, Mausbewegungen oder Sitzungsaufzeichnungen. Besondere Kategorien nach Art. 9 DSGVO werden nicht gezielt verarbeitet.</p>
|
||||
|
||||
<p>(4) Betroffene: Besucher der Website des Kunden.</p>
|
||||
|
||||
<p>(5) WEBklar verwendet die Daten ausschließlich für den Kunden und gibt sie nicht an Dritte weiter, außer an Unterauftragsverarbeiter nach Nummer 7.</p>
|
||||
|
||||
<h2>4. Weisungen</h2>
|
||||
|
||||
<p>(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, es sei denn, eine gesetzliche Pflicht verlangt etwas anderes. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es erlaubt.</p>
|
||||
|
||||
<p>(2) Die Weisungen ergeben sich aus diesem Vertrag, der Leistungsbeschreibung und den Einstellungen des Kunden im Kundenportal (Einschalten und Ausschalten; die Akzentfarbe des Banners wird beim Einschalten gewählt). Weitere Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin und darf die Ausführung aussetzen.</p>
|
||||
|
||||
<h2>5. Vertraulichkeit</h2>
|
||||
|
||||
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
|
||||
|
||||
<h2>6. Technische und organisatorische Maßnahmen</h2>
|
||||
|
||||
<p>(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
|
||||
|
||||
<p>(2) Die Maßnahmen der Anlage 2 zu den Abo-Bedingungen gelten ergänzend.</p>
|
||||
|
||||
<h2>7. Unterauftragsverarbeiter</h2>
|
||||
|
||||
<p>(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Messdienst läuft, und für die Datensicherung. Ort der Verarbeitung ist ein Rechenzentrum in Deutschland (Falkenstein). Die Daten werden nicht in Drittländer übermittelt.</p>
|
||||
|
||||
<p>(2) Will WEBklar einen Unterauftragsverarbeiter hinzuziehen oder ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann aus wichtigem datenschutzrechtlichem Grund widersprechen. Einigen sich die Parteien nicht, kann der Kunde die Website-Analyse zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt werden soll.</p>
|
||||
|
||||
<p>(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf dieselben Datenschutzpflichten, die sich aus diesem Vertrag ergeben.</p>
|
||||
|
||||
<p>(4) Die Länderzuordnung nutzt eine lokal gespeicherte Datenbank („DB-IP Lite Country“, Lizenz CC BY 4.0). Dabei werden keine Daten an den Anbieter der Datenbank übermittelt; er ist kein Unterauftragsverarbeiter.</p>
|
||||
|
||||
<h2>8. Unterstützung des Kunden, Betroffenenrechte</h2>
|
||||
|
||||
<p>(1) WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter und unterstützt ihn bei der Beantwortung, soweit das mit den eingesetzten Systemen möglich ist.</p>
|
||||
|
||||
<p>(2) Die gespeicherten Daten enthalten keine IP-Adresse, keinen Namen und keine Kennung, die über einen Tag hinaus gilt. WEBklar kann einzelne Besucher daher in der Regel nicht identifizieren und Auskunfts-, Berichtigungs- oder Löschverlangen zu einzelnen Besuchen nicht umsetzen, solange die betroffene Person keine zusätzlichen Angaben bereitstellt (Art. 11 DSGVO). Der Kunde weist Betroffene darauf in seiner Datenschutzerklärung hin.</p>
|
||||
|
||||
<p>(3) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO.</p>
|
||||
|
||||
<p>(4) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</p>
|
||||
|
||||
<p>(5) Der Kunde ist dafür verantwortlich, dass die Besucher seiner Website informiert werden (Art. 13 DSGVO). WEBklar stellt dafür die Zusatzseite „/datenschutz-analyse.html“ bereit. Der Kunde ergänzt seine eigene Datenschutzerklärung um einen Hinweis darauf.</p>
|
||||
|
||||
<h2>9. Löschung und Rückgabe</h2>
|
||||
|
||||
<p>(1) Einzelne Ereignisse und Klickpositionen löscht WEBklar automatisch nach 30 Tagen. Zusammengefasste Tageswerte ohne Besucher-Kennung bleiben bis zum Ende der Website-Analyse (Ausschalten oder Ende des Wartungs-Abos) und weitere 60 Tage gespeichert; danach löscht WEBklar alle Daten der Website. Eine Kopie der Tageswerte stellt WEBklar dem Kunden auf Verlangen in Textform innerhalb dieser Frist zur Verfügung.</p>
|
||||
|
||||
<p>(2) Ob und wie lange Sicherungskopien Daten der Website-Analyse enthalten, ist noch zu klären (Prüfpunkt P8, siehe Anlage); soweit sie enthalten sind, werden sie im Rahmen der regulären Rotation überschrieben.</p>
|
||||
|
||||
<p>(3) Gesetzliche Aufbewahrungspflichten bleiben unberührt.</p>
|
||||
|
||||
<h2>10. Nachweise und Kontrollen</h2>
|
||||
|
||||
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Prüfpunkt P13). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
|
||||
|
||||
<h2>11. Pflichten des Kunden</h2>
|
||||
|
||||
<p>Der Kunde ist Verantwortlicher für die Verarbeitung. Er sorgt dafür, dass das Banner nicht entfernt oder verdeckt wird, dass keine weiteren Messwerkzeuge ohne Abstimmung eingebunden werden, und dass seine Datenschutzerklärung auf die Zusatzseite hinweist (Leistungsbeschreibung „Website-Analyse“).</p>
|
||||
|
||||
<h2>12. Haftung und Schlussbestimmungen</h2>
|
||||
|
||||
<p>(1) Für die Haftung gilt Art. 82 DSGVO. Im Übrigen gelten die Haftungsregeln der Abo-Bedingungen.</p>
|
||||
|
||||
<p>(2) Bei Widersprüchen geht dieser Vertrag in Datenschutzfragen den Abo-Bedingungen und der Leistungsbeschreibung vor.</p>
|
||||
|
||||
<p>(3) Änderungen dieses Vertrags richten sich nach § 15 der Abo-Bedingungen; eine neue Fassung wird erst wirksam, wenn der Kunde sie im Portal bestätigt hat.</p>
|
||||
|
||||
<p>(4) Es gilt deutsches Recht.</p>
|
||||
|
||||
<h2>Anlage: Technische und organisatorische Maßnahmen der Website-Analyse (Art. 32 DSGVO)</h2>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Bereich</th><th>Maßnahmen</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Übertragung</td><td>Skript und Messdaten werden nur über TLS (HTTPS) übertragen.</td></tr>
|
||||
<tr><td>Keine Cookies</td><td>Es werden keine Cookies gesetzt. Auf dem Gerät des Besuchers wird nur die Banner-Auswahl im lokalen Speicher (Eintrag „wk-consent“) gespeichert; sie gilt sechs Monate.</td></tr>
|
||||
<tr><td>Datenminimierung</td><td>IP-Adresse und Browser-Kennzeichen werden nicht gespeichert und nicht protokolliert. Die IP-Adresse wird nur im Moment der Abfrage verwendet für: Länderzuordnung, Schlüssel der Mengenbegrenzung (Hash mit Tagessalz, nur im Arbeitsspeicher) und, nur nach Einwilligung, die tägliche Besucher-Kennung. Das Browser-Kennzeichen wird nur im Moment der Abfrage verwendet für: Geräteklasse, Bot-Erkennung und, nur nach Einwilligung, die tägliche Besucher-Kennung. Pfade ohne Suchparameter, Elementpfade ohne Kennungen und Klassennamen. Keine Formulareingaben.</td></tr>
|
||||
<tr><td>Pseudonymisierung</td><td>Die Besucher-Kennung (nur nach Einwilligung) ist ein gekürzter Hash mit Tagessalz. Das Salz liegt nur im Arbeitsspeicher und wird täglich um 00:00 Uhr (Europe/Berlin) neu erzeugt; ältere Kennungen sind danach nicht mehr verknüpfbar.</td></tr>
|
||||
<tr><td>Einwilligung</td><td>Ohne „Annehmen“ sendet das Skript nur Seitenaufruf und Banner-Zähler. „Global Privacy Control“ und „Do Not Track“ gelten als Ablehnen.</td></tr>
|
||||
<tr><td>Zugriffskontrolle</td><td>Die Verwaltungsschnittstelle des Messdienstes (Auswertung, Website-Verwaltung) läuft auf einem getrennten, internen Port, der nicht öffentlich erreichbar ist, und ist durch ein Zugriffstoken geschützt. Zugriff nur über das Kundenportal und die Verwaltung von WEBklar. Im Portal sieht jeder Kunde nur die Daten seiner eigenen Website.</td></tr>
|
||||
<tr><td>Eingangsprüfung</td><td>Messdaten werden nur angenommen, wenn die Website registriert ist und Origin bzw. Referer zu ihren Domains passen; Bots werden verworfen; Größen- und Mengenbegrenzung je Website und Anfrager (nur im Arbeitsspeicher).</td></tr>
|
||||
<tr><td>Heatmap-Zugriff</td><td>Die Heatmap wird über einen signierten Link abgerufen, der 30 Minuten gültig ist.</td></tr>
|
||||
<tr><td>Löschung</td><td>Täglicher automatischer Job (02:30 Uhr, Europe/Berlin): Rohdaten und Klicks nach 30 Tagen; Websites, die seit mehr als 60 Tagen abgeschaltet sind, werden vollständig gelöscht.</td></tr>
|
||||
<tr><td>Trennung</td><td>Daten sind je Website getrennt (Website-Schlüssel); eigener Container und eigenes Datenverzeichnis des Messdienstes; kein Zugriff auf andere Kundendaten.</td></tr>
|
||||
<tr><td>Rechenzentrum</td><td>Hetzner Online GmbH, Falkenstein (Deutschland). <span class="offen">[Prüfpunkt: Zertifizierung des Rechenzentrums (ISO/IEC 27001) vor Verwendung belegen, bevor sie genannt wird.]</span></td></tr>
|
||||
<tr><td>Protokolle des Reverse-Proxys</td><td>Das Zugriffsprotokoll für die Messdienst-Adresse wird ohne Client-IP geführt. <span class="offen">[Prüfpunkt P7: Einstellung beim Livegang verifizieren.]</span></td></tr>
|
||||
<tr><td>Datensicherung</td><td>Die tägliche Serversicherung (7 Tage) erfasst voraussichtlich auch das Datenverzeichnis des Messdienstes. <span class="offen">[Prüfpunkt P8: ob das Verzeichnis in der Sicherung enthalten ist und wie lange dort gelöschte Daten verbleiben, ist noch zu klären.]</span></td></tr>
|
||||
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nr. 8 Abs. 4), Überprüfung der Maßnahmen mindestens einmal im Jahr.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
+8
-9
@@ -169,15 +169,14 @@ const server = app.listen(config.port, '0.0.0.0', () => {
|
||||
.catch((err) => console.warn('[funktionen] Freischalten fehlgeschlagen:', err.message))
|
||||
setInterval(activateFeatures, 5 * 60 * 1000).unref()
|
||||
|
||||
// Website-Analyse: aktive Analysen ohne laufendes Wartungs-Abo beenden (taeglich, 2 Minuten nach Start beginnend)
|
||||
if (config.analyse.enabled) {
|
||||
const analyseAboCheck = () =>
|
||||
runAboCheck({ now: new Date() })
|
||||
.then((r) => r.deactivated.length && console.log(`[analyse] ${r.deactivated.length} Analyse(n) ohne Abo beendet`))
|
||||
.catch((err) => console.warn('[analyse] Abo-Prüfung fehlgeschlagen:', err.message))
|
||||
setTimeout(analyseAboCheck, 2 * 60 * 1000).unref()
|
||||
setInterval(analyseAboCheck, 24 * 60 * 60 * 1000).unref()
|
||||
}
|
||||
// Website-Analyse: aktive Analysen ohne laufendes Wartungs-Abo beenden (taeglich, 2 Minuten nach Start beginnend).
|
||||
// Laeuft unabhaengig vom Schalter ANALYSE_ENABLED: er liest nur Analyse-Dokumente und ist ohne solche wirkungslos.
|
||||
const analyseAboCheck = () =>
|
||||
runAboCheck({ now: new Date() })
|
||||
.then((r) => r.deactivated.length && console.log(`[analyse] ${r.deactivated.length} Analyse(n) ohne Abo beendet`))
|
||||
.catch((err) => console.warn('[analyse] Abo-Prüfung fehlgeschlagen:', err.message))
|
||||
setTimeout(analyseAboCheck, 2 * 60 * 1000).unref()
|
||||
setInterval(analyseAboCheck, 24 * 60 * 60 * 1000).unref()
|
||||
|
||||
// Neue Gitea-Repos erkennen, Zuordnungs-Mail senden, Erinnerungen/Wiederholungen (alle 10 Minuten)
|
||||
const repoAssign = () =>
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
ELIGIBILITY_MESSAGES,
|
||||
activateAnalyse,
|
||||
analyseDeps,
|
||||
analyseDomains,
|
||||
analyseHosts,
|
||||
isDevPreview,
|
||||
analyseEligibility,
|
||||
deactivateAnalyse,
|
||||
} from '../services/analyse.js'
|
||||
@@ -41,8 +42,14 @@ const ANALYSE_ERROR = 'Die Website-Analyse konnte gerade nicht geändert werden.
|
||||
async function analyseView(customerId, projectId) {
|
||||
if (!config.analyse.enabled) return { comingSoon: true }
|
||||
const [doc, eligibility] = await Promise.all([getFeatureDoc(customerId, projectId, 'analyse'), analyseEligibility(customerId, projectId)])
|
||||
let devMode = false
|
||||
if (doc?.state === 'active') {
|
||||
const project = eligibility.project || (await getDocument(config.collections.websiteProjects, projectId).catch(() => null))
|
||||
devMode = isDevPreview(project)
|
||||
}
|
||||
return {
|
||||
state: doc?.state || 'off',
|
||||
devMode,
|
||||
accentColor: doc?.accentColor || '#111111',
|
||||
eligible: eligibility.ok,
|
||||
reason: eligibility.reason || null,
|
||||
@@ -233,7 +240,7 @@ router.post('/analyse/heatmap-link', analyseGuard, async (req, res) => {
|
||||
if (!doc?.siteKey || doc.state !== 'active') {
|
||||
return res.status(409).json({ error: 'Die Website-Analyse ist für dieses Projekt nicht aktiv.' })
|
||||
}
|
||||
const domains = analyseDomains(project)
|
||||
const domains = analyseHosts(project)
|
||||
if (!domains.length) return res.status(409).json({ error: 'Keine gültige Domain' })
|
||||
const host = project.liveDomain ? domains[domains.length - 1] : domains[0]
|
||||
const token = analyseDeps.client.heatToken({ siteKey: doc.siteKey, path, vp })
|
||||
|
||||
@@ -58,8 +58,8 @@ export async function analyseEligibility(customerId, projectId) {
|
||||
|
||||
const HOSTNAME_RE = /^(?=.{4,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/
|
||||
|
||||
/** `<subdomain>.project.webklar.com` plus Live-Domain (klein, ohne Schema/Pfad), ohne Doppelte; ungueltige Hosts werden uebersprungen. */
|
||||
export function analyseDomains(project) {
|
||||
/** Gespeicherte Hosts: `<subdomain>.project.webklar.com` plus Live-Domain (klein, ohne Schema/Pfad), ohne Doppelte; ungueltige werden uebersprungen. */
|
||||
export function analyseHosts(project) {
|
||||
const domains = []
|
||||
if (project.subdomain) domains.push(`${String(project.subdomain).trim().toLowerCase()}.project.webklar.com`)
|
||||
const live = String(project.liveDomain || '')
|
||||
@@ -71,6 +71,17 @@ export function analyseDomains(project) {
|
||||
return [...new Set(domains)].filter((d) => HOSTNAME_RE.test(d))
|
||||
}
|
||||
|
||||
/** Erlaubte Origin-Domains des Messdienstes: gespeicherte Hosts plus www-/Apex-Gegenstueck der Live-Domain (Besucher kommen mit und ohne www), max. 5. */
|
||||
export function analyseDomains(project) {
|
||||
const hosts = analyseHosts(project)
|
||||
const live = project.liveDomain ? hosts[hosts.length - 1] : ''
|
||||
const extra = live && hosts.length > 1 ? (live.startsWith('www.') ? live.slice(4) : `www.${live}`) : ''
|
||||
return [...new Set([...hosts, extra].filter(Boolean))].filter((d) => HOSTNAME_RE.test(d)).slice(0, 5)
|
||||
}
|
||||
|
||||
/** Dev-Server-Projekte (previewDetected 'dev…') bekommen kein Messskript: der Runner injiziert nur in veroeffentlichte Ordner. */
|
||||
export const isDevPreview = (project) => String(project?.previewDetected || '').startsWith('dev')
|
||||
|
||||
async function customerOf(customerId) {
|
||||
try {
|
||||
return await getDocument(config.collections.customers, customerId)
|
||||
@@ -111,7 +122,7 @@ async function mailSafe(customerId, template, project) {
|
||||
|
||||
function privacyTicket(customerId, project, siteKey) {
|
||||
const name = project.projectName || project.subdomain || ''
|
||||
const domains = analyseDomains(project)
|
||||
const domains = analyseHosts(project)
|
||||
return analyseDeps.ticket({
|
||||
customerId,
|
||||
project,
|
||||
|
||||
+12
-1
@@ -89,7 +89,7 @@ test('Einschalten: Dokument, Sammeldienst, Neubau, Ticket, Mail K18', async () =
|
||||
assert.equal(d.avvVersion, config.analyse.avvVersion)
|
||||
assert.equal(d.avvAcceptedBy, 'customer')
|
||||
assert.ok(d.avvAcceptedAt && d.activatedAt)
|
||||
assert.deepEqual(puts, [{ siteKey: d.siteKey, projectId: 'p1', domains: ['baeckerei.project.webklar.com', 'baeckerei.de'], active: true }])
|
||||
assert.deepEqual(puts, [{ siteKey: d.siteKey, projectId: 'p1', domains: ['baeckerei.project.webklar.com', 'baeckerei.de', 'www.baeckerei.de'], active: true }])
|
||||
assert.deepEqual(rebuilds, ['p1'])
|
||||
const tickets = analyseTickets()
|
||||
assert.equal(tickets.length, 1)
|
||||
@@ -243,3 +243,14 @@ test('gesperrte Analyse: Kunde 403, Mitarbeiter darf', async () => {
|
||||
const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p10', consent: consent(), by: 'Kenso' })
|
||||
assert.equal(r.doc.state, 'active')
|
||||
})
|
||||
|
||||
test('analyseDomains: www-/Apex-Gegenstück der Live-Domain, ohne Doppelte, max. 5, nur gültige Hosts', () => {
|
||||
const d = (live, sub = 'x') => an.analyseDomains({ subdomain: sub, liveDomain: live })
|
||||
assert.deepEqual(d('example.de'), ['x.project.webklar.com', 'example.de', 'www.example.de'])
|
||||
assert.deepEqual(d('https://www.Example.de/start'), ['x.project.webklar.com', 'www.example.de', 'example.de'])
|
||||
assert.deepEqual(d(''), ['x.project.webklar.com'])
|
||||
assert.deepEqual(d('kaputt!'), ['x.project.webklar.com'])
|
||||
assert.ok(an.analyseDomains({ subdomain: 'x', liveDomain: 'a.b.c.example.de' }).length <= 5)
|
||||
// gespeicherter Host bleibt die Live-Domain (Heatmap-Link, Ticket)
|
||||
assert.deepEqual(an.analyseHosts({ subdomain: 'x', liveDomain: 'example.de' }), ['x.project.webklar.com', 'example.de'])
|
||||
})
|
||||
@@ -52,6 +52,8 @@ aw.seed('subscriptions', wartungDoc)
|
||||
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', subdomain: 'baeckerei', liveDomain: 'baeckerei.de', isPublic: true })
|
||||
aw.seed('websiteProjects', { $id: 'p2', customerId: 'c1', projectName: 'Privat', subdomain: 'privat', isPublic: false })
|
||||
aw.seed('websiteProjects', { $id: 'p3', customerId: 'c1', projectName: 'Café', subdomain: 'cafe', isPublic: true })
|
||||
aw.seed('websiteProjects', { $id: 'pd1', customerId: 'c1', projectName: 'Dev', subdomain: 'devsite', isPublic: true, previewDetected: 'dev:npm' })
|
||||
aw.seed('websiteProjects', { $id: 'pd2', customerId: 'c1', projectName: 'Build', subdomain: 'buildsite', isPublic: true, previewDetected: 'build:npm' })
|
||||
aw.seed('websiteProjects', { $id: 'p5', customerId: 'c2', projectName: 'Fremd', subdomain: 'fremd', isPublic: true })
|
||||
|
||||
const cookie = customerCookie('c1')
|
||||
@@ -100,7 +102,7 @@ test('GET /api/funktionen: analyse-Zustand je Projekt, mit und ohne Schalter', a
|
||||
const body = await get('/api/funktionen', { cookie })
|
||||
const p = (id) => body.projects.find((x) => x.projectId === id)
|
||||
assert.equal(p('p1').features.news.state, 'off')
|
||||
assert.deepEqual(p('p1').analyse, { state: 'off', accentColor: '#111111', eligible: true, reason: null, reasonText: null, avvVersion: config.analyse.avvVersion })
|
||||
assert.deepEqual(p('p1').analyse, { state: 'off', devMode: false, accentColor: '#111111', eligible: true, reason: null, reasonText: null, avvVersion: config.analyse.avvVersion })
|
||||
assert.equal(p('p2').analyse.eligible, false)
|
||||
assert.equal(p('p2').analyse.reason, 'nicht_oeffentlich')
|
||||
assert.equal(p('p2').analyse.reasonText, an.ELIGIBILITY_MESSAGES.nicht_oeffentlich)
|
||||
@@ -150,6 +152,17 @@ test('Stats: Weiterleitung, Zeitraum, 503 bei Ausfall', async () => {
|
||||
assert.equal((await rawGet(`${A}/stats?projectId=p6`, { cookie })).status, 409)
|
||||
})
|
||||
|
||||
test('GET /api/funktionen: devMode nur bei aktiver Analyse eines Dev-Projekts', async () => {
|
||||
const view = async (id) => (await get('/api/funktionen', { cookie })).projects.find((x) => x.projectId === id).analyse
|
||||
assert.equal((await view('pd1')).devMode, false, 'nicht aktiv: kein Hinweis')
|
||||
await post(`${A}/activate`, { projectId: 'pd1', consent: consent() }, { cookie })
|
||||
await post(`${A}/activate`, { projectId: 'pd2', consent: consent() }, { cookie })
|
||||
assert.equal((await view('pd1')).devMode, true)
|
||||
assert.equal((await view('pd2')).devMode, false, 'node_build wird gemessen')
|
||||
await post(`${A}/deactivate`, { projectId: 'pd1' }, { cookie })
|
||||
await post(`${A}/deactivate`, { projectId: 'pd2' }, { cookie })
|
||||
})
|
||||
|
||||
test('Heatmap-Link: Prüfung, URL, Token', async () => {
|
||||
await post(`${A}/activate`, { projectId: 'p1', consent: consent() }, { cookie })
|
||||
const key = (await featureDoc('c1', 'p1')).siteKey
|
||||
@@ -245,7 +258,7 @@ test('Fix b: Abschalten durch Mitarbeiter meldet Sammeldienst active:false', asy
|
||||
|
||||
test('Härtung: ungültige Hosts, Pfadzeichen, Fehler bei einem Projekt', async () => {
|
||||
// analyseDomains: ungueltige Hosts werden uebersprungen
|
||||
assert.deepEqual(an.analyseDomains({ subdomain: 'cafe', liveDomain: 'https://Cafe.DE/x' }), ['cafe.project.webklar.com', 'cafe.de'])
|
||||
assert.deepEqual(an.analyseDomains({ subdomain: 'cafe', liveDomain: 'https://Cafe.DE/x' }), ['cafe.project.webklar.com', 'cafe.de', 'www.cafe.de'])
|
||||
assert.deepEqual(an.analyseDomains({ subdomain: 'cafe', liveDomain: 'evil.de@x y' }), ['cafe.project.webklar.com'])
|
||||
assert.deepEqual(an.analyseDomains({ subdomain: 'ca fe', liveDomain: 'a_b.de' }), [])
|
||||
assert.deepEqual(an.analyseDomains({ subdomain: 'cafe', liveDomain: 'localhost' }), ['cafe.project.webklar.com'])
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
|
||||
const { config } = await import('../server/config.js')
|
||||
const root = (rel) => new URL(`../${rel}`, import.meta.url)
|
||||
|
||||
test('AVV Website-Analyse: öffentliche Seite existiert, trägt Fassung und Entwurfshinweis, keine internen Kommentare', () => {
|
||||
const file = root('public/rechtliches/avv-website-analyse.html')
|
||||
assert.ok(existsSync(file))
|
||||
const html = readFileSync(file, 'utf8')
|
||||
assert.ok(html.includes(`Fassung ${config.analyse.avvVersion}`), 'avvVersion aus der Konfiguration steht auf der Seite')
|
||||
assert.match(html, /Entwurf – vor Verwendung anwaltlich prüfen lassen/)
|
||||
assert.doesNotMatch(html, /<!--/)
|
||||
assert.match(html, /<h2>12\. Haftung/)
|
||||
})
|
||||
|
||||
test('AVV-Link im Portal zeigt auf eine vorhandene Datei unter public/', () => {
|
||||
const fn = readFileSync(root('public/funktionen.js'), 'utf8')
|
||||
const url = fn.match(/const AVV_URL = '([^']+)'/)?.[1]
|
||||
assert.equal(url, '/rechtliches/avv-website-analyse.html')
|
||||
assert.ok(existsSync(root(`public${url}`)))
|
||||
})
|
||||
|
||||
test('Portal: Dev-Modus-Hinweis an der Analyse-Kachel', () => {
|
||||
const fn = readFileSync(root('public/funktionen.js'), 'utf8')
|
||||
assert.match(fn, /Im Dev-Modus wird nicht gemessen/)
|
||||
assert.match(fn, /a\.devMode/)
|
||||
})
|
||||
|
||||
test('Texte: Akzentfarbe wird beim Einschalten gewählt (kein späteres Portal-Setting behauptet)', () => {
|
||||
for (const f of ['docs/rechtliches/avv-website-analyse.md', 'docs/rechtliches/leistungsbeschreibung-website-analyse.md']) {
|
||||
assert.match(readFileSync(root(f), 'utf8'), /beim Einschalten gewählt/, f)
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user