From 1d53b66e3f077e79a9d4e8fe688ab357d07e6ff9 Mon Sep 17 00:00:00 2001 From: Clemens Creutzburg Date: Fri, 21 Aug 2026 00:14:17 +0200 Subject: [PATCH] Wasserzeichen fuer den kostenlosen Tarif Mandanten im Tarif "free" (bis 10 Teilnehmer) zeigen unten rechts eine kleine Marke mit Logo und Adresse der Kaffeeliste; zahlende Mandanten behalten ihre Oberflaeche ohne fremdes Logo. Die Sichtbarkeit haengt allein an tenant_billing.plan_code und wird als reine Leseabfrage geprueft - billing_fetch_or_init() waere ein Schreibzugriff im Seitenaufbau. Laesst sich der Tarif nicht lesen, bleibt die Marke aus. Das Logo ist ein Inline-SVG, das Linkziel kommt aus APP_MARKETING_URL bzw. aus APP_HOST ohne App-Subdomain. Co-Authored-By: Claude Opus 5 --- app/bootstrap.php | 27 ++++++++++++ app/plan-watermark.php | 93 ++++++++++++++++++++++++++++++++++++++++++ assets/css/app.css | 52 +++++++++++++++++++++++ docs/billing.md | 34 +++++++++++++++ env.local.example.php | 5 +++ footer.php | 11 +++++ scripts/http-smoke.php | 4 +- 7 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 app/plan-watermark.php diff --git a/app/bootstrap.php b/app/bootstrap.php index 92b9f74..7b4a047 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -99,6 +99,33 @@ function app_primary_url(string $path): string return $scheme . '://' . $host . '/' . ltrim($path, '/'); } +/** + * Adresse der Marketing-Domain (z. B. https://kaffeeliste.de) - Ziel des + * Wasserzeichens im kostenlosen Tarif. Explizit ueber APP_MARKETING_URL + * setzbar; sonst wird sie aus APP_HOST abgeleitet, indem die + * App-Subdomain wegfaellt (app.kaffeeliste.de -> kaffeeliste.de). In der + * lokalen Entwicklung ohne APP_HOST bleibt der aktuelle Host uebrig, im + * CLI-Betrieb ein leerer String. + */ +function app_marketing_url(): string +{ + $configured = app_env('APP_MARKETING_URL'); + if ($configured !== null) { + return rtrim($configured, '/'); + } + + $scheme = app_is_https() ? 'https' : 'http'; + $host = app_primary_host(); + + if ($host !== null) { + return $scheme . '://' . preg_replace('/^app\./i', '', $host); + } + + $requestHost = (string)($_SERVER['HTTP_HOST'] ?? ''); + + return $requestHost !== '' ? $scheme . '://' . $requestHost : ''; +} + function app_start_session(): void { if (PHP_SAPI === 'cli' || session_status() === PHP_SESSION_ACTIVE) { diff --git a/app/plan-watermark.php b/app/plan-watermark.php new file mode 100644 index 0000000..f710ef6 --- /dev/null +++ b/app/plan-watermark.php @@ -0,0 +1,93 @@ +prepare('SELECT plan_code FROM tenant_billing WHERE tenant_id = ?'); + $stmt->execute([$tenantId]); + $planCode = $stmt->fetchColumn(); + $cache[$tenantId] = $planCode === false ? 'free' : (string)$planCode; + } catch (Throwable $e) { + // Laesst sich der Tarif nicht lesen, bleibt die Oberflaeche sauber - + // ein Wasserzeichen bei einem zahlenden Kunden waere der schlimmere + // Fehler als ein fehlendes bei einem kostenlosen. + $cache[$tenantId] = 'unknown'; + } + + return $cache[$tenantId]; +} + +/** + * Zeigt dieser Mandant das Wasserzeichen? Nur der kostenlose Tarif tut das. + */ +function plan_watermark_visible(PDO $pdo, int $tenantId): bool +{ + if ($tenantId <= 0) { + return false; + } + + return plan_watermark_plan_code($pdo, $tenantId) === 'free'; +} + +/** + * Wortmarke der Kaffeeliste als Inline-SVG. Als Inline-Grafik statt als + * Bilddatei, damit das Wasserzeichen ohne zusaetzlichen Request auskommt, + * auf jedem Bildschirm scharf bleibt und die Farbe der Umgebung uebernimmt. + */ +function plan_watermark_logo_svg(): string +{ + return ''; +} + +/** + * Markup des Wasserzeichens. Ohne erreichbare Marketing-Domain (CLI, + * ungewoehnliche Hosts) bleibt es bei Logo und Namen ohne Link - ein + * Verweis ins Leere waere schlechter als keiner. + */ +function plan_watermark_html(): string +{ + $url = app_marketing_url(); + $label = $url !== '' ? preg_replace('#^https?://#i', '', $url) : 'Kaffeeliste'; + $inner = plan_watermark_logo_svg() + . '' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ''; + + if ($url === '') { + return '
' . $inner . '
'; + } + + return '' . $inner . ''; +} diff --git a/assets/css/app.css b/assets/css/app.css index 3654de6..a4daac3 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -163,3 +163,55 @@ tr.tarif-aktuell > td { tr.tarif-aktuell > td:first-child { box-shadow: inset 5px 0 0 0 #4caf50; } + +/* --------------------------------------------------------------------------- + * Wasserzeichen im kostenlosen Tarif (app/plan-watermark.php) + * + * Fest unten rechts, damit es unabhaengig von der Seitenlaenge sichtbar + * bleibt. Zurueckhaltend gehalten: die Kaffeeliste ist eine Arbeitsflaeche, + * der Hinweis soll erkennbar sein, aber nichts verdecken - deshalb halb + * transparent, erst beim Darueberfahren voll sichtbar. + * ------------------------------------------------------------------------ */ +.plan-watermark { + align-items: center; + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 999px; + bottom: 0.9rem; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12); + color: #4a4a4a; + display: inline-flex; + font-size: 0.7rem; + gap: 0.4em; + letter-spacing: 0.02em; + line-height: 1; + opacity: 0.65; + padding: 0.45em 0.8em; + position: fixed; + right: 0.9rem; + text-decoration: none; + transition: opacity 0.2s ease; + z-index: 900; +} + +a.plan-watermark { + border-bottom: 0; +} + +.plan-watermark:hover, +.plan-watermark:focus { + color: #4a4a4a; + opacity: 1; +} + +.plan-watermark-logo { + flex: 0 0 auto; +} + +/* Auf schmalen Bildschirmen bleibt nur das Logo - der Schriftzug wuerde dort + ueber den Inhalt laufen. */ +@media screen and (max-width: 480px) { + .plan-watermark-text { + display: none; + } +} diff --git a/docs/billing.md b/docs/billing.md index 44455f6..8773e4a 100644 --- a/docs/billing.md +++ b/docs/billing.md @@ -327,3 +327,37 @@ mandant-einstellungen.php (vollständige Tarif-Vergleichstabelle mit Bestell-But Zugriff wurden hier durchgeführt. Vor Go-Live einmal im Stripe-Testmodus gegen einen echten Test-Mandanten durchspielen (Upgrade, Downgrade zwischen zwei bezahlten Stufen, Downgrade auf `free`). + +## Wasserzeichen im kostenlosen Tarif (umgesetzt) + +Stand: 2026-08-21. Mandanten im Tarif `free` (bis 10 Teilnehmer) zeigen +unten rechts eine kleine Marke mit Logo und Adresse der Kaffeeliste. +Zahlende Mandanten sehen die Oberfläche ohne fremdes Logo. + +Umgesetzte Dateien: + +```text +app/plan-watermark.php (Sichtbarkeit + Markup) +app/bootstrap.php (app_marketing_url()) +footer.php (Einbindung vor den Skripten) +assets/css/app.css (.plan-watermark) +scripts/http-smoke.php (Dashboard-Check) +``` + +- **Nur der Tarif entscheidet.** `plan_watermark_visible()` blendet die + Marke ein, wenn `tenant_billing.plan_code` `free` ist – fehlt der + Datensatz, gilt wie beim Anlegen `free`. Lässt sich der Tarif nicht + lesen, bleibt die Oberfläche sauber: ein Wasserzeichen bei einem + zahlenden Kunden wäre der schlimmere Fehler. +- **Keine Schreibzugriffe im Seitenaufbau.** Bewusst nicht + `billing_fetch_or_init()`, das eine fehlende Zeile anlegen würde; die + Prüfung ist eine reine Leseabfrage und wird pro Request zwischengespeichert. +- **Logo als Inline-SVG.** Spart einen Request, bleibt auf jedem Bildschirm + scharf und übernimmt die Farbe der Umgebung. Ein eigenes Bild lässt sich + in `plan_watermark_logo_svg()` durch ein `` ersetzen. +- **Ziel der Verlinkung** ist die Marketing-Domain: `APP_MARKETING_URL`, + ersatzweise `APP_HOST` ohne die App-Subdomain + (`app.kaffeeliste.de` → `kaffeeliste.de`). +- Getrennt von `app/branding.php`: dort geht es um das Logo *des Mandanten* + (Kopfzeile), hier um das *des Anbieters*. Beides kann gleichzeitig + sichtbar sein. diff --git a/env.local.example.php b/env.local.example.php index 64874fb..ba7ae1a 100644 --- a/env.local.example.php +++ b/env.local.example.php @@ -24,6 +24,11 @@ putenv('APP_TIMEZONE=Europe/Berlin'); // stattdessen die Landingpage. Siehe index.php. putenv('APP_HOST=app.kaffeeliste.de'); +// Ziel des Wasserzeichens im kostenlosen Tarif (unten rechts in der App). +// Ohne Angabe wird die Marketing-Domain aus APP_HOST abgeleitet, indem die +// App-Subdomain wegfaellt - hier also https://kaffeeliste.de. +putenv('APP_MARKETING_URL=https://kaffeeliste.de'); + putenv('APP_DB_DRIVER=mysql'); putenv('DB_HOST='); putenv('DB_PORT=3306'); diff --git a/footer.php b/footer.php index 879348b..01ef441 100644 --- a/footer.php +++ b/footer.php @@ -142,6 +142,17 @@ if ($saasNavUser !== null) { + 0) { + require_once __DIR__ . '/app/plan-watermark.php'; + if (plan_watermark_visible(app_db_pdo(), $saasNavTenantId)) { + echo plan_watermark_html(); + } +} +?> + diff --git a/scripts/http-smoke.php b/scripts/http-smoke.php index ea4ec1b..afa8d00 100644 --- a/scripts/http-smoke.php +++ b/scripts/http-smoke.php @@ -52,7 +52,9 @@ $checks = [ 'label' => 'Dashboard', 'path' => 'index.php', 'auth' => true, - 'contains' => ['Aktueller Stand', 'Jahresübersicht', 'Letzte Einzahlungen'], + // Der Smoke-Mandant laeuft im kostenlosen Tarif, also muss das + // Wasserzeichen unten rechts mitkommen. + 'contains' => ['Aktueller Stand', 'Jahresübersicht', 'Letzte Einzahlungen', 'class="plan-watermark"'], ], [ 'label' => 'Striche erfassen',