From 19627f18b8557c4aa52a5abfd80df61b37154a73 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 28 Apr 2026 22:17:18 +0200 Subject: [PATCH] =?UTF-8?q?docs(demo-personas):=20Runbook=20f=C3=BCr=20ech?= =?UTF-8?q?te-Account-Demo-Workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorgehen pro Demo-Persona dokumentiert: Recherche → Live-Account auf Mac-Mini-Prod → Club-Space → idempotentes Seed-Skript → Smoke-Test. Inkl. Modul-Mapping (appId/tableName), Common Pitfalls (Prod-Schema-Drift field_timestamps vs field_meta, forced RLS auf sync_changes), und Lessons aus Persona 1 (Chor Tägerwilen). Verworfener Fork-System-Plan bleibt nicht im Repo — siehe Memory-Pointer project_demo_personas_workflow.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/demo-personas/HOW_TO.md | 233 +++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 docs/demo-personas/HOW_TO.md diff --git a/docs/demo-personas/HOW_TO.md b/docs/demo-personas/HOW_TO.md new file mode 100644 index 000000000..1870a5ef7 --- /dev/null +++ b/docs/demo-personas/HOW_TO.md @@ -0,0 +1,233 @@ +# Demo-Personas — Runbook + +Wie wir Pitch-Demo-Accounts mit echten Vereinen / Solo-Selbstständigen / +Familien befüllen. Pro Persona ein **echter, lebender User-Account** auf +`mana.how`-Prod, gefüllt mit recherchierten echten Daten des Ziels. Kein +spezielles Demo-Subsystem, keine Fork-Infrastruktur, keine Resets. + +Wenn der Ziel-Verein irgendwann tatsächlich Kunde wird, übernimmt er den +existierenden Account. Null Migration. + +--- + +## Account-Konvention + +| | | +|---|---| +| Email | `@mana.how` ohne `demo-`-Prefix | +| Initial-Passwort | Zufällig generiert, an Till übergeben | +| Tier | `founder` | +| Role | `admin` | +| Mailbox | Stalwart-Provisionierung läuft automatisch | +| Spaces | `personal` (auto) + meist `club`/`practice`/`family`/`brand` | + +--- + +## Phasen pro Persona + +### Phase 1 — Recherche (~20–40 min) + +Primärquelle ist die offizielle Website. Sekundärquellen nur bei Lücken. +Output: `docs/demo-personas//README.md` mit drei Pflicht-Sektionen: + +1. **Quellen** — Liste aller URLs mit Datum +2. **Daten** — strukturiert nach Modul +3. **Lücken** — was fehlt + wie wir damit umgehen (kein Erfinden) + +### Phase 2 — Account anlegen (~5 min, auf Mac-Mini-Prod) + +```bash +ssh mana-server +export PATH="/opt/homebrew/bin:$PATH" + +# 1. Register via Better-Auth +curl -sS -X POST http://localhost:3001/api/v1/auth/register \ + -H "Content-Type: application/json" \ + -d '{"email":"@mana.how","password":"","name":""}' + +# 2. Promote: founder + admin + verified + sync gifted + 10k credits +docker exec -i mana-infra-postgres psql -U postgres -d mana_platform <<'SQL' +UPDATE auth.users + SET email_verified = true, + access_tier = 'founder'::access_tier, + role = 'admin'::user_role, + updated_at = NOW() + WHERE email = '@mana.how'; + +INSERT INTO credits.sync_subscriptions (user_id, active, billing_interval, amount_charged, activated_at, is_gifted, gifted_by, gifted_at, created_at, updated_at) +SELECT id, true, 'monthly', 0, NOW(), true, 'demo-personas-runbook', NOW(), NOW(), NOW() + FROM auth.users WHERE email = '@mana.how' +ON CONFLICT (user_id) DO UPDATE SET active = true, is_gifted = true, updated_at = NOW(); + +INSERT INTO credits.balances (user_id, balance, total_earned, total_spent, version, created_at, updated_at) +SELECT id, 10000, 10000, 0, 0, NOW(), NOW() + FROM auth.users WHERE email = '@mana.how' +ON CONFLICT (user_id) DO UPDATE SET balance = GREATEST(credits.balances.balance, 10000), updated_at = NOW(); +SQL +``` + +User-ID notieren für Phase 4. + +### Phase 3 — Zweiten Space anlegen (direkt via SQL) + +```sql +WITH new_org AS ( + INSERT INTO auth.organizations (id, name, slug, metadata, created_at, updated_at) + VALUES (REPLACE(gen_random_uuid()::text, '-', ''), '', '-2', + '{"type":"club","tier":"founder"}'::jsonb, NOW(), NOW()) + RETURNING id +) +INSERT INTO auth.members (id, organization_id, user_id, role, created_at) +SELECT REPLACE(gen_random_uuid()::text, '-', ''), new_org.id, '', 'owner', NOW() + FROM new_org; +``` + +Personal-Space-Slug ist `` (vom Email-Localpart abgeleitet); der +zweite Space bekommt `-2` — Convention vom Slug-Resolver in +`createPersonalSpaceFor`. Space-ID notieren für Phase 4. + +### Phase 4 — Persona-Skript schreiben + ausführen + +Skript-Layout: `scripts/demo/personas//{data.ts, seed.ts}`. Pures +Bun. Pattern aus `services/mana-auth/src/services/bootstrap-singletons.ts`. + +```bash +ssh -L 5433:localhost:5432 -N -f mana-server +SYNC_DATABASE_URL="postgresql://postgres:manacore123@localhost:5433/mana_sync" \ + bun scripts/demo/personas//seed.ts +``` + +Wichtige Konventionen: + +- `actor = { kind: 'system', principalId: 'system:demo-seed', displayName: 'Demo-Seed' }` +- `client_id = 'system:demo-seed'` +- `field_timestamps` (Prod-Schema) — **NICHT `field_meta`** (das ist das + Sync-Field-Meta-Overhaul-Schema, auf Prod noch nicht deployed) +- `schema_version = 1` +- Deterministische `record_id` via `${slug}:${module}:${index}`-Schema +- RLS-Bypass: `SELECT set_config('app.current_user_id', '', + false)` + `set_config('app.current_user_space_ids', ...)` — Prod hat + forced-RLS auf `sync_changes` +- Idempotency: am Anfang `DELETE WHERE client_id = 'system:demo-seed' + AND space_id IN (...)` — Re-Run ersetzt sauber + +**Reihenfolge der Inserts:** + +1. kontextDoc (1 record) +2. contacts (Vorstand + Mitglieder) +3. calendar (1 Calendar-Liste + recurring + Termine + Konzerte = je 2 + Records: timeBlock + event) +4. events (1 SocialEvent für öffentliches Konzert) +5. library (Repertoire + Konzertarchiv) +6. notes (Vereinsphilosophie / Vorstand / Aktuelles Repertoire) +7. website (1 site + 4 pages + Blocks) +8. ai-missions (3 paused: Probenrückblick / Newsletter / Geburtstage) + +### Phase 5 — Smoke-Test im Browser + +Auf `https://mana.how/login` als der Persona-User einloggen. Jedes +befülltes Modul abklicken: contacts mit Tags, Calendar mit Recurring +Probe + Konzerten, Event-Public-Share-Link, kontextDoc rendert, +Website-Pages, AI-Workbench mit Companion. + +### Phase 6 — Lessons ins Runbook + +Konkrete Lessons aus dem Lauf in §Lessons unten ergänzen. + +--- + +## Modul-Mapping-Konventionen + +| Modul | appId | tableName(s) | Was rein | +|---|---|---|---| +| `contacts` | `contacts` | `contacts` | firstName/lastName, Tags je Funktion + Stimmgruppe/Mannschaft. Adressen+Geburtstage nur wenn öffentlich. | +| `calendar` | `calendar` | `calendars`, `events` | 1 default-Calendar + Events; jedes Event braucht ein TimeBlock unter `timeblocks/timeBlocks` mit sourceModule='calendar'. Recurring via `recurrenceRule: 'FREQ=WEEKLY;BYDAY=TH'`. | +| `events` | `events` | `socialEvents`, `eventGuests`, `eventInvitations`, `eventItems` | Nur öffentliche, beworbene Termine (Konzerte). Jedes SocialEvent braucht ebenfalls einen TimeBlock mit sourceModule='events'. `publicToken` für Share-Link. | +| `notes` | `notes` | `notes` | Vereinsphilosophie / Vorstand / Repertoire. `content` ist Markdown. Encryption: client tolerant für Klartext-Inserts. | +| `library` | `library` | `libraryEntries` | Discriminator `kind: 'book'` reicht für alle Werke + Konzertarchive. | +| `website` | `website` | `websites`, `websitePages`, `websiteBlocks` | Spiegelbild der echten Vereinswebsite. NICHT eine bessere erfinden — Replik ist wertvoller. | +| `kontext` | `kontext` | `kontextDoc` | Vereins-DNA in Markdown-Sektionen. Wichtigstes Stück Inhalt — Companion injiziert das in jede Mission. | +| `ai-missions` | `ai` | `aiMissions` | 3 generische paused: Probenrückblick weekly / Newsletter monthly / Geburtstagsgrüsse daily. Status `paused`. | +| `invoices`, `finance`, `broadcast` | div | div | Nur befüllen wenn echte Daten öffentlich (Beitragshöhe, Bilanz, Newsletter-Archiv). Sonst leer lassen. | + +Die TimeBlock-Doppelung pro Calendar-Event/SocialEvent ist die Konsequenz +des unified time-models — siehe `apps/mana/apps/web/src/lib/data/time-blocks/`. + +--- + +## Common Pitfalls + +- **Prod-Sync-DB hat altes Schema** — `field_timestamps` (jsonb), kein `field_meta`, kein `origin`-Column. Das + Sync-Field-Meta-Overhaul (2026-04-26 lokal shipped) ist auf Prod nicht + deployed. Bootstrap-Singletons in mana-auth schlagen entsprechend bei + jeder Registrierung fehl — wir schreiben Singletons via unser + Seed-Skript selbst. +- **Forced RLS auf sync_changes** — selbst Postgres-Superuser ist + betroffen. `SET LOCAL app.current_user_id` + `app.current_user_space_ids` + vor dem INSERT setzen. `set_config(...)` in postgres.js. +- **Slug-Collision** — Personal-Space klaut die Email-Localpart als Slug. + Zweiter Space muss `-2` Suffix bekommen (Convention vom + `resolveUniqueSlug`-Resolver). +- **Ports auf Dev-Maschine** — auf macOS hört Docker oft schon auf 5432. + Tunnel-Port 5433 ist aber häufig auch belegt durch Docker-eigenen + Postgres. SSH-Tunnel auf 5433 via IPv6 funktioniert trotzdem; wenn + nicht, auf 5434/5435 ausweichen. +- **DB-Connection ohne RLS-Setting** schlägt INSERT silent fehl mit „new + row violates row-level security policy". Immer `setRlsContext()` als + erste Aktion. +- **mkdir -p vor Schreibvorgängen** — manche Tool-Layer legen parent-dirs + nicht selbst an. Lieber explizit `mkdir -p` als verlorene Files. + +--- + +## Helpers + +*(emergiert ab Persona zwei. Erste Persona = alles inline.)* + +--- + +## Lessons + +### Aus Persona 1 — Chor Tägerwilen (2026-04-28) + +- **ClubDesk-Self-Hosting-Tag**: Der Verein war bereits ClubDesk-Kunde + (alte Site auf `.clubdesk.com`). Das ist kein Zufall, sondern + potenzielles Pattern: viele Vereine, deren Website seriös aussieht und + öffentlich Mitgliederlisten zeigt, sind ClubDesk-Kunden. Solche + Vereine sind für Pitches Gold — der Pitch zeigt die direkte + Migrations-Story. +- **Mitgliederlisten in Stimmgruppen-Subseiten** — chor-taegerwilen.ch + listet alle 54 Mitglieder mit vollem Namen unter + `/verein/register_{sopran,alt,tenor,bass}`. Das ist ein häufiger + ClubDesk-Modul-Auto-Render. Bei künftigen Chören erstmal nach + `/verein/register_*` oder `/mitglieder/*` suchen. +- **Vorstand-Vornamen-Only** — die Vorstandsseite nennt manche nur mit + Vornamen. Nachnamen via Cross-Reference mit Stimmgruppen-Listen + ableitbar (eindeutig bei seltenen Vornamen). Annahmen explizit im + Recherche-Brief notieren. +- **kontextDoc ist 80% des Pitch-Wertes** — der Companion zieht es + automatisch in jede Mission. Mehr Substanz dort = jeder AI-Demo wirkt + vereinsspezifisch. Lieber 120 Zeilen Markdown als 30. +- **calendar = 2 Records pro Event** — 1 timeBlock (sourceModule) + 1 + event (calendarId, timeBlockId). Vergessen heißt: Event taucht im + Kalender nicht auf. Pattern in Helper kapseln, sobald Persona zwei. +- **Forced RLS hat mich kostbar 5 Minuten gekostet** — INSERTs liefen + silent durch ohne Wirkung, bis `set_config('app.current_user_id', …)` + gesetzt war. Künftige Skripte: setRlsContext() ist die erste + Tu-Sache nach Connection-Open. +- **Prod-Sync-DB-Schema-Drift** vs. der lokalen Codebase ist ein echter + Befund — Bootstrap-Singletons haben bei jeder Prod-Registrierung + silent gefehlt. Backlog-Eintrag wert: Sync-Field-Meta-Overhaul nach + Prod ziehen, oder Bootstrap-Code mit Schema-Fallback ausstatten. +- **Write-Tool-Sandbox-Quirk** — Files in noch-nicht-existierenden + Subdirs gingen in ein Limbo, das für Bash unsichtbar war. Workaround: + immer `mkdir -p` per Bash vor Write. + +--- + +## Liste angelegter Personas + +| Slug | Verein/Person | Datum | Status | User-ID | Club-Space-ID | +|---|---|---|---|---|---| +| `chor-taegerwilen` | chor tägerwilen | 2026-04-28 | aktiv (118 Records geseedet) | `TCYOdiUdpMSCkw4OW8i7JB7Vn6XI81qf` | `6a3a4d4c1c0e4e5ea918dd30102067cb` |