# 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 ✅ **Generierung steht (15.08.2026), Verbessern-Strecke offen**

**Modell generieren ist gebaut.** Anlegen erzeugt für Person, Produkt und Kulisse je **vier Entwurfs-Versionen** plus vier `modell_gen`-Jobs; freigegeben wird nichts, bis der Nutzer im Ergebnis-Screen eine Variante wählt. Die übrigen werden **archiviert, nicht gelöscht**, und `won_against_version_id` hält fest, wogegen sich die Siegerin durchgesetzt hat.

Prompt je Typ mit Identity-Lock am Anfang **und** am Ende, `merkmale` wörtlich, nur der Blickwinkel variiert über die vier Varianten. Der Studio-Block ist wörtlich aus P07 übernommen, wie es dort verlangt wird. Gesichter entstehen im 3:4-Headshot, Produkte und Kulissen quadratisch.

**Logos: zwei Wege, die sich ausschließen.** Die Brand-Regel schützt ein *vorhandenes* Markenzeichen davor, verzerrt, beschnitten, umgefärbt oder neu interpretiert zu werden. Eine Marke ohne Logo hat nichts zu schützen. Daraus:

- **Datei hochgeladen** → wird v1 und sofort freigegeben, ab da nie wieder generiert.
- **Keine Datei, nur Beschreibung** → vier Entwürfe. Es gibt kein Original, das verfremdet werden könnte.
- **Ist einmal etwas freigegeben, ist Schluss.** Der Dispatcher weist jeden weiteren `modell_gen`-Job auf dieses Asset ab – geprüft: „Dieses Logo ist bereits freigegeben und wird laut Brand-Regel nicht neu generiert."

Beim Logo-Entwurf entfallen die globalen Regeln und der Schlusssatz „No text, no logo, no watermark" – beide wären widersinnig, wenn genau ein Schriftzug entstehen soll.

**KI-Kennzeichnung** sitzt an jeder erzeugten Kachel (EU AI Act, §10).

Durchgespielt über die Oberfläche: Kulisse „Badezimmer, Morgenlicht" ohne Referenzbild angelegt → 4 Varianten erzeugt → v2 freigegeben, v1/v3/v4 archiviert. Fehlerpfad mit `ark` geprüft: Variante bleibt `entwurf`, der Screen zeigt „fehlgeschlagen" mit der Anbieter-Meldung und einen Knopf „Nochmal"; nach dem Wiederholen war die Variante da.

> **Drei Fehler, die ein paralleler Prüflauf über `prompts/` gefunden hat:** der Kulissen-Prompt hatte keine Schluss-Klammer (die vier Varianten wären auseinandergedriftet); ein gescheiterter Job hat die Version auf `archiviert` gesetzt und damit „vom Nutzer verworfen" mit „Generierung kaputt" verwechselt; und der Dispatcher hat `limit` **vor** dem Statusfilter angewendet – bei gefüllter `jobs`-Tabelle hätte er wartende Jobs irgendwann nie mehr gefunden. Alle drei behoben.

### E4 · Grundlage 🟡 **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.
**P7 ist verdrahtet.** Der Dispatcher lädt die aktive P7-Zeile aus `prompt_templates`, füllt die fünf Slots (`USER_PROMPT`, `REGELN`, `ASSETS`, `ATTRIBUTE`, `ANWEISUNGEN` – leere bekommen das Literal `(leer)`) und lässt P7 den englischen Bild-Prompt schreiben. Geprüft: alle fünf Platzhalter werden befüllt, keiner bleibt stehen. Bei Erfolg wird `jobs.prompt_template_version` mitgeschrieben – sonst wäre später nicht rekonstruierbar, welche Prompt-Fassung ein Bild erzeugt hat.

Ist kein LLM erreichbar (heute: kein OpenRouter-Guthaben), greift der deterministische Zusammenbau als **Notlauf** und meldet das im Log. Die Kette läuft damit auch ohne Guthaben, nur mit dem einfacheren Prompt. Der Job hält in `prompt_sent` fest, was wirklich rausging.

**Referenzbilder gehen als Bild-Eingabe mit.** P7 verlangt es so: bei vorhandener Referenz die Person nicht neu beschreiben, sondern nur einen kurzen visuellen Anker setzen. Der Dispatcher lädt dafür das Bild der **freigegebenen** Version jedes belegten Slots und hängt es an – bei OpenRouter als `image_url`, bei Ark als `image[]`. Höchstens vier, weil mehr die Merkmale mittelt statt sie zu schärfen (§8).

**Wirkung, im direkten Vergleich geprüft:** Vorher war die Person im Werbebild nur „eine blonde Frau" und die Kulisse ein ähnliches Badezimmer. Danach ist es dieselbe Person bis zu den Trägern des Tops, dieselbe Ablage, dieselbe Armatur, dasselbe Fenster. Die Identitätsdrift zwischen Modell und fertiger Szene ist damit weitgehend erledigt.

### Verbessern-Strecke ✅ **erledigt (15.08.2026)**

Im Ergebnis-Screen gibt es „Nachschärfen": ein Feld für die Korrektur, dann vier weitere Varianten. Die freigegebene Version bleibt gültig, bis bewusst eine neue gewählt wird.

Zwei Dinge, die dabei anders sind als in der ersten Runde:

- **Die Korrektur geht in P7s `ANWEISUNGEN`-Slot**, ausdrücklich mit Vorrang vor den Standardvorgaben. Ohne das gewinnt P7s eigener Satz „Neutral relaxed expression, no smile" gegen den Nutzerwunsch – genau das ist im Test passiert.
- **Die Runde ankert an der freigegebenen Version**, nicht am ursprünglichen Upload. Sonst driftet das Aussehen mit jeder Runde weiter weg.

Durchgespielt an „Lena" mit der Korrektur „weicheres Licht, leichtes Lächeln": Licht und Mimik geändert, Gesicht erhalten.

> **Drei Fehler, die dieser eine Test aufgedeckt hat:** `ANWEISUNGEN` war leer, die Korrektur erreichte P7 also nie. Der Blickwinkel-Text schrieb „neutral relaxed expression" fest, was jede Mimik-Änderung unmöglich machte. Und beim Freigeben wurde nur `entwurf` archiviert – die abgelöste Version blieb auf `freigegeben` stehen und erschien in der Liste als „Entwurf".

**Offen:** Einwilligungs-Upload für Personen.

### Enum-Erweiterung: Risiko geprüft, nicht mehr theoretisch

§2.6 warnte, Enum-Änderungen könnten die Spalte neu anlegen und Daten vernichten – mit dem Zusatz „je nach Version". **Für Appwrite 1.8.1 am 15.08.2026 gegen eine Wegwerf-Tabelle geprüft:** additiver `PATCH` liefert 200, alle Bestandswerte überleben, der neue Wert ist sofort nutzbar. `setup-appwrite.mjs` zieht Enums seither automatisch nach – **rein additiv**, live vorhandene Zusatzwerte werden gemeldet, nie entfernt.

**Zwei Auflagen:** Nach jedem Appwrite-Upgrade den Test wiederholen, bevor das Skript gegen Produktion läuft. Und `prompt_templates.key` steht auf einer Tabu-Liste – die Tabelle ist mit 22 Zeilen gefüllt und trägt die Identität jedes Prompts; dort ist jede Enum-Berührung ein bewusstes Migrationsprojekt, kein Skriptlauf.

### 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.
