Add privacy-friendly analytics and cookie consent, integrated into the CMS

Adds a first-party analytics system with no third-party trackers and no
persistent per-visitor identifiers: events are grouped into sessions via a
daily-rotating pseudonymous hash (IP+UA+host, HMAC-salted, never stored raw).

- New "Analytics" section in the Payload admin, gated by a canViewAnalytics
  permission on Users, following the existing custom-admin-view pattern
  used by the calendar feature
- Sessions, page views, unique visitors, top/entry/exit pages, events,
  device/browser breakdown, date range filter with period comparison
- Raw events and daily aggregate rollups as their own collections, with
  configurable retention and a cron endpoint that rolls up before purging
- Cookie consent banner (necessary/preferences/statistics) with categories
  and copy editable from a new CMS global; a track() helper that is a
  complete no-op without statistics consent, so nothing is ever sent
  before a visitor opts in
- track() calls wired into the booking flow, contact form, and navigation
This commit is contained in:
2026-09-06 21:59:59 +02:00
parent 82c4261a81
commit 65b6eeac51
30 changed files with 2274 additions and 7 deletions
@@ -0,0 +1,188 @@
.page {
padding: 24px;
max-width: 1100px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #1f1f1f;
}
.title {
font-size: 22px;
font-weight: 600;
margin: 0 0 4px;
}
.subtitle {
color: #666;
font-size: 14px;
margin: 0 0 24px;
}
.filterBar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 24px;
}
.rangeLink {
display: inline-flex;
align-items: center;
padding: 6px 14px;
border-radius: 999px;
border: 1px solid #e2e2e2;
color: #333;
text-decoration: none;
font-size: 13px;
font-weight: 500;
}
.rangeLink:hover {
background: #f5f5f5;
}
.rangeLinkActive {
background: #8b616d;
border-color: #8b616d;
color: #fff;
}
.customForm {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #555;
}
.customForm input[type="date"] {
border: 1px solid #e2e2e2;
border-radius: 6px;
padding: 5px 8px;
font-size: 13px;
}
.customForm button {
border: 1px solid #e2e2e2;
border-radius: 999px;
padding: 6px 14px;
font-size: 13px;
background: #fff;
cursor: pointer;
}
.compareToggle {
margin-left: auto;
font-size: 13px;
}
.compareToggle a {
color: #8b616d;
text-decoration: none;
}
.compareToggle a:hover {
text-decoration: underline;
}
.tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin-bottom: 28px;
}
.tile {
border: 1px solid #e6e6e6;
border-radius: 12px;
padding: 14px 16px;
background: #fafafa;
}
.tileLabel {
font-size: 12px;
color: #888;
margin: 0 0 6px;
}
.tileValue {
font-size: 24px;
font-weight: 700;
margin: 0;
}
.tileDelta {
font-size: 12px;
font-weight: 600;
margin-top: 4px;
}
.deltaUp {
color: #3a4f30;
}
.deltaDown {
color: #7a2020;
}
.deltaFlat {
color: #888;
}
.chartCard {
border: 1px solid #e6e6e6;
border-radius: 12px;
padding: 18px;
margin-bottom: 28px;
}
.sectionTitle {
font-size: 15px;
font-weight: 600;
margin: 0 0 12px;
}
.tableGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 20px;
margin-bottom: 28px;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.table th {
text-align: left;
font-size: 11px;
text-transform: uppercase;
color: #888;
padding: 4px 8px;
border-bottom: 1px solid #e6e6e6;
}
.table td {
padding: 6px 8px;
border-bottom: 1px solid #f0f0f0;
}
.table tr:last-child td {
border-bottom: none;
}
.tableEmpty {
color: #888;
font-size: 13px;
padding: 8px;
}
.privacyPanel {
border: 1px solid #e6e6e6;
border-radius: 12px;
padding: 18px;
background: #f7f4f1;
font-size: 13px;
color: #444;
}
.privacyPanel h3 {
margin: 0 0 8px;
font-size: 14px;
color: #1f1f1f;
}
.privacyPanel ul {
margin: 8px 0;
padding-left: 18px;
}
.privacyPanel p {
margin: 6px 0;
}