Neutralise demo pages: stack-agnostic copy, non-stale dates

- Demo bars now read "Beispielseite · So könnte Ihr Online-Auftritt aussehen"
  (was "Tier A (Google-Stack)" / "Tier B (Self-Hosted)" / "für Gäste").
- Booking widgets relabelled generic: "Online-Terminbuchung" / "Sichere Buchung"
  (dropped Google Kalender + branded dots, Cal.com, self-hosted).
- Removed Tier/Google/Cal.com refs from rendered copy AND HTML comments.
- Demo booking dates now render on the current week via JS (no stale dates).
- TODO.md: tick off completed items; add onboard back-office (Leads/Clients CRUD).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 08:47:22 +02:00
parent cde0cb0fa8
commit a04ad82e73
4 changed files with 95 additions and 34 deletions
+34 -12
View File
@@ -6,24 +6,46 @@ client's site is self-hosted or runs on Google's free services) is a decision ma
**between me and the client**, case by case — not something baked into a demo. **between me and the client**, case by case — not something baked into a demo.
Implications to work through: Implications to work through:
- **Demos are stack-agnostic starters.** Each demo page is just an example business with - [x] **Demos are stack-agnostic starters.** ~~Drop tier/stack labels and jargon (self-hosted,
*no committed stack* — it shows what the client could get (website + booking / orders / Cal.com, Google-Stack, "≈ 0 €") from the public-facing demos & chooser.~~ DONE 2026-06-25:
lead capture), not "Tier A = Google" vs "Tier B = self-hosted". Drop tier/stack labels and chooser reworded; all three demo pages neutralised — demo bars now read "Beispielseite · So
jargon (self-hosted, Cal.com, Google-Stack, "≈ 0 €") from the public-facing demos & könnte Ihr Online-Auftritt aussehen", booking widgets relabelled to generic
chooser. (Chooser already reworded 2026-06-25; the demo *pages* still carry stack framing — "Online-Terminbuchung"/"Sichere Buchung", Google-Kalender/Cal.com/self-hosted/Tier labels
tier-a is Google-Kalender-themed, tier-b is self-hosted/Datenschutz-themed. Neutralise later.) removed from rendered copy *and* HTML comments, demo booking dates now render on the current
- After a client signs, **I decide hosting**: self-host the page on the homelab, or stand it week via JS so nothing looks stale.
- [ ] After a client signs, **I decide hosting**: self-host the page on the homelab, or stand it
up on the client's free Google account. The CRM already has `tier` + `stack_notes` columns up on the client's free Google account. The CRM already has `tier` + `stack_notes` columns
to record that decision per client. to record that decision per client.
- Revisit the `Clients.tier` field semantics: keep A/B as an internal effort/price band, or - [ ] Revisit the `Clients.tier` field semantics: keep A/B as an internal effort/price band, or
replace with something hosting-agnostic (e.g. `hosting = google | selfhosted`, `plan = …`). replace with something hosting-agnostic (e.g. `hosting = google | selfhosted`, `plan = …`).
- Onboarding form + workflow currently ask for Tier A/B — realign once the model is settled. - [ ] Onboarding form + workflow currently ask for Tier A/B — realign once the model is settled.
## Onboard dashboard → manage Leads & Clients (CRUD)
The operator onboarding page (https://onboard.mivanchenko.de) currently only **creates** records.
Extend it into a small back-office where I can **read, edit, delete and add** both Leads and
Clients directly:
- [ ] **Read:** pull and list the current `Leads` and `Clients` rows (via n8n webhook → Sheets
`values.get`, or Sheets API) in a clean table on the onboard page.
- [ ] **Add:** new Lead / new Client manually (Clients add already exists via the onboarding form;
add a lighter "new lead" form too).
- [ ] **Edit:** update a row in place (e.g. change a lead's status, fix a client's fee/renewal).
- [ ] **Delete:** cleanly remove a row (Sheets `deleteDimension`), so test/junk rows don't pile up
— replaces the current scratchpad `del_rows.py`/`del_lead.py` manual cleanup.
- [ ] Stays behind the existing Caddy basic-auth (operator-only; these are billable/CRM records).
- [ ] Needs new n8n webhooks (list/update/delete) keyed by `lead_id` / `client_id`.
## Other ## Other
- Harden n8n auth: compose still has deprecated `N8N_BASIC_AUTH_*` with password `password` - [ ] Harden n8n auth: compose still has deprecated `N8N_BASIC_AUTH_*` with password `password`
(legacy/ignored by modern n8n owner-login). Confirm the owner account is strong; remove the (legacy/ignored by modern n8n owner-login). Confirm the owner account is strong; remove the
dead env. dead env.
- Orders: the pizzeria demo posts orders into the `Leads` tab via the lead-intake webhook as a - [ ] Orders: the pizzeria demo posts orders into the `Leads` tab via the lead-intake webhook as a
stop-gap. Build a proper **Orders** flow + sheet tab (items, total, mode, status) when the stop-gap. Build a proper **Orders** flow + sheet tab (items, total, mode, status) when the
order-management product is real. order-management product is real.
- Booking-sync workflow (Tier-B Cal.com webhook + Tier-A Google Calendar poll) — next up. - [ ] Booking-sync workflow (Tier-B Cal.com webhook + Tier-A Google Calendar poll).
## Done
- [x] 2026-06-25 — Latency fix (n8n executions 14 min → ~6 s; DNS + PostHog telemetry).
- [x] 2026-06-25 — Chooser reworded to client-friendly, jargon-free copy.
- [x] 2026-06-25 — Pizzeria demo with online ordering added.
- [x] 2026-06-25 — Barbershop "Google Kalender" dot-overlap styling fixed.
- [x] 2026-06-25 — All three demo pages neutralised (stack-agnostic, non-stale dates).
+1 -1
View File
@@ -130,7 +130,7 @@
</style> </style>
</head> </head>
<body> <body>
<div class="demo-bar">Demo · Pizzeria mit Online-Bestellung — Beispielseite für Gäste <button class="cb-open" type="button" onclick="cbOpen()">📞 Rückruf anfordern</button></div> <div class="demo-bar">Beispielseite · So könnte Ihr Online-Auftritt aussehen <button class="cb-open" type="button" onclick="cbOpen()">📞 Rückruf anfordern</button></div>
<header class="nav"> <header class="nav">
<div class="wrap nav-inner"> <div class="wrap nav-inner">
+16 -7
View File
@@ -90,7 +90,7 @@
.booking-grid { display: grid; grid-template-columns: 1.05fr 1fr; gap: 40px; align-items: center; } .booking-grid { display: grid; grid-template-columns: 1.05fr 1fr; gap: 40px; align-items: center; }
.booking-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 26px; box-shadow: var(--shadow); } .booking-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 26px; box-shadow: var(--shadow); }
.gcal-head { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: .82rem; border-bottom: 1px solid var(--line); padding-bottom: 14px; margin-bottom: 18px; } .gcal-head { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: .82rem; border-bottom: 1px solid var(--line); padding-bottom: 14px; margin-bottom: 18px; }
.gcal-dot { flex: 0 0 auto; width: 12px; height: 12px; border-radius: 50%; background: #4285F4; box-shadow: 18px 0 0 #34A853, 36px 0 0 #FBBC05; margin-right: 40px; } .gcal-dot { flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%; background: #4caf7d; box-shadow: 0 0 0 3px rgba(76,175,125,.18); margin-right: 12px; }
.slots { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } .slots { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.slot { text-align: center; padding: 11px 6px; border: 1px solid var(--line); border-radius: 9px; color: var(--ink); font-size: .9rem; cursor: pointer; transition: .15s; background: #1b1611; } .slot { text-align: center; padding: 11px 6px; border: 1px solid var(--line); border-radius: 9px; color: var(--ink); font-size: .9rem; cursor: pointer; transition: .15s; background: #1b1611; }
.slot:hover { border-color: var(--gold); color: var(--gold-2); } .slot:hover { border-color: var(--gold); color: var(--gold-2); }
@@ -133,7 +133,7 @@
</style> </style>
</head> </head>
<body> <body>
<div class="demo-bar">Demo · Tier A (Google-Stack) — Beispielseite für Kund:innen <button class="cb-open" type="button" onclick="cbOpen()">📞 Rückruf anfordern</button></div> <div class="demo-bar">Beispielseite · So könnte Ihr Online-Auftritt aussehen <button class="cb-open" type="button" onclick="cbOpen()">📞 Rückruf anfordern</button></div>
<header class="nav"> <header class="nav">
<div class="wrap nav-inner"> <div class="wrap nav-inner">
@@ -198,11 +198,11 @@
<li>Automatische Erinnerung am Vortag</li> <li>Automatische Erinnerung am Vortag</li>
<li>Kostenlos umbuchen bis 24 Std. vorher</li> <li>Kostenlos umbuchen bis 24 Std. vorher</li>
</ul> </ul>
<!-- Tier A: dieser Button öffnet in der echten Version den Google Kalender „Terminplan" (Appointment Schedule). --> <!-- Demo: in der Live-Version öffnet dieser Button die echte Online-Terminbuchung. -->
<a href="#" class="btn" onclick="return false;">Verfügbare Zeiten ansehen →</a> <a href="#" class="btn" onclick="return false;">Verfügbare Zeiten ansehen →</a>
</div> </div>
<div class="booking-card"> <div class="booking-card">
<div class="gcal-head"><span class="gcal-dot"></span> Google&nbsp;Kalender · Terminplan — Heute, 23. Juni</div> <div class="gcal-head"><span class="gcal-dot"></span> Online-Terminbuchung — <span id="bookDate">heute</span></div>
<div class="slots"> <div class="slots">
<div class="slot off">09:00</div> <div class="slot off">09:00</div>
<div class="slot">10:30</div> <div class="slot">10:30</div>
@@ -214,7 +214,7 @@
<div class="slot off">17:15</div> <div class="slot off">17:15</div>
<div class="slot">18:00</div> <div class="slot">18:00</div>
</div> </div>
<p class="form-note" style="margin-top:16px;">Demo-Vorschau. In der Live-Version wird hier der echte Google-Kalender-Terminplan eingebettet.</p> <p class="form-note" style="margin-top:16px;">Demo-Vorschau. In der Live-Version buchen Ihre Kund:innen hier rund um die Uhr selbst einen freien Termin — mit sofortiger Bestätigung.</p>
</div> </div>
</div> </div>
</section> </section>
@@ -247,7 +247,7 @@
<h2>Frage stellen oder Rückruf anfordern</h2> <h2>Frage stellen oder Rückruf anfordern</h2>
<p>Schreib uns kurz — wir melden uns am selben Tag.</p> <p>Schreib uns kurz — wir melden uns am selben Tag.</p>
</div> </div>
<!-- Tier A: dieses Formular postet an einen n8n-Webhook -> Google Sheet (Leads) + Telegram. --> <!-- Demo: dieses Formular postet an einen n8n-Webhook -> CRM (Leads) + Telegram. -->
<form class="lead" onsubmit="return submitLead(event, this)"> <form class="lead" onsubmit="return submitLead(event, this)">
<input type="hidden" name="client_id" value="DEMO-A" /> <input type="hidden" name="client_id" value="DEMO-A" />
<input type="hidden" name="source" value="demo-tier-a" /> <input type="hidden" name="source" value="demo-tier-a" />
@@ -283,7 +283,7 @@
<div class="wrap" style="margin-top:30px;opacity:.6;font-size:.8rem;">© 2026 Schnittpunkt Barbershop · Demo-Seite · Impressum · Datenschutz</div> <div class="wrap" style="margin-top:30px;opacity:.6;font-size:.8rem;">© 2026 Schnittpunkt Barbershop · Demo-Seite · Impressum · Datenschutz</div>
</footer> </footer>
<script> <script>
// Lead-Formular -> n8n Webhook -> Google Sheet (Leads) + Telegram-Benachrichtigung. // Lead-Formular -> n8n Webhook -> CRM (Leads) + Telegram-Benachrichtigung.
const LEAD_WEBHOOK = 'https://n8n.mivanchenko.de/webhook/lead-intake'; const LEAD_WEBHOOK = 'https://n8n.mivanchenko.de/webhook/lead-intake';
async function submitLead(e, form) { async function submitLead(e, form) {
e.preventDefault(); e.preventDefault();
@@ -311,6 +311,15 @@
return false; return false;
} }
</script> </script>
<script>
// Keep the demo booking header on today's date so the example never looks stale.
(function () {
const M = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const t = new Date();
const el = document.getElementById('bookDate');
if (el) el.textContent = 'heute, ' + t.getDate() + '. ' + M[t.getMonth()];
})();
</script>
<script>window.CB_CTX = { client_id: "DEMO-A", source: "callback-tier-a" };</script> <script>window.CB_CTX = { client_id: "DEMO-A", source: "callback-tier-a" };</script>
<!-- ===== Rückruf-Widget (lead capture popup) ===== --> <!-- ===== Rückruf-Widget (lead capture popup) ===== -->
<style> <style>
+44 -14
View File
@@ -85,7 +85,7 @@
.badges { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 20px; } .badges { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 20px; }
.badge { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); border-radius: 999px; padding: 8px 15px; font-size: .82rem; font-weight: 600; color: #eafaf7; } .badge { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); border-radius: 999px; padding: 8px 15px; font-size: .82rem; font-weight: 600; color: #eafaf7; }
/* booking (Cal.com style) */ /* booking (calendar widget) */
.booking { background: var(--sand); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } .booking { background: var(--sand); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.booking-grid { display: grid; grid-template-columns: 1fr 1.05fr; gap: 40px; align-items: center; } .booking-grid { display: grid; grid-template-columns: 1fr 1.05fr; gap: 40px; align-items: center; }
.cal { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; } .cal { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
@@ -149,7 +149,7 @@
</style> </style>
</head> </head>
<body> <body>
<div class="demo-bar">Demo · Tier B (Self-Hosted / Datenschutz) — Beispielseite <button class="cb-open" type="button" onclick="cbOpen()">📞 Rückruf anfordern</button></div> <div class="demo-bar">Beispielseite · So könnte Ihr Online-Auftritt aussehen <button class="cb-open" type="button" onclick="cbOpen()">📞 Rückruf anfordern</button></div>
<header class="nav"> <header class="nav">
<div class="wrap nav-inner"> <div class="wrap nav-inner">
@@ -187,11 +187,10 @@
</div> </div>
<aside class="hero-card"> <aside class="hero-card">
<h3>Nächste freie Termine</h3> <h3>Nächste freie Termine</h3>
<div class="mini-cal"> <div class="mini-cal" id="miniCal">
<span>Mo</span><span>Di</span><span>Mi</span><span>Do</span><span>Fr</span><span>Sa</span><span>So</span> <span>Mo</span><span>Di</span><span>Mi</span><span>Do</span><span>Fr</span><span>Sa</span><span>So</span>
<b>23</b><b class="free">24</b><b class="sel">25</b><b class="free">26</b><b class="free">27</b><b>28</b><b style="opacity:.4">29</b>
</div> </div>
<p class="form-note" style="margin-top:14px;">Mi, 25. Juni — freie Zeiten:</p> <p class="form-note" style="margin-top:14px;"><span id="miniLabel">Freie Zeiten</span> — freie Zeiten:</p>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;"> <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;">
<span class="pill" style="background:#fff;border:1px solid var(--teal);">08:30</span> <span class="pill" style="background:#fff;border:1px solid var(--teal);">08:30</span>
<span class="pill" style="background:#fff;border:1px solid var(--teal);">11:00</span> <span class="pill" style="background:#fff;border:1px solid var(--teal);">11:00</span>
@@ -226,7 +225,7 @@
<div> <div>
<span class="eyebrow" style="color:#7fd8cb">Datenschutz by Design</span> <span class="eyebrow" style="color:#7fd8cb">Datenschutz by Design</span>
<h2>Ihre Gesundheitsdaten bleiben in Deutschland.</h2> <h2>Ihre Gesundheitsdaten bleiben in Deutschland.</h2>
<p>Buchungen, Kontaktanfragen und Patientendaten laufen über unsere eigene, in Deutschland gehostete Infrastruktur — keine Weitergabe an Werbenetzwerke, kein Tracking durch Dritte. Verschlüsselt übertragen, vom ersten Klick an.</p> <p>Buchungen, Kontaktanfragen und Patientendaten werden verschlüsselt übertragen und in Deutschland gespeichert — keine Weitergabe an Werbenetzwerke, kein Tracking durch Dritte. Sicher, vom ersten Klick an.</p>
<div class="badges"> <div class="badges">
<span class="badge">🇩🇪 Hosting in Deutschland</span> <span class="badge">🇩🇪 Hosting in Deutschland</span>
<span class="badge">🔐 Verschlüsselte Übertragung (TLS)</span> <span class="badge">🔐 Verschlüsselte Übertragung (TLS)</span>
@@ -237,27 +236,25 @@
</div> </div>
</section> </section>
<!-- BOOKING (Cal.com style) --> <!-- BOOKING (calendar widget) -->
<section class="booking" id="buchen"> <section class="booking" id="buchen">
<div class="wrap booking-grid"> <div class="wrap booking-grid">
<div> <div>
<span class="eyebrow">Termin buchen</span> <span class="eyebrow">Termin buchen</span>
<h2 style="font-size:clamp(2rem,4.6vw,2.9rem);margin:8px 0 12px;">In drei Klicks zum Termin.</h2> <h2 style="font-size:clamp(2rem,4.6vw,2.9rem);margin:8px 0 12px;">In drei Klicks zum Termin.</h2>
<p style="color:var(--muted);max-width:420px;">Wählen Sie Behandlung, Tag und Uhrzeit — Sie erhalten sofort eine verschlüsselte Bestätigung. Erinnerung folgt automatisch, Umbuchung jederzeit möglich.</p> <p style="color:var(--muted);max-width:420px;">Wählen Sie Behandlung, Tag und Uhrzeit — Sie erhalten sofort eine verschlüsselte Bestätigung. Erinnerung folgt automatisch, Umbuchung jederzeit möglich.</p>
<p class="form-note" style="margin-top:18px;">Self-hosted Buchung — läuft in der Live-Version über Ihre eigene Cal.com-Instanz auf der Praxis-Infrastruktur.</p> <p class="form-note" style="margin-top:18px;">Demo-Vorschau. In der Live-Version buchen Ihre Patient:innen hier direkt einen freien Termin — mit sofortiger, verschlüsselter Bestätigung.</p>
</div> </div>
<!-- Tier B: in der Live-Version ist hier die self-hosted Cal.com-Instanz eingebettet. --> <!-- Demo: in der Live-Version ist hier die echte Online-Terminbuchung eingebettet. -->
<div class="cal"> <div class="cal">
<div class="cal-top"> <div class="cal-top">
<div class="who"><span class="av">PV</span><div><b style="font-family:'Inter';font-weight:600;">Physiotherapie · 30 Min</b><br><small>PhysioVital Erlangen</small></div></div> <div class="who"><span class="av">PV</span><div><b style="font-family:'Inter';font-weight:600;">Physiotherapie · 30 Min</b><br><small>PhysioVital Erlangen</small></div></div>
<small>🔒 self-hosted</small> <small>🔒 Sichere Buchung</small>
</div> </div>
<div class="cal-body"> <div class="cal-body">
<div class="cal-days"> <div class="cal-days">
<div class="row" style="margin-bottom:6px;"><span>Mo</span><span>Di</span><span>Mi</span><span>Do</span><span>Fr</span><span>Sa</span><span>So</span></div> <div class="row" style="margin-bottom:6px;"><span>Mo</span><span>Di</span><span>Mi</span><span>Do</span><span>Fr</span><span>Sa</span><span>So</span></div>
<div class="row"> <div class="row" id="calRow"></div>
<b style="opacity:.3">21</b><b style="opacity:.3">22</b><b>23</b><b class="free">24</b><b class="sel">25</b><b class="free">26</b><b class="free">27</b>
</div>
</div> </div>
<div class="cal-times"> <div class="cal-times">
<div class="t">08:30</div> <div class="t">08:30</div>
@@ -279,7 +276,7 @@
<h2>Rezept einreichen oder Frage stellen</h2> <h2>Rezept einreichen oder Frage stellen</h2>
<p>Schreiben Sie uns — wir melden uns zeitnah und vertraulich zurück.</p> <p>Schreiben Sie uns — wir melden uns zeitnah und vertraulich zurück.</p>
</div> </div>
<!-- Tier B: dieses Formular postet an einen n8n-Webhook -> CRM (Leads) + Telegram. --> <!-- Demo: dieses Formular postet an einen n8n-Webhook -> CRM (Leads) + Telegram. -->
<form class="lead" onsubmit="return submitLead(event, this)"> <form class="lead" onsubmit="return submitLead(event, this)">
<input type="hidden" name="client_id" value="DEMO-B" /> <input type="hidden" name="client_id" value="DEMO-B" />
<input type="hidden" name="source" value="demo-tier-b" /> <input type="hidden" name="source" value="demo-tier-b" />
@@ -350,6 +347,39 @@
return false; return false;
} }
</script> </script>
<script>
// Render the demo calendar on the current week so the example never shows stale dates.
(function () {
const WD = ['Mo','Di','Mi','Do','Fr','Sa','So'];
const M = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const sameDay = (a, b) => a.toDateString() === b.toDateString();
const today = new Date(); today.setHours(0, 0, 0, 0);
// selected = first weekday (MonFri) on/after today
const sel = new Date(today);
while (sel.getDay() === 0 || sel.getDay() === 6) sel.setDate(sel.getDate() + 1);
const selDow = (sel.getDay() + 6) % 7; // 0 = Mon
const monday = new Date(sel); monday.setDate(sel.getDate() - selDow);
const week = Array.from({ length: 7 }, (_, i) => { const d = new Date(monday); d.setDate(monday.getDate() + i); return d; });
let freeLeft = 2;
const cells = week.map(d => {
const dow = (d.getDay() + 6) % 7; // 0 = Mon
const weekend = dow > 4;
if (sameDay(d, sel)) return `<b class="sel">${d.getDate()}</b>`;
if (d < today) return `<b style="opacity:.3">${d.getDate()}</b>`;
if (!weekend && d > sel && freeLeft) { freeLeft--; return `<b class="free">${d.getDate()}</b>`; }
if (weekend) return `<b style="opacity:.4">${d.getDate()}</b>`;
return `<b>${d.getDate()}</b>`;
}).join('');
const calRow = document.getElementById('calRow');
if (calRow) calRow.innerHTML = cells;
const miniCal = document.getElementById('miniCal');
if (miniCal) miniCal.insertAdjacentHTML('beforeend', cells);
const label = document.getElementById('miniLabel');
if (label) label.textContent = `${WD[selDow]}, ${sel.getDate()}. ${M[sel.getMonth()]}`;
})();
</script>
<script>window.CB_CTX = { client_id: "DEMO-B", source: "callback-tier-b" };</script> <script>window.CB_CTX = { client_id: "DEMO-B", source: "callback-tier-b" };</script>
<!-- ===== Rückruf-Widget (lead capture popup) ===== --> <!-- ===== Rückruf-Widget (lead capture popup) ===== -->
<style> <style>