# Programmier-Plan **Stand:** 28. Juli 2026 **Repo:** `git.webklar.com/knso/videogen` · Backend: Appwrite `appwrite.webklar.com/v1`, Projekt `6a5cee34002bb8360c34`, DB `brandloop` **Grundlage:** `app-aufbau.md` (Screens) · `konzept-bilder-feed.md` (Konzept) · `datenbank-aufbau.md` (Schema) --- ## 1. Ausgangslage – was schon da ist | | Stand | |---|---| | **Appwrite-Schema** | ✅ **22 Tabellen, Indizes, 5 Buckets, Team `internal`** – live und vollständig, `scripts/setup-appwrite.mjs` reproduziert diesen Stand idempotent (Stand 14.08.2026) | | **Prompts** | P1–P18 als 22 Zeilen geseedet (P8 = Kern + 4 Adapter) via `scripts/seed-prompts.mjs`, versioniert | | **Regeln** | 4 globale Brand-Regeln in `rules` | | **Prototyp** | `prototyp-app.html`, 21 Screens, Handy-Ansicht, vollständig ohne Backend | | **App-Code** | **existiert noch nicht** – es gibt einen Expo-QR-Code, aber keine Anwendung | > **Nachtrag 14.08.2026 – E0 ist erledigt.** Der Abschnitt unten beschreibt den Stand vom 28.07. und ist ab hier **Historie, keine Aufgabenliste mehr**. Die Live-Datenbank war bereits vollständig migriert (alle 7 neuen Tabellen, alle 4 Enum-Erweiterungen, `attributes` aufgetrennt, 5. Bucket) – nur `setup-appwrite.mjs` im Repo hing auf dem alten Stand hinterher und hätte beim Ausführen die Score-Spalten in `attributes` wieder angelegt. Das Skript ist inzwischen auf den Live-Stand gezogen und gegen ihn geprüft: **0 angelegt, 251 existierten schon, 0 Warnungen.** Auch die beiden Enum-Entscheidungen aus §7.1 (Nischen-Liste, `typ_tags`) sind getroffen und stehen als `NISCHEN` und `TYP_TAGS` im Skript. --- ## 2. Schema-Delta – was konkret fehlt Abgeglichen gegen `scripts/setup-appwrite.mjs`. ### 2.1 Neue Tabellen (6) | Tabelle | Zweck | |---|---| | `folders` | Wissens-Scope: `brand_id`, `name`, `theme_md`, `zweck`, `startwert_modus`, `ist_default`, `post_count`, `signal_count` | | `attribute_scores` | Score je Attribut je Ordner – **Herzstück** | | `posts` | Slot-Rezept, `slot_summary`, `sichtbarkeit`, `feed_score` | | `post_images` | Bilderkette, `typ` = motiv \| text_overlay | | `post_folders` | m:n Post ↔ Ordner, `ist_fremd`, `quelle` | | `post_metrics` | Views, Likes, Kopien, Social-Reichweite | ### 2.2 Neue Tabelle wegen des Folgen-Konzepts (1) | Tabelle | Zweck | |---|---| | `follows` | `follower_brand_id`, `followed_brand_id`, unique auf beide, Index in beide Richtungen | ### 2.3 Änderungen an bestehenden Tabellen | Tabelle | Änderung | Art | |---|---|---| | `brands` | `+ nische`, `+ plan`, `+ anzeigename`, `+ avatar_file_id`, `+ follower_count`, `+ ist_oeffentlich` | Spalten ergänzen | | `attributes` | `− score, k_factor, start_value, used_count, wins, losses, last_used_at` → wandern nach `attribute_scores`; Index `idx_topn` entfällt hier | **Umbau** | | `categories` | `+ folder_id` | Spalte + Index | | `assets` | `+ nische`, `+ typ_tags[]`, `+ feed_score`, `+ feed_score_updated_at`, `+ ist_teilbar`; `typ`-Enum **um `kulisse`** | Spalten + **Enum-Änderung** | | `score_events` | `+ folder_id`, `+ post_id`; `event_type`-Enum **um `feed_import`, `post_reichweite`** | Spalten + **Enum-Änderung** | | `scenes` | `+ folder_id` | Spalte | | `jobs` | `typ`-Enum **um `slot_analyse`, `ordner_vorschlag`, `werbetext`** | **Enum-Änderung** | | `prompt_templates` | `key`-Enum **P18 → P22** (`P_KEYS` im Skript: `length: 18` → `22`) | **Enum-Änderung** | ### 2.4 Neuer Bucket `generated-images` – öffentlich lesbar, sobald der zugehörige Post veröffentlicht ist. ### 2.5 Neue Functions (4) `ordner-initialisieren` · `feed-score-berechnen` (Cron) · `feed-import` · `social-reichweite-holen` (Cron) Dazu die aus dem alten Plan noch offenen: `elo-update`, `favorit-berechnen`, `ads-metriken-holen`, `kategorie-durchschnitt`, `archivierung`, `job-dispatcher`. ### 2.6 Zwei Fallen bei der Migration **Enum-Änderungen sind kein Anhängen.** Vier Spalten brauchen neue Enum-Werte (`assets.typ`, `score_events.event_type`, `jobs.typ`, `prompt_templates.key`). Appwrite behandelt das als Spaltenänderung, nicht als Ergänzung – je nach Version bedeutet das Löschen und Neuanlegen der Spalte, also **Datenverlust**. Das ist der Grund, warum diese Änderung **jetzt** passieren muss, solange die Tabellen leer sind. In sechs Wochen mit echten Daten ist es ein Migrationsprojekt. **Die `attributes`-Auftrennung ist ein Umbau, kein Zusatz.** Sieben Spalten verschwinden aus einer Tabelle und tauchen in einer neuen wieder auf, mit einer zusätzlichen Dimension. Solange keine Brand existiert, ist das eine Textänderung im Setup-Skript. Danach ist es eine Datenwanderung mit Ausfallzeit. > **Deshalb ist „Fundament zuerst" nicht nur die aufgeräumte, sondern die einzig günstige Reihenfolge.** --- ## 3. Etappen Jede Etappe hat ein Abnahmekriterium – etwas, das man vorführen kann. Ohne das ist nicht entscheidbar, ob sie fertig ist. ### E0 · Schema nachziehen ✅ **erledigt (14.08.2026)** **Inhalt:** `setup-appwrite.mjs` um die 7 neuen Tabellen, die geänderten Spalten, die Enum-Erweiterungen und den Bucket ergänzen. `attributes` auftrennen. `P_KEYS` auf 22. **Abnahme erfüllt:** 22 Tabellen und 5 Buckets stehen live; das Skript läuft ohne eine einzige Änderung durch (`0 angelegt, 251 existierten schon, 0 Warnungen`). **Datenbestand:** nur Stammdaten – 4 Zeilen `rules`, 22 Zeilen `prompt_templates` (P1–P18, P8 als Familie). Keine Brand-Daten. **Enum-Änderungen sind ab jetzt trotzdem nicht mehr gefahrlos**, weil `prompt_templates.key` gefüllt ist. > **Lehre aus dieser Etappe:** Die Datenbank war der Quelle voraus, nicht umgekehrt. Wer `setup-appwrite.mjs` in diesem Zustand ausgeführt hätte, hätte die sieben Score-Spalten in `attributes` neu angelegt und die Auftrennung halb zurückgedreht – ein idempotentes Skript schützt nur vor doppeltem Anlegen, nicht vor einer veralteten Definition. Deshalb: **nach jeder Schema-Änderung an der Konsole das Skript nachziehen und mit `scripts/check-appwrite.mjs` gegenprüfen.** ### E1 · Projektgerüst Expo ✅ **erledigt im Browser (14.08.2026), nativ ungeprüft** **Inhalt:** Expo-Projekt in `client/`, TypeScript, Expo Router (Stack + Tabs), Appwrite-SDK verdrahtet, Theme aus dem Prototyp. Fake-Tastatur und `setInterval`-Shader sind bewusst nicht übernommen. **Abnahme:** `npx tsc --noEmit` fehlerfrei · `npx expo export --platform web` erzeugt alle drei Routen · im Browser rendern `/`, `/erstellen` und `/profil` mit Tab-Leiste, keine Konsolen-Fehler. **Offen: der Start auf einem echten Gerät** – dafür fehlt ein Testgerät bzw. ein Dev-Build. **Struktur (`client/src/`):** | Pfad | Inhalt | |---|---| | `app/_layout.tsx` | Wurzel-**Stack**, nicht direkt die Tabs – Willkommen/Anmelden liegen laut app-aufbau.md §3 vor den Tabs, das Onboarding als Modal darüber. Beide brauchen eine Ebene ohne Tab-Leiste. | | `app/(tabs)/` | Feed · Erstellen · Profil | | `lib/appwrite.ts` + `.web.ts` | plattform-getrennter Client. `react-native-appwrite` läuft nicht im Browser, das Web-SDK `appwrite` kennt kein React Native – beide exportieren dieselben Klassen, der Rest der App importiert nur `@/lib/appwrite`. | | `lib/config.ts` | Endpoint, Projekt-ID, DB-ID aus `app.json` → `expo.extra.appwrite`. **Kein Server-Key** – der landet sonst im Bundle. | | `theme/tokens.ts` | Farben, Radien, Abstände aus `prototyp-app.html`. Dark-only, weil der Prototyp kein Light-Theme hat. | **Zwei bewusste Abweichungen:** Das ➕ ist vorerst ein normaler Tab statt des Popovers aus §3 – der kommt mit den drei Erstellen-Abläufen in E6. Und der Profil-Screen zeigt provisorisch Endpoint und Projekt-ID, damit belegt ist, dass der Client wirklich lädt; das fliegt in E2 raus. ### E2 · Auth und Mandant ✅ **erledigt (14.08.2026)** **Inhalt:** Willkommen/Registrieren/Anmelden/Abmelden, Session-Persistenz, bei der Registrierung Team + `brands`-Zeile mit Zeilenrechten. **Abnahme erfüllt:** `scripts/test-mandanten.mjs` legt zwei echte Konten an und prüft **über die API**: B listet nur die eigene Zeile · Direktzugriff B→A `404 row_not_found` · Schreibzugriff B→A `401 user_unauthorized` · Gegenprobe, dass der Server-Key beide Zeilen sieht. Räumt sich selbst auf. **Der eigentliche Inhalt war das Rechte-Modell, nicht die Formulare.** Zwei Dinge, die vorher niemandem aufgefallen waren: 1. **Kein Client konnte irgendetwas anlegen.** Bei `rowSecurity: true` regeln Zeilenrechte lesen/ändern/löschen – aber eine Zeile, die es noch nicht gibt, hat keine Rechte. Ohne Tabellen-Recht zum Anlegen war jede Client-Schreiboperation blockiert. 16 Tabellen haben jetzt `create("users")`; `score_events`, `video_metrics`, `post_metrics` und `usage_records` bewusst **nicht** – sonst könnte ein Nutzer seine eigenen Elo-Werte und Feed-Signale fälschen (§11, „Manipulation des Feed-Scores"). 2. **`setup-appwrite.mjs` hat Rechte nie abgeglichen.** Bei bestehenden Tabellen lieferte `POST` nur ein 409, die Rechte blieben unangetastet. Das Skript vergleicht jetzt und zieht per `PUT` nach – sonst driftet das Rechte-Modell genauso wie zuvor das Schema. **Nebenfund: SDK-Version passte nicht zum Server.** Appwrite läuft in **1.8.1**, `create-expo-app` hatte SDKs mit Response-Format 1.9.5 gezogen. Gepinnt auf `appwrite@23.0.0` und `react-native-appwrite@0.25.0` (beide Response-Format 1.8.0), **exakt statt Caret** – der Sinn der Pinnung ist ja gerade der Gleichstand mit dem Server. > Das ist die Etappe, bei der man am ehesten schludert und es am teuersten wird. Die Mandantentrennung ist das Fundament des Rechte-Modells aus `datenbank-aufbau.md` §5. ### E3 · Onboarding **Inhalt:** Die vier Onboarding-Screens mit echten Feldern, inklusive **Nische**. Uploads in den Bucket `uploads`. P2 als erster echter LLM-Aufruf über `jobs`. **Abnahme:** Nach dem Onboarding stehen in `brands` sechs ausgefüllte Felder und in `attribute_scores` die ersten Attribute auf der brand-weiten Ebene (`folder_id = null`). **Aufwand:** mittel. ### E4 · Modelle 🟡 **Kern steht (14.08.2026), Verbessern-Strecke offen** **Inhalt:** Modell anlegen (Person, Produkt, **Kulisse**), echte Referenzbild-Uploads, Versionierung mit Release-Prinzip, Modelle-Übersicht im Profil. **Erledigt:** Anlegen mit Typwahl und Mehrfach-Bildauswahl (`modell/neu`), Upload nach `asset-references` mit Team-Rechten, Version 1 wird angelegt und sofort freigegeben, `assets.released_version_id` zeigt darauf. Übersicht im Profil nach Typ gruppiert, mit Titelbild aus der freigegebenen Version. **Belegt:** Vier Modelle (2 Produkte, 2 Kulissen) über `scripts/seed-demo.mjs` als **Client** angelegt – das beweist nebenbei, dass die Tabellen- und Bucket-Rechte aus E2 ausreichen. Alle vier Bilder laden in der App in Originalgröße. **Offen:** weitere Versionen anlegen und freigeben (Verbessern-Strecke), `merkmale` als Token-Lock, Einwilligungs-Upload für Personen. ### E5 · Ordner ✅ **erledigt (14.08.2026)** **Inhalt:** Ordner-Liste, Anlegen mit den zwei Schaltern, Ordner-Detail, aktiver Ordner als App-Zustand. Function `ordner-initialisieren`. **Abnahme erfüllt** – nach dem Anlegen über die Oberfläche steht in `attribute_scores`: | Ordner | Modus | Scores | `start_quelle` | |---|---|---|---| | (brand-weit, `folder_id = null`) | – | 12 | `neutral` | | Sommerkampagne | `erben` | 12 | `geerbt` | | Cleane Studioshots | `aus_posts` | 0 | – | | Herbstlinie (über die App angelegt) | `erben` | 12 | `geerbt` | Zweck und Startwerte sind als Auswahl **mit Erklärung** umgesetzt, nicht als nackter Schalter: wer sie missversteht, baut sich einen Ordner, der nicht tut, was er erwartet. **Beim Erben wird der Wert kopiert, aber nicht die Sicherheit** – `k_factor` geht zurück auf 32, weil im neuen Scope noch nichts belegt ist. **Abweichung:** `ordner-initialisieren` läuft vorerst im Client statt als Appwrite-Function. Vertretbar, weil nur eigene Zeilen kopiert werden und die Zeilenrechte das ohnehin begrenzen; beim Umzug in eine Function ändert sich nur der Ort. ### E6 · Post erstellen 🟡 **Rezept-Hälfte steht (14.08.2026), Generierung extern blockiert** **Inhalt:** Create-Screen mit Ordner, Format, Kette, Slots. `jobs` + `job-dispatcher` + Realtime-Wartezustand. P7 auf das Slot-System umbauen. P22 als Bild-Tagger. Post-Ergebnis mit privat/veröffentlichen. **Erledigt:** `erstellen/post` mit Ordnerwahl in der Kopfzeile, Format, Kettenlänge und Slot-Auswahl für Kulisse, Produkt und Person – als Bildkacheln, weil bei Kulissen und Produkten das Aussehen die Information ist und ein Name wie „Halle, Metallwand" nichts darüber sagt, ob es passt. Der Post entsteht **sofort** mit `status: entwurf`, bevor irgendetwas generiert wird; ohne diese Zeile gäbe es keinen Ort, an den man nach dem Warten zurückkehrt. Je Kettenbild eine `jobs`-Zeile. Ordner-Detail zeigt die Posts mit aufgelösten Slot-Namen. **Nachweis über die Oberfläche:** Post „Serum auf Waschtisch" → Ordner Herbstlinie, `4:5`, 3 Bilder, `privat`, Slots Halle-Metallwand + Refine-&-Renew-Serum, dazu **3 Jobs `bild_gen/wartend` mit `prompt_template_key = P7`**. **Job-Dispatcher steht** – `scripts/job-dispatcher.mjs`, serverseitig, weil der Anbieter-Schlüssel nicht ins Client-Bundle darf. Er baut aus Slot-Rezept, den Beschreibungen der freigegebenen Asset-Versionen, den Top-Attributen **des gewählten Ordners** und den globalen Regeln einen Prompt, erzeugt das Bild, legt es in `generated-images` mit Team-Rechten ab, schreibt `post_images` und setzt den Post auf `generiert`, sobald die Kette vollständig ist. Drei Anbieter über `BILD_ANBIETER`: | Wert | Ergebnis (geprüft 14.08.2026) | |---|---| | `stub` | ✅ erzeugt Platzhalterbilder – **die ganze Kette ist damit prüfbar, ohne einen Cent auszugeben** | | `ark` | ✗ `ModelNotOpen: account 3003959567 has not activated seedream-5-0` | | `openrouter` | ✗ `Insufficient credits. This account never purchased credits` | **Durchgespielt mit `stub`:** Post „Serum auf Waschtisch" → 3 Jobs → 3 Bilder erzeugt, hochgeladen, Post auf `generiert`, Kette im Ordner-Detail sichtbar (3/3 geladen). Bei den echten Anbietern greift der Fehlerpfad: Job auf `fehler` mit der Anbieter-Meldung, Post auf `fehler`. **Damit fehlt nur noch die Freischaltung.** Sobald eines der beiden Konten offen ist, liefert `BILD_ANBIETER=ark node scripts/job-dispatcher.mjs` echte Bilder – ohne Codeänderung. **Offen:** Umzug des Dispatchers in eine Appwrite-Function (läuft jetzt lokal/per Cron), P7 als versionierter Prompt statt der Vorstufe im Skript, P22 als Tagger, Wartezustand mit Realtime, Ergebnis-Screen mit Veröffentlichen. ### E7 · Feed **Inhalt:** Veröffentlichen mit Zeilen-Permission, Feed-Deck mit den drei Reitern, Post-Detail, `post_metrics` schreiben, Function `feed-score-berechnen` mit Decay und Explorations-Slot. **Abnahme:** Ein veröffentlichter Post von Konto A erscheint im Feed von Konto B. `prompt_sent` ist über die API von Konto B **nicht** abrufbar. **Aufwand:** groß. ### E8 · Kopieren **Inhalt:** P19 (Slot-Analyst), Kopier-Screen mit Chips und Kompatibilitätsprüfung, P21 (Werbetext), Function `feed-import`, `copied_from_post_id`, Kopien-Zähler. **Abnahme:** Konto B kopiert den Post von Konto A, ersetzt beide Modelle, generiert – und in der Knowledge Base von Konto B stehen danach neue Attribute mit `start_quelle = feed_import` im gewählten Ordner. **Aufwand:** groß. Das ist der Moment, in dem das Produkt das tut, was es verspricht. ### E9 · Folgen **Inhalt:** `follows`, Fremdprofil, Folgen-Button, Reiter „Folge ich" chronologisch, Blockieren und Melden. **Abnahme:** Konto B folgt Konto A und sieht dessen neuen Post im Reiter „Folge ich", vor allem anderen. **Aufwand:** mittel. ### E10 · Video-Strecke **Inhalt:** Die bestehende Video-Strecke aus dem Prototyp echt machen: `scenes`, P3/P4/P5/P8, `videos`, Vote, `elo-update`, gezielte Frage. **Abnahme:** Ein Prompt erzeugt vier Videos von vier KI-Modellen; nach dem Vote haben sich Scores in `attribute_scores` messbar verändert und `score_events` hat die passenden Zeilen. **Aufwand:** sehr groß, und mit laufenden Kosten ab dem ersten Test. --- ## 4. Reihenfolge und Abhängigkeiten ``` E0 Schema └─ E1 Gerüst └─ E2 Auth ──────────────┐ ├─ E3 Onboarding │ ├─ E4 Modelle ───────┤ └─ E5 Ordner ────────┤ └─ E6 Post erstellen ├─ E7 Feed │ ├─ E8 Kopieren │ └─ E9 Folgen └─ E10 Video ``` **E3, E4 und E5 sind untereinander unabhängig** – sie können in beliebiger Reihenfolge oder parallel entstehen. Alles ab E6 hängt an allen dreien. **E10 (Video) steht bewusst hinten,** obwohl es das Kernprodukt ist: Es ist die teuerste Strecke in der Umsetzung *und* im Betrieb, und der Bild-Strang erzeugt vorher die Daten, mit denen sich der Video-Teil überhaupt sinnvoll testen lässt. --- ## 5. Was im ersten Durchgang bewusst wegbleibt | Weggelassen | Warum | |---|---| | Desktop-Ansicht | Handy zuerst; der Handoff im Repo ist laut dir noch nicht gültig | | Stripe / Abrechnung | Preismodell ist offen (offener Punkt 3) – ohne Entscheidung nicht baubar | | Meta-/TikTok-Ads-Anbindung | App-Reviews dauern; Antrag trotzdem **jetzt** stellen, unabhängig vom Code | | Social-Reichweite (echte Instagram-Zahlen) | eigene App-Review; bis dahin läuft der Feed-Score nur auf In-App-Signalen | | P12–P18, P20 | nachrüstbar, ohne sie funktioniert der Loop | | Verbessern-Strecke | vorhanden im Prototyp, aber nicht auf dem kritischen Pfad | | Developer-Modus | intern, kein Nutzerwert – erst wenn es echte Prompts zu debuggen gibt | --- ## 6. Risiken | Risiko | Wirkung | Gegenmaßnahme | |---|---|---| | **Enum-Migration nach dem Livegang** | Datenverlust in vier Spalten | E0 vor allem anderen, solange die DB leer ist | | **Multi-Referenz-Komposition** (Person + Produkt + Kulisse konsistent in einem Bild) | Der Kern des Bild-Modus funktioniert nicht wie gedacht | Vor E6 mit zwei, drei Kandidatenmodellen einen Wegwerf-Test fahren – **bevor** UI dafür gebaut wird | | **Generierungskosten im Test** | schleichende Kosten ohne Gegenwert | `jobs.cost_usd` ab E6 mitschreiben und ein hartes Tageslimit einbauen | | **Leerer Feed** | E7 ist nicht bewertbar, E8 nicht testbar | Vor E7 mit dem eigenen Konto 15–20 Posts erzeugen | | **Prototyp als Vorlage überschätzt** | Screens werden 1:1 übernommen inklusive der Attrappen | `app-aufbau.md` §6 ist die verbindliche Liste, nicht die HTML-Datei | | **Öffentlich/privat nur im UI durchgesetzt** | fremde Knowledge Bases werden über die API auslesbar | Abnahme von E7 ausdrücklich **über die API** prüfen, nicht über den Bildschirm | --- ## 7. Was vor E0 zu entscheiden ist 1. ~~**Nischen-Liste** und **`typ_tags`-Liste**~~ – ✅ entschieden und live. 20 Nischen, 17 Motiv-Tags, stehen als `NISCHEN` und `TYP_TAGS` in `scripts/setup-appwrite.mjs`. 2. **Bildmodell** – blockiert nicht E0, aber E6. Der **Anbieter steht** (OpenRouter, siehe `projekt-uebersicht.md` §9); offen ist nur, welches der dortigen Bildmodelle die Multi-Referenz-Komposition trifft. Der Wegwerf-Test dafür sollte parallel zu E1–E5 laufen. 3. **Mindestalter und Moderationsweg** – blockiert nicht den Code, aber E7 im Livebetrieb.