diff --git a/docs/demo-personas/chor-taegerwilen/README.md b/docs/demo-personas/chor-taegerwilen/README.md new file mode 100644 index 000000000..b09c92b0f --- /dev/null +++ b/docs/demo-personas/chor-taegerwilen/README.md @@ -0,0 +1,140 @@ +# Recherche-Brief — chor tägerwilen + +**Persona-Slug:** `chor-taegerwilen` +**Account-Email:** `chor-taegerwilen@mana.how` +**Initial-Passwort:** `Chor-Taegerwilen-2026` +**User-ID:** `TCYOdiUdpMSCkw4OW8i7JB7Vn6XI81qf` +**Personal-Space-ID:** `PzzwRkbDTcmFGdotQYwGn` (slug `chor-taegerwilen`) +**Club-Space-ID:** `6a3a4d4c1c0e4e5ea918dd30102067cb` (slug `chor-taegerwilen-2`) +**Recherche-Datum:** 2026-04-28 +**Status:** Live auf `mana.how`-Prod, 118 Records geseedet + +> **Pitch-Hook:** Der chor tägerwilen ist aktuell selbst ClubDesk-Kunde +> (alte Site auf `chor-taegerwilen.clubdesk.com`, neue Site nennt +> ClubDesk-Software in der Code-Spur). Sie sind buchstäblich der +> Verein, den der Mana-Pitch aus dem ClubDesk-Vergleich +> (`docs/reports/clubdesk-vs-mana-comparison.md`) abgreifen will. +> Migrations-Story ist eingebaut. + +--- + +## 1. Quellen + +| URL | Geprüft | Hauptinhalt | +|---|---|---| +| https://www.chor-taegerwilen.ch/ | 2026-04-28 | Startseite, Motto, kommende Termine | +| https://www.chor-taegerwilen.ch/verein/vorstand | 2026-04-28 | 5-köpfiger Vorstand (Vornamen) | +| https://www.chor-taegerwilen.ch/verein/chorleitung | 2026-04-28 | Wolfgang Feucht — Bio | +| https://www.chor-taegerwilen.ch/verein/geschichte | 2026-04-28 | Gegründet 1880, ~55 Mitglieder | +| https://www.chor-taegerwilen.ch/verein/register_sopran | 2026-04-28 | 16 Sopranistinnen | +| https://www.chor-taegerwilen.ch/verein/register_alt | 2026-04-28 | 22 Altistinnen | +| https://www.chor-taegerwilen.ch/verein/register_tenor | 2026-04-28 | 7 Tenöre | +| https://www.chor-taegerwilen.ch/verein/register_bass | 2026-04-28 | 9 Bässe | +| https://www.chor-taegerwilen.ch/agenda | 2026-04-28 | Probetermine April–Juni 2026 | +| https://www.chor-taegerwilen.ch/konzerte | 2026-04-28 | Frühlingskonzert 14./15.3. | +| https://www.chor-taegerwilen.ch/impressum | 2026-04-28 | Adresse, Telefon, Email | +| https://thurgau-singt.ch/chor/chor-taegerwilen/ | 2026-04-28 | Verbands-Eintrag, bestätigt Daten | + +--- + +## 2. Daten + +### 2.1 Stammdaten + +| Feld | Wert | +|---|---| +| Vereinsname | chor tägerwilen *(klein-geschrieben, bewusst)* | +| Gegründet | 1880 | +| Mitglieder | 54 (öffentlich, Stand 2026-04) | +| Adresse | c/o Ralf Schneider, Hauptstrasse 142, CH-8274 Tägerwilen | +| Telefon | +41 79 176 21 02 | +| E-Mail | info@chor-taegerwilen.ch | +| Probe | Donnerstag 20:00–21:45, Aula Sekundarschule Tägerwilen | +| Motto | „Singen macht Spass" | + +### 2.2 Vorstand (5 Personen) + +| Name | Funktion | Quellen-Konfidenz | +|---|---|---| +| **Ralf Schneider** | Präsident | hoch — Impressum + Tenor-Liste | +| **Monika Friemelt** | Kassierin | mittel — einzige Monika in Stimmgruppen | +| **Sonja Hegermann** | Aktuarin | tief — zwei Sonjas im Alt; Annahme dokumentiert | +| **Nadine Ruf** | Sponsoring & Werbung | mittel — einzige Nadine | +| **Liesbeth Zürcher** | Mitgliederbetreuung | mittel — einzige Liesbeth | + +### 2.3 Chorleitung + +**Wolfgang Feucht** (seit November 2022), Methode Complete Vocal +Technique, Studium Schulmusik, Aufbaustudium Chorpädagogik Örebro. +Persönliche Site: www.wolfgang-feucht.de. + +### 2.4 Mitglieder-Roster (54 Personen) + +Komplett gepflegt in `scripts/demo/personas/chor-taegerwilen/data.ts`. +Verteilung: Sopran 16, Alt 22, Tenor 7, Bass 9. + +### 2.5 Termine + +7 Probetermine April–Juni 2026 (Donnerstag 20:00, mit Sonderfall 4.6. +um 19:30) plus 5 Konzerte 2026 (14./15.3. Happy Together, 26.6. Chornacht, +26.9. Herbstkonzert, Dezember Adventskonzerte). + +### 2.6 Konzert-Historie + +11 Programme von 2015 bis 2025. Stilistisches Profil: Pop/Crossover mit +gelegentlichen geistlichen Werken (aktuell Vivaldi Magnificat). + +--- + +## 3. Lücken + Annahmen + +| Lücke | Wie wir damit umgehen | +|---|---| +| Vorname-only beim Vorstand | Nachnamen via Cross-Reference Stimmgruppen abgeleitet, Annahmen in `data.ts` notiert | +| Mitglieder-Beitragshöhe | `invoices` leer — keine Erfindung | +| Vereinsbilanz | `finance` leer | +| IBAN | Nicht in kontextDoc | +| Mitglieder-Geburtstage + Adressen | `contacts.birthday` + `address` leer | +| Statuten-PDF | `storage` leer | +| Mitglieder-Emails | Nur `info@chor-taegerwilen.ch` als Sammelpostfach | +| Newsletter-Archiv | `broadcast` leer | + +--- + +## 4. Geseedete Inhalte (Stand 2026-04-28) + +| Modul | Records | Tabelle(n) | +|---|---|---| +| kontext | 1 | kontextDoc | +| contacts | 56 | contacts | +| calendar | 14 | calendars (1) + events (13) | +| timeblocks | 14 | timeBlocks | +| events | 1 | socialEvents | +| library | 12 | libraryEntries | +| notes | 3 | notes | +| website | 14 | websites (1) + websitePages (4) + websiteBlocks (9) | +| ai-missions | 3 | aiMissions | + +**Total: 118 Records** in `mana_sync.sync_changes`. + +--- + +## 5. Pitch-Story-Hooks + +1. **„Das ist euer Verein"** — Empfänger erkennt echte Mitglieder, Konzerte, Repertoire wieder. +2. **„Ihr seid auf ClubDesk; das hier wäre die Mana-Variante"** — direkte Vergleichbarkeit. +3. **„Wolfgang Feucht nutzt Complete Vocal Technique"** — kontextDoc kennt das Detail. +4. **„22 Altistinnen, 7 Tenöre — Newsletter mit Tag-Filter"** — broadcast-Audience-Demo. +5. **„Ein Klick, Public-Share-Link für Sommerkonzert"** — events vs. ClubDesk-Anmeldeformular. + +--- + +## 6. Status + +- [x] Recherche +- [x] Live-Account auf Mac-Mini-Prod +- [x] Personal-Space (auto via createPersonalSpaceFor) +- [x] Club-Space „Chor Tägerwilen" (slug `chor-taegerwilen-2`) +- [x] Persona-Skript geschrieben + ausgeführt (118 Records) +- [ ] Smoke-Test im Browser +- [x] Lessons ins Runbook diff --git a/scripts/demo/personas/chor-taegerwilen/data.ts b/scripts/demo/personas/chor-taegerwilen/data.ts new file mode 100644 index 000000000..cb00909ed --- /dev/null +++ b/scripts/demo/personas/chor-taegerwilen/data.ts @@ -0,0 +1,323 @@ +/** + * Chor Tägerwilen — recherchierte Daten zur Befüllung der Demo-Persona. + * + * Quellen + Lücken-Diskussion: docs/demo-personas/chor-taegerwilen/README.md + */ + +export interface BoardMember { + firstName: string; + lastName: string; + role: string; + stimmgruppe: 'Sopran' | 'Alt' | 'Tenor' | 'Bass'; + notes?: string; +} + +export const VORSTAND: BoardMember[] = [ + { + firstName: 'Ralf', + lastName: 'Schneider', + role: 'Präsident', + stimmgruppe: 'Tenor', + notes: 'Vereinsadresse via Impressum: c/o Ralf Schneider, Hauptstrasse 142, 8274 Tägerwilen', + }, + { firstName: 'Monika', lastName: 'Friemelt', role: 'Kassierin', stimmgruppe: 'Alt' }, + { + firstName: 'Sonja', + lastName: 'Hegermann', + role: 'Aktuarin', + stimmgruppe: 'Alt', + notes: 'Annahme — Vorstandsseite nennt nur Vornamen.', + }, + { firstName: 'Nadine', lastName: 'Ruf', role: 'Sponsoring & Werbung', stimmgruppe: 'Sopran' }, + { + firstName: 'Liesbeth', + lastName: 'Zürcher', + role: 'Mitgliederbetreuung', + stimmgruppe: 'Sopran', + }, +]; + +export const CHORLEITER = { + firstName: 'Wolfgang', + lastName: 'Feucht', + since: '2022-11-01', + bio: 'Studium Schulmusik mit Schwerpunkt Chorleitung; Aufbaustudium Chorpädagogik in Örebro (Schweden); Jazz- und Popularmusik-Pädagogik. Methode Complete Vocal Technique. Spielt Jazz-Klavier. Daneben Musiklehrer am Alexander-von-Humboldt-Gymnasium und Organist Sulgen-Kradolf + Gachnang.', + website: 'https://www.wolfgang-feucht.de', +}; + +export interface Member { + firstName: string; + lastName: string; +} + +export const SOPRAN: Member[] = [ + { firstName: 'Bettina', lastName: 'Nessensohn' }, + { firstName: 'Corina', lastName: 'Signer' }, + { firstName: 'Friederike', lastName: 'Volkenand' }, + { firstName: 'Gabi', lastName: 'Robinson' }, + { firstName: 'Heidi', lastName: 'Sauter' }, + { firstName: 'Jacqueline', lastName: 'Somm' }, + { firstName: 'Karina', lastName: 'Zabel' }, + { firstName: 'Katharina', lastName: 'Siegrist' }, + { firstName: 'Lea', lastName: 'Straub' }, + { firstName: 'Liesbeth', lastName: 'Zürcher' }, + { firstName: 'Lisanne', lastName: 'Robinson' }, + { firstName: 'Lucia', lastName: 'Zingg' }, + { firstName: 'Nadine', lastName: 'Ruf' }, + { firstName: 'Petra', lastName: 'Schroff' }, + { firstName: 'Pia', lastName: 'Hepp' }, + { firstName: 'Renate', lastName: 'Signer' }, +]; + +export const ALT: Member[] = [ + { firstName: 'Alexandra', lastName: 'Bär' }, + { firstName: 'Ana Maria', lastName: 'Thurnheer' }, + { firstName: 'Andrea', lastName: 'Eckert' }, + { firstName: 'Astrid', lastName: 'Mell' }, + { firstName: 'Christiane', lastName: 'Wuddel' }, + { firstName: 'Daniela', lastName: 'Opprecht' }, + { firstName: 'Daniela', lastName: 'Braun' }, + { firstName: 'Denise', lastName: 'Zürcher' }, + { firstName: 'Gudrun', lastName: 'Haenselt' }, + { firstName: 'Jolanda', lastName: 'Schär' }, + { firstName: 'Liddy', lastName: 'Schwemer' }, + { firstName: 'Martina', lastName: 'Aeschbacher' }, + { firstName: 'Melanie', lastName: 'Bättig' }, + { firstName: 'Monika', lastName: 'Friemelt' }, + { firstName: 'Rosmarie', lastName: 'Testa-Stäheli' }, + { firstName: 'Silke', lastName: 'Mühlhoff' }, + { firstName: 'Sonja', lastName: 'Hegermann' }, + { firstName: 'Sonja', lastName: 'Helmstätter' }, + { firstName: 'Susanne', lastName: 'Ribi' }, + { firstName: 'Sybille', lastName: 'Hierling' }, + { firstName: 'Uta', lastName: 'Rausch' }, + { firstName: 'Ute', lastName: 'Schneider' }, +]; + +export const TENOR: Member[] = [ + { firstName: 'Anita', lastName: 'Arnold' }, + { firstName: 'Beat', lastName: 'Schwarz' }, + { firstName: 'Jean-Pierre', lastName: 'Golliez' }, + { firstName: 'Markus', lastName: 'Wiesli' }, + { firstName: 'Max', lastName: 'Aeberhard' }, + { firstName: 'Ralf', lastName: 'Schneider' }, + { firstName: 'Reto', lastName: 'Oberhänsli' }, +]; + +export const BASS: Member[] = [ + { firstName: 'Fritz', lastName: 'Weigle' }, + { firstName: 'Guido', lastName: 'Häberlin' }, + { firstName: 'Hartmut', lastName: 'Rausch' }, + { firstName: 'Jan', lastName: 'Schneider' }, + { firstName: 'Martin', lastName: 'De Boni' }, + { firstName: 'Roland', lastName: 'Kolb' }, + { firstName: 'Sepp', lastName: 'Teuber' }, + { firstName: 'Stefan', lastName: 'Borkert' }, + { firstName: 'Thomas', lastName: 'Schneider' }, +]; + +export interface Termin { + date: string; + startTime: string; + endTime: string; + title: string; + description: string; + location: string; +} + +export const TERMINE: Termin[] = [ + { + date: '2026-04-30', + startTime: '20:00', + endTime: '21:45', + title: 'Probe — Vivaldi Magnificat & sakrale Werke', + description: 'Probe Vivaldi Magnificat und sakrale Chorwerke.', + location: 'Aula Sekundarschule Tägerwilen', + }, + { + date: '2026-05-07', + startTime: '20:00', + endTime: '21:45', + title: 'Probe — Magnificat & Pop-Repertoire', + description: 'Magnificat-Wiederholung und Pop-Repertoire.', + location: 'Aula Sekundarschule Tägerwilen', + }, + { + date: '2026-05-28', + startTime: '20:00', + endTime: '21:45', + title: 'Gemeinsame Probe mit Pop-Chor (Leitung Dirk Werner)', + description: 'Gemeinsame Probe mit dem Popchor; Leitung an diesem Abend Dirk Werner.', + location: 'Aula Sekundarschule Tägerwilen', + }, + { + date: '2026-06-04', + startTime: '19:30', + endTime: '21:45', + title: 'Probe mit Streichensemble Divertimento + Apéro', + description: + 'Probe mit dem Streichorchester Divertimento, anschließend Apéro (Männer organisieren Snacks).', + location: 'Aula Sekundarschule Tägerwilen', + }, + { + date: '2026-06-11', + startTime: '20:00', + endTime: '21:45', + title: 'Programm-Probe „Chornacht"', + description: 'Programm-Probe für die Chornacht.', + location: 'Aula Sekundarschule Tägerwilen', + }, + { + date: '2026-06-18', + startTime: '20:00', + endTime: '21:45', + title: 'Programm-Probe „Chornacht"', + description: 'Programm-Probe Chornacht.', + location: 'Aula Sekundarschule Tägerwilen', + }, + { + date: '2026-06-25', + startTime: '20:00', + endTime: '21:45', + title: 'Programm-Probe „Chornacht"', + description: 'Programm-Probe Chornacht — letzte Probe vor dem Festival.', + location: 'Aula Sekundarschule Tägerwilen', + }, +]; + +export interface Konzert { + date: string; + startTime: string; + endTime: string; + title: string; + description: string; + location: string; + partners?: string[]; + isPublic: boolean; +} + +export const KONZERTE: Konzert[] = [ + { + date: '2026-03-14', + startTime: '20:00', + endTime: '22:00', + title: 'Frühlingskonzert „Happy Together"', + description: + 'Zwei Chöre — ein Konzert. Der chor tägerwilen und der Popchor Konstanz präsentieren ein gemeinsames Programm.', + location: 'Bürgerhalle Tägerwilen', + partners: ['Popchor Konstanz'], + isPublic: true, + }, + { + date: '2026-03-15', + startTime: '12:00', + endTime: '14:00', + title: 'Frühlingskonzert „Happy Together" (Sonntag)', + description: 'Zweite Aufführung des Frühlingskonzerts.', + location: 'Bürgerhalle Tägerwilen', + partners: ['Popchor Konstanz'], + isPublic: true, + }, + { + date: '2026-06-26', + startTime: '20:00', + endTime: '22:30', + title: 'Chornacht — Konstanzer Chorfestival', + description: 'Auftritt im Rahmen der Chornacht des Konstanzer Chorfestivals.', + location: 'Altkatholische Kirche St. Konrad, Konstanz', + isPublic: true, + }, + { + date: '2026-09-26', + startTime: '19:30', + endTime: '21:30', + title: 'Herbstkonzert', + description: 'Herbstkonzert in der Mehrzweckhalle Wollmatingen, Konstanz.', + location: 'Mehrzweckhalle Wollmatingen, Konstanz', + isPublic: true, + }, + { + date: '2026-12-12', + startTime: '20:00', + endTime: '21:45', + title: 'Adventskonzert mit Streichorchester Divertimento', + description: + 'Adventskonzert mit dem Streichorchester Divertimento. Genaues Datum + Venue noch in Planung.', + location: 'tba', + partners: ['Streichorchester Divertimento'], + isPublic: true, + }, +]; + +export interface KonzertArchiv { + year: string; + title: string; +} + +export const KONZERT_ARCHIV: KonzertArchiv[] = [ + { year: '2025', title: 'Sounds of Love' }, + { year: '2024', title: 'Summer in the City' }, + { year: '2023', title: 'Sang & Klang' }, + { year: '2022', title: 'Viva la Vida' }, + { year: '2021', title: 'Rocking around the Christmas Tree' }, + { year: '2019', title: 'Follow that Star' }, + { year: '2019', title: 'The Sound of Silence' }, + { year: '2018', title: 'Concerto Finale' }, + { year: '2017', title: 'Im Musikhimmel' }, + { year: '2016', title: 'Jukebox Night' }, + { year: '2015', title: 'TägAirWilen' }, +]; + +export interface Werk { + composer: string; + title: string; + notes?: string; +} + +export const REPERTOIRE: Werk[] = [ + { + composer: 'Antonio Vivaldi', + title: 'Magnificat (RV 610/611)', + notes: 'Hauptwerk Frühling/Sommer 2026.', + }, +]; + +export const KONTEXT_DOC_MARKDOWN = `# chor tägerwilen — Vereins-Kontext + +## Verein +- Gemischter Chor, gegründet **1880**, statutarisch dokumentiert ab 1892 +- Rechtsform: Verein nach Schweizer ZGB +- ~54 aktive Mitglieder aus 18 Ortschaften +- Adresse: c/o Ralf Schneider, Hauptstrasse 142, CH-8274 Tägerwilen +- Kontakt: info@chor-taegerwilen.ch · +41 79 176 21 02 +- Website: https://www.chor-taegerwilen.ch +- Motto: „Singen macht Spass" + +## Stimmgruppen +- Sopran: 16 · Alt: 22 · Tenor: 7 · Bass: 9 + +## Chorleitung +**Wolfgang Feucht** (seit November 2022) — Studium Schulmusik mit Schwerpunkt Chorleitung; Aufbaustudium Chorpädagogik in Örebro (Schweden); Jazz- und Popularmusik-Pädagogik. Methode: Complete Vocal Technique. + +## Vorstand 2026 +- Ralf Schneider — Präsident (Tenor) +- Monika Friemelt — Kassierin (Alt) +- Sonja Hegermann — Aktuarin (Alt) +- Nadine Ruf — Sponsoring & Werbung (Sopran) +- Liesbeth Zürcher — Mitgliederbetreuung (Sopran) + +## Probe +Donnerstag 20:00–21:45, Aula Sekundarschule Tägerwilen. + +## Repertoire +Pop / Crossover als Schwerpunkt, gelegentliche geistliche Werke. Aktuell: Vivaldi Magnificat. + +## Termine 2026 +- 14./15.03. Frühlingskonzert „Happy Together" (mit Popchor Konstanz) +- 26.06. Chornacht / Konstanzer Chorfestival +- 26.09. Herbstkonzert, Wollmatingen +- Dezember: Adventskonzerte mit Streichorchester Divertimento + +## Aktuelle Vereinsverwaltung +Läuft auf ClubDesk (alte Site: chor-taegerwilen.clubdesk.com). Mana-Migration als Demo befüllt. +`; diff --git a/scripts/demo/personas/chor-taegerwilen/seed.ts b/scripts/demo/personas/chor-taegerwilen/seed.ts new file mode 100644 index 000000000..2496ce833 --- /dev/null +++ b/scripts/demo/personas/chor-taegerwilen/seed.ts @@ -0,0 +1,812 @@ +/** + * Chor Tägerwilen — Persona-Seed-Skript. + * + * Schreibt ~120 Records in `mana_sync.sync_changes` über mehrere Module + * hinweg (kontextDoc, contacts, calendar, library, notes, website, + * ai-missions, social events). Alle Inserts laufen über das Prod-Schema: + * `field_timestamps` (nicht `field_meta`), kein `origin`-Column. + * + * Recherche-Brief: docs/demo-personas/chor-taegerwilen/README.md + * + * ─── Aufruf ──────────────────────────────────────────────── + * + * ssh -L 5433:localhost:5432 -N -f mana-server + * SYNC_DATABASE_URL="postgresql://postgres:manacore123@localhost:5433/mana_sync" \ + * bun scripts/demo/personas/chor-taegerwilen/seed.ts + * + * Idempotent: räumt vor dem Insert alle Rows mit + * client_id='system:demo-seed' aus den Demo-Spaces, dann INSERT von + * vorne. Re-Run = Update. + */ + +import postgres from 'postgres'; +import { + VORSTAND, + CHORLEITER, + SOPRAN, + ALT, + TENOR, + BASS, + TERMINE, + KONZERTE, + REPERTOIRE, + KONZERT_ARCHIV, + KONTEXT_DOC_MARKDOWN, + type Member, +} from './data'; + +const USER_ID = 'TCYOdiUdpMSCkw4OW8i7JB7Vn6XI81qf'; +const PERSONAL_SPACE_ID = 'PzzwRkbDTcmFGdotQYwGn'; +const CLUB_SPACE_ID = '6a3a4d4c1c0e4e5ea918dd30102067cb'; +const SLUG = 'chor-taegerwilen'; + +const ACTOR = { + kind: 'system', + principalId: 'system:demo-seed', + displayName: 'Demo-Seed', +} as const; +const CLIENT_ID = 'system:demo-seed'; + +const SYNC_DATABASE_URL = process.env.SYNC_DATABASE_URL; +if (!SYNC_DATABASE_URL) { + console.error('❌ SYNC_DATABASE_URL not set. See header for usage.'); + process.exit(1); +} +const sql = postgres(SYNC_DATABASE_URL); + +interface SyncRow { + app_id: string; + table_name: string; + record_id: string; + user_id: string; + space_id: string; + op: 'insert'; + data: Record; +} + +function buildFieldTimestamps(data: Record, at: string): Record { + const out: Record = {}; + for (const k of Object.keys(data)) { + if (k === 'id') continue; + out[k] = at; + } + return out; +} + +const collected: SyncRow[] = []; +function emit(row: SyncRow): void { + collected.push(row); +} + +async function flushAll(): Promise { + if (collected.length === 0) return; + const at = new Date().toISOString(); + const BATCH = 200; + for (let i = 0; i < collected.length; i += BATCH) { + const slice = collected.slice(i, i + BATCH); + const values = slice.map((r) => ({ + app_id: r.app_id, + table_name: r.table_name, + record_id: r.record_id, + user_id: r.user_id, + space_id: r.space_id, + op: r.op, + data: r.data, + field_timestamps: buildFieldTimestamps(r.data, at), + client_id: CLIENT_ID, + schema_version: 1, + actor: ACTOR, + created_at: at, + })); + await sql`INSERT INTO sync_changes ${sql(values as never)}`; + } + console.log(` → ${collected.length} rows inserted`); + collected.length = 0; +} + +async function cleanupPriorSeed(): Promise { + const result = await sql` + DELETE FROM sync_changes + WHERE client_id = ${CLIENT_ID} + AND (space_id = ${CLUB_SPACE_ID} OR space_id = ${PERSONAL_SPACE_ID} OR user_id = ${USER_ID}) + RETURNING 1 + `; + console.log(`✓ cleanup: removed ${result.length} prior demo-seed rows`); +} + +async function setRlsContext(): Promise { + await sql`SELECT set_config('app.current_user_id', ${USER_ID}, false)`; + await sql`SELECT set_config('app.current_user_space_ids', ${[PERSONAL_SPACE_ID, CLUB_SPACE_ID].join(',')}, false)`; +} + +function rid(module: string, key: string | number): string { + return `${SLUG}:${module}:${key}`; +} + +// ─── kontextDoc ───────────────────────────────────────────── + +function emitKontextDoc(): void { + const id = rid('kontext', 'singleton'); + emit({ + app_id: 'kontext', + table_name: 'kontextDoc', + record_id: id, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { id, content: KONTEXT_DOC_MARKDOWN }, + }); +} + +// ─── contacts ─────────────────────────────────────────────── + +const VORSTAND_FUNKTION = new Map(VORSTAND.map((v) => [`${v.firstName} ${v.lastName}`, v.role])); + +function emitContacts(): void { + const all: Array = [ + ...SOPRAN.map((m) => ({ ...m, stimmgruppe: 'Sopran' })), + ...ALT.map((m) => ({ ...m, stimmgruppe: 'Alt' })), + ...TENOR.map((m) => ({ ...m, stimmgruppe: 'Tenor' })), + ...BASS.map((m) => ({ ...m, stimmgruppe: 'Bass' })), + ]; + for (const [i, m] of all.entries()) { + const id = rid('contacts', i); + const fullName = `${m.firstName} ${m.lastName}`; + const tags = [m.stimmgruppe, 'Aktiv']; + const vorstandRolle = VORSTAND_FUNKTION.get(fullName); + if (vorstandRolle) { + tags.push('Vorstand', vorstandRolle); + } + emit({ + app_id: 'contacts', + table_name: 'contacts', + record_id: id, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id, + firstName: m.firstName, + lastName: m.lastName, + tags, + isFavorite: !!vorstandRolle, + isArchived: false, + }, + }); + } + const dirigentId = rid('contacts', 'dirigent'); + emit({ + app_id: 'contacts', + table_name: 'contacts', + record_id: dirigentId, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id: dirigentId, + firstName: CHORLEITER.firstName, + lastName: CHORLEITER.lastName, + tags: ['Chorleitung', 'Vorstand'], + website: CHORLEITER.website, + notes: CHORLEITER.bio, + isFavorite: true, + isArchived: false, + }, + }); + const postfachId = rid('contacts', 'postfach'); + emit({ + app_id: 'contacts', + table_name: 'contacts', + record_id: postfachId, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id: postfachId, + firstName: 'Vereinspostfach', + lastName: 'chor tägerwilen', + email: 'info@chor-taegerwilen.ch', + phone: '+41 79 176 21 02', + street: 'Hauptstrasse 142', + postalCode: '8274', + city: 'Tägerwilen', + country: 'CH', + website: 'https://www.chor-taegerwilen.ch', + tags: ['Verein', 'Sammelpostfach'], + isFavorite: true, + isArchived: false, + }, + }); +} + +// ─── calendar ─────────────────────────────────────────────── + +function emitCalendar(): void { + const calId = rid('calendar', 'main'); + emit({ + app_id: 'calendar', + table_name: 'calendars', + record_id: calId, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id: calId, + name: 'chor tägerwilen', + color: '#7c3aed', + isDefault: true, + isVisible: true, + timezone: 'Europe/Zurich', + }, + }); + + const writeBlockAndEvent = ( + key: string, + dateISO: string, + startTime: string, + endTime: string, + title: string, + description: string, + location: string, + recurrenceRule: string | null = null + ): void => { + const tbId = rid('timeblock', key); + const evId = rid('event', key); + const startISO = `${dateISO}T${startTime}:00.000+02:00`; + const endISO = `${dateISO}T${endTime}:00.000+02:00`; + emit({ + app_id: 'timeblocks', + table_name: 'timeBlocks', + record_id: tbId, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id: tbId, + startDate: startISO, + endDate: endISO, + allDay: false, + isLive: false, + timezone: 'Europe/Zurich', + recurrenceRule, + kind: 'scheduled', + type: 'event', + sourceModule: 'calendar', + sourceId: evId, + linkedBlockId: null, + title, + description, + color: null, + icon: null, + projectId: null, + }, + }); + emit({ + app_id: 'calendar', + table_name: 'events', + record_id: evId, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id: evId, + calendarId: calId, + timeBlockId: tbId, + title, + description, + location, + color: null, + tagIds: [], + }, + }); + }; + + writeBlockAndEvent( + 'probe-recurring', + '2026-04-30', + '20:00', + '21:45', + 'Probe', + 'Wöchentliche Probe Donnerstag.', + 'Aula Sekundarschule Tägerwilen', + 'FREQ=WEEKLY;BYDAY=TH' + ); + + for (const t of TERMINE) { + writeBlockAndEvent( + `termin-${t.date}`, + t.date, + t.startTime, + t.endTime, + t.title, + t.description, + t.location + ); + } + + for (const k of KONZERTE) { + writeBlockAndEvent( + `konzert-${k.date}-${k.startTime.replace(':', '')}`, + k.date, + k.startTime, + k.endTime, + k.title, + k.description, + k.location + ); + } +} + +// ─── socialEvent (1 publicly shared concert) ──────────────── + +function emitSocialEvent(): void { + const k = KONZERTE[0]; + const tbId = rid('timeblock', 'social-konzert-1'); + const seId = rid('socialEvent', 'happy-together'); + const startISO = `${k.date}T${k.startTime}:00.000+01:00`; + const endISO = `${k.date}T${k.endTime}:00.000+01:00`; + + emit({ + app_id: 'timeblocks', + table_name: 'timeBlocks', + record_id: tbId, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id: tbId, + startDate: startISO, + endDate: endISO, + allDay: false, + isLive: false, + timezone: 'Europe/Zurich', + recurrenceRule: null, + kind: 'scheduled', + type: 'event', + sourceModule: 'events', + sourceId: seId, + linkedBlockId: null, + title: k.title, + description: k.description, + color: null, + icon: '🎶', + projectId: null, + }, + }); + + const publicToken = `cht-happy-together-${Math.random().toString(36).slice(2, 10)}`; + emit({ + app_id: 'events', + table_name: 'socialEvents', + record_id: seId, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id: seId, + timeBlockId: tbId, + title: k.title, + description: `${k.description}\n\nGemeinsames Konzert mit dem Popchor Konstanz unter dem Motto "Zwei Chöre — ein Konzert". Eintritt frei, Kollekte für die Vereinsarbeit.`, + location: k.location, + capacity: null, + isPublished: true, + publicToken, + status: 'published', + visibility: 'public', + }, + }); +} + +// ─── library ──────────────────────────────────────────────── + +function emitLibrary(): void { + let i = 0; + for (const w of REPERTOIRE) { + const id = rid('library', `repertoire-${i}`); + emit({ + app_id: 'library', + table_name: 'libraryEntries', + record_id: id, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id, + kind: 'book', + title: w.title, + creator: w.composer, + status: 'reading', + rating: null, + notes: w.notes ?? null, + tags: ['Repertoire', 'Aktuell'], + }, + }); + i++; + } + for (const a of KONZERT_ARCHIV) { + const id = rid('library', `archiv-${a.year}-${i}`); + emit({ + app_id: 'library', + table_name: 'libraryEntries', + record_id: id, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id, + kind: 'book', + title: a.title, + creator: 'chor tägerwilen', + status: 'completed', + rating: null, + notes: `Konzertprogramm ${a.year}.`, + tags: ['Konzertarchiv', a.year], + }, + }); + i++; + } +} + +// ─── notes ────────────────────────────────────────────────── + +function emitNotes(): void { + const notes = [ + { + key: 'philosophie', + title: 'Vereinsphilosophie', + content: `# Singen macht Spass + +Der chor tägerwilen ist eine bunte, fröhliche und gesellige Truppe, die das gemeinsame Singen liebt. Wert wird auf Singfreude und Genuss gelegt — ohne dabei den Anspruch an die Aufführungsqualität aufzugeben. + +Konzerte werden meist von einer kleinen Band oder einem Streichensemble begleitet. Wolfgang Feucht (Chorleiter seit 11/2022) arbeitet mit der Methode Complete Vocal Technique und legt Wert auf gesunden Stimmgebrauch. + +> Quelle: chor-taegerwilen.ch (Stand 2026-04-28)`, + }, + { + key: 'vorstand-2026', + title: 'Vorstand 2026', + content: `# Vorstand + +- **Ralf Schneider** — Präsident (Tenor) +- **Monika Friemelt** — Kassierin (Alt) +- **Sonja Hegermann** — Aktuarin (Alt) +- **Nadine Ruf** — Sponsoring & Werbung (Sopran) +- **Liesbeth Zürcher** — Mitgliederbetreuung (Sopran) + +Adresse Verein: c/o Ralf Schneider, Hauptstrasse 142, 8274 Tägerwilen.`, + }, + { + key: 'aktuelles-repertoire', + title: 'Aktuelles Repertoire & Programm 2026', + content: `# Programm Frühling/Sommer 2026 + +## Hauptwerk +- **Vivaldi Magnificat (RV 610/611)** — Hauptwerk der Saison + +## Konzert-Termine +- **14./15.03.2026** — Frühlingskonzert „Happy Together" mit Popchor Konstanz, Bürgerhalle Tägerwilen +- **26.06.2026** — Chornacht / Konstanzer Chorfestival, Altkatholische Kirche St. Konrad +- **26.09.2026** — Herbstkonzert, Mehrzweckhalle Wollmatingen, Konstanz +- **Dezember 2026** — Zwei Adventskonzerte mit Streichorchester Divertimento + +## Probenrhythmus +- Donnerstag 20:00–21:45 (Aula Sekundarschule Tägerwilen) +- 28.05. gemeinsame Probe mit Popchor Konstanz, Leitung Dirk Werner +- 04.06. Probe mit Streichensemble Divertimento + Apéro`, + }, + ]; + for (const n of notes) { + const id = rid('note', n.key); + emit({ + app_id: 'notes', + table_name: 'notes', + record_id: id, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id, + title: n.title, + content: n.content, + color: null, + isPinned: n.key === 'philosophie', + isArchived: false, + }, + }); + } +} + +// ─── website ──────────────────────────────────────────────── + +function emitWebsite(): void { + const websiteId = rid('website', 'main'); + emit({ + app_id: 'website', + table_name: 'websites', + record_id: websiteId, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id: websiteId, + slug: 'chor-taegerwilen', + name: 'chor tägerwilen', + theme: { preset: 'classic', overrides: { primary: '#7c3aed' } }, + navConfig: { + items: [ + { label: 'Home', pagePath: '/' }, + { label: 'Über uns', pagePath: '/ueber-uns' }, + { label: 'Termine', pagePath: '/termine' }, + { label: 'Kontakt', pagePath: '/kontakt' }, + ], + }, + footerConfig: { + text: 'chor tägerwilen · gegründet 1880 · Singen macht Spass', + links: [ + { label: 'Impressum', href: '/impressum' }, + { label: 'Original-Website', href: 'https://www.chor-taegerwilen.ch' }, + ], + }, + settings: { + favicon: null, + defaultSeo: { + title: 'chor tägerwilen', + description: 'Gemischter Chor aus Tägerwilen — gegründet 1880. Singen macht Spass.', + }, + }, + publishedVersion: null, + draftVersion: 1, + }, + }); + + const pages = [ + { + key: 'home', + path: '/', + title: 'Home', + order: 0, + seo: { title: 'chor tägerwilen', description: 'Gemischter Chor aus Tägerwilen.' }, + }, + { + key: 'ueber-uns', + path: '/ueber-uns', + title: 'Über uns', + order: 1, + seo: { title: 'Über uns — chor tägerwilen', description: 'Geschichte und Vorstand.' }, + }, + { + key: 'termine', + path: '/termine', + title: 'Termine', + order: 2, + seo: { title: 'Termine — chor tägerwilen', description: 'Proben und Konzerte 2026.' }, + }, + { + key: 'kontakt', + path: '/kontakt', + title: 'Kontakt', + order: 3, + seo: { title: 'Kontakt — chor tägerwilen', description: 'Kontakt aufnehmen.' }, + }, + ]; + + for (const p of pages) { + const id = rid('page', p.key); + emit({ + app_id: 'website', + table_name: 'websitePages', + record_id: id, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { id, websiteId, path: p.path, title: p.title, order: p.order, seo: p.seo }, + }); + } + + const blocks = [ + { + pageKey: 'home', + type: 'hero', + order: 0, + props: { + title: 'chor tägerwilen', + subtitle: 'Singen macht Spass — seit 1880', + cta: { label: 'Termine', href: '/termine' }, + }, + }, + { + pageKey: 'home', + type: 'richText', + order: 1, + props: { + html: '

Eine bunte, fröhliche und gesellige Truppe von rund 55 Sängerinnen und Sängern aus 18 Ortschaften. Wir lieben das gemeinsame Singen und legen Wert auf Singfreude und Genuss. Konzerte werden meist von einer kleinen Band oder einem Streichensemble begleitet.

', + }, + }, + { + pageKey: 'home', + type: 'cta', + order: 2, + props: { + title: 'Frühlingskonzert „Happy Together"', + body: '14. und 15. März 2026 — Bürgerhalle Tägerwilen — gemeinsam mit dem Popchor Konstanz.', + button: { label: 'Mehr erfahren', href: '/termine' }, + }, + }, + { + pageKey: 'ueber-uns', + type: 'richText', + order: 0, + props: { + html: '

Geschichte

Gegründet 1880. Statutarisch dokumentiert ab 1892. Mehrere Phasen mit unterschiedlichen Dirigenten — u.a. Walter Ammann (Ehrendirigent, 1946–1980). Seit November 2022 unter der Leitung von Wolfgang Feucht. 2011 Namensänderung von „Gemischter Chor" zu „chor tägerwilen". 2016 mit dem 125-Jahr-Jubiläum gefeiert.

', + }, + }, + { + pageKey: 'ueber-uns', + type: 'richText', + order: 1, + props: { + html: '

Vorstand 2026

  • Ralf Schneider — Präsident
  • Monika Friemelt — Kassierin
  • Sonja Hegermann — Aktuarin
  • Nadine Ruf — Sponsoring & Werbung
  • Liesbeth Zürcher — Mitgliederbetreuung
', + }, + }, + { + pageKey: 'ueber-uns', + type: 'richText', + order: 2, + props: { + html: '

Chorleitung

Wolfgang Feucht (seit November 2022). Studium Schulmusik mit Schwerpunkt Chorleitung; Aufbaustudium Chorpädagogik in Örebro (Schweden); Jazz- und Popularmusik-Pädagogik. Methode: Complete Vocal Technique.

', + }, + }, + { + pageKey: 'termine', + type: 'richText', + order: 0, + props: { + html: '

Konzerte 2026

  • 14./15.03. — Frühlingskonzert „Happy Together", Bürgerhalle Tägerwilen, mit Popchor Konstanz
  • 26.06. — Chornacht / Konstanzer Chorfestival
  • 26.09. — Herbstkonzert, Mehrzweckhalle Wollmatingen, Konstanz
  • Dezember — Zwei Adventskonzerte mit Streichorchester Divertimento
', + }, + }, + { + pageKey: 'termine', + type: 'richText', + order: 1, + props: { + html: '

Probe

Donnerstag 20:00–21:45, Aula Sekundarschule Tägerwilen.

', + }, + }, + { + pageKey: 'kontakt', + type: 'richText', + order: 0, + props: { + html: '

Kontakt

chor tägerwilen
c/o Ralf Schneider
Hauptstrasse 142
CH-8274 Tägerwilen

E-Mail: info@chor-taegerwilen.ch
Telefon: +41 79 176 21 02

', + }, + }, + ]; + + let bIdx = 0; + for (const b of blocks) { + const id = rid('block', `${b.pageKey}-${bIdx}`); + const pageId = rid('page', b.pageKey); + emit({ + app_id: 'website', + table_name: 'websiteBlocks', + record_id: id, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id, + websiteId, + pageId, + parentBlockId: null, + type: b.type, + props: b.props, + order: b.order, + }, + }); + bIdx++; + } +} + +// ─── ai-missions ──────────────────────────────────────────── + +function emitAiMissions(): void { + const missions = [ + { + key: 'probenrueckblick', + title: 'Probenrückblick', + objective: + 'Lies die letzten Probenotizen aus dem notes-Modul und schreibe eine kurze Zusammenfassung als neue Notiz.', + cadence: 'WEEKLY', + }, + { + key: 'newsletter-entwurf', + title: 'Newsletter-Entwurf monatlich', + objective: + 'Lies Termine und Notizen der letzten 4 Wochen. Schreibe einen freundlichen Vereinsnewsletter-Entwurf im Tonfall „Singen macht Spass" als broadcast-Entwurf.', + cadence: 'MONTHLY', + }, + { + key: 'geburtstagsgruesse', + title: 'Geburtstagsgrüsse-Entwurf', + objective: + 'Prüfe Geburtstage der nächsten 7 Tage und schreibe einen kurzen Glückwunsch-Entwurf pro Mitglied. (Aktuell sind keine Geburtstage hinterlegt.)', + cadence: 'DAILY', + }, + ]; + + for (const m of missions) { + const id = rid('mission', m.key); + emit({ + app_id: 'ai', + table_name: 'aiMissions', + record_id: id, + user_id: USER_ID, + space_id: CLUB_SPACE_ID, + op: 'insert', + data: { + id, + title: m.title, + objective: m.objective, + status: 'paused', + cadence: m.cadence, + inputs: [], + ownerAgentId: null, + }, + }); + } +} + +// ─── Main ─────────────────────────────────────────────────── + +async function main(): Promise { + console.log('🎵 Chor Tägerwilen — Persona Seed'); + console.log(` user: ${USER_ID}`); + console.log(` personal: ${PERSONAL_SPACE_ID}`); + console.log(` club: ${CLUB_SPACE_ID}`); + console.log(''); + + await setRlsContext(); + await cleanupPriorSeed(); + + console.log('→ kontextDoc'); + emitKontextDoc(); + await flushAll(); + + console.log('→ contacts (Mitglieder + Vorstand + Chorleiter + Postfach)'); + emitContacts(); + await flushAll(); + + console.log('→ calendar (1 Hauptkalender + recurring + 7 Probetermine + 5 Konzerte)'); + emitCalendar(); + await flushAll(); + + console.log('→ events (1 öffentliches Konzert mit Public-Token)'); + emitSocialEvent(); + await flushAll(); + + console.log('→ library (Repertoire + Konzertarchiv)'); + emitLibrary(); + await flushAll(); + + console.log('→ notes (3)'); + emitNotes(); + await flushAll(); + + console.log('→ website (1 site + 4 pages + 9 blocks)'); + emitWebsite(); + await flushAll(); + + console.log('→ ai-missions (3 paused)'); + emitAiMissions(); + await flushAll(); + + console.log(''); + console.log('✓ Done. Login at https://mana.how/login as chor-taegerwilen@mana.how'); +} + +main() + .catch((err) => { + console.error('❌ Seed failed:', err); + process.exit(1); + }) + .finally(() => sql.end());