From 441f95697b028bb5e3fc966c9a22269c9eb4d338 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 23 Apr 2026 18:42:42 +0200 Subject: [PATCH] docs(website): smoketest walkthrough + manual-test-backlog entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The code is shipped (M1–M7) but nothing has run against real Postgres + mana-sync + mana-media + a browser. This smoke-test doc is the click-through a human needs to do before we trust the feature in production. - docs/plans/website-builder-smoketest.md — 10 scenarios end-to-end from migrations + dev-stack through create/publish, block coverage (image upload, gallery lightbox, columns container), forms with honeypot + rate-limit, moduleEmbed with public-flag enforcement, templates + AI tools, subdomain rewrite, custom-domain DNS verify, rollback + analytics, metrics + GC script, edge-cases + security. Lists bekannte Limits (CF SaaS gap, target-delivery, AiProposalInbox) explicitly so the tester knows what NOT to expect. - docs/optimizable/manual-test-backlog.md — new release-blocker entry pointing at the walkthrough. Follows the same format as the Shared Space + Data Export entries. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/optimizable/manual-test-backlog.md | 9 + docs/plans/website-builder-smoketest.md | 515 ++++++++++++++++++++++++ 2 files changed, 524 insertions(+) create mode 100644 docs/plans/website-builder-smoketest.md diff --git a/docs/optimizable/manual-test-backlog.md b/docs/optimizable/manual-test-backlog.md index e7af6c36f..e972f9032 100644 --- a/docs/optimizable/manual-test-backlog.md +++ b/docs/optimizable/manual-test-backlog.md @@ -115,6 +115,15 @@ Each entry carries: --- +### Website Builder — end-to-end smoke across M1–M7 + +- **Priority:** 🔴 release blocker (new feature surface, no browser validation yet) +- **Shipped:** folded into `54a12ffd5` + `89258eb45` (M1+M2), `7a4f8894e` (M3), `57be0f61b` (M4), `13efae8cd` (M5), `3eca5ac20` (M6), folded into `4fc9d6c59` + `d518169ce` (M7). Plan: [`docs/plans/website-builder.md`](../plans/website-builder.md). Memory: `project_website_builder.md`. +- **Why it's here:** ~7000 lines of code, 11 block types, 3 Postgres tables, `/metrics` endpoint, hooks-rewrite, dns-verify, analytics injection. Unit + type-checks green but nothing has run against real Postgres + mana-sync + mana-media + a browser. Blind spots: Dexie v37→v41 upgrade, SSR render, CF-friendly cache headers, same-origin form submit proxy, custom-domain `dns.resolveTxt` against real resolvers, `event.url.pathname` rewrite semantics in SvelteKit. +- **Steps:** full walkthrough at [`docs/plans/website-builder-smoketest.md`](../plans/website-builder-smoketest.md) — 10 scenarios (create/publish, block-coverage, forms, module-embed, templates/AI, subdomain, custom-domain, rollback/analytics, metrics/GC, edge-cases). Brings up dev-stack, applies three SQL migrations, walks through `/website` → edit → publish → `/s/` → custom host. + +--- + ### Articles — PWA share-target - **Priority:** 🟡 nice to have diff --git a/docs/plans/website-builder-smoketest.md b/docs/plans/website-builder-smoketest.md new file mode 100644 index 000000000..ebf9be551 --- /dev/null +++ b/docs/plans/website-builder-smoketest.md @@ -0,0 +1,515 @@ +# Website Builder — Smoketest + +Schritt-für-Schritt-Anleitung, um den Website-Builder End-to-End zu validieren — vom Site-Anlegen über Publish bis hin zu Custom-Domains, Forms und Rollback. + +**Warum nötig:** 7 Milestones (M1–M7) mit ca. 7000 Zeilen Code, alle Unit-/Type-Checks grün, aber nichts davon ist bisher durch einen echten Browser mit echtem Postgres gelaufen. Unit-Tests decken nicht ab: Dexie-v37-Upgrade aus existierender DB, Encryption-Registry-Integration, mana-sync Wire-Format, SSR-Public-Render-Pfad, Cloudflare-Cache-Header-Propagation, DNS-Resolver mit echten Records, Form-Submit-Flow über same-origin Proxy, AI-Proposal-Staging. + +**Scope:** Das, was Code tun soll. Außerhalb: Cloudflare-SaaS-Hostname-Provisioning für Custom-Domain-TLS (ops-Lücke, dokumentiert in §M6 des Plans) und Target-Delivery für Form-Submissions zu `contacts`/`notify` (M4.x). + +Siehe [`docs/plans/website-builder.md`](website-builder.md) für das Design, [`docs/observability/website.md`](../observability/website.md) für Metrics. + +--- + +## Vorbereitung + +### 1. Schema-Migrationen anwenden + +Der Website-Builder hat **drei** SQL-Migrationen im `website.*` pg-Schema. Vor dem ersten Test anwenden: + +```bash +export DATABASE_URL=postgresql://mana:devpassword@localhost:5432/mana_platform +psql "$DATABASE_URL" -f apps/api/drizzle/website/0000_init.sql +psql "$DATABASE_URL" -f apps/api/drizzle/website/0001_submissions.sql +psql "$DATABASE_URL" -f apps/api/drizzle/website/0002_custom_domains.sql +``` + +Verify: + +```bash +psql "$DATABASE_URL" -c "\dt website.*" +``` + +Erwartet: `published_snapshots`, `submissions`, `custom_domains`. + +### 2. Dev-Stack starten + +```bash +pnpm docker:up # postgres + redis + minio +pnpm run mana:dev # mana-auth (3001) + mana-sync (3050) + web (5173) + api (3060) +``` + +Wenn irgendeiner der Services nicht hochkommt: +- `pnpm docker:logs mana-postgres` — Postgres-Probleme +- `services/mana-auth`-Konsole — JWT/Org-Fehler + +### 3. Dev-User mit Founder-Tier + +Custom-Domains brauchen `founder` Tier. Nutze den existierenden Dev-User oder leg einen neuen an: + +```bash +pnpm setup:dev-user +``` + +Legt `tills95@gmail.com` / `tilljkb@gmail.com` / `rajiehq@gmail.com` alle als founder an. Passwort jeweils `Aa-123456789`. + +Der Signup-Hook vergibt jedem Account automatisch einen `personal` Space — den nutzen wir. + +### 4. Env-Vars optional + +Für **Cloudflare-Purge** (M6 ops-Stub) optional: +``` +CF_API_TOKEN=… +CF_ZONE_ID=… +``` +Ohne das loggt die API nur `[website] CF onboard TODO` — alle lokalen Tests funktionieren trotzdem. + +Für den **Orphan-Asset-GC-Script** (M7): +``` +MANA_SERVICE_KEY=dev-service-key # für internal /media/list +``` + +--- + +## Szenario 1 — Grundfluss (M1 + M2) + +### Schritt 1: Site anlegen + +1. In den Browser: `http://localhost:5173/website` +2. Klick **"+ Neue Website"** → landet auf `/website/new` (TemplatePicker) +3. Wähle Template **"Leer"** (oder "Portfolio" wenn du mehr Content willst) +4. Name: `Mein Test`, Slug wird automatisch zu `mein-test` +5. Klick **"Mit Leer starten"** + +**Erwartet:** +- Redirect zu `/website//edit/` +- Drei-Pane-Editor: links Site-Meta + PageList + InsertPalette, Mitte leere Vorschau, rechts Inspector-Placeholder +- Header zeigt "Mein Test" + ⚙-Button + +**Dev-Tools:** +- Application → IndexedDB → `mana` → `websites` → eine Row mit deinem Namen +- Application → IndexedDB → `mana` → `websitePages` → eine Row mit `path='/'` + +### Schritt 2: Blöcke einfügen + +1. InsertPalette (unten links) → klick **"Hero"** → erscheint mittig, wird selektiert +2. Inspector rechts: Titel ändern zu "Willkommen", Untertitel "Meine erste Seite" +3. Palette → **"Text"** → klick in Vorschau um zu wählen → Inspector → Content tippen, zwei Absätze mit Leerzeile zwischen +4. Palette → **"Abstand"** → Größe "Groß" + +**Erwartet:** +- Live-Updates in Vorschau beim Tippen (Reaktivität via Dexie) +- Klick auf einen Block → outline indigo, Inspector zeigt die Felder dieses Blocks +- Pfeil-Buttons (↑↓) im Inspector ändern Reihenfolge +- × im Inspector-Header löscht + +### Schritt 3: Publish + +1. Oben PublishBar → **"Veröffentlichen"** klicken +2. Pill wechselt von "Entwurf" → "Live" +3. Link `/s/mein-test` wird geklickbar + +**Erwartet:** +- POST zu `/api/v1/website/sites//publish` (DevTools → Network → 201) +- Response hat `snapshotId`, `publishedAt`, `publicUrl: '/s/mein-test'` + +### Schritt 4: Public-Render + +1. Link öffnen (oder `http://localhost:5173/s/mein-test` direkt) +2. **Erwartet:** + - SSR-Render: View-Source zeigt Hero-HTML **ohne** JavaScript-Rehydrate-Marker (pure SSR) + - Keine Navigation-Leiste (kein `navConfig.items`), nur Brand-Link + - Network: GET `/api/v1/website/public/sites/mein-test` mit `Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400` + - Response-Header `Cache-Tag: site-` + +### Schritt 5: Editor + Draft-Ahead-Indicator + +1. Zurück in den Editor +2. Hero-Titel ändern zu "Willkommen!" +3. PublishBar zeigt Pill **"Unveröffentlichte Änderungen"** +4. Öffne `/s/mein-test` in anderem Tab → **immer noch der alte Titel** (erwartet, Draft ≠ Live) +5. PublishBar → "Änderungen veröffentlichen" → Reload des Public-Tabs → neuer Titel + +### Schritt 6: Rollback (M7-Polish) + +1. PublishBar → **"Versionen"** klicken → Dialog mit 2 Snapshots +2. Obere Row = "Aktuell live" mit grünem Pill +3. Untere Row (erste Publish) → **"Wiederherstellen"** klicken → bestätigen +4. Dialog reload: neue aktuelle Row ist die alte +5. `/s/mein-test` reloaden → Titel ohne `!` + +--- + +## Szenario 2 — Block-Coverage (M3) + +### Image-Upload + +1. Neuer Block **"Bild"** → Inspector rechts → Dropzone +2. Klicke/ziehe ein JPG oder PNG rein (< 25 MB, irgendein Testbild) +3. **Erwartet:** + - POST an `/api/v1/media/upload` mit `app=website` + - Response `{ id }`, URL wird automatisch in `block.props.url` geschrieben + - Vorschau zeigt das Bild + +Fehlerfälle: +- Nicht-Bild-Datei → "Bitte wähle ein Bild (PNG, JPG, WEBP, GIF)." +- Datei > 25 MB → "Datei zu groß (max 25 MB)." + +### Gallery-Block + +1. Block **"Galerie"** → Inspector → "+ Bilder hinzufügen" → mehrere Dateien gleichzeitig +2. Alt-Text pro Bild eintragen (Pflicht für A11y — sollte Screenreader-Crawl später nicht stören) +3. Layout/Columns/Lightbox-Toggle testen +4. **Im Editor:** Gallery zeigt alle Bilder im Grid, Klick auf ein Bild wählt nur den ganzen Block (Editor-Mode) +5. **Public (`/s/mein-test`):** mit `lightbox=true`, Klick auf Bild → Fullscreen-Overlay mit Prev/Next (‹ ›) + Escape +6. Escape/Klick außerhalb schließt + +### Columns-Container + +1. Block **"Spalten"** → 2 oder 3 Columns +2. Klick in eine Column-Drop-Zone ("Spalte 1") +3. Insert-Palette nochmal öffnen, einen Text-Block einfügen — landet in der Column +4. **Erwartet:** Text ist Child des Container-Blocks, `parentBlockId` in der Dexie-Row zeigt auf Columns-Block-ID, `slotKey = 'col-0'` +5. Public-Mode: responsive, < 720px stapeln sich die Columns untereinander (stackOnMobile=true Default) + +**Manuelle Verify:** +```bash +# In DevTools Console: +(await db.websiteBlocks.toArray()).map(b => ({ type: b.type, parentBlockId: b.parentBlockId, slotKey: b.slotKey })) +``` + +Sollte mindestens eine Row zeigen mit `parentBlockId: ` und `slotKey: 'col-0'`. + +### Theme wechseln + +1. Header → **⚙-Button** → SiteSettingsDialog +2. Unter "Theme" auf eines der drei Presets klicken (Classic / Modern / Warm) +3. Farb-Overrides testen: Color-Picker für Primary, Background, Foreground +4. "Speichern" → Dialog schließt +5. **Public-Reload** → neue Farben + Font angewandt (CSS-Variablen) + +--- + +## Szenario 3 — Forms (M4) + +1. Editor → Block **"Formular"** (kommt mit 3 Default-Fields: Name, E-Mail, Nachricht) +2. Inspector: Feld hinzufügen — "Telefon" Typ `tel`, optional +3. Titel und Description setzen, Submit-Label auf "Absenden" lassen +4. Site **neu publishen** (wichtig: das Form-Schema wird im Snapshot eingefroren) +5. `/s/mein-test` öffnen → Formular sichtbar + +### Submit-Flow testen + +1. Felder ausfüllen (required: Name + E-Mail + Nachricht) +2. Absenden → **"Danke! Wir melden uns bald."** + +**Erwartet:** +- POST an `/s/mein-test/__submit/` (same-origin SvelteKit proxy) +- Von dort Forward an `/api/v1/website/public/submit/mein-test/` +- Response 201 mit `submissionId` + +### Honeypot-Check + +1. In der gleichen Form-Instanz: DevTools → Elements → finde `` in der verborgenen `.wb-form__honeypot`-Wrapper +2. Temporär sichtbar machen (`display: block`), Wert "spam-bot" eintragen, normal absenden +3. **Erwartet:** Success-Message erscheint (act-as-success-for-bots), aber **nichts** in der Submissions-Tabelle + +Verify: +```bash +psql "$DATABASE_URL" -c "SELECT created_at, payload, status FROM website.submissions ORDER BY created_at DESC LIMIT 3" +``` + +### Rate-Limit + +1. Submit-Endpoint 11× schnell in Folge (Script / Postman / Hand-Refresh) +2. Ab der 11. Response: **429 "Rate limit überschritten"** + +### Submissions-Inbox + +1. Im Editor zurück zu Site → Top-Level-Submenu hat (noch) keinen Link, also URL direkt: `/website//submissions` +2. **Erwartet:** Deine Submissions (die echten, nicht die Bot-Spam-Try) als Liste +3. Löschen-Button funktioniert + +--- + +## Szenario 4 — Module-Embed (M4) + +### Picture-Board einbetten + +Vorbereitung: in `/picture` mindestens ein Board anlegen, ein paar Bilder hochladen + auf Board ziehen, Board auf **"Öffentlich"** flippen (Board-Detail → Einstellungen). + +1. Zurück im Website-Editor → Block **"Modul einbetten"** +2. Inspector → Quelle: `picture.board`, Quellen-ID: die UUID deines Public-Boards (copy aus `/picture/board/`-URL) +3. Layout: "Grid", Max 12 +4. **Im Editor:** Platzhalter-Text "Nicht aufgelöst. Quelle: picture.board …" +5. **Publish!** Während des Publishes wird der Embed aufgelöst: + - DevTools → Network → POST /publish hat einen `blob.pages[0].blocks[].props.resolved.items: [...]` +6. Public-Mode: Grid mit den Board-Images + +### Public-Gate verify + +1. Board auf **"nicht öffentlich"** flippen +2. Site erneut publishen +3. Public-Mode: Platz mit Error-Pill "Einbettung fehlgeschlagen: Board ist nicht öffentlich …" +4. Board wieder öffentlich, erneut publishen → wieder ok + +### Library-Embed (optional) + +1. In `/library` 2-3 Bücher/Filme anlegen, eines favorisieren (⭐) +2. ModuleEmbed mit `library.entries`, Filter `isFavorite=true` +3. Publish → Public zeigt nur Favoriten + +--- + +## Szenario 5 — Templates + AI (M5) + +### Template-Flow + +Schon in Szenario 1 gemacht wenn du nicht "Leer" gewählt hast. Teste alle vier: +- **Portfolio** — 4 Seiten, inkl. Gallery + Form +- **Link-Sammlung** — 1 Seite mit 6 CTAs +- **Event** — 3 Seiten inkl. RSVP-Formular +- **Leer** — 1 leere Seite + +**Erwartet:** +- Jedes Template setzt `navConfig.items` passend zu den Seiten +- Alle Blöcke haben frische UUIDs (nicht die Template-`localId`s) +- `parentBlockId`-Chains bleiben korrekt nach dem Clone + +Verify mit: +```bash +# Alle Site-UUIDs sollten eindeutig sein — keine Collisions mit früheren Template-Nutzungen +``` + +### AI-Tool-Flow + +Voraussetzung: Companion-Chat / Mission funktioniert lokal, mana-llm antwortet. + +1. `/ai-missions` → neue Mission: Objective `"Erstelle mir eine Portfolio-Website für meinen Freund Alex"` +2. Run Mission → warte auf Planner-Response +3. **Erwartet:** Proposals im Proposal-Inbox (falls `AiProposalInbox` gewired ist — siehe bekannte Limits) mit: + - `apply_website_template(templateId: 'portfolio', name: ..., slug: ...)` + - oder einzelne `create_website` / `add_website_block` Steps +4. Approve → Website taucht in `/website` auf + +**Bekannt:** Wenn `AiProposalInbox` UI noch nicht existiert (laut Plan), bleiben Proposals in `pendingProposals` Dexie-Table. Verify mit: +```javascript +// DevTools Console +(await db.pendingProposals.toArray()).filter(p => p.toolName?.startsWith('website')) +``` + +Foregroung-Execution (ohne Approval-UI) von `list_websites` in einem Chat klappt — das ist `defaultPolicy: 'auto'`. + +--- + +## Szenario 6 — Subdomain-Publishing (M6) + +**Lokal:** Subdomains auf `localhost` funktionieren nicht out-of-the-box — `mein-test.localhost` wird vom Browser nicht automatisch zu `localhost` aufgelöst. Drei Optionen: + +**A) `/etc/hosts` Eintrag** (einfach): +``` +127.0.0.1 mein-test.localhost +``` +Dann: `http://mein-test.localhost:5173/` sollte deine publizierte Site laden. + +**B) In Production (`mana.how`):** automatisch via Wildcard-DNS — Config ist Cloudflare-Sache. + +**C) Override via SvelteKit-Test-Param** (nur Code-Check): öffne DevTools → Network → resend das Request mit custom `Host: mein-test.mana.how` Header. Response-Body sollte der HTML der Public-Seite sein. + +**Erwartet:** +- `hooks.server.ts` rewritet `event.url.pathname` intern zu `/s/mein-test` +- URL im Browser bleibt auf `mein-test.mana.how` (kein 302-Redirect!) + +### App-Subdomain wins + +1. Versuche: Site mit Slug `todo` anzulegen → `InvalidSlugError` "reserved" +2. Auch wenn du durch die Validierung kämst: `todo.mana.how` ist in `APP_SUBDOMAINS`, Hook leitet auf `/todo` (Todo-Modul) weiter, nicht auf die Website. + +--- + +## Szenario 7 — Custom-Domain (M6) + +**Voraussetzung:** Account ist `founder` Tier. Nicht-Founder sieht "Eigene Domain" Section gar nicht oder bekommt 403 vom API. + +### Domain hinzufügen + +1. Editor → **⚙** → SiteSettingsDialog → ganz unten "Eigene Domain" +2. Input: `portfolio.example.com` → klick **"+ Domain"** +3. **Erwartet:** + - Row in Liste mit Pill `pending` + - DNS-Konfigurations-Box zeigt: + ``` + CNAME portfolio.example.com → custom.mana.how + TXT _mana-challenge.portfolio.example.com → <32-char-hex> + ``` + - Click-to-Copy funktioniert auf den Werten +4. Verify in DB: + ```bash + psql "$DATABASE_URL" -c "SELECT hostname, status, verification_token FROM website.custom_domains" + ``` + +### DNS-Verify (mit echter Domain) + +Wenn du eine echte Domain zur Hand hast: + +1. In deinem DNS-Provider: + - CNAME `portfolio.example.com` → `custom.mana.how` + - TXT `_mana-challenge.portfolio.example.com` → `` +2. DNS-Propagation abwarten (`dig @1.1.1.1 TXT _mana-challenge.portfolio.example.com` sollte den Token zeigen) +3. **"Verify"** klicken +4. **Erwartet:** Pill wechselt auf `verifying` (kurz) → `verified` (grün). `verified_at` in DB gesetzt. + +### DNS-Verify (Simulation mit /etc/hosts — funktioniert NICHT) + +Node's `dns.resolveTxt` nutzt System-DNS, nicht `/etc/hosts`. Der Verify-Call wird immer `ENOTFOUND` liefern für erfundene Hosts. Alternative: temporär `status` per SQL auf `verified` setzen: + +```bash +psql "$DATABASE_URL" -c "UPDATE website.custom_domains SET status='verified', verified_at=now() WHERE hostname='mein-test.local'" +``` + +Dann: `/etc/hosts`-Eintrag, Resolver-Cache im Hook leeren (60s TTL, oder Server restart), und `http://mein-test.local:5173/` → sollte deine Site laden. + +### Reserved-Hostname + +1. Versuche `mana.how` oder `api.mana.how` einzutragen → 400 "Ungültiger oder reservierter Hostname" +2. Versuche `*.mana.how` (deine Subdomain-Root) → Reserved + +### Invalid Tier + +1. Log out, einloggen als ein Nicht-Founder-User (`public` Tier) +2. Gehe zu gleicher Site (wenn in shared space) oder eigene → SettingsDialog → "Eigene Domain"-Section +3. Add → **Erwartet:** 403 vom API, UI zeigt "Forbidden" oder ähnliche Fehlermeldung + +--- + +## Szenario 8 — Analytics-Block (M7) + +1. Editor → Block **"Analytics"** → Inspector +2. Provider **"Plausible"**, Domain `deineseite.com` (beliebiger String, real muss nicht existieren) +3. Script-URL leer lassen (nutzt `https://plausible.io/js/script.js`) +4. Im Editor sichtbar als dezente Pill 📊 Analytics: plausible (deineseite.com) +5. **Publish!** +6. Public-Mode → View-Source → finde: + ```html + + ``` +7. Wechsel zu **"Umami"**, Site-Key `abc12345-1234-…`, self-hosted URL testen + +**A11y-Check:** Analytics-Block hat `aria-hidden="true"` im Edit-Mode (Meta-Pill) und emittiert nur ein `