0Kurz
Alles Neue ist Hub-Code und Hub-Daten: neue Tabellen in hub.db, neue Module in hub/src/, neue Kacheln in Zentrale und Kundenportal. Kein neuer Container, keine neue Datenbank, kein neuer Dienst, mit einer Ausnahme (Telefon-Partner, falls gewollt, und der bleibt extern). Performance ist bei unserer Größe kein Thema, wenn drei Regeln eingehalten werden (Teil 3). Sicherheit hängt an fünf Punkten, die alle schon Muster im Hub haben (Teil 4). Die Übersichtlichkeit gewinnen wir nicht durch weniger Funktionen, sondern durch ein Modul „Assistent" mit einer Kachel, einer Tabelle je Zweck und einer lebenden Wiki-Seite (Teil 5). Der erste Sprint ist P2 + P3 (Kontakte, Opt-in, Anlass-Engine, Check-in-Quellen), weil beides Konzepte trägt.
1Ist-Zustand, gemessen am 13.09.2026
| Wert | Bedeutung | |
|---|---|---|
| Hub-Code | 179 Module in hub/src, 60.000 Zeilen TypeScript, app.js 12.700 Zeilen, 60 Testdateien, Version 0.287.1 | groß, aber in klaren Modulen: xyzCore.ts = reine Logik mit Tests, xyz.ts = Routen und DB |
| Hub-Datenbank | eine SQLite-Datei hub.db, 19 MB, 111 Tabellen, WAL-Modus, foreign_keys an; größte Tabellen events 22.000 Zeilen, Spiegel Magicline 987 Mitglieder und 987 Verträge (Sandbox) | winzig; SQLite trägt problemlos das Hundertfache |
| Hintergrundjobs | 20 Module mit eigenem Takt (Event-Verarbeiter minütlich, Nachtabgleich, Health alle 10 min, Wiedervorlagen, Erinnerungen, Bewertungen, Ads-Optimierer usw.) | das Muster für die Anlass-Engine existiert (Tick + Tabelle + Ereignis) |
| Container | 31 laufend; die Zentrale = deploy-sim-hub-<n> (manuelles Compose, rollout.sh), Nebendienste assistant/ads/post/claude unter Coolify, dazu Hermes-Agenten, Postiz mit Temporal-Stack (8 Container), Ticketsystem-Postgres | der Hub braucht 181 MB RAM und 0,2 % CPU |
| Server | 12 Kerne, 22 GB RAM, 451 GB Platte (36 % belegt), 4 GB Swap | genug; das Engpassbild vom Juli (15 GB, 87 % Platte) ist Geschichte |
| Backup | täglich 03:30 UTC, sqlite3 .backup von hub.db, assistant.db, ads.db, post.db, content.db, verschlüsselt aufs Hetzner-Volume, Restore-Test grün | neue Tabellen sind automatisch drin |
| Sicherheit vorhanden | Zentrale: Session oder x-admin-token; Portal: Sessions 30 Tage, Magic-Link, Rate-Limits je IP (echte Client-IP seit trustProxy), Scoping über portalUserOf() in 13 Modulen; x-frame-options SAMEORIGIN; Meta-Webhook mit HMAC gegen den Roh-Body; Magicline-Keys verschlüsselt in der Anbieter-Registry; Secrets nur in .env | die neuen Bausteine hängen an genau diesen Stellen |
Befund vom 13.09., der nichts mit dem Hub zu tun hat: Load 15 bei 12 Kernen, Swap voll. Ursache: 140 verwaiste Headless-Chromium-Prozesse (Playwright aus Claude-Sitzungen, Elternprozess systemd, 9 Stunden bis 5 Tage alt, je bis 100 % CPU). Am 13.09. bereinigt. Regel ab jetzt: Browser-Prüfungen immer mit Timeout und browser.close() im finally; zusätzlich ein Host-Wächter (Timer, stündlich), der Headless-Prozesse ohne Elternprozess und älter als 30 Minuten beendet. Das ist der einzige echte Performance-Punkt, den ich gefunden habe.
2Wo das Neue hingehört
| Baustein | Ort | Neu oder bestehend |
|---|---|---|
| Kontakte mit Opt-in (P2) | hub.db Tabelle kontakte; Modul kontakteCore.ts (rein) + kontakte.ts (DB, Routen) | neu, kleine Tabelle (Nummer, Mitglieds-Referenz je System, Opt-in-Felder, letzter Check-in, Wochenschnitt) |
| Anlass-Engine (P3) | Tabellen anlaesse, anlass_versand; Modul anlassCore.ts (Regelauswertung, Sperrzeiten, Frequenz, getestet) + anlass.ts (Tick minütlich, Versand über postfach.nachrichtSenden) | neu, folgt dem Muster von wiedervorlagen.ts und terminErinnerungen.ts |
| Vorlagensatz (P2) | bestehende postfach_vorlagen + Spalten anlass_key, standard | Erweiterung |
| Check-in-Quellen (P3) | Magicline: magiclineOpenCore.RELEVANTE_EVENT_TYPEN um CHECKIN/CHECKOUT erweitern, Verarbeitung schreibt nur kontakte.letzter_checkin_am und den Wochenschnitt; Agilea: Poller in agilea.ts (alle 5 min, checkInHistory mit Checkin greaterThan); aidoo: Nachtlauf über member | Erweiterung dreier bestehender Module |
| Erinnerungen, Nachfassen, Feedback (P4) | Regeln in anlaesse je Kunde, Texte in Vorlagen; terminErinnerungen.ts bekommt den Kanal WhatsApp | Erweiterung |
| Mitglieder-Anlässe (P5) | Regeln in anlaesse; Datenquellen Spiegel, Verkäufe, Leads, Scouting | Konfiguration + wenige Zeilen Code |
| Portal und Reporting (P6) | Portal-Bereich assistent in PORTAL_BEREICHE, portalAssistent.ts, Zentrale-Karte in der Kundenakte, Modul-Katalog kunden_module/flags für die Buchung | Erweiterung nach dem Muster Scouting/Postfach |
| Service-Modus Bot (P7) | chatbotCore.ts Mitglieder-Regeln, Verifikation über mitgliedCore.ts (Magic-Link, gebaut) | Erweiterung |
| Cockpit und Analytics (K1, K2) | cockpitCore.ts (Kennzahlen aus Spiegel, Verkäufen, Leads, Ads; reine Funktionen, getestet) + cockpit.ts (Routen, Cache); Portal-Startseite | neu, nur lesend, kein neuer Speicher außer einem Tages-Cache je Kunde |
| Kampagnen-Objekt (K3) | Tabelle kampagnen (Formular, Angebot, Ads-Kampagnen, Vorlagen, Zeitraum) | neu, klein |
| Kunden-Copilot (K5) | assistant-service existiert schon als Dienst (Wissen, Voice, Gesprächsverlauf); der Kunden-Copilot ist ein Werkzeugsatz im Hub (copilotWerkzeuge.ts: Liste bauen, Vorlage vorbereiten, Anlass anlegen), Modell über OpenRouter wie der Bot | Erweiterung, kein neuer Dienst |
| Telefon (P8, Option) | externer Partner mit Webhook in den Lead-Einlauf; bei uns nur telefon.ts als Empfänger | extern |
| Meta-Ads (P9) | ads-service (bestehender Coolify-Dienst) nach dem Meta-Bauplan | Erweiterung des Nebendienstes |
Alles im Hub, alles in hub.db, deployt mit rollout.sh, gesichert vom Nacht-Backup. Keine neue Datenbank: ein zweites SQLite oder ein Postgres würde nur Konsistenzprobleme (Transaktionen über zwei Dateien) und einen zweiten Backup-Pfad bringen, ohne einen Vorteil bei 19 MB. Kein neuer Container: die Anlass-Engine ist ein Tick im Hub wie zwanzig andere; ein eigener Dienst wäre nur sinnvoll, wenn er unabhängig deployt oder skaliert werden müsste, und beides ist nicht der Fall.
3Performance: drei Regeln statt Sorgen
Größenordnung: 21 Kunden, ein Club hat 500 bis 1.500 Mitglieder, eine Kette 5.000. Zehn angebundene Clubs sind 10.000 Kontakte, 20.000 Anlass-Versände im Jahr, 50.000 Check-in-Ereignisse im Monat (flüchtig). Für SQLite im WAL-Modus sind das Millisekunden, wenn:
- Ereignisse nicht speichern, sondern verarbeiten. Check-ins werden im Event-Verarbeiter zu zwei Feldern am Kontakt (Zeitstempel, Wochenschnitt) und dann verworfen. Kein Verlauf, keine wachsende Tabelle, und es hält die Magicline-Zusage („no visit history is stored"). Die
events-Tabelle des Hubs (heute 22.000 Zeilen) bekommt einen Aufräumlauf (älter als 90 Tage weg, Zusammenfassungen bleiben); das steht ohnehin an. - Ein Schreiber, kurze Transaktionen. SQLite hat einen Schreiber gleichzeitig. Der Tick der Anlass-Engine liest je Kunde die fälligen Regeln (Index auf
kontakte(customer_key, letzter_anlass_am)), schreibt je Versand eine Zeile, sendet außerhalb der Transaktion. Poller (Agilea 5 min, aidoo nachts) schreiben nur Änderungen. Kein Job hält eine Transaktion offen, während er auf Meta oder Agilea wartet. Genau so arbeiten heute Nachtabgleich und Event-Verarbeiter. - Cockpit-Zahlen vorrechnen. Bestand, Zu-/Abgang, Beitragsumsatz und Vertragsanalyse werden einmal täglich je Kunde berechnet und als JSON in einer Cache-Tabelle abgelegt (wie
berichte-cacheheute), das Portal liest nur. Live-Zahlen (offene Leads, Gespräche heute) sind kleine Zählabfragen mit Index. Nie eine Portal-Seite, die 5.000 Verträge aggregiert, während der Kunde wartet.
Außenlast: Meta-Vorlagenversand bei 320 Mitgliedern mit Opt-in sind 640 Aufrufe im Monat, Agilea-Polling 288 Aufrufe am Tag je Club (Antwort klein, Filter nach Zeit), Magicline-Webhooks kommen ohnehin. Nichts davon ist nennenswert; Deckel je Kunde und Tag (Meta-Qualitätsbewertung) sind Produktregel, nicht Lastschutz.
Wenn wir in zwei Jahren bei 100 Clubs sind, ist der erste Engpass nicht SQLite, sondern der eine Hub-Prozess für alles (Web, Ticks, Poller). Dann trennt man die Ticks in einen zweiten Hub-Container mit derselben Datei (SQLite WAL erlaubt mehrere Prozesse auf einem Host) oder wechselt den Speicher. Das ist ein Tag Arbeit, wenn er kommt, und muss heute nicht vorgebaut werden.
4Sicherheit: fünf Punkte
- Einwilligung als Daten, nicht als Haken.
kontaktespeichert Wortlaut, Zeitpunkt, Quelle, Kanal und Stopp-Zeitpunkt; die Engine sendet Werbung nur mit gesetztem Opt-in, Service (Termin, Bestätigung) nur an Kontakte aus einem laufenden Vorgang. „STOPP" wird im Eingang erkannt, bevor der Bot antwortet. Löschung eines Kontakts kaskadiert Versände (Muster Lead-Löschung mit DSGVO-Export existiert). - Mandantentrennung. Jede neue Route im Portal nimmt
customerKeyausschließlich ausportalUserOf(); jede neue Tabelle hatcustomer_keyin der ersten Spalte und im Index; Tests prüfen die Fremdzugriffe (Muster Scouting: „identische Antwort wie unbekannter Kunde"). Zentrale-Routen bleiben hinter Session oder Admin-Token. - Eingehende Webhooks nur signiert. Meta: HMAC gegen den Roh-Body (gebaut). Magicline:
x-api-keyje Studio (gebaut). Agilea und aidoo werden gepollt, es gibt keinen offenen Eingang. Telefon-Partner (falls): nur mit Geheimnis im Header und Whitelist der Absender-IP. - Geheimnisse und Rechte. API-Keys der Studiosoftware bleiben verschlüsselt in der Anbieter-Registry (
HUB_VERTRAEGE_SECRET_KEY), Meta-Token im.env(Kundennummern per Embedded Signup, Kunde sieht keinen Token). Neue Env-Variablen: keine, außer der Wahl des Copilot-Modells. Der Agilea-Restapi-Benutzer liest alles (Befund 25.08.): Poller-Anfragen bleiben aufcheckInHistorymit Filter, keine Volltextabzüge. - Bot-Leitplanken. Mitglieder-Modus liest Vertragsdaten nur nach Magic-Link-Verifikation (Standard) oder nach ausdrücklicher Kundenentscheidung „Nummer reicht"; handelnde Aktionen (Kurs, Stilllegung, Tarifwechsel) mit frischer Bestätigung; Copilot für Kunden bereitet vor und sendet nie ohne Klick; Gesundheitsdaten werden nicht erfragt; KI-Kennzeichnung bleibt im Kopf jeder Unterhaltung. AVV-Baustein „WhatsApp-Assistent" und Datenschutz-Text im Template gehören zu P6, nicht ans Ende.
Zwei Dinge außerhalb des Codes, die wichtiger sind als jede Zeile: SSH mit Passwort-Login auf beiden Servern (Befund Juli, auf FACEFORCE mit fail2ban gemildert) und die Disziplin bei Headless-Browsern (Teil 1). Beides steht auf der Betriebsliste, nicht in diesem Projekt.
5Übersichtlichkeit: was wir festlegen, bevor die erste Zeile entsteht
- Ein Modul, ein Name. Alles aus diesem Projekt heißt
assistent: Portal-Bereichassistent, Zentrale-Karte „Assistent", Tabellen mit Präfixkontakte,anlaesse,anlass_versand,kampagnen, ModuleanlassCore.ts,anlass.ts,kontakte.ts,cockpitCore.ts,cockpit.ts. Keine Streuung inpostfach.tsoderleads.tshinein, nur Aufrufe nach außen. - Kern getrennt von Rand. Regelauswertung, Frequenzlogik, Kennzahlen: reine Funktionen in
*Core.tsmit Tests, ohnedb.js-Import (Regel seit dem Scouting-Vorfall: Tests dürfen nie die echtehub.dbberühren). Routen, DB, Versand in*.ts. - Eine Karte pro Kunde. In der Kundenakte eine Karte „Assistent" mit vier Reitern (Kontakte und Opt-in, Anlässe, Versände, Zahlen) statt vier Karten. Im Portal eine Startseite (Cockpit) und ein Bereich „Assistent" mit denselben Reitern in Kundenfassung.
- Schalter im Katalog. Neue Funktionen als Module in
studioSoftwareCore.ts(Kann, Nutzt, Fehlt) und im Produkt-Katalog; nichts, was sich nur per SQL einschalten lässt. - Suche und Hilfe pflegen.
KM_STICHWORTEfür die zentrale Suche und die Hilfe-Seite je Etappe, nicht am Ende. - Eine lebende Wiki-Seite „Assistent Stand und To-Dos" nach dem Muster der Postfach-Seite, nach jedem Teilschritt fortgeschrieben; die beiden Konzepte bleiben die Referenz.
- Aufräumen im selben Zug. Was durch den Assistenten überflüssig wird, fliegt raus: Typeform-artige Sonderwege, doppelte Erinnerungslogik (Mail-Erinnerung und WhatsApp-Erinnerung aus derselben Regel), tote Terminquellen (MyTime #1/#2). Weniger Wege = mehr Übersicht.
6Reihenfolge und erster Sprint
| Sprint | Inhalt | AT | Ergebnis, das René sieht |
|---|---|---|---|
| S1 | P2 Kontakte + Opt-in + Vorlagensatz; P3 Anlass-Engine + Magicline-CHECKIN-Verarbeitung + Agilea-Poller + 401-Sperre/Alarm; Wiki-Stand-Seite | 8 | SimGym: Mitglied per QR eintragen, Feedback-Nachricht nach Test-Check-in, STOPP wirkt; Life X: Poller liest Check-ins auf der Testinstanz |
| S2 | P1 Rest (zwei Slots, Template v1, Agilea-Terminquelle) + P4 Begleitung (Erinnerung 24 h/90 min, Verschieben, Feedback, Nachfassen) | 6 | AktiVita-Strecke komplett auf WhatsApp, Erinnerungen mit Tasten |
| S3 | P5 Mitglieder-Anlässe + P6 Portal-Bereich, Reporting, AVV/Datenschutz, Preisseite | 8 | Produkt verkaufbar, erster Pilot je System |
| S4 | K1 Cockpit + K2 Analytics (inkl. Vertragsanalyse, Trichter) | 8 | Portal-Startseite mit Zahlen für Impuls/K5 und Life X |
| S5 | P7 Service-Modus, K3 Kampagnen-Objekt, K4 Selbstpflege | 12 | Mitglieder-Bot, Kunde pflegt Bot-Wissen und Regeln selbst |
| S6 | K5 Kunden-Copilot, Checkout-Ausbau (A/B, Schritt-Analytics, Entfernung), Optionen | 8 | „Frag dein Studio" im Portal |
Voraussetzungen, die nicht Code sind: Meta-Review business_management (Tobias), erster Magicline-Produktionsclub aktiviert (Impuls oder K5), Agilea-Live-Freigabe für Life X (Renés Go, Live-Sperre im Code lösen), aidoo-Fragen an Jens (Bestandsliste, Check-in-Liste), Florian-Fragen (Werber-Feld, Gutschrift).
7Was ich vor S1 noch klären würde
- Regel-Liste statt Node-Editor (Vorschlag ja).
- Verifikation im Mitglieder-Modus: Magic-Link als Standard (Vorschlag ja).
- Preis und Modulname (aus dem Assistent-Konzept).
- Headless-Wächter als Host-Timer einrichten (Vorschlag ja, 30 Minuten).