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) <noreply@anthropic.com>
21 KiB
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 für das Design, docs/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:
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:
psql "$DATABASE_URL" -c "\dt website.*"
Erwartet: published_snapshots, submissions, custom_domains.
2. Dev-Stack starten
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-Problemeservices/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:
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
- In den Browser:
http://localhost:5173/website - Klick "+ Neue Website" → landet auf
/website/new(TemplatePicker) - Wähle Template "Leer" (oder "Portfolio" wenn du mehr Content willst)
- Name:
Mein Test, Slug wird automatisch zumein-test - Klick "Mit Leer starten"
Erwartet:
- Redirect zu
/website/<siteId>/edit/<homePageId> - 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 mitpath='/'
Schritt 2: Blöcke einfügen
- InsertPalette (unten links) → klick "Hero" → erscheint mittig, wird selektiert
- Inspector rechts: Titel ändern zu "Willkommen", Untertitel "Meine erste Seite"
- Palette → "Text" → klick in Vorschau um zu wählen → Inspector → Content tippen, zwei Absätze mit Leerzeile zwischen
- 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
- Oben PublishBar → "Veröffentlichen" klicken
- Pill wechselt von "Entwurf" → "Live"
- Link
/s/mein-testwird geklickbar
Erwartet:
- POST zu
/api/v1/website/sites/<id>/publish(DevTools → Network → 201) - Response hat
snapshotId,publishedAt,publicUrl: '/s/mein-test'
Schritt 4: Public-Render
- Link öffnen (oder
http://localhost:5173/s/mein-testdirekt) - 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-testmitCache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400 - Response-Header
Cache-Tag: site-<id>
Schritt 5: Editor + Draft-Ahead-Indicator
- Zurück in den Editor
- Hero-Titel ändern zu "Willkommen!"
- PublishBar zeigt Pill "Unveröffentlichte Änderungen"
- Öffne
/s/mein-testin anderem Tab → immer noch der alte Titel (erwartet, Draft ≠ Live) - PublishBar → "Änderungen veröffentlichen" → Reload des Public-Tabs → neuer Titel
Schritt 6: Rollback (M7-Polish)
- PublishBar → "Versionen" klicken → Dialog mit 2 Snapshots
- Obere Row = "Aktuell live" mit grünem Pill
- Untere Row (erste Publish) → "Wiederherstellen" klicken → bestätigen
- Dialog reload: neue aktuelle Row ist die alte
/s/mein-testreloaden → Titel ohne!
Szenario 2 — Block-Coverage (M3)
Image-Upload
- Neuer Block "Bild" → Inspector rechts → Dropzone
- Klicke/ziehe ein JPG oder PNG rein (< 25 MB, irgendein Testbild)
- Erwartet:
- POST an
/api/v1/media/uploadmitapp=website - Response
{ id }, URL wird automatisch inblock.props.urlgeschrieben - Vorschau zeigt das Bild
- POST an
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
- Block "Galerie" → Inspector → "+ Bilder hinzufügen" → mehrere Dateien gleichzeitig
- Alt-Text pro Bild eintragen (Pflicht für A11y — sollte Screenreader-Crawl später nicht stören)
- Layout/Columns/Lightbox-Toggle testen
- Im Editor: Gallery zeigt alle Bilder im Grid, Klick auf ein Bild wählt nur den ganzen Block (Editor-Mode)
- Public (
/s/mein-test): mitlightbox=true, Klick auf Bild → Fullscreen-Overlay mit Prev/Next (‹ ›) + Escape - Escape/Klick außerhalb schließt
Columns-Container
- Block "Spalten" → 2 oder 3 Columns
- Klick in eine Column-Drop-Zone ("Spalte 1")
- Insert-Palette nochmal öffnen, einen Text-Block einfügen — landet in der Column
- Erwartet: Text ist Child des Container-Blocks,
parentBlockIdin der Dexie-Row zeigt auf Columns-Block-ID,slotKey = 'col-0' - Public-Mode: responsive, < 720px stapeln sich die Columns untereinander (stackOnMobile=true Default)
Manuelle Verify:
# 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: <columns-id> und slotKey: 'col-0'.
Theme wechseln
- Header → ⚙-Button → SiteSettingsDialog
- Unter "Theme" auf eines der drei Presets klicken (Classic / Modern / Warm)
- Farb-Overrides testen: Color-Picker für Primary, Background, Foreground
- "Speichern" → Dialog schließt
- Public-Reload → neue Farben + Font angewandt (CSS-Variablen)
Szenario 3 — Forms (M4)
- Editor → Block "Formular" (kommt mit 3 Default-Fields: Name, E-Mail, Nachricht)
- Inspector: Feld hinzufügen — "Telefon" Typ
tel, optional - Titel und Description setzen, Submit-Label auf "Absenden" lassen
- Site neu publishen (wichtig: das Form-Schema wird im Snapshot eingefroren)
/s/mein-testöffnen → Formular sichtbar
Submit-Flow testen
- Felder ausfüllen (required: Name + E-Mail + Nachricht)
- Absenden → "Danke! Wir melden uns bald."
Erwartet:
- POST an
/s/mein-test/__submit/<blockId>(same-origin SvelteKit proxy) - Von dort Forward an
/api/v1/website/public/submit/mein-test/<blockId> - Response 201 mit
submissionId
Honeypot-Check
- In der gleichen Form-Instanz: DevTools → Elements → finde
<input name="honeypot">in der verborgenen.wb-form__honeypot-Wrapper - Temporär sichtbar machen (
display: block), Wert "spam-bot" eintragen, normal absenden - Erwartet: Success-Message erscheint (act-as-success-for-bots), aber nichts in der Submissions-Tabelle
Verify:
psql "$DATABASE_URL" -c "SELECT created_at, payload, status FROM website.submissions ORDER BY created_at DESC LIMIT 3"
Rate-Limit
- Submit-Endpoint 11× schnell in Folge (Script / Postman / Hand-Refresh)
- Ab der 11. Response: 429 "Rate limit überschritten"
Submissions-Inbox
- Im Editor zurück zu Site → Top-Level-Submenu hat (noch) keinen Link, also URL direkt:
/website/<siteId>/submissions - Erwartet: Deine Submissions (die echten, nicht die Bot-Spam-Try) als Liste
- 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).
- Zurück im Website-Editor → Block "Modul einbetten"
- Inspector → Quelle:
picture.board, Quellen-ID: die UUID deines Public-Boards (copy aus/picture/board/<id>-URL) - Layout: "Grid", Max 12
- Im Editor: Platzhalter-Text "Nicht aufgelöst. Quelle: picture.board …"
- Publish! Während des Publishes wird der Embed aufgelöst:
- DevTools → Network → POST /publish hat einen
blob.pages[0].blocks[].props.resolved.items: [...]
- DevTools → Network → POST /publish hat einen
- Public-Mode: Grid mit den Board-Images
Public-Gate verify
- Board auf "nicht öffentlich" flippen
- Site erneut publishen
- Public-Mode: Platz mit Error-Pill "Einbettung fehlgeschlagen: Board ist nicht öffentlich …"
- Board wieder öffentlich, erneut publishen → wieder ok
Library-Embed (optional)
- In
/library2-3 Bücher/Filme anlegen, eines favorisieren (⭐) - ModuleEmbed mit
library.entries, FilterisFavorite=true - 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.itemspassend zu den Seiten - Alle Blöcke haben frische UUIDs (nicht die Template-
localIds) parentBlockId-Chains bleiben korrekt nach dem Clone
Verify mit:
# 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.
/ai-missions→ neue Mission: Objective"Erstelle mir eine Portfolio-Website für meinen Freund Alex"- Run Mission → warte auf Planner-Response
- Erwartet: Proposals im Proposal-Inbox (falls
AiProposalInboxgewired ist — siehe bekannte Limits) mit:apply_website_template(templateId: 'portfolio', name: ..., slug: ...)- oder einzelne
create_website/add_website_blockSteps
- Approve → Website taucht in
/websiteauf
Bekannt: Wenn AiProposalInbox UI noch nicht existiert (laut Plan), bleiben Proposals in pendingProposals Dexie-Table. Verify mit:
// 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.tsrewritetevent.url.pathnameintern zu/s/mein-test- URL im Browser bleibt auf
mein-test.mana.how(kein 302-Redirect!)
App-Subdomain wins
- Versuche: Site mit Slug
todoanzulegen →InvalidSlugError"reserved" - Auch wenn du durch die Validierung kämst:
todo.mana.howist inAPP_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
- Editor → ⚙ → SiteSettingsDialog → ganz unten "Eigene Domain"
- Input:
portfolio.example.com→ klick "+ Domain" - 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
- Row in Liste mit Pill
- Verify in DB:
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:
- In deinem DNS-Provider:
- CNAME
portfolio.example.com→custom.mana.how - TXT
_mana-challenge.portfolio.example.com→<token>
- CNAME
- DNS-Propagation abwarten (
dig @1.1.1.1 TXT _mana-challenge.portfolio.example.comsollte den Token zeigen) - "Verify" klicken
- Erwartet: Pill wechselt auf
verifying(kurz) →verified(grün).verified_atin 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:
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
- Versuche
mana.howoderapi.mana.howeinzutragen → 400 "Ungültiger oder reservierter Hostname" - Versuche
*.mana.how(deine Subdomain-Root) → Reserved
Invalid Tier
- Log out, einloggen als ein Nicht-Founder-User (
publicTier) - Gehe zu gleicher Site (wenn in shared space) oder eigene → SettingsDialog → "Eigene Domain"-Section
- Add → Erwartet: 403 vom API, UI zeigt "Forbidden" oder ähnliche Fehlermeldung
Szenario 8 — Analytics-Block (M7)
- Editor → Block "Analytics" → Inspector
- Provider "Plausible", Domain
deineseite.com(beliebiger String, real muss nicht existieren) - Script-URL leer lassen (nutzt
https://plausible.io/js/script.js) - Im Editor sichtbar als dezente Pill 📊 Analytics: plausible (deineseite.com)
- Publish!
- Public-Mode → View-Source → finde:
<script defer data-domain="deineseite.com" src="https://plausible.io/js/script.js"></script> - 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 <script>-Tag im Public-Mode. Keine visuellen Artefakte für Screenreader.
Privacy-Check: Keine Cookies im document.cookie nach Page-Load. Plausible + Umami sind cookieless.
Szenario 9 — Metrics + GC (M7)
Metrics-Endpoint
curl -s http://localhost:3060/metrics | grep website
Erwartet: Zeilen wie:
mana_api_website_publish_total{service="mana-api",result="success"} 3
mana_api_website_publish_duration_seconds_bucket{...,le="0.1"} 2
mana_api_website_submissions_total{service="mana-api",result="received"} 1
mana_api_website_submissions_total{service="mana-api",result="spam"} 1
mana_api_website_host_resolve_total{service="mana-api",result="miss"} 42
mana_api_website_public_reads_total{service="mana-api",result="hit"} 17
mana_api_website_public_read_age_seconds_bucket{...,le="60"} 5
Je nachdem wie viel du in den Szenarien vorher gemacht hast. Counts nach jedem Publish/Submit neu checken — die Werte müssen steigen.
Orphan-Asset-GC
MANA_SERVICE_KEY=dev-service-key bun apps/api/scripts/gc-website-assets.ts
Erwartet:
- Log: "scanning published_snapshots + submissions for media references"
- Log: "referenced mediaIds: N"
- Log: "media items in scope: M"
- Report-Datei:
/tmp/gc-website-assets-<ts>.json
Wenn MANA_SERVICE_KEY nicht gesetzt: Script warnt und zeigt 0 Media-Items. Erwartet-Verhalten, kein Bug.
Zum Testen eines Orphans: uploade ein Bild über mana-media (DevTools → uploadImage direkt), setze es NICHT in einen Block. Script sollte es nach 30d Grace als Orphan listen — für den Test kannst du den GRACE_MS-Konstant im Script temporär auf 0 setzen.
Szenario 10 — Edge-Cases + Sicherheit
Space-Scope-Check
- Zwei Personal-Spaces (User A + User B), beide haben eine Website mit Slug
mein-test - User A's Slug
mein-testgeht live - User B versucht zu publishen mit gleichem Slug → 409 "Slug is already taken"
- User A unpublished → User B kann jetzt publishen
SSRF / Host-Header-Injection
- DevTools → resend request mit
Host: internal-metrics.local→ Hook ruft/resolve-host?host=internal-metrics.local→ 404 → kein Rewrite, SvelteKit sieht das als normale Route, zeigt 404
Form-Validation an Server-Seite
-
Manuell POST zum submit-Endpoint mit einem extra Feld das nicht im Block deklariert ist:
curl -X POST http://localhost:5173/s/mein-test/__submit/<blockId> \ -H 'content-type: application/json' \ -d '{"name":"A","email":"a@b.c","message":"hi","admin":true}' -
Erwartet: 201, aber
adminwird ignoriert (nur deklarierte Fields gehen inpayload) -
Verify:
SELECT payload FROM website.submissions ORDER BY created_at DESC LIMIT 1—adminsollte nicht drin sein. -
Missing required field:
curl -X POST … -d '{"name":"","email":"a@b.c","message":"hi"}'Erwartet: 400 "Pflichtfeld Name fehlt"
Dexie-v37-Upgrade aus existierender DB
- Browser-Profile mit bestehender Mana-DB (v36 oder früher)
- App-Reload → Dexie upgraded auto auf v37 → v41 (wenn wardrobe schon drin)
- DevTools → Application → IndexedDB →
mana→ Version zeigt 41 (oder höher) - Öffne
/website→ funktioniert ohne Fehler
Bekannte Limits / was NICHT getestet wird
- Cloudflare-SaaS-Hostname-Provisioning —
cloudflareOnboard()ist gestubbed. Live-TLS für Custom-Domains braucht CF-Credentials in prod-env. Details:docs/plans/website-builder.md§M6. - Target-Delivery für Submissions — Formular-Submissions landen in
website.submissions(Inbox). Forwarding zucontacts.createodermana-notifyist M4.x, braucht Server-side Tool-Handler-Infrastructure. - AiProposalInbox UI — Component existiert nicht im Repo; Proposal-Staging funktioniert, aber die Inbox-View für Website-Blocks muss jemand bauen.
- Multi-Member-Co-Editing — Shared-Spaces-RLS erlaubt mehreren Members, dieselbe Site zu editieren. mana-sync field-level LWW kümmert sich um Konflikte pro Feld. Nicht im Smoke-Scope, aber architektonisch vorgesehen.
- Drag-Drop-Block-Reorder — aktuell nur Pfeil-Buttons im Inspector. @dnd-kit-Integration ist Backlog.
Verify-Checkliste (Kurzform)
Nach allen Szenarien solltest du folgendes beobachtet haben:
- Site erstellen + Blöcke einfügen + Live-Preview updated
- Publish →
/s/<slug>serviert SSR-HTML - Draft-ahead-Pill zeigt wenn Editor ≠ Live
- Rollback-Dialog restored ältere Snapshot
- Image-Upload via mana-media (25 MB Limit greift)
- Gallery-Lightbox im Public-Mode mit Escape + Pfeiltasten
- Columns-Container: Child-Blöcke haben korrekten
parentBlockId + slotKey - Theme-Switch propagiert CSS-Variablen in Public-Mode
- Form-Submit landet in Inbox + 429 nach 10 Submits
- Honeypot blockt Bot-Submits silent
- ModuleEmbed: Public-Flag wird enforced (private Board → Error-Pill)
- Template-Clone hat frische UUIDs + intakte parentBlockId-Chain
- AI-Tool
list_websitesim Chat liefert echte Daten - Subdomain-Rewrite funktioniert (via /etc/hosts oder Host-Header)
- Custom-Domain Add + DNS-Verify-UI + 403 für Non-Founder
- Reserved-Hostnames werden abgelehnt
- Analytics-Block emittiert genau ein
<script>-Tag im public-Mode /metrics-Endpoint hat Counter-Values für alle getesteten Operations- GC-Script läuft und schreibt JSON-Report
Wenn alle Punkte grün sind, Eintrag aus docs/optimizable/manual-test-backlog.md löschen.