Zum Hauptinhalt springen

Website mit v0 und Cockpit — nur Schritte

Neu? Zuerst die Checkliste

★ Start hierWebsite mit v0 (Kurzpfad) (8 Schritte). Diese Seite ist zum Kopieren der Instructions A–J — danach hierher.

Erst lesen? Die Einstiegs-Anleitung A–Z für Redaktion (ohne Fachchinesisch): v0-Website A–Z (für Redaktion).

Hier geht es nicht um Technik, sondern nur: was du wann klickst und was du wohin kopierst.

Ziel Cockpit + v0: Redaktion baut in natürlicher Sprache performante, ansprechende, moderne Center-Websites mit Daten aus dem cockpitOS. Das Dashboard liefert stabile, öffentliche Daten (Branding, Inhalte, Hinweise unter apiHints). In v0 gestaltest du beliebige Layouts — Grids, Karussells, Landingpages — und bindest die Endpunkte per fetch (meist Server Components). Nach Schritt 2 (public-visitor-surface) stehen dir u. a. fertige URL-Pfade in data.apiHints (inkl. Hot Picks, Aktuelles-Bundle, DOOH, …).

Neu in v2 (gegenüber älteren Instruction-Kopien):

  • Recht: Impressum/Datenschutz als Pflichtseiten (Teil D), DSGVO-Checkbox im Kontaktformular (Teil A), Cookie-Consent-Härtung + next/font statt Google-CDN (Teil C), Go-Live erst wenn Rechtsseiten befüllt (Teil J)
  • Barrierefreiheit (BFSG): eigener Pflichtblock in Teil E, Plan-Alternative als Liste (Teil F), Kiosk-Idle-Reset + Terminal-A11y (Teil H), Mobile-A11y (Teil I)
  • SEO: robots/sitemap/Metadata/JSON-LD ab erstem Deploy (Teil E), Preview-noindex-Umschaltung + Canonical auf Custom Domain (Teil J), pro Domain bei Multi-Center (Teil G)
  • Design/Anti-Slop: Design-DNA in Alltagssprache, Variation pro Center, Signature-Details, keine Emojis im UI, Font-Blacklist (Teil C) — Qualitäts-Check (Impeccable) macht v0 von selbst, Redaktion sagt nur z. B. „bitte prüfen"
  • Qualität: Go-Live-Checkliste (Teil J) — v0 bestätigt intern, dass der Check durch ist
  • Shops: Einzelshop vs. Kette/Filiale (Teil D) — lokale Mieter nicht automatisch als „Kette mit 1 Filiale“ anlegen

v0-Limits (Stand Produkt): max. 10 Custom Instructions, je max. 5000 Zeichen (nur Text im grauen Kasten, ohne Markdown). Vor dem Einfügen Zeichen zählen — zu lang → nur Docusaurus/Chat, nicht als Instruction. Zeichenzahlen stehen unter jedem Kasten.

10 Slots (alphabetisch in v0 — A … J):

SlotInstructionWann
1Cockpit Aimmer (Routen + Formulare)
2Cockpit Bimmer (JSON/API + Code-Struktur)
3Cockpit Cimmer (Medien, Chat, Design-DNA)
4Cockpit Dimmer (Workflow + Pflichtseiten)
5Cockpit Eimmer (Performance, A11y, SEO, Resilienz)
6Cockpit FCenterplan / Wegfindung
7Cockpit Gnur Multi-Center (sonst frei)
8Cockpit HSignage/Kiosk (mit I)
9Cockpit ICompanion (mit H)
10Cockpit Jnach erstem Vercel-Deploy + Go-Live

Nicht als Instruction: Teil F2 (3D-Feintuning), der Centerplan-Chat-Prompt und der Chatbot-Verdrahtung-Chat-Prompt (einmalig in v0-Chat nach Neu-Einspielen) → spart Slots bei vollem Set A–J.

Kern Website: A + B + C + D + E (+ J nach Go-Live) = 6 Instructions; F/G/H/I je nach Projekt.

Anderer Weg? Neues Design nur als ZIP ins CockpitTemplates.


1. Kurz vorbereiten

  • v0 öffnen (v0.dev).
  • Im Cockpit den Slug deines Centers aufschreiben (kurzer Name).

2. Instructions in v0 anlegen (Kern + optional)

cockpitOS ist die einzige Source of Truth

Shops, News, Events, Angebote — diese Daten gehören ins cockpitOS, nicht nur als Dummy-Code in v0. Der Teil D-Text sorgt dafür, dass der KI-Assistent den richtigen Workflow einhält.

  1. In v0: Instructions öffnen.
  2. Alphabetisch A → J anlegen (Cockpit ACockpit J). Je Teil den Kasten unten (Reihenfolge A–J auf dieser Seite) kopieren → einfügen → speichern.
  3. Cockpit D heißt in v0 Cockpit Workflow (Pflicht).
  4. Cockpit J erst nach erstem Vercel-Deploy (enthält Vercel-Env Team Shared vs. Projekt).
  5. Optional: F (Plan), G (Multi-Center), H+I (Signage). F2 nur Chat, nicht als 11. Instruction.
  6. Nach v2-Umstellung: Teil A–J in bestehenden v0-Projekten ersetzen (nicht nur anhängen) — besonders A, C, D, E, J.

3. Center eintragen (Teil A und ggf. G)

Einzel-Center: Im Text von Teil A steht HIER_SLUG_EINTRAGEN — dort den Center-Slug einsetzen (für v0-Preview). Die API-Basis ist bereits https://dashboard.cockpit-os.de (Staging nur wenn die IT eine andere Basis vorgibt: dann die erste Zeile in Teil A manuell anpassen).

Multi-Center (mehrere Center, ein Layout): Teil G einschalten. In Teil A den Slug nur als Preview-Beispiel lassen oder ein beliebiges Referenz-Center eintragen — live entscheidet die Domain (by-domain), nicht der Slug in A. In v0 beim Start schreiben: „Multi-Center-Layout — gleiches Design für alle Center mit demselben websiteTemplate; Center zur Laufzeit über Host auflösen.“


4. Website in v0 beschreiben

  • Screenshots reinziehen oder schreiben, z. B.: Helle Startseite, große Bilder, Shops als Karten.
  • Multi-Center: Ein Layout für alle Center mit demselben websiteTemplate; Inhalte pro Domain aus Cockpit; kein festes centerId.
  • Centerplan 3D: Instruction F; bei Problemen F2 in den Chat„Baue 2D/3D-Centerplan nach Teil F2 — mapSvg mit SVGLoader, nicht mapImage.“
  • Signage / Kiosk + NOW!: Instructions H + I; im Chat: „Kiosk Terminal-Style (Teil H): Plan 75%+, kompakter Header, Floating Pills, schmale Toolbar — plus Companion (Teil I).“
  • Wenn Chat gewünscht: Chat unten rechts, vorher Zustimmungsfenster.

5. Fertig an IT geben

  • Was v0 ausgibt (ZIP oder Link) weitergeben.
  • Zwei Sätze mitgeben:
    1. Bitte auf Vercel NEXT_PUBLIC_DASHBOARD_URL setzen — gleiche Adresse wie Teil A.
    2. Bitte Deploy-Hook ans Cockpit (Anleitung A–Z Abschnitt F3 + Instruction Teil J).

Hosting: Einzel-Center → oft eigenes Repo + Vercel-Projekt (F2). Multi-Center (shared layout)ein Repo + ein Vercel-Projekt mit allen Center-Domains (F2b).


IDs: Center vs. Etage (häufiger Fehler)

Die URL im Cockpit …/dashboard/centerplaene/{UUID}/edit enthält die Etagen-ID (floorId), nicht die Center-ID. Für die öffentliche API (…/wayfinding/floors?centerId=, …/shops, …) immer centerId aus GET …/by-slug/{slug}id verwenden. Sonst sind floors leer oder ohne mapLocations — dann wirkt es wie „keine Mappings“.

Wenn etwas fehlt

  • Keine Shops / leer: Slug stimmt? Website am Center im Cockpit aktiv?
  • Keine Bilder: IT fragen.
  • Chat fehlt: Im Cockpit Chatbot eingeschaltet?
  • DOOH leer / playlist null: Playlist im Cockpit aktiv und im Datumsfenster? Slug in der URL exakt wie in DOOH? Center-Website muss aktiv sein für …/dooh/public/….
  • SVG-Shop-Klick / shop-56: Verknüpfung nur über GET …/wayfinding/floorsmapLocations (svgId + shop), nicht über die reine Shops-Liste.
  • Hybrid-Plan (Bild im Plan): Der Plan ist ein schönes Bild mit Klickflächen darüber — ideal für ansprechende Gestaltung (z. B. aus ChatGPT). Tipp: Bild groß und scharf hochladen (lange Kante eher 2000–3000 px oder mehr), sonst wirkt starkes Zoomen weich. Für Shop-Detail-Ausschnitte im Cockpit die Zuweisungen wie gewohnt pflegen. Mehr: Hybrid-Centerplan · Plan mit KI erzeugen: KI-Masterprompt (DE/EN).
  • Wegfindung: Entweder POST …/routing mit startWaypointId (aus …/entrances) und endLocationId (= mapLocations[].id) — oder Pfad aus mapSvg/#routes selbst berechnen; #routes nicht als sichtbare Mall-Linien für Besucher übernehmen.
  • Keine mapLocations / leere floors: centerId wirklich die Center-UUID aus by-slug? Nicht die UUID aus der Centerplan-Edit-URL (das ist floorId).
  • **mapLocations leer, aber _count.mapLocations > 0:** Zuordnungen existieren in der DB; die öffentliche API liefert nur **aktive** Shops/Filialen/Services/Büros. Bleibt mapLocationsleer bei hohem_count, sind die verknüpften Einträge vermutlich **archiviert**. Klick-Zuordnung: mapLocations[].svgId+mapLocations[].shop.id` (nicht die Shops-Liste).
  • Nur eine Etage / OG fehlt in v0: API liefert oft beide — MCP-Chat kürzt große mapSvg-Antworten. Zuerst omitMapSvg=true oder cockpit_center_context mit floors_summary; dann floorNumber=1 oder Server-Fetch.
  • 3D „kaputt“ / v0 will nur 2D: floors[].mapSvg prüfen (String mit <svg und shop-?). 3D aus mapSvg mit SVGLoader — nicht aus mapImage. Instruction Teil F (Abschnitt „V0-Typische Fehler“) neu einspielen.
  • Shop-Klick öffnet falsche/fremde Website: v0 muss eigene Detail-Routen nutzen (/shops/{slug|id}) — siehe Teil F „V0-Navigation“. Cockpit liefert nur shop.id, keine v0-URLs.
  • mapLocations leer trotz ?includeMapLocations=…: Diese Route wertet nur centerId aus — Zusatz-Parameter werden ignoriert; mapLocations sind immer enthalten, wenn sie in der DB existieren und veröffentlicht sind. Leere Arrays = keine aktiven Mappings oder falsche Center-ID.

v0-Zeichenlimit: Pro Instruction max. 5000 Zeichen (innerer Text der Kästen „Teil A“ bis „Teil J“). Vor dem Speichern zählen.

SEO/GEO ohne neuen Instruction-Slot: MCP cockpit_v0_geo_standards (SMG-Audit-kompatibel). Pflicht: anwendbare p0Blockers im Code. ≥80 % GEO nur mit Redaktions-Fakten Park/ÖPNV — sonst ~65–78 % realistisch.

WoZeile zum Einfügen
Teil J (Go-Live)Vor Go-Live: cockpit_v0_geo_standards — alle p0Blockers implementieren, selfVerification abhaken.
Teil D (Fallback)- GEO Pflicht: cockpit_v0_geo_standards → p0Blockers im Code (JSON-LD, Sitemap, Shop-Links)

Nach MCP-Deploy reicht für v0 oft auch: cockpit_center_project_init aufrufen — Schritt 7 verweist auf dasselbe Tool.

Teil A — nur diesen Kasten in Instruction „Cockpit A“

cockpitOS — Teil A (Routen & Ablauf)
API-Basis: https://dashboard.cockpit-os.de (HTTPS, kein Slash am Ende). Slug-Platzhalter: HIER_SLUG_EINTRAGEN

VERCEL ENV (Redaktion: nichts manuell; v0 scaffoldet, IT einmal Team Shared):
- Team Shared (IT einmal, jedes Projekt verlinken + Redeploy): COCKPIT_DASHBOARD_URL, REVALIDATION_SECRET, COCKPIT_FRONTEND_CHANNEL=website. v0 erzeugt KEIN neues Secret wenn Shared verlinkt.
- Nur dieses Projekt (v0 setzt): NEXT_PUBLIC_DASHBOARD_URL (= API-Basis oben), COCKPIT_REGISTER_TOKEN (frt_…), COCKPIT_CENTER_SLUG (= HIER_SLUG_EINTRAGEN). REVALIDATION_SECRET + Register-Route: Teil J.
- Gleicher Key auf Projekt- und Team-Ebene: Projekt-Env gewinnt.

Next.js App Router, öffentliche Site: Server Components wo möglich; alle URLs mit dieser Basis.

WARNUNG UUID: …/dashboard/centerplaene/XXXX/edit → floorId, NICHT centerId. Öffentlich immer centerId aus GET …/centers/by-slug/{slug} → Feld id.

Ablauf:
1) GET https://dashboard.cockpit-os.de/api/centers/by-slug/HIER_SLUG_EINTRAGEN → centerId (= id). Alternativ …/api/centers/by-domain?domain={hostname}
2) GET …/api/centers/{centerId}/public-visitor-surface (Branding, Öffnungszeiten, templatePublicContent, Chatbot-UI, Tracking, apiHints)
3) GET …/api/centers/by-slug/HIER_SLUG_EINTRAGEN/theme-config

4) Inhalte ({centerId} einsetzen; … = gleiche Basis):
- Shops …/api/centers/{centerId}/shops?publicWebsite=true&status=Aktiv&limit=5000&offset=0 — optional includeWayfindingLinkages=true (svgId-Hints)
- Kategorien …/website-categories?status=Aktiv&includeGlobal=true&minShopCount=1
- Inhaltskategorien …/content-categories?centerId=CENTER_ID&type=news&includeGlobal=true (News/Events/Angebote, nicht Shop-Kategorien)
- Category-Themes …/category-themes-for-website
- News …/news?published=true (Zeitfenster wie Bundle; Filter contentCategorySlug=…; Stele: forSignage=true; forToGo=true optional; ohne published nur Legacy)
- Bundle …/aktuelles-bundle (News+Events+Offers+Jobs zusammen)
- Hot Picks …/hotpicks (≠ homepage-tiles) | Homepage-Kacheln …/homepage-tiles
- Baustellen …/news?constructionDiary=true
- Einzel-Angebot …/offers?offerId={uuid|slug}
- Events …/events; Offers/Jobs …/offers | …/jobs (Publish-Fenster wie Bundle)
- Services …/services?publicWebsite=true&limit=5000
- Büros …/api/offices?centerId={centerId}&status=Aktiv
- CMS …/page-content (pageType filtern; auch impressum/datenschutz: Teil D)
- Template-Reiter (Hero, Footer, Optionen): public-visitor-surface → data.templatePublicContent.content — NICHT website-config (401)
- Umami nur über Schritt 2 → data.tracking (kein /public-config erfinden)
- Centerplan …/api/wayfinding/centerplan?centerId={centerId}
- Floors …/api/wayfinding/floors?centerId={centerId} (mapLocations dabei; optional omitMapSvg=true, floorNumber=0|1|…)
- Plan: svgId/Treffer über floors[].mapLocations; Shop.floor oft leer
- entrances …/entrances → id = startWaypointId
- Routing POST …/api/wayfinding/routing — endLocationId = mapLocations[].id (UUID), nie svgId
- mapSvg #routes = Netz für Pfadberechnung, nie Original-Linien zeigen
- Chat POST …/api/ai/visitor-chatbot (centerId im Body)
- DOOH: relative Pfade in data.apiHints (doohPublic…)

5) Kontakt- & Vermietungsformular (E-Mail/Resend nur im Dashboard):
- Layout/Formular in v0; Versand NICHT direkt ans Dashboard vom Browser (kein CORS).
- Pflicht: eigene Next.js-Routen: app/api/kontakt/route.ts und ggf. app/api/vermietung/route.ts.
- Frontend: fetch("/api/kontakt", { method:"POST", body: JSON.stringify({ centerId, name, email, message, phone?, subject?, formKey?, recipientEmail? }) }) — centerId = UUID aus Schritt 1.
- Server-Route proxyt serverseitig: POST …/api/centers/{centerId}/send-contact-inquiry bzw. …/send-rental-inquiry (zusätzlich company?, spaceInterest?).
- Pflichtfelder: name, email, message. Antwort: { success:true, recipient? } oder { success:false, error }.
- DSGVO PFLICHT: Checkbox „Ich habe die Datenschutzerklärung gelesen" mit Link auf /datenschutz, NICHT vorausgewählt; ohne Haken kein Submit. Honeypot-Feld gegen Spam. Erfolg/Fehler als Text mit aria-live melden, nie nur über Farbe.
- Anderer Empfänger als Standard: NICHT beliebig im Body; zuerst im Cockpit freigeben: templateContent.formRecipients.{formKey} (MCP update_center_website_config), dann Body formKey. recipientEmail nur wenn bereits in Cockpit-Allowlist. customRecipient = Alias.
- Kein RESEND_API_KEY in v0/Vercel. Standard-Empfänger: Center-Stammdaten email oder Website-Reiter Kontakt.

Zeichen: 4486 / 5000

Teil B — nur diesen Kasten in Instruction „Cockpit B“

cockpitOS — Teil B (Antworten, Öffnungszeiten, Code-Struktur)

JSON-Formen:
- GET by-slug: flaches Objekt OHNE success/data — id, name, slug, logo, logoUrl (oft gleich), baseColor, secondaryColor.
- GET public-visitor-surface: { success:true, data: { center, seo, chatbot, templatePublicContent, v0Integration, centerplan, features, apiHints, … } }. Branding + Center-Öffnungszeiten: data.center.openingHours, openingHoursNote, specialDays. Template (Hero/Footer): data.templatePublicContent.content (rootKey z.B. rgw). Chat-UI: data.chatbot. apiHints: pageContentGet, homepageTilesGet, aktuellesBundleGet, hotPicksGet, …
- Hot Picks: GET …/hotpicks → { success, hotPicks: [ { id, contentType, type, title, description, image, priority, position, startDate, endDate, channels, content, … } ] }. Hot Pick = Cockpit-Highlight mit Verweis auf Shop/Event/News/Angebot; Layout (Karten, Slider, Liste) = Frontend. Nicht verwechseln: …/homepage-tiles = Startseiten-Kacheln, andere Struktur/Zweck.
- DOOH public: GET …/dooh/public/playlists → { success, playlists }. GET …/playlist?slug= → { success, playlist | null } (null = inaktiv/außerhalb Gültigkeit). Items: type VIDEO|IMAGE|SLIDESHOW|IFRAME, payload URL(s), durationSeconds (0 bei Video oft volle Länge im Client).
- GET shops: { success, data: […], total, pagination }. Medien: logo, coverImage. floor/location oft null — Karte: Floors-API oder ?includeWayfindingLinkages=true → wayfindingLinkages[] (svgId, floorName, mapLocationId).
- GET wayfinding/floors?centerId=: { success, floors: […] }. centerId = Center-UUID (by-slug → id), NICHT UUID aus …/centerplaene/…/edit (floorId). Pro Etage: mapSvg (String!), mapImage (CDN), shopViewBoxes (optional JSON svgId→viewBox), mapLocations[], optional shopSvgIdsWithOffers. mapLocations: svgId (z.B. shop-56), type, shop/shopLocation/service/office mit UUID + Anzeigedaten. SVG-Klick: mapLocation per svgId → shop.id für Detail-Link. Fehlendes svgId/Shop = Cockpit-Zuordnung fehlt, nicht per Namen erraten.
- GET entrances: { success, entrances: [ { id, floorId, position, floor, … } ] } — id = startWaypointId für POST routing.
- POST …/wayfinding/routing: Route über DB-Waypoints; Ziel endLocationId = mapLocations[].id (UUID), nicht svgId. Ohne Waypoints im Cockpit leer → SVG-#routes oder „Weg nicht verfügbar".
- SVG #routes: Datenlayer, Original-Linien nie 1:1 zeigen; Besucher sehen nur berechnete Route als Overlay. Referenz: @mall-os/wayfinding (parseSvgRoutes, findPathThroughNetwork).
- Andere Reads: oft { success, data } — bei Unsicherheit Struktur prüfen.

Öffnungszeiten (Center — primäre Quelle):
- GET public-visitor-surface → data.center.openingHours (nicht by-slug, nicht page-content für Header/Widget).
- Zusätzlich: data.center.openingHoursNote, data.center.specialDays (Feiertage/Sonderzeiten).
- openingHours: null | Freitext-String | Objekt. Dashboard-Format bevorzugt:
- regularHours: { monday: { open, close }, … sunday } — ggf. dayOpen24Hours, open24Hours
- flach: weekdays, saturday, sunday (Strings)
- Legacy: monday…sunday mit { hours: "10:00 - 20:00" } oder { open: "closed", close: "closed" }
- specialDays: [ { date: "YYYY-MM-DD", label?, hours?: { open, close } | null } ] — DB-Spalte vor openingHours.specialDays.
- Shops/Services: eigenes openingHours in GET …/shops bzw. …/services.
- UI: nie Roh-JSON — Tabelle/Liste, deutsche Wochentage, „Heute geöffnet bis …" wo möglich. String: JSON.parse try/catch, sonst Fallback.
- page-content (pageType oeffnungszeiten) nur redaktionelle CMS-Seite, kein Ersatz für Center-Stammdaten.

Chatbot-Fehler: 429 = Cockpit-Rate-Limit; 502 openai_rate_limit = OpenAI-Quota (IT); 403 = Consent fehlt; 502 openai_error (400) = oft falsches Modell im Webseiten-Tab → GPT-4o mini.

Öffentliche GETs: CORS *. POST page-content ohne Auth nicht von fremden Sites. Chatbot-/Routing-POST kann 429 — Nutzer freundlich informieren.
Fehler: if (!res.ok) → leeres Array/null/Defaults — nie Error-Objekt in JSX (React #418). Kein GET website-config/agencyos ohne Auth.

Fehlerseiten (Pflicht): app/not-found.tsx + error.tsx im Center-Branding (Logo, Farben, Link zu Start/Shops/Öffnungszeiten) — keine Next/Vercel-Defaults. loading.tsx mit Skeletons statt Spinner-Vollbild.

Code-Struktur (Pflicht):
- App Router: schlanke page.tsx; Daten in Server Components; use client nur kleine UI-Blöcke.
- components/ nach Feature (shops, layout, wayfinding …), ggf. ui/. Keine 1000+ Zeilen/Datei — Logik, View, API trennen.
- lib/api/cockpit (eine Basis-URL), eine Funktion/Ressource + Typen (lib/types/…); Fehler zentral behandeln.
- lib/utils/, lib/media für CDN (Teil C); Konstanten zentral. layout + Header/Footer getrennt.
- Ziel: wartbar, auffindbare Fehler, kein Monolith.

Zeichen: 4731 / 5000

Teil C — nur diesen Kasten in Instruction „Cockpit C“

cockpitOS — Teil C (Medien, Chatbot, Design-DNA)

Bilder/Medien: relative API-Pfade (/uploads/…, centers/…, global/…) mit CDN-Basis prefixen: https://cockpitos.b-cdn.net. Env optional: NEXT_PUBLIC_BUNNY_CDN_URL.

Besucher-Chatbot:
- Zuerst public-visitor-surface → data.chatbot. enabled=false → kein Widget.
- Consent-Texte: data.visitorPrivacy (chatbotConsentTitle/-Description, privacyPolicyUrl).
- Consent → POST /api/ai/visitor-chatbot (centerId, client=website, chatConsentAccepted:true, sessionId, messages). Kein OpenAI im Client.
- pageContext bei jedem POST (Teil D): page, entity, localContent, siteKnowledge.
- ANTWORT: data.contentBlocks (Karten: shops/offers/…) + items + actions — Verdrahtung: Chat-Prompt „Chatbot-Verdrahtung“.

Auth/Grenzen: kein website-config/agencyos ohne Auth (401); keine Secrets im Frontend. Template-Inhalte: public-visitor-surface → data.templatePublicContent.content + data.v0Integration. Fallback bei !res.ok (Teil B). Kontakt/Vermietung: Proxy (Teil A).

Umami/Consent + next/font: Pflicht, Teil E.

DESIGN-DNA (Pflicht, VOR dem ersten Screen — kein AI-Slop, kein Einheitslook):
1) Aus Center-Daten + Redaktion 3 Charakter-Adjektive festlegen (z.B. „urban, industriell, jung" / „premium, ruhig, hell"). JEDE Designentscheidung (Font, Radius, Bildsprache, Weißraum, Motion) folgt dieser DNA.
SPRACHE MIT DER REDAKTION: Alltagssprache, keine Fachbegriffe (Archetyp/Hairline intern). 2–3 fertige Vorschläge je ein Satz — Redaktion wählt, v0 übersetzt. Nie offene Fachfragen.
2) Tokens aus public-visitor-surface (baseColor, accentColor, secondaryColor, fontFamily, templatePublicContent) als CSS-Variablen. EIN dominanter Akzent, Rest neutral; Tints/Shades aus baseColor ableiten statt neuer Buntfarben. Radius bewusst (oft 4–8px). Hell/Dunkel nur mit Grund.
3) Typografie: Display + Body als unverwechselbares Pairing PRO CENTER via next/font. Als Default verboten: Inter, Roboto, Poppins, Montserrat, Space Grotesk, Arial, System-UI. Je nach DNA z.B. Fraunces, Instrument Serif, Bricolage Grotesque, Sora, Newsreader, Archivo. Body 16–18px, line-height ≥1.5. Mono nur für Daten (Zeiten, Etage, Shop-Nr.).
4) Layout-Archetyp intern wählen: editorial/Magazin | asymmetrisches Raster | Split-Hero | typo-dominant | bild-dominant.
5) Hero: asymmetrisch/links, kein zentrierter Gradient-Headline-Block mit zwei Buttons + drei Feature-Cards. Signature statt Stock (s.u.).
6) Sektionen: Alternativen zum 2x2-Card-Grid (nummerierte Liste mit Hairlines, Bild-Text-Wechsel) — Inhalte aus Cockpit-API.
7) Rest screen-für-screen im Chat — nie alles in einem Prompt (Drift zu Defaults).

VARIATION (Pflicht — gegen Einheitsbrei im Portfolio): Font-Paar, Archetyp und Details nie aus Gewohnheit wählen, sondern aus Center-Eigenschaften ableiten: baseColor-Ton (warm → Serif-Display prüfen, kühl/kräftig → charaktervolle Grotesk), Mieter-Mix (Mode → editorial, Nahversorgung → funktional-warm, Gastro/Freizeit → plakativ). Zwei Center nie mit gleicher Kombination (Font-Paar+Archetyp+Details).

DATEN VOR LAYOUT: Inhalte zählen (Shops, Events 30 Tage, Angebote, Gastro-Anteil), Startseiten-Sektionen danach priorisieren — keine Standard-Reihenfolge. <30 Shops: kuratierte Darstellung statt Filter-Grid. Kaum Events: Sektion weglassen statt leer. Starker Foodcourt: eigene Gastro-Sektion.

SIGNATURE-DETAILS (genau 2 pro Center): z.B. übergroße Öffnungszeiten-Typo; Shop-Namen-Laufband; Duotone-Bilder in baseColor; Trenner aus Logo-Geometrie; Hover als Farbfläche statt Schatten. 0 = generisch, >2 = unruhig. Bilder: EIN Treatment (Filter, Masken, Ratios) — nie Roh-Fotos gemischt.

MUT-REGEL: mind. eine markante Entscheidung pro Site (dunkles Theme, Serif-Display, H1 bis clamp 6–8rem, asymmetrischer Header) — der Redaktion als einfache A/B-Wahl anbieten.

VERBOTEN (Anti-Slop): Gradient-Text; Icon-im-Farbkreis; Card-Grid mit Schatten als Default; bunte Status-Pills; Stock-Illustrationen; Violett-Blau-Verlauf; Glassmorphism; Floskeln; Hover/Scroll-Reveals ohne Grund (Teil E).
STATTDESSEN: klare Hierarchie mit echten Cockpit-Daten; leere Zustände = klare CTA; Status dezent (Rahmen-Label, wenige Farben). Motion: Teil E.

ICONS: NIE Emojis im UI (auch nicht in Buttons, Listen, Badges, Kategorien) — ein Icon-Set (z.B. Lucide; nur genutzte Icons, Teil E), einheitliche Strichstärke/Größe, Farbe aus Tokens.

QUALITÄT (v0-PFLICHT, Nutzer nicht technisch): Vor „fertig"/Deploy/Go-Live und bei „prüfen/fertig/live?": (1) npx impeccable detect app/ components/ lib/ — Exit 2=fixen bis 0; (2) Selbstcheck: Gradient? Kreis-Icons? Emojis? Card-Grid? Pills? Stock? (3) Antwort in Alltagssprache, kein Jargon. Nutzer kennen Impeccable nicht — du führst es aus. Projektstart: npx impeccable skills install -y --scope=project.

BRANDING (Pflicht): app/icon (Favicon aus Logo), app/opengraph-image (Logo + baseColor + Name), metadataBase: Teil J. Gebrandete not-found/error: Teil B.

Zeichen: 4902 / 5000

Qualität vor Go-Live (v0 prüft intern)

Vor „fertig“, Deploy und Go-Live prüft v0 den Code mit Impeccable (Teil C). Redaktion braucht kein Terminal — ein Satz im Chat reicht:

SituationBeispiel
Seite wirkt generisch„Sieht das noch nach KI aus? Bitte prüfen und verbessern.“
Vor Übergabe an IT„Ist die Website wirklich fertig? Bitte durchchecken.“
Vor Go-Live„Können wir live gehen? Erst prüfen, dann Teil J.“

v0 antwortet in Alltagssprache (z. B. „Gradient entfernt, Buttons vereinheitlicht“) — ohne Terminal-Ausgabe.

Technik (IT) — optional
npx impeccable skills install -y --scope=project # einmalig im v0-Projekt
npx impeccable detect app/ components/ lib/ # Exit 0 = ok, Exit 2 = offene Treffer

Optional in package.json: "check:impeccable": "impeccable detect app/ components/ lib/" — Node 22.12+.

Abgrenzung: Cockpit-Dashboard (dieses Repo) nutzt check-dashboard-ui-slop. v0-Websites nutzen Impeccable.

Chatbot — Verdrahtung (Chat-Prompt, nicht als Instruction)

Wann: Nach Teil C + Teil F (Centerplan oder Embed). Einmalig in den v0-Chat kopieren — nicht in Custom Instructions (Zeichenlimit).

cockpitOS — Chatbot-Verdrahtung (Antwort → Karten)

KONTEXT: Teil C aktiv. POST visitor-chatbot → data.answer, data.contentBlocks, data.items, data.actions, data.suggestedActions, data.detectedIntent.

PFLICHT UI (unter der Antwort, vor Follow-ups):
1) data.contentBlocks.blocks[] — kind shops|offers|events|news (Karussell wie Aktuelles). payload pro Item: title, image, slug, shop, validUntil, discount …
2) data.items[] — Shop-Karten wenn kein shops-Block (Legacy).
3) actions + suggestedActions → Buttons (keine Emojis).

contentBlocks: { version:1, blocks:[{ id, kind, layout:"carousel", items:[{ refType, refId, payload }] }] }.
Bei Angebots-Fragen liefert Cockpit kind:"offers" (auch wenn die KI nur Text listet).

v0-FALLBACK (nur wenn blocks leer, Antwort listet aber Angebote/Events):
- GET apiHints.aktuellesBundleGet (public-visitor-surface) → offers/events/news.
- Titel aus Antwort (**fett**/Bullets) matchen; sonst Top 6–12 aktive Einträge.
- Gleiche Karten-Komponenten wie /angebote — kein reines Markdown ohne Karten.

SHOP-HANDLER (lib/chat/handle-concierge-action.ts):
- open_shop / open_service: router.push(`/shops/${slug||id}`) bzw. `/services/…` — slug aus items[] zu targetId.
- show_on_map: iframe embedUrl + `&shop=${uuid}` oder `/centerplan?shop=${uuid}`; parking|center|shops → Plan ohne Fokus.
- start_route: POST {DASHBOARD}/api/wayfinding/routing → Plan wayfinding=1&shop={uuid}.
- offers-Block: Link `/angebote/${slug||id}` (Center-Routing).

EMBED: postMessage cockpit:centerplan:open-detail (Teil F).

TEST: „Gibt es aktuelle Angebote?" → Text + Angebots-Karten (contentBlocks oder Fallback). „Wo ist H&M?" → Shop-Chip + Karte.

Teil D — nur diesen Kasten in Instruction „Cockpit Workflow“

cockpitOS — Teil D (Pflicht-Workflow)

GOLDENE REGEL: cockpitOS ist die einzige Source of Truth für Inhalte.
Shops, News, Events, Angebote, Texte, Bilder — niemals als Dummy/Placeholder im Frontend-Code hartcoden. Alles, was auf der Website erscheint, kommt aus der cockpitOS-API.

VERBOTENE MUSTER (niemals):
- getWebsiteConfig()/getHeroSlides() gegen /api/agencyos/…/website-config oder /api/centers/…/website-config ohne Auth (401)
- const shops = [ { name: "H&M", … } ] // hardcodierte Shops; ebenso News/Events/Offers
- Platzhalter-Arrays oder Mock-Objekte für echte Inhalte
- Eigene API-Endpunkte erfinden, die die Doku nicht nennt (z.B. /public-config, /analytics-config)
- Bei API-Fehlern Error-Objekte rendern — immer Fallback (leeres Array, Defaults)
- Resend/RESEND_API_KEY in v0 — E-Mail nur über Dashboard-Proxy (Teil A)
- Analytics-Config selbst bauen: Daten kommen aus GET …/public-visitor-surface → data.tracking

PFLICHTSEITEN (Recht, jede Website):
- /impressum und /datenschutz IMMER anlegen; Footer-Link auf jeder Seite.
- Inhalte aus page-content (pageType impressum/datenschutz). Wenn leer: Redaktion fragen, Texte via MCP anbieten. Keine juristischen Texte erfinden; Go-Live erst wenn befüllt (Teil J).
- Datenschutzerklärung muss abdecken: Umami (nach Opt-in), Chatbot (KI-Verarbeitung), Kontaktformular, Hosting. Cookie-Banner + Formular-Checkbox: Teil C/A.

PROJEKT-START (immer so beginnen):
1. cockpit_center_project_init (center_slug, api_base_url) → centerId, Checkliste, Quick-Reference
2. cockpit_update_center_website_config wenn Website-Config (Theme, Analytics, Domain) gesetzt/geändert wird
3. Inhalte lesen: cockpit_center_context (centerId)
4. Fehlende Inhalte direkt ins cockpitOS schreiben: cockpit_content_push
5. Nie direkt in der DB ändern — immer offizielle MCP-Tools oder API

MCP-KERNTOOLS:
- Start: cockpit_center_project_init(centerSlug | centerId)
- Lesen: cockpit_center_context(centerId, include="shops,news,events,offers,…")
- Schreiben: cockpit_content_push({ centerId, shops?, news?, events?, offers?, … })
- Config/Theme/Analytics: cockpit_get_center_website_config + update_center_website_config
- Deploy melden: cockpit_register_frontend_deployment (Teil J)
- Cache: cockpit_revalidate_website nach größeren Pushes
- Tool unbekannt: cockpit_mcp_discover_tools — nie raten, nie DB direkt
- GEO Pflicht: cockpit_v0_geo_standards → p0Blockers (JSON-LD, Sitemap, Shop-Links; Grundgerüst Teil E)

SHOPS — EINZEL VS KETTE:
- Lokal/nur dieses Center: content_push shops:[{name,category,…}] OHNE chainName/kette/marke/chainSlug (optional standaloneShop:true). Kein create_chain+location.
- Echte Kette (Filialen): list_chains → create_chain → create_shop_location.
- locationCount=1 ≠ jeder Shop ist Kette. „Lokaler Shop" = Einzelshop.

CONTENT-WORKFLOW — PROAKTIV ANBIETEN:
- Beschreibt die Redaktion einen Inhalt (Shop, News, Event, Angebot), sofort anbieten ihn direkt ins cockpitOS zu schreiben. Nicht warten: „Soll ich das direkt ins cockpitOS eintragen?"
- cockpit_content_push direkt verwenden (Live-Push aktiv). cockpit_content_push_preview nur auf Wunsch oder bei komplexen Mehrfach-Aktionen.
- Nach dem Push: cockpit_center_context aufrufen und kurz bestätigen, dass der Inhalt angekommen ist.

VERÖFFENTLICHUNGSZEITFENSTER (wie Cockpit „Sichtbar ab/bis" — beim MCP-Schreiben mitliefern):
- News: publishDate (+ optional publishEndDate)
- Events: startDate/endDate = Termin; zusätzlich publishStartDate/publishEndDate wenn Website-Sichtbarkeit abweicht
- Angebote: startDate/endDate = Aktionszeitraum; publishStartDate/publishEndDate für Website-Laufzeit. cockpit_update_offer hat keine Publish-Felder — dafür content_push oder API-PUT nutzen.
- Öffentliche GETs filtern serverseitig (news/events/jobs/bundle). Fenster trotzdem setzen wenn Redaktion Zeiten nennt.

ANALYTICS (Umami):
- Über cockpit_update_center_website_config: analyticsConfig mit umamiWebsiteId + umamiUrl. Tracking-Code nie selbst hartcoden — das Dashboard steuert Analytics über die Config.
- Verifikation: GET /api/agencyos/v1/centers/{centerId}/website-config

LIVE-BETRIEB & DEPLOY:
- Inhalte parallel im v0-Chat oder Cockpit — gleiche DB (content_push).
- Nach Vercel-Deploy: Teil J (register_frontend_deployment oder IT /api/cockpit-register). Status: Website-Management → Frontend-Kanäle (v0).

WENN INHALTE FEHLEN: nicht erfinden, nicht mocken. Sofort anbieten, den Inhalt über cockpit_content_push anzulegen. Beispiel: „Ich sehe, dass noch kein Angebot vorhanden ist. Soll ich direkt eines im cockpitOS anlegen?"

ZIEL: Die Redaktion greift so wenig wie möglich manuell ein. Der Assistent schreibt aktiv und proaktiv ins cockpitOS.

Zeichen: 4978 / 5000

Einzelshop vs. Kette (Kurz für Redaktion)

SituationRichtigFalsch
Lokaler Mieter, nur in diesem CenterEinzelshop via content_pushKette + Filiale anlegen
Bekannte Marke mit Filialen (Deichmann, DM, …)Kette prüfen/anlegen + Filiale
Redaktion sagt „keine Kette“ / „lokaler Shop“standaloneShop: true, keine Ketten-Feldercreate_chain weil Shop neu ist

Warum: Felder wie chainName oder kette im Push legen automatisch eine ShopChain an — auch für Einzelmieter. Das bläht die Ketten-Liste auf und verwechselt Filial-Logik mit lokalen Shops.

Teil E — nur diesen Kasten in Instruction „Cockpit E“

cockpitOS — Teil E (Performance, Barrierefreiheit, SEO-Basis, Resilienz)

Ziel: 60fps ohne Ruckeln, kleiner JS-Bundle, schnelle Interaktion (INP), für alle bedienbar, ab Tag 1 indexierbar richtig konfiguriert.

ARCHITEKTUR:
- Standard: Server Components; "use client" nur für echte Interaktion (Slider, Overlay, Formular-State). Keine ganze page.tsx als Client wegen einem Button.
- Schwere Client-Bausteine: dynamic(() => import(…), { ssr:false }) + loading-Fallback.
- Keine komplette shadcn/ui- oder Icon-Library — nur genutzte Komponenten/Icons importieren.

REACT (bei Listen/Slidern mit Ruckeln): keine Inline-Objekte/Funktionen pro Item (style={{…}}, onClick={() => …}); useMemo/useCallback nur bei nachgewiesenem Re-Render-Problem.

ANIMATIONEN (Pflicht):
- Nur transform und opacity. Nicht animieren: top, left, width, height, margin, padding, filter, blur, box-shadow, backdrop-filter.
- Kein scale-hover auf jedem Karten-Grid; max. eine Einstiegs-Sequenz, Rest statisch oder leichtes CSS :hover.
- prefers-reduced-motion: reduce → Animationen aus/stark verkürzt, Slider-Autoplay aus.
- Slider: kein Layout-Shift beim Bildwechsel; Intervall ≥5s; bei document.hidden pausieren.
- Bei Konflikt mit Teil C „Motion": diese Performance-Regeln haben Vorrang.

SCROLL & EVENTS: passive Listener; throttle/debounce ≥100ms; kein setState pro Scroll-Pixel; kein Maus-Parallax, kein scroll-gebundenes blur/filter.

BILDER: next/image mit width/height oder fill+sizes; Hero priority, darunter lazy; keine Multi-MB-Rohbilder (CDN: Teil C); Autoplay-Video nur muted loop, kurz, komprimiert.

BUNDLE: eine Animations-Lib (CSS oft genug; Framer Motion nur wenn nötig); keine ungenutzten Chart-/Map-/UI-Pakete.

BARRIEREFREIHEIT (BFSG — Pflicht ab Start, nicht nachrüsten):
- Semantik: header/nav/main/footer, genau eine h1 pro Seite, html lang="de".
- Alt-Texte aus API-Daten (Shop-Name, Event-Titel); dekorative Bilder alt="".
- Kontrast ≥4.5:1 — Akzentfarbe auf Weiß prüfen, sonst abgedunkelte Textvariante ableiten.
- Volle Tastaturbedienung (Menü, Slider, Akkordeon, Chat); sichtbarer Fokus-Ring (nie outline:none ohne Ersatz); Skip-Link zum Inhalt.
- Formulare: label je Feld; Fehler als Text via aria-describedby + aria-live, nicht nur rot.
- Touch-Targets ≥44px. Centerplan: Shops zusätzlich als durchsuchbare Liste erreichbar (Teil F) — Canvas allein reicht nicht.

SEO-GRUNDGERÜST (ab erstem Deploy):
- app/robots.ts: Preview (*.vercel.app bzw. VERCEL_ENV!==production) → alles disallow + noindex; Live-Domain → allow + Sitemap-Verweis. Umschalten beim Go-Live: Teil J.
- app/sitemap.ts dynamisch aus API (Start, Shops, News, Events, statische Seiten).
- generateMetadata je Seite: unique Title „{Seite} | {Center-Name}", echte Description; canonical über metadataBase (Live-Domain, Teil J).
- JSON-LD: ShoppingCenter + openingHoursSpecification (Home), Event/Offer auf Detailseiten. Vollstandard: cockpit_v0_geo_standards (Teil D/J).

UMAMI & COOKIE-CONSENT (Pflicht bei Tracking):
- public-visitor-surface → data.tracking + data.visitorPrivacy. NICHT /public-config.
- <V0AnalyticsSnippet centerId={centerUuid} /> — Umami erst NACH Cookie-Opt-in (cc_cookie, analytics); Banner-Texte aus visitorPrivacy; „Ablehnen" gleichrangig neben „Akzeptieren" (kein Dark Pattern). Ohne Opt-in: null Requests an Analytics.
- Optional Footer „Analytics deaktivieren" wenn tracking.trackingOptOut → localStorage cockpit-tracking-opt-out=1.
- Webfonts IMMER über next/font (self-hosted, kein Google-CDN-Request zur Laufzeit — DSGVO). Externe Maps/Embeds nur nach Cookie-Kategorie external (Platzhalter mit Klick-Freigabe).

RESILIENZ (Fallback wenn API kurz down — Pflicht): ISR + statisches JSON-Fallback + Try-Catch. Nie leere Dummy-Shops/News bei API-Fehler.
1) ISR: export const revalidate = 300 auf Seiten/Segments mit Cockpit-fetch.
2) Statisches JSON-Fallback (Build): Skript vor next build (scripts/generate-fallback-json.mjs) holt Shops/News/Events/Bundle von NEXT_PUBLIC_DASHBOARD_URL + Slug (Teil A) → public/fallback/*.json. package.json: "prebuild": "node scripts/generate-fallback-json.mjs".
3) Laufzeit: try { fetch…; if(!res.ok) throw } catch { JSON.parse(await readFile('public/fallback/shops.json','utf8')) } — gleiches Muster für news/events/bundle. Optional dezentes Banner „Offline-Modus".

ISR/CACHE (Publish & Live): on-demand POST /api/revalidate + REVALIDATION_SECRET (Vorlage). Nach MCP content_push invalidiert das Dashboard; cockpit_revalidate_website nach großen Pushes. Zeitfenster News/Events/Angebote: serverseitige Filter + Revalidate — nicht stale-forever.

ABNAHME (IT): DevTools Performance (Long Tasks >50ms reduzieren), React Profiler (kein Voll-Rerender pro State), optional ANALYZE=true npm run build. Ziel: Lighthouse mobil Performance ≥90, Accessibility ≥95 (Gate: Teil J).

Zeichen: 4803 / 5000

Teil F — nur diesen Kasten in Instruction „Cockpit F“

cockpitOS — Teil F (Centerplan: SVG, Hybrid, 3D)

DATEN (öffentlich, kein website-config)
1) GET …/public-visitor-surface → data.centerplan: initialViewMode (2d|3d), initialZoom3D, hoverColor3d, hoverColorSvg, noExtrudeLayerIds[], showLogosOnPlan; data.v0Integration.centerplan
2) GET …/api/wayfinding/floors?centerId= → floors[]: mapSvg (String!), mapImage, shopViewBoxes, mapLocations[], enable3D, …
MCP/v0: zuerst omitMapSvg=true (alle Etagen zählen) — volle Antwort oft zu groß für Chat (nur EG sichtbar). Dann floorNumber=0|1|… oder Server-Fetch; oder cockpit_center_context include=floors_summary.
3) Optional GET …/wayfinding/centerplan?centerId= (Metadaten/Bounds)
4) Routing: GET …/entrances; POST …/wayfinding/routing — endLocationId = mapLocations[].id (UUID), startWaypointId = entrances[].id

HYBRID (PNG in SVG)
- mapSvg = <image> Hintergrund + path/polygon id="shop-…"
- 2D: <image> pointer-events:none; Klicks nur auf Vektor-IDs
- mapImage + relative Pfade → CDN https://cockpitos.b-cdn.net
- shopViewBoxes: JSON svgId→viewBox für Detail-Zoom

SVG-ID-KONVENTION (nicht raten)
shop-… → Shop (extrudieren/klickbar) | service-… → Service (dünn)
entrance-… | mall-floor-… Boden | foodcourt-… | display-… nur Signage
decorative/decorative-… → Deko, NICHT klickbar, NICHT extrudieren
<g id="logos">/nopointer-logos → flach | <g id="hover-layer"> → Hybrid-Klickflächen, transparent
<g id="routes"> oder Routes → Wegenetz UNSICHTBAR (display:none); Route nur als berechnetes Overlay

KLICK & NAVIGATION
mapLocations: svgId === DOM-id → shop.id/service.id. Niemals svgId als Shop-UUID. Kein Mapping ≠ per Name erraten.
2D (v0, @mall-os/wayfinding privat): buildSvgIdToLocationMap → linkableSvgIds; click delegation path/g/polygon; onPoiClick
HOVER 2D (Pflicht): NICHT mouseover/mouseout auf path/g — feuert pro Kind, Hover klebt. Stattdessen: svg mousemove → elementFromPoint/closest POI → eine hoveredSvgId; svg mouseleave → null. hoverColorSvg aus surface. CSS :hover nur optional.
V0-NAVIGATION (Pflicht): Plan nur IDs — Ziele in v0: /shops/{slug|id}, /services/{slug|id}; Modal-CTA intern, NIE Cockpit-Website. modal-then-detail|direct. ?shop=uuid gleiche Logik.
OPTION EMBED: /embed/centerplan. chrome=plan|minimal|standard|full. showFloors|showViewMode|showLegend|showSidebar=0|1. embedBg=transparent oder Hex/RGB. chromeGhost=1. viewMode=2d|3d. floorNumber=0|1|-1. detailMode=parent|cockpit, hideHeader=1. THEME (iframe-intern, URL): accent/accentColor, accentFg, chromeBg, chromeFg, chromeBorder, chromeMuted, chromeRadius 0-32, hoverColor/hoverColorSvg, hoverColor3d — v0 stylt Controls nicht von außen per CSS.
A11y (Pflicht): Plan nie einziger Zugang — parallele Shop-Liste mit Suche/Etagen-Filter (Tastatur + Screenreader); Info-Card mit Fokus-Management, per Esc schließbar.

3D-DARSTELLUNG (wenn gewünscht)
- Client-only: dynamic(() => import("./centerplan-3d"), { ssr:false })
- Stack: @react-three/fiber + three + SVGLoader (oder Port @mall-os/wayfinding)
- Ablauf: mapSvg parsen → Shapes; Typ via id + parent <g id>
- ExtrudeGeometry: Shops höher, Services flacher, floor als Platte
- Globale Gruppe: SCALE ~0.01, Y-Flip SVG→3D
- initialViewMode respektieren; noExtrudeLayerIds + data-no-extrude="true" → flach
- #routes in 3D unsichtbar; Etagen-Tabs über floors[].floorNumber; enable3D prüfen
- model3D (GLB) nur Fallback wenn mapSvg leer — sonst SVG-3D

V0-TYPISCHE FEHLER — 3D NICHT VORZEITIG AUF 2D UMSCHALTEN
- API liefert 3D-taugliche Daten: floors[].mapSvg = SVG-String (beginnt oft mit "<svg")
- NICHT „kein verwertbares Format" behaupten ohne mapSvg geprüft zu haben
- NICHT mapImage extrudieren — mapImage = nur 2D-Hintergrund; 3D aus mapSvg-Vektoren
- NICHT mapSvg als JSON/XML missverstehen; NICHT <img>/dangerouslySetInnerHTML statt SVGLoader
- centerId = Center-UUID aus by-slug → id; NICHT UUID aus Centerplan-Edit-URL (floorId)
- Vor Aufgeben (Debug): mapSvg vorhanden? „shop-" im String? mapLocations.length > 0?
- Hybrid: <image> nicht extrudieren — nur path/polygon mit id shop-…/service-…
- Einzelne Paths können ExtrudeGeometry werfen → try/catch: Path überspringen, Rest rendern
- 2D-only NUR wenn mapSvg leer ODER keine shop-Polygone ODER enable3D=false
- Nicht pauschal „3D entfernen" — erst SVGLoader-Pipeline + classify + Teil-F-Regeln umsetzen

ROUTEN
Primär POST …/wayfinding/routing. Fallback: #routes aus mapSvg parsen (Graph aus Segment-Endpunkten). Anker-IDs: p-shop-…, p-service-…, pf-stairs…, pfb-elevator…

CODE-STRUKTUR
Server: floors + surface laden. Client: nur Canvas/Interaktion. components/wayfinding/ (2d-plan, 3d-scene, floor-tabs getrennt); lib/wayfinding/ (classify, route). Bei Konflikt Performance: Teil E.

VERBOTEN
Demo-Shops auf Plan | website-config für Plan | Error in JSX (#418) | #routes sichtbar | decorative/hover-layer als Shops behandeln | Cockpit-URL als v0 Shop-Detail

Center-UUID aus by-slug — nicht floorId aus Centerplan-Edit-URL.
Doku: centerplan/svg-struktur.md, hybrid-png-mit-polygonen.md, hybrid-ai-masterprompt.md (Cockpit-Docs). 2D-Klick-Nachbau: Chat-Prompt „Centerplan“ unten auf dieser Seite.

Zeichen: 4656 / 5000

Centerplan — Chat-Prompt (nach Neu-Einspielen von Teil F)

Wenn der Plan SVG zeigt aber Klicks fehlen oder Shops auf die falsche Website verlinken: diesen Kasten einmalig in den v0-Chat (nicht als Instruction).

Centerplan 2D fertigstellen (Palais Vest / Basis-Template).

KONTEXT: Teil F Instruction ist aktiv. PlanView2D rendert mapSvg — fehlt: Klick-Delegation + v0-Navigation.

DATEN (bereits in Teil A/B/F):
by-slug → centerId; public-visitor-surface; GET …/wayfinding/floors?centerId=; Client GET shops.

AUFGABE
1) lib/wayfinding/build-svg-id-map.ts (client, useMemo):
buildSvgIdToLocationMap(mapSvg, mapLocations) — svgId direkt, dann x/y in BBox
2) plan-view-2d.tsx erweitern:
- Nach SVG-Mount: decorative, #routes, hover-layer, <image> → pointer-events:none
- Click delegation: closest path/g/polygon/rect; id ∈ linkableSvgIds oder shop-/service-*
- Hover: svg.onmousemove → elementFromPoint + closest POI → setHoveredSvgId (eine ID); svg.onmouseleave → null. NICHT mouseover/mouseout auf Kind-Pfade.
- Pan nur wenn Bewegung > 8px (sonst Klick)
- onPoiClick({ type, id, mapLocationId, svgId })
3) lib/navigation/resolve-href.ts:
resolveShopDetailHref → /shops/{slug||id}
resolveServiceDetailHref → /services/{slug||id}
KEIN Link auf dashboard oder Cockpit-Center-Website
4) lib/center-config.ts: navigationMode = 'modal-then-detail' (Default)
Klick → ShopDetailSheet mit Link href={resolveShopDetailHref(shop)}
mode 'direct' → router.push statt Sheet
5) app/centerplan/page.tsx: Etagen-Tabs, PlanView2D, optional Shop-Liste (A11y)
6) Deep-Link ?shop=uuid beim Mount

DATEIEN (Vorschlag)
lib/wayfinding/build-svg-id-map.ts
components/wayfinding/plan-view-2d.tsx
components/wayfinding/shop-detail-sheet.tsx
lib/navigation/resolve-href.ts
lib/center-config.ts

DEBUG: console.log({ linked: Object.keys(map).length, mapSvgLen, mapLocs: floor.mapLocations?.length })
3D erst wenn 2D-Klicks stehen (Teil F2 Chat).

ALTERNATIVE EMBED (empfohlen — ohne PlanView2D-Nachbau):
1) cockpitOrigin = data.center.customDomain (z.B. https://palais-vest.de) oder https://{slug}.cockpit-os.de — NICHT websitePublicUrl (separate v0-URL, kann null sein).
2) v0Url = NEXT_PUBLIC_V0_SITE_URL (eure Vercel/Staging-URL).
3) iframe src="{cockpitOrigin}/embed/centerplan?slug=palais-vest&partnerOrigin={v0Url}&detailMode=parent&chrome=plan&viewMode=3d&embedBg=transparent&chromeGhost=1&hideHeader=1"
chrome: plan=nur Karte | minimal=Karte+Etagen+2D/3D (Default) | standard=+Legende | full=+Sidebar
Einzelflags: showFloors=1, showViewMode=1, showLegend=0, showSidebar=0, wayfinding=0
Theme (iframe-intern): accent, accentFg, chromeBg, chromeFg, chromeBorder, chromeRadius, hoverColor — v0 stylt Controls nicht von außen per CSS
4) postMessage: type cockpit:centerplan:open-detail (2D-, 3D-Fläche, Sidebar, Legende), e.origin === cockpitOrigin → v0 ShopDetailSheet.
Ausführlich (Panel links, kein Cockpit-Modal): v0-centerplan-embed-eigenes-panel.md
5) Shop-CTA: /shops/{slug|id} auf v0 — nie palais-vest.de/shops als v0-Ziel.
Nach Cockpit-Deploy testen; weiße Seite = alter Build oder falsche URL.

Teil F2 — 3D-Nachbau (Chat-Prompt oder Instruction „Cockpit F2“)

Wann: v0 baut den Centerplan falsch, will 3D abschalten oder nutzt mapImage statt mapSvg. Teil F bleibt aktiv; F2 zusätzlich als Instruction oder einmalig in den Chat kopieren.

Vollständiger Ablauf

Der Kasten unten ist der komplette Bau-Prompt für v0. Bei Zeichenlimit-Problemen in v0: Kasten nur in den Chat, nicht als zweite Instruction.

Teil F2 — nur diesen Kasten in Instruction „Cockpit F2“ oder in den v0-Chat

cockpitOS — Teil F2 (Centerplan 2D+3D nachbauen wie cockpitOS)

Ziel: 2D/3D-Umschalter wie Center-Website. NICHT auf mapImage-only fallback wenn mapSvg existiert. NICHT 3D entfernen ohne SVGLoader-Pipeline.

DATEN (Server lädt, Client rendert)
centerId = UUID aus GET …/centers/by-slug/{slug} → id (NICHT floorId aus Centerplan-Edit-URL!)
1) GET …/public-visitor-surface → data.centerplan (initialViewMode, initialZoom3D, hoverColor3d, hoverColorSvg, noExtrudeLayerIds)
2) GET …/wayfinding/floors?centerId= → floors[].mapSvg (STRING!), mapImage, mapLocations[], enable3D
3) GET …/shops?publicWebsite=true&status=Aktiv&limit=5000
Pro Etage: mapSvg leer → 2D mit mapImage (CDN https://cockpitos.b-cdn.net). Sonst 3D aus mapSvg.

Mapping aus mapLocations + shops: svgId→Location, shopLogos, shopNames, linkableSvgIds.

DATEIEN
lib/centerplan/load-data.ts (Server) | centerplan-view.tsx (client)
centerplan-3d-scene.tsx (dynamic ssr:false) | parse-svg-for-3d.ts | svg-id-classify.ts
Etagen-Tabs, Toggle 2D|3D, min-h-[60vh].

3D-PIPELINE (Pflicht)
Deps: three, @react-three/fiber, @react-three/drei
1) strip fill/stroke url(#…) — SVGLoader crasht sonst
2) SVGLoader.parse(mapSvg) → paths; id = node.id ODER parent.id (<g id="shop-56"><path/>)
3) classify: shop-* shop | service-* service | mall-floor floor | decorative skip
#routes/route → unsichtbar | logos/nopointer-logos → flach
noExtrudeLayerIds + data-no-extrude → flach
4) SVGLoader.createShapes → ExtrudeGeometry; fehlerhafte Paths try/catch skip
depth shop=60 foodcourt=50 floor=20 service=4; rotateX(-PI/2); scale(1,-1,1); shop GAP 0.95
5) Gruppe scale={[0.01,-0.01,-0.01]} + BBox zentrieren
6) Kamera isometrisch: fov=50, dist=max(w,d)/initialZoom3D, pos center+(d*0.707,d*0.707,d*0.707)
7) Hybrid: <image> NICHT extrudieren — nur path/polygon ids
8) Klick onPointerUp → svgId → mapLocation → shop.id
9) Kein Demo-Plan, kein website-config

2D: mapSvg inline; Hybrid pointer-events:none auf <image>; hoverColorSvg; Hover per svg mousemove+mouseleave (nicht mouseover auf paths)

DEBUG: console.log({ mapSvgLen, hasShop: mapSvg?.includes('shop-'), mapLoc: mapLocations?.length })

FIX wenn kaputt: mapSvg.slice(0,300) zeigen; mapImage aus Three entfernen; centerId prüfen.

VERBOTEN: mapImage extrudieren | SVG als <img> für 3D | „kein Format" ohne Debug | Error in JSX
Ergänzt Teil F — bei Konflikt Performance: Teil E.
Zusätzlich: Plan-A11y aus Teil F umsetzen (parallele Shop-Liste, Fokus-Management der Info-Card).

Teil G — nur diesen Kasten in Instruction „Cockpit G“

cockpitOS — Teil G (Multi-Center: ein Layout, viele Domains)

PRINZIP
Mehrere Shopping Center teilen EIN v0-Layout (gleiches websiteTemplate im Cockpit). Unterschiede (Name, Shops, Hero, Plan, Farben, Fonts) kommen pro Center aus der API — nicht aus dupliziertem Code. Gilt für jede Template-Familie (ilg, mec-shopbox, rgw, …).

ZIEL: Ein v0-Projekt pro Layout/Template-Familie — KEIN Duplizieren des Layouts pro Center-ID.

WANN: Pflicht wenn mehrere Center dasselbe Layout nutzen. Einzel-Center: Teil G optional, fester Slug in Teil A reicht.

VERBOTEN
- centerId oder Slug in Komponenten hardcoden (kein const centerId = "uuid…")
- HIER_SLUG_EINTRAGEN in page.tsx, Hero, Footer, Shops (nur .env.example/Preview)
- Pro Center eigene Kopie von Homepage/Shops-Seiten im Repo
- Links immer mit /{slug}/-Prefix — auf Custom Domain falsch (/shops, nicht /slug/shops)

PFLICHT: lib/cockpit/resolve-center.ts (einzige Quelle)
export async function resolveCenter(): Promise<{ centerId, slug, websiteTemplate } | null>
Auflösungs-Reihenfolge (Server/Middleware):
1) host = Request-Host ohne Port; www. entfernen
2) Production (Host nicht *.vercel.app): GET {NEXT_PUBLIC_DASHBOARD_URL}/api/centers/by-domain?domain={host} → { success, center: { id, slug, websiteTemplate, … } } — id = centerId
3) Preview/Dev (vercel.app/localhost): a) Query ?center={slug} ODER b) DEFAULT_CENTER_SLUG ODER c) optional Subdomain {slug}.vercel.app in Middleware
4) slug → GET …/api/centers/by-slug/{slug} → centerId (= id)
5) GET …/public-visitor-surface(centerId) → data.center.websiteTemplate
6) Optional Env EXPECTED_WEBSITE_TEMPLATE: Abweichung → 404/Fehlerseite, nie falsches Layout rendern

ARCHITEKTUR
- middleware.ts: Host/Query auswerten; centerId+slug an Layout (Header x-center-id/x-center-slug oder React Context)
- app/layout.tsx: einmal resolveCenter → CenterProvider; alle Pages lesen Context
- Daten-fetch immer mit aufgelöstem centerId (Server Components)
- Neues Center = Domain in Cockpit + Vercel oder Recast pflegen — kein neues v0-Projekt

LINKS & ROUTEN
- Custom Domain: Pfade OHNE Slug (/shops, /aktuelles)
- Preview/Slug-Modus: /{slug}/shops — lib/cockpit/paths.ts generatePath(path, { slug, isCustomDomain })

SEO & RECHT PRO DOMAIN (Pflicht)
- generateMetadata, canonical, OG-Image, Favicon, JSON-LD: host-basiert aus resolveCenter — pro Center Name/Logo/baseColor, nie geteilt hardcoden.
- app/sitemap.ts + robots.ts pro Host dynamisch (nur Inhalte des aufgelösten Centers; Preview: noindex, Teil E/J).
- /impressum und /datenschutz je Center aus page-content (Teil D) — Betreiber unterscheiden sich, niemals einen gemeinsamen Text für alle Domains ausliefern.
- Design-DNA (Teil C) pro Center anwenden: gleiche Struktur ja, aber Farben/Signature-Element aus API, damit Domains nicht klonhaft wirken.

VERCEL & ENV
Modell A (empfohlen): EIN Projekt, ALLE Domains dieser Template-Familie unter Vercel → Domains
Modell B: Ein Repo, mehrere Vercel-Projekte — nur DEFAULT_CENTER_SLUG unterscheidet sich
Modell C (Partner Recast): gleiches Repo, Docker/GHCR statt oder parallel zu Vercel — siehe RECAST unten
Pflicht: NEXT_PUBLIC_DASHBOARD_URL=https://dashboard.cockpit-os.de (ohne Slash)
Shared (Modell A): COCKPIT_VERCEL_PROJECT_MODE=shared, DEFAULT_CENTER_SLUG nur Preview, EXPECTED_WEBSITE_TEMPLATE optional
Dedicated: COCKPIT_REGISTER_TOKEN + COCKPIT_CENTER_SLUG pro Vercel-Projekt
VERBOTEN Shared: COCKPIT_REGISTER_TOKEN oder COCKPIT_CENTER_SLUG als feste Production-Projekt-Env (nur ein Center würde sonst „gewinnen“)

ENV-AUDIT (Pflicht bei Setup / nach Modus-Wechsel)
1) Alle Vercel-Env-Namen auflisten (Project + verlinkte Team Shared)
2) Modus erkennen: COCKPIT_VERCEL_PROJECT_MODE=shared|dedicated (fehlt → Dedicated annehmen wenn Token+Slug gesetzt)
3) Shared: COCKPIT_CENTER_SLUG/COCKPIT_REGISTER_TOKEN aus Projekt-Env ENTFERNEN (wenn gesetzt); DEFAULT_CENTER_SLUG setzen; postbuild-Register ist bei shared deaktiviert
4) Dedicated: Token+Slug müssen zum Cockpit-Center passen; COCKPIT_VERCEL_PROJECT_MODE=dedicated
5) REVALIDATION_SECRET nie neu erfinden wenn Team Shared schon gesetzt — nur verlinken
6) Nutzer vor Änderungen in Vercel fragen; Änderungen als Tabelle: Variable | Ist | Soll | Aktion

RECAST/DOCKER (wenn im Repo — smg-mec-template-*)
Deploy-Infrastruktur, Layout-unabhängig: Dockerfile, docker-compose.yml, docker/, .github/workflows/build-push-ghcr.yml, app/api/health/, app/api/cockpit-register/, app/api/revalidate/, RECAST-DEPLOY.md
VERBOTEN: bei Layout-/Design-Änderungen löschen, umbenennen oder „Projekt aufräumen" — auch auf Anweisung nicht entfernen
next.config.mjs: output 'standalone' beibehalten
Push main → GHCR-Image (ghcr.io/sawmuedev/<repo>:latest); Recast: docker compose pull — Vercel-Deploy kann parallel laufen
Recast-Server-Env (.env.recast.example): COCKPIT_DEPLOY_ORIGIN, COCKPIT_REGISTER_TOKEN, COCKPIT_CENTER_SLUG, REVALIDATION_SECRET — Register wie Teil J

DATEN & UNTERSCHIEDE
Nach resolveCenter: gleiche APIs wie Teil A (surface, shops, page-content, floors, …). Branding, Hero, Shops, Plan — alles pro Center aus API; Layout/JSX identisch.

CHATBOT & FORMULARE
POST visitor-chatbot / Proxy kontakt: centerId aus resolveCenter — nie fest verdrahtet. Chatbot: pageContext bei jedem POST (Teil C).

ABNAHME
- Domain A → Logo/Shops/Impressum von Center A; Domain B → Center B, gleiches Layout
- v0-Preview: ?center=slug funktioniert
- grep: keine hardcodierte Center-UUID in components/ oder app/
- Sitemap von Domain A enthält keine URLs von Center B

Zeichen: 4577 / 5000

Teil H — Signage, Stelen & Kiosk (Info-Terminal, nicht Handy)

Wann: v0 baut Kiosk oder Wayfinder-Stele (z. B. 55″, oft 1080×1920). Das ist ein Info-Terminalkein vergrößertes Handy. Companion/NOW!Teil I (eigenes Mobil-Layout).

Referenz: Premium AI / Wayfinding, Kiosk + Companion NOW!, System-Infos Kiosk+Companion für v0.

Terminal-Layout (Zielbild für v0)

┌─────────────────────────────┐ ← 0% Bezel tot
│ HEADER kompakt (~40px) │ ← Logo · Uhr · minimal — kein Hero-Banner
├─────────────────────────────┤
│ │
│ CENTERPLAN HERO (75%+) │ 2D/3D — Hauptelement, volle Breite/Höhe
│ │
│ ┌─ [EG][1.OG][2.OG] ─┐ │ Floating Floor-Pills (Overlay im Plan)
│ │
├─────────────────────────────┤
│ TOOLBAR schmal (Icons) │ Suche · Shops · Route · Handoff-QR
├─────────────────────────────┤ ← 100% Sockel tot
└─────────────────────────────┘

Shop-/Route-Info = kompakte Card über dem Plan — kein Mobile-Vollbild-Panel

Design-Prinzipien: hohe Informationsdichte · kleinere Typografie als Companion · Plan dominiert · UI am Rand schmal · professioneller Terminal-Look.

Physische Stele: oben/unten Bezel oft tot (~400–480 px oben, ~290–380 px unten) — Haupt-Taps in unterer Toolbar und Floating Pills (greifbare Zone). 3D: bottomUIFraction ~0.2–0.35 damit Shops nicht hinter Toolbar verschwinden.

Teil H — nur diesen Kasten in Instruction „Cockpit H“

cockpitOS — Teil H (Kiosk/Stele — Info-Terminal)

NUR KIOSK/STELE — Companion → Teil I (Handy-UI). Signage: H + I zusammen.

KEIN GROSSES HANDY
55"-Stele (oft 1080×1920) = Info-Terminal, NICHT Companion vergrößert. Mehr Dichte, Plan dominiert.

TERMINAL-LAYOUT (Pflicht)
1) PLAN = HERO: min. 75% der nutzbaren Fläche — 2D/3D-Centerplan (Teil F/F2)
2) HEADER kompakt: max ~40–48px — Logo, Uhr, Center-Name klein — kein Hero-Banner
3) FLOOR-TABS: floating Pills als Overlay unten IM Plan — keine volle Breiten-Leiste
4) TOOLBAR unten: schmale Icon-Leiste (Suche, Shops, Route, Handoff-QR) — kompakt
5) SHOP/ROUTE-INFO: kompakte Cards über dem Plan — keine Mobile-Vollbild-Sheets
6) Typo kleiner/dichter als Companion; professioneller Terminal-Look; Design-Tokens/DNA aus Teil C (Center-Branding, kein Generic-Kiosk-Grau)

TOUCH & BEZEL (physische Stele)
- Oben/unten Bezel oft tot (~400–480px oben, ~290–380px unten) — Haupt-Taps in unterer Toolbar + Floor-Pills (greifbare Zone)
- Touch-Targets min. 48px in Toolbar/Pills; KEINE Hover-only-Interaktion (kein Cursor)
- Hoher Kontrast für Tageslicht/Reflexionen; Schrift größer als Desktop-Body (Lesedistanz ~1m)
- 3D: bottomUIFraction ~0.25, damit Ziel-Shop über Toolbar sichtbar bleibt
- Handoff-QR in Toolbar → Companion (Teil I)

KIOSK-BETRIEB (Pflicht)
- Idle-Reset: nach ~90s ohne Touch zurück zur Startansicht; Suche, Route, Chat-Verlauf und Eingaben zurücksetzen — der nächste Besucher darf NICHTS vom Vorgänger sehen (Datenschutz).
- Kein Cookie-Banner am Terminal: kein personenbezogenes Tracking; Nutzung nur aggregiert über Cockpit-Analytics-Config (Tracking: Teil I).
- Keine externen Login-/Zahlungs-Flows; Tastatur nur als On-Screen bei Suche.
- Fehler-Resilienz: API down → Fallback-Daten (Teil E) + Hinweis; nie weißer Screen im Dauerbetrieb. Auto-Reload bei fatalem Fehler (error.tsx mit Timer).
- Uhr/Öffnungszeiten live aus public-visitor-surface; „Heute geöffnet bis …" prominent.

DATEN
Teil A: floors/mapSvg, shops, surface — kein Dummy.
Kiosk-Etage: GET {DASHBOARD}/api/centers/{centerId}/signage-config → signageConfig.touchscreens[] (id, floorNumber, floorName) — NICHT NEXT_PUBLIC_KIOSK_FLOOR. Pro Stele nur ?touchscreenId={uuid} oder Env NEXT_PUBLIC_KIOSK_TOUCHSCREEN_ID; floorNumber aus API. Centerplan-Slide nur diese Etage; Companion-QR ?floorNumber={n}.

VERBOTEN
- Mobile-App-Layout auf Stele (große Cards, Handy-Bottom-Sheets, riesige Header)
- Plan als kleine Karte statt Hero
- Nur Kiosk ohne Companion (Teil I)
- Branding-Zone 30%, die den Plan verdrängt
- Session-Daten, die den Idle-Reset überleben

v0-Prompt: „Kiosk Terminal-Style: Plan 75%+, kompakter Header, Floating Floor-Pills, schmale Toolbar unten."

HANDOFF
- Toolbar: QR/Button „Auf Handy weiter" → POST {DASHBOARD}/api/public/handoff-sessions { centerId, touchscreenId?, location? } → sessionKey
- Gleiche Origin für Kiosk + Companion (ein v0-Projekt/eine Vercel-URL)
- Details Tracking/PATCH: Teil I (Companion-Mount, Usage-Events)

REFERENZ-DOKU (Cockpit-Docs): premium-ai-template.md | kiosk-companion-now-app.md | kiosk-companion-system-info-for-v0.md

Zeichen: 2670 / 5000

Teil I — Companion / NOW! App (zum Kiosk-Paar)

Wann: Zu jeder Signage-/Kiosk-App gehört die Companion (NOW! — Handy-App). User scannt QR am Kiosk und macht auf dem Smartphone weiter (gleiche Route/Shop).

Immer Paar

Faustregel: v0-Projekt für Signage = Kiosk (Teil H) + Companion (Teil I) im selben Next.js-Repo — gemeinsame API-/Plan-Komponenten, getrennte Shells/Layouts.

Ausführliche Feature-Liste: System-Infos Kiosk & Companion für v0

Teil I — nur diesen Kasten in Instruction „Cockpit I“

cockpitOS — Teil I (Companion/NOW! — Paar zur Signage-App)

PRINZIP
Jede Signage/Kiosk-App hat eine Companion (NOW!) — Handy per QR vom Kiosk.
EIN v0-Projekt, ZWEI Oberflächen:
- /kiosk oder /signage → Stele (Teil H — Touch-Zonen)
- /companion → Mobil (eigenes UI — NICHT Stele-Zonen)

ARCHITEKTUR
Shared: lib/cockpit/, lib/centerplan/ (Teil F/F2), Shop-Detail, Branding/Design-Tokens (Teil C)
Getrennt: kiosk-shell vs companion-shell (Layout, Navigation, Typo)
Gleiche centerId, gleiche floors/shops/APIs (Teil A)

HANDOFF KIOSK → COMPANION (Pflicht)
1) Kiosk: Button/QR „Auf Handy weiter"/Route mitnehmen (Toolbar, Teil H)
2) Session: POST {DASHBOARD}/api/public/handoff-sessions { centerId, touchscreenId?, location? } → sessionKey
3) QR/Link: /companion?centerId=UUID&sessionKey=…&shop=…&screen=companion-home|wayfinding (centerId immer in URL — Umami/Reporting)
4) Companion Mount: PATCH {DASHBOARD}/api/public/handoff-sessions { sessionKey, centerId, companionOpened:true }
5) Companion: URL-Params lesen → Ziel-Shop/Route setzen, optional Handoff-Banner
6) Journey-Schritte: POST {DASHBOARD}/api/analytics/usage-events { centerId, source:kiosk|companion, eventType, payload:{ sessionKey } }

COMPANION UI (MOBIL)
- mobile-first Portrait; env(safe-area-inset-*)
- Fix oben: Logo + „NOW!" | Fix unten: Concierge/Chat (Teil C visitor-chatbot inkl. Consent-Dialog vor erster Nachricht)
- Scroll nur zwischen Header und Chatbar — kein mitscrollender fixed-Bug
- Home: Vollbild-Karte (gleiche mapSvg/3D wie Kiosk) + Highlights + Chat
- pointer-events: Karte tappbar — kein unsichtbares Overlay über Canvas (Wrapper pointer-events:none, Karte + interaktive UI auto)
- Shop-Sheet z-index über Chatbar; Route inline auf Home-Karte möglich
- A11y mobil (Pflicht, Teil E): Touch-Targets ≥44px in Daumenzone; Bottom-Sheets mit Fokus-Trap, per Esc/Swipe schließbar, Fokus zurück zum Auslöser; Kontrast ≥4.5:1 auch auf Kartenoverlays; prefers-reduced-motion respektieren

ROUTEN COMPANION
/companion — Home (Karte + Highlights)
/companion/shops, /companion/shops/[id]
/companion/wayfinding (optional wenn Home schon Plan hat)
/companion/qr/[qrCodeId] — Standort-QR-Resolver (Pflicht für gedruckte Service/Shop/Park-QRs)
Menü: News, Events, Angebote → Cockpit-GETs (Teil A)

STANDORT-QR (nicht Handoff — separate Session-API)
1) QR-Bild = stabile Einstiegs-URL auf aktiver Companion-Domain (v0 oder Cockpit-Signage), z. B. {companionOrigin}/companion/qr/{qrCodeId}
2) GET {DASHBOARD}/api/public/qr-codes/{qrCodeId}/resolve → targetType, redirectMode, finalRedirectPath|finalRedirectUrl
ODER (bevorzugt): POST {DASHBOARD}/api/public/qr-codes/{qrCodeId}/scan { centerId, sessionKey? } — Track + Resolve
3) Alternativ getrennt: POST {DASHBOARD}/api/qr-codes/{qrCodeId}/track { centerId, sessionKey? }
4) Redirect: companion → router.push(finalRedirectPath); external → window.location.href = finalRedirectUrl (nicht router.push mit absoluter URL)
5) Website/NOW!-QRs: Einstieg {websiteOrigin}/qr/{qrCodeId} — gleiche Resolve-API, redirectMode website|external
Handoff-QRs (Kiosk→Handy) bleiben POST/PATCH /api/public/handoff-sessions — nicht mit Standort-QR vermischen.

DATEN
Wie Center-Website: public-visitor-surface, floors, shops, aktuelles-bundle, hotpicks. Kein website-config ohne Auth. centerId aus URL/Handoff — nie hardcoden.

RECHT (mobil öffentlich erreichbar)
Footer/Menü: Links auf /impressum + /datenschutz des Centers (Teil D). Umami erst nach Opt-in (Teil C) — Handoff-Session ist KEIN Consent.

TRACKING (optional — sonst unsichtbar in Cockpit-Reportings)
Umami: GET {DASHBOARD}/api/umami-app-config?app=companion + Script (center_id in Events)
Nutzung: POST {DASHBOARD}/api/analytics/usage-events — source kiosk|companion, centerId, eventType, payload.sessionKey
Handoff: POST {DASHBOARD}/api/public/handoff-sessions (QR) + PATCH (companionOpened) — sichtbar unter Companion → QR-Sessions
QR-Sessions-Tab: alle Handoffs mit POST/PATCH public API (auch v0)

DESIGN
Companion ≠ verkleinerter Kiosk — eigene Handy-Optik (Cards, Bottom-Sheets, Daumenzone unten), aber gleiche Design-DNA/Farben wie Website/Kiosk (Teil C).

v0-START-PROMPT
„Baue Kiosk (Teil H) UND Companion NOW! (Teil I) mit Handoff-QR und gemeinsamem Centerplan."

VERBOTEN
- Signage nur Kiosk ohne /companion
- Companion mit Stele-Zonen-Layout
- Hardcodierte Session ohne sessionKey in URL
- Dummy-Shops | getWebsiteConfig ohne Auth

Siehe auch: kiosk-companion-system-info-for-v0.md (Cockpit-Docs)

Zeichen: 3144 / 5000

Teil J — Instruction „Cockpit J“ (Deploy melden)

Optional, aber empfohlen nach erstem Vercel-Deploy. Redaktion bleibt in v0 — Cockpit erfährt die Live-URL automatisch.

Teil J — nur diesen Kasten in Instruction „Cockpit J“

cockpitOS — Teil J (Deploy, Revalidate, Go-Live)

WANN: Nach erstem Vercel-Deploy oder wenn sich die Deploy-URL ändert.
ZIEL: Cockpit kennt die Live-Adresse; Publish erscheint schnell auf der Website; Redaktion trägt keine URLs/Secrets ein.

REVALIDATION_SECRET (v0 automatisch — Redaktion nie anfassen):
- Pflicht: app/api/revalidate/route.ts (Cockpit-Vorlage: POST JSON { secret, centerSlug, paths?, fullRevalidation?, pathMode? }).
- v0 beim Erst-Setup/wenn Route fehlt:
1) Existiert Vercel-Env REVALIDATION_SECRET (Team Shared verlinkt)? → nutzen, NICHT neu generieren.
2) Sonst: crypto.randomBytes(32).toString('hex') → als REVALIDATION_SECRET in Vercel Project Env; .env.example nur mit Platzhalter.
3) Redaktion: kein Schritt. Interner IT-Hinweis (Secret nie in Chat/Git/Client): Cockpit Render braucht denselben Wert — SMG-Standard: Team Shared mit globalem Cockpit-Secret verlinken statt pro Center neu.
- Nach MCP content_push/Cockpit-Publish ruft das Dashboard POST /api/revalidate auf websitePublicUrl — nur bei übereinstimmenden Secrets. Sonst ISR-Fallback revalidate=300 (Teil E). Manuell: cockpit_revalidate_website.

AUTOMATISCH (bevorzugt, IT richtet Team Shared einmal ein):
- Route app/api/cockpit-register + optional postbuild scripts/cockpit-register-after-deploy.mjs
- Vercel Env:
A) Team Shared (jedes Projekt verlinken + Redeploy): COCKPIT_DASHBOARD_URL, REVALIDATION_SECRET, COCKPIT_FRONTEND_CHANNEL (website|signage|companion)
B) Dedicated Projekt: COCKPIT_REGISTER_TOKEN (frt_…), COCKPIT_CENTER_SLUG, NEXT_PUBLIC_DASHBOARD_URL (= Teil A), COCKPIT_VERCEL_PROJECT_MODE=dedicated
C) Shared Multi-Center Projekt: COCKPIT_VERCEL_PROJECT_MODE=shared, DEFAULT_CENTER_SLUG (Preview), NEXT_PUBLIC_DASHBOARD_URL — KEIN Token/Slug als feste Projekt-Env; Register pro Center via Go-Live/MCP
Optional: VERCEL_PROJECT_ID=prj_…
ENV-AUDIT: Bei Setup Modus prüfen, Ist/Soll-Tabelle, Nutzer fragen vor Vercel-Änderung (Teil G ENV-AUDIT)

MANUELL IM CHAT (wenn kein Register-Hook):
0. Optional: cockpit_frontend_channels — isV0WebsiteConnected?
1. Deploy-Origin (https://…vercel.app ohne Pfad)
2. cockpit_register_frontend_deployment: centerId, channel website|signage|companion, origin, optional vercelProjectId/Mode
3. Bestätigen: „Mit Cockpit verbunden — nach Publish automatische Aktualisierung (wenn Revalidate aktiv)."

GO-LIVE-CHECKLISTE (vor Custom Domain — Pflicht, jeden Punkt bestätigen):
1) cockpit_v0_geo_standards: alle p0Blockers implementiert, selfVerification abgehakt
2) robots/index: Live-Domain allow + Sitemap-Verweis; Preview-noindex-Sperre greift NUR noch auf vercel.app; canonical + metadataBase = Custom Domain (nicht vercel.app, nicht cockpit-os.de)
3) /impressum + /datenschutz befüllt (aus page-content, Teil D) und im Footer verlinkt — ohne diese KEIN Go-Live
4) Cookie-Banner blockt Umami/Externes bis Opt-in; „Ablehnen" gleichrangig (Teil C)
5) Kontakt-/Vermietungsformular real getestet: Testanfrage kommt beim richtigen Empfänger an (formRecipients prüfen)
6) 404/error gebrandet (Teil B); Favicon + OG-Image gesetzt — Link-Vorschau testen
7) Lighthouse mobil: Performance ≥90 anstreben, Accessibility ≥95 (Teil E); Centerplan auf Touch bedienbar + Listen-Alternative (Teil F)
8) Öffnungszeiten, specialDays, Anfahrt stimmen mit Cockpit überein; Sitemap erreichbar
9) v0 hat Qualitäts-Check Teil C (Impeccable Exit 0 + Selbstcheck) bestanden — nicht nur behauptet; Redaktion musste kein Tool kennen

GO-LIVE CUSTOM DOMAIN (nur IT/mit Freigabe):
- cockpit_dns_status → cockpit_go_live_domain (dryRun true, dann false)
- Dashboard-Env: VERCEL_API_TOKEN, UD_RESELLING_* (IT)

STATUS: Website-Management → Frontend-Kanäle (v0)
LIVE-BETRIEB: Inhalte über Teil D (content_push) — v0-Chat oder Cockpit. Revalidate automatisch wenn Secret + Route stimmen.
NIEMALS: Register-Token (frt_…), REVALIDATION_SECRET oder AgencyOS-Keys in Client/öffentliche Repos.
TOKEN (IT/MCP): Dashboard → Frontend-Kanäle → Register-Token; oder cockpit_center_project_init + register_frontend_deployment
KANÄLE: website = Center-Website | signage = Kiosk (Teil H) | companion = NOW! (Teil I)

Zeichen: 3674 / 5000

Vercel-Hosting & Dashboard-Statistik (alle Frontends)

v0-Layouts sind zusätzliche Kanäle — Cockpit-Website, Signage und Preview bleiben. API immer Dashboard.

Ausführlich: Parallele Frontends (Website, Signage, Companion)

ThemaKurz
Daten/APIImmer Dashboard — CORS erlaubt Vercel
v0 verbindenWebsite-Management → Frontend-Kanäle (v0) — ein Ort; nach Deploy oft automatisch (Teil J)
Handoff-QRKiosk + Companion gleiche Origin; POST + PATCH /api/public/handoff-sessions
Companion → QR-SessionsSichtbar wenn POST (QR) + PATCH (Companion-Open) genutzt wird
Statistik sichtbar machenUmami (umami-app-config?app=companion, center_id in Events) + POST /api/analytics/usage-events; Companion-URL mit centerId

Für IT (JSON-Beispiele, Feldlisten): Beispiele & Medien

Nutzungsstatistik: Seitenaufrufe werden anonymisiert erfasst. Im Umami-Dashboard nach diesem Pfad filtern: /content-creator-handbuch/center-website-v0-api-ready-to-go