feat: redesign CMS and unify German admin UI
This commit is contained in:
@@ -18,7 +18,7 @@ function bump(map: Record<string, number>, key: string | null | undefined) {
|
||||
map[key] = (map[key] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/** Sorted (descending) [key, count] pairs, capped at `limit` — for dashboard tables. */
|
||||
/** Sorted (descending) [key, count] pairs, capped at `limit` - for dashboard tables. */
|
||||
export function topEntries(map: Record<string, number>, limit = 10): [string, number][] {
|
||||
return Object.entries(map)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
@@ -26,7 +26,7 @@ export function topEntries(map: Record<string, number>, limit = 10): [string, nu
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure aggregation over a set of raw AnalyticsEvents rows (any date range —
|
||||
* Pure aggregation over a set of raw AnalyticsEvents rows (any date range -
|
||||
* one day for the cleanup rollup, or a whole dashboard-selected range for
|
||||
* live queries). Kept side-effect free and DB-agnostic so both callers
|
||||
* (lib/analytics/cleanup.ts and lib/analytics/dashboardQueries.ts) share
|
||||
|
||||
@@ -14,7 +14,7 @@ function addDays(date: Date, days: number): Date {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolls up every not-yet-aggregated, fully-completed calendar day (UTC —
|
||||
* Rolls up every not-yet-aggregated, fully-completed calendar day (UTC -
|
||||
* matching the UTC-day salt rotation in lib/analytics/salt.ts) from raw
|
||||
* analytics-events into analytics-daily-stats, THEN purges raw events older
|
||||
* than the configured retention, THEN purges old daily-stats rows. Order
|
||||
@@ -22,7 +22,7 @@ function addDays(date: Date, days: number): Date {
|
||||
* deleted, or that history is gone for good.
|
||||
*
|
||||
* Only aggregates days that don't already have a daily-stats row (each day
|
||||
* is aggregated exactly once, ever) — safe to call this daily via
|
||||
* is aggregated exactly once, ever) - safe to call this daily via
|
||||
* app/(payload)/api/cron/analytics-cleanup/route.ts, and safe to re-run.
|
||||
*/
|
||||
export async function runAnalyticsCleanup(payload: Payload) {
|
||||
@@ -60,7 +60,7 @@ export async function runAnalyticsCleanup(payload: Payload) {
|
||||
{ createdAt: { less_than: dayEnd.toISOString() } },
|
||||
],
|
||||
},
|
||||
limit: 0, // no limit — pull the whole day, small-site scale
|
||||
limit: 0, // no limit - pull the whole day, small-site scale
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
/**
|
||||
* Public developer API + consent storage for the analytics system. Importing
|
||||
* this module has zero side effects — no network call, nothing initialized
|
||||
* — until `track()` is actually called, and even then it's a no-op unless
|
||||
* this module has zero side effects - no network call, nothing initialized
|
||||
* - until `track()` is actually called, and even then it's a no-op unless
|
||||
* consent was actually granted. Callers never check consent themselves:
|
||||
*
|
||||
* track("booking_started");
|
||||
*
|
||||
* The CMS-configured part of consent (whether the "statistics" category is
|
||||
* enabled at all, and the current consent version) is pushed in once by
|
||||
* components/Analytics.tsx via setAnalyticsConfig() — see that file. Until
|
||||
* components/Analytics.tsx via setAnalyticsConfig() - see that file. Until
|
||||
* that happens, `cmsConfig` is null and track() fails safe (no-op), so a
|
||||
* component whose effects happen to run before Analytics.tsx's can never
|
||||
* accidentally send data before the live config is known.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Central, extensible registry of known analytics event names. Adding a new
|
||||
* event elsewhere in the app never requires a migration — the database
|
||||
* column (collections/AnalyticsEvents.ts#eventName) is free-text — this
|
||||
* event elsewhere in the app never requires a migration - the database
|
||||
* column (collections/AnalyticsEvents.ts#eventName) is free-text - this
|
||||
* list exists purely so `track()` calls get autocomplete/type-checking and
|
||||
* the dashboard can show a nice German label instead of a raw event name.
|
||||
*
|
||||
@@ -18,7 +18,7 @@ export const ANALYTICS_EVENTS = {
|
||||
|
||||
export type KnownAnalyticsEvent = keyof typeof ANALYTICS_EVENTS;
|
||||
|
||||
/** Any string works (see the collection comment above) — known ones just get a label. */
|
||||
/** Any string works (see the collection comment above) - known ones just get a label. */
|
||||
export type AnalyticsEventName = KnownAnalyticsEvent | (string & {});
|
||||
|
||||
export function analyticsEventLabel(eventName: string): string {
|
||||
|
||||
@@ -27,7 +27,7 @@ function parseDateParam(value: string | null): Date | null {
|
||||
|
||||
/**
|
||||
* Resolves the dashboard's period filter (?range=&from=&to=) into an actual
|
||||
* date range, clamped to `maxDaysBack` (the configured raw-event retention —
|
||||
* date range, clamped to `maxDaysBack` (the configured raw-event retention -
|
||||
* see globals/AnalyticsSettings.ts) so the UI never claims to show data that
|
||||
* has already been purged. Also computes the matching "previous period" for
|
||||
* comparison (same length, immediately preceding).
|
||||
|
||||
@@ -2,14 +2,14 @@ import { createHmac } from "node:crypto";
|
||||
|
||||
/**
|
||||
* Derives a daily-rotating, non-reversible pseudonymous visitor identifier
|
||||
* for session grouping — the same approach used by privacy-friendly
|
||||
* for session grouping - the same approach used by privacy-friendly
|
||||
* analytics tools (Plausible/Fathom-style): hash(ip + userAgent + host)
|
||||
* salted with a value that changes every calendar day. Raw IP/UA are never
|
||||
* persisted anywhere, only this hash — and since the salt rotates daily,
|
||||
* persisted anywhere, only this hash - and since the salt rotates daily,
|
||||
* the same visitor hashes differently tomorrow, so there is no cross-day
|
||||
* linkage and no long-term individual profile.
|
||||
*
|
||||
* ANALYTICS_SALT_SECRET is a dedicated secret (not PAYLOAD_SECRET) — this
|
||||
* ANALYTICS_SALT_SECRET is a dedicated secret (not PAYLOAD_SECRET) - this
|
||||
* codebase keeps one secret per purpose on purpose (see
|
||||
* MEETING_SESSION_SECRET's comment in .env.example).
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ const SESSION_IDLE_WINDOW_MS = 30 * 60 * 1000;
|
||||
* Groups events into sessions without any client-side identifier: looks up
|
||||
* the most recent event from the same `visitorHash` within the last 30
|
||||
* minutes and reuses its sessionId, or mints a new one. One extra indexed
|
||||
* read per tracked request — acceptable at this site's traffic volume, and
|
||||
* read per tracked request - acceptable at this site's traffic volume, and
|
||||
* simpler/safer than trying to hold a lock across an unauthenticated,
|
||||
* high-frequency public endpoint (unlike lib/booking/lock.ts's advisory
|
||||
* lock, an occasional extra spurious session here has no correctness
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Coarse device/browser classification from the User-Agent header. No
|
||||
* library, no raw UA string ever stored — just one bucket per request,
|
||||
* library, no raw UA string ever stored - just one bucket per request,
|
||||
* computed on the fly and discarded after classification. Not meant to be
|
||||
* precise (e.g. WebView vs. real browser); good enough for the "device
|
||||
* type" / "browser" breakdown in the Analytics dashboard.
|
||||
@@ -19,7 +19,7 @@ export function classifyDevice(userAgent: string): DeviceType {
|
||||
export function classifyBrowser(userAgent: string): BrowserFamily {
|
||||
const ua = userAgent.toLowerCase();
|
||||
// Order matters: Edge/Chrome UAs also contain "safari"; Chrome UAs also
|
||||
// contain "chrome" but so do Edge/Opera — check the most specific tokens first.
|
||||
// contain "chrome" but so do Edge/Opera - check the most specific tokens first.
|
||||
if (/edg\//.test(ua)) return "edge";
|
||||
if (/chrome\/|crios\//.test(ua)) return "chrome";
|
||||
if (/firefox\/|fxios\//.test(ua)) return "firefox";
|
||||
|
||||
Reference in New Issue
Block a user