Zum Hauptinhalt springen

Website mit v0 und Cockpit

Du kopierst die grauen Kästen in v0, trägst den Center-Kurznamen ein und beschreibst, wie die Website aussehen soll. Die langen Zeilen in den Kästen nicht umschreiben. Die braucht v0. Du musst sie nicht verstehen.

v0 baut das Aussehen. Das Cockpit bleibt die Quelle für Shops, News, Farben und Texte. Was im Cockpit fehlt, erscheint auch auf der Website nicht — das gehört dann ins Cockpit, nicht als erfundener Text in v0.

Neu im Team?

Zuerst ★ Start hier und den Kurzpfad (8 Schritte). Die ausführliche Begleitung: v0-Website A–Z. Diese Seite ist die Kopiervorlage.


Was / Warum / Wer / Wo

WasZehn kopierbare Kästen (A–J) für die festen Regeln in v0. Plus ein paar Texte nur für den Chat, falls etwas hakt.
WarumOhne diese Regeln rät v0 — dann fehlen echte Center-Daten oder die Seite lädt die falschen Adressen.
WerRedaktion kopiert und beschreibt. IT schaltet live (Vercel, Domain).
WoKästen hier → in v0 unter Instructions. Center-Kurzname aus dem Cockpit.

Dein Ablauf

  1. v0 öffnen (v0.dev) und im Cockpit den Center-Kurznamen (Slug) aufschreiben.
  2. In v0 Instructions öffnen. Kästen laut Tabelle unten anlegen. Jeden grauen Kasten kopieren → einfügen → speichern. Cockpit D heißt in v0 Cockpit Workflow.
  3. Im Text von Teil A steht HIER_SLUG_EINTRAGEN — dort den Kurznamen einsetzen.
  4. In v0 beschreiben, wie die Seite aussehen soll, oder Screenshots reinziehen. Keine Shop-Namen erfinden.
  5. Entwurf an die IT geben — Übergabe.

Unbenutzte Slots leer lassen. Hat die IT die Kästen auf dieser Seite aktualisiert: alte Instructions in v0 ersetzen, nicht unten anhängen.

Daten nur im Cockpit

Shops, News, Events und Angebote gehören ins Cockpit — nicht als Platzhalter in v0. Teil D hält v0 auf diesem Weg.


Welche Instruction?

v0 erlaubt genau 10 Instructions, je höchstens 5000 Zeichen (nur der Text im grauen Kasten). Ein elfter Slot geht nicht. Die Zahl unter jedem Kasten ist die Zeichenanzahl.

SituationDiese Kästen
Normale Center-WebsiteA–E immer. J nach dem ersten Live-Gang.
Lageplan / Centerplanzusätzlich F
Ein Design für mehrere Centerzusätzlich G. In Teil A den Kurznamen nur als Vorschau lassen — live entscheidet die Internet-Adresse. Beim Start in v0 schreiben: „Ein Layout für alle Center; Inhalte kommen pro Domain aus dem Cockpit.“
Stele / Kiosk im CenterH und I zusammen
Nur Website, ein Center, kein PlanA–E (+ J später). F, G, H, I leer lassen.
SlotName in v0Inhalt in einem Satz
1Cockpit AWelche Daten die Website holt, inkl. Formulare
2Cockpit BWie Antworten aussehen und der Code aufgebaut ist
3Cockpit CBilder, Chat, Design — damit die Seite nicht nach Vorlage aussieht
4Cockpit WorkflowPflicht: Inhalte ins Cockpit, Impressum und Datenschutz
5Cockpit ETempo, Barrierefreiheit, Suchmaschinen, Ausfallsicherheit
6Cockpit FCenterplan
7Cockpit GMehrere Center, ein Layout
8Cockpit HStele / Kiosk
9Cockpit IHandy-App zur Stele (NOW!)
10Cockpit JNach dem ersten Deploy: mit Cockpit verbinden

Nie als Instruction speichern (nur in den Chat, wenn nötig): siehe Abschnitt Nur für den Chat. Sonst wäre es ein 11. Slot.

Anderer Weg ohne diese Instructions? Design als ZIP ins Cockpit → Templates.

Hakt etwas nach dem Kopieren? → Wenn etwas nicht erscheint.


Kern — immer A bis E

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

Kasten kopieren. Nur HIER_SLUG_EINTRAGEN durch den Center-Kurznamen ersetzen. Die erste Adresse (https://dashboard.cockpit-os.de) nur ändern, wenn die IT eine andere vorgibt.

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: impressum|datenschutz|kontakt|anfahrt|services|oeffnungszeiten — GEO: Teil D/E)
- 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: Host POST /api/chat im v0-Projekt (mallpilot-chat-core, Teil C). Legacy bis Migration: …/visitor-chatbot
- 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: 4983 / 5000

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

Unverändert kopieren. Kein Ausfüllen.

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“

Unverändert kopieren. Danach reicht im Chat ein Satz wie „Bitte prüfen“, wenn die Seite zu generisch wirkt.

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.

Bot (mallpilot-chat-core 0.8.3+ — alle v0-Kanäle Website/Signage/Companion):
- Master /mallpilot-chat-core-kJyZM1UHiHR | github.com/sawmuedev/mallpilot-chat-core — Reference-Copy packages/mallpilot-chat/, nie inline.
- UI: getChatWidgetConfig → MallPilotSiteShell (Website SearchBar+Chat+Voice; Kiosk/Companion gleiche Widgets, andere Shell).
- Config: public-visitor-surface; enabled=false/companionChatEnabled. Runtime: Host /api/chat (docs/api-chat-route.production.example.ts), chatApiUrl=/api/chat, MALLPILOT_CHAT_SOURCE ab Core 0.8.4. Analytics: chatInteractionsPost+chatGapsPost. Kein OpenAI-Key im Browser. Legacy: visitor-chatbot nur bis Migration.
- Consent: chatConsentAccepted:true vor erster Nachricht (Teil E). Legacy visitor-chatbot: pageContext (Teil D) + Chat „Chatbot-Verdrahtung“ für contentBlocks.

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

Datenschutz/Tracker-Scan + 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 (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 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; Radius 4–8px.
3) Typografie: Display+Body als unverwechselbares Pairing PRO CENTER via next/font. Verboten: Inter, Roboto, Poppins, Montserrat, Space Grotesk, Arial, System-UI. Je nach DNA z.B. Fraunces, Bricolage Grotesque, Sora, Newsreader. Body 16–18px, line-height ≥1.5.
4) Layout-Archetyp intern: 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): Font-Paar, Archetyp, Details aus Center-Eigenschaften — baseColor warm→Serif prüfen, kühl→Grotesk; Mieter-Mix (Mode→editorial, Nahversorgung→warm, Gastro→plakativ). Zwei Center nie gleiche Kombination.
DATEN VOR LAYOUT: Shops, Events (30 Tage), Angebote, Gastro-Anteil zählen; Startseiten-Sektionen danach. <30 Shops: kuratiert statt Filter-Grid. Kaum Events: weglassen. Starker Foodcourt: eigene Gastro-Sektion.

SIGNATURE-DETAILS (genau 2/Center): z.B. übergroße ÖZ-Typo; Shop-Namen-Laufband; Duotone in baseColor; Trenner aus Logo-Geometrie; Hover als Farbfläche statt Schatten. 0=generisch, >2=unruhig. Bilder EIN Treatment — kein Stock-Mix.

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

VERBOTEN (Anti-Slop): Gradient-Text; Icon-im-Farbkreis; Card-Grid/Schatten als Default; Schatten nur bei echter Elevation; bunte Status-Pills; Stock; Violett-Blau-Verlauf; Glassmorphism; Floskeln; Eyebrow+Titel+Untertitel-Stuffing (Titel reicht oft); Hover/Scroll-Reveals ohne Grund (Teil E).
STATTDESSEN: Hierarchie mit Cockpit-Daten; leere Zustände = CTA; Status dezent. Motion: Teil E.
ICONS: nie Emojis im UI — ein Set (Lucide; nur genutzte, Teil E), einheitliche Strichstärke/Größe, Token-Farbe.

QUALITÄT (v0-PFLICHT): Vor fertig/Deploy/Go-Live/deslop: (1) npx impeccable detect app/ components/ lib/ — Exit 2→0; (2) Spot-it/Anti-Slop: zu generisch? ohne Center-Bezug? Wiederholung? + VERBOTEN; (3) DESLOP: Verhalten behalten, zuerst löschen; (4) Alltagssprache. v0 führt Impeccable aus. Einmal: 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: 4568 / 5000

Seite prüfen — ein Satz im Chat reicht

Bevor du „fertig“ sagst oder an die IT übergibst, schreib v0 einen der Sätze. v0 prüft intern (Teil C). Du brauchst kein Terminal.

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, zum Beispiel „Verlauf 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.

Chat-Texte für Bot, Suche oder Plan-Probleme stehen unten unter Nur für den Chat — nicht zwischen die Instructions legen.

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

Pflicht. In v0 heißt der Slot Cockpit Workflow, nicht Cockpit D.

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 + GEO — jede Website):
- /impressum + /datenschutz: page-content, Footer-Link; leer → Redaktion/MCP; Go-Live erst befüllt (Teil J). Datenschutz: Umami, Chatbot, Formular, Hosting (Teil C/A).
- GEO-ROUTEN (SMG-Audit — 200 oder Redirect, Nav/Footer): /oeffnungszeiten, /shops (+ /shops/{slug}), /aktuelles, /kontakt oder /anfahrt, /services; Gastro via Shop-Kategorie oder /gastronomie; optional /parken, /faq.
- page-content für GEO laden (nicht erfinden): kontakt, anfahrt, services, oeffnungszeiten → Park/Auto/ÖPNV/Services-Fakten (Teil E).

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 → alle p0Blockers im Code (Teil E/J); Tool nur lesen reicht nicht

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: 4783 / 5000

Einzelshop oder Kette?

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: „Kette“ nur bei echten Marken mit mehreren Filialen. Ein lokaler Mieter ist ein Einzelshop — sonst landet er fälschlich in der Ketten-Liste.

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

Unverändert kopieren. Tempo, Barrierefreiheit und Suche stecken hier — du musst die Fachbegriffe nicht kennen.

cockpitOS — Teil E (Performance, Barrierefreiheit, SEO/GEO P0, Resilienz)

Ziel: 60fps ohne Ruckeln, kleiner JS-Bundle, schnelle Interaktion (INP), für alle bedienbar, ab Tag 1 SEO/GEO P0.

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/WCAG AA — Pflicht; Scanner=Erstload/axe):
- Semantik: header/nav/main/footer; eine h1; h2–h6 ohne Sprung; html lang="de".
- Alt aus API (Shop/Event); dekorativ alt="". iframe title (Maps/Embeds).
- Kontrast ≥4.5:1 (Akzent auf Weiß prüfen/abdunkeln).
- Tastatur (Menü/Slider/Akkordeon/Chat); Fokus-Ring (nie outline:none ohne Ersatz); Skip-Link.
- <button> Aktion, <a href> Nav; Icon-only → aria-label. Dialoge (Cookie/Chat/Modal): Fokus-Trap, Esc, Fokus zurück.
- Formulare: label; Fehler Text aria-describedby+aria-live, nie nur Farbe. Kein Bild-/Google-Captcha (Honeypot; Teil A).
- Touch ≥44px. Centerplan: Liste parallel (Teil F) — kein Canvas-only.

SEO/GEO P0 (ab erstem Deploy; Port seo-utils.ts → lib/seo-jsonld.ts):
- robots.ts: Preview disallow+noindex; Live allow+Sitemap; AI-Crawler nicht extra blocken (nur /api/,/_next/).
- sitemap: dynamisch alle /shops/{slug}+Aktuelles-Details+200-Statik; kein 404 (/anfahrt ohne Route → Redirect oder weglassen).
- generateMetadata: unique title/desc/canonical/OG je Route (Shop: „{Name} – {Center}").
- JSON-LD=SSR-HTML: Center hours+email+geo(wenn lat/lng); /shops ItemList; /shops/[slug] Store; Aktuelles Event/Offer/NewsArticle+datePublished.
- SSR-Fakten: Shop-Karten→/shops/{slug}; Park/Auto/ÖPNV+Services aus page-content; curl: ÖZ+Adresse ohne JS.
- Routen 200: /oeffnungszeiten,/shops,/aktuelles,/kontakt|/anfahrt,/services (+Gastro). Details: cockpit_v0_geo_standards (D/J).

DATENSCHUTZ / TRACKER-SCAN (Pflicht — Erstload OHNE Banner-Klick):
- Nur Umami via <V0AnalyticsSnippet centerId={uuid} />; Config: public-visitor-surface → data.tracking + visitorPrivacy (kein /public-config). Umami/Embeds erst NACH Opt-in (cc_cookie analytics|external); Ablehnen gleichrangig. Ohne Opt-in: null Analytics-Requests. Opt-out optional: tracking.trackingOptOut → localStorage cockpit-tracking-opt-out=1.
- VERBOTEN: GA/gtag, GTM, Meta-/FB-Pixel, Hotjar, @vercel/analytics, Speed Insights, Google-Fonts-CDN, Formular-Keylogging, Pixel vor Consent.
- Fonts: nur next/font. Maps/Embeds: Platzhalter bis Cookie external.
- Headers (next.config headers | vercel.json): Content-Security-Policy + Strict-Transport-Security (HSTS).
- Canvas/WebGL nur Centerplan (Teil F), nie Profiling. Erlaubt: Bunny-CDN, Dashboard-API, Umami nach Opt-in — keine weiteren Tracker-Hosts.

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/catch → fallback/*.json (shops/news/events/bundle); optional Banner „Offline-Modus".

ISR/CACHE (Publish & Live): POST /api/revalidate + REVALIDATION_SECRET. Nach content_push: Dashboard-Revalidate + cockpit_revalidate_website. Publish-Fenster News/Events/Angebote: serverseitig filtern + Revalidate — nicht stale-forever.

ABNAHME (IT): Long Tasks >50ms reduzieren; optional ANALYZE=true npm run build. Ziel Lighthouse mobil Perf ≥90, A11y ≥95 (Gate: Teil J).

Zeichen: 4983 / 5000

Bei Bedarf — Plan, mehrere Center, Stele

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

Nur wenn die Website einen Lageplan braucht. Kasten kopieren, nichts kürzen.

cockpitOS — Teil F (Centerplan: SVG, Hybrid, 3D) — allein ausreichend; kein Extra-Slot F2 nötig

DATEN (öffentlich, kein website-config)
centerId = by-slug → id (NICHT floorId aus Centerplan-Edit-URL)
1) GET …/public-visitor-surface → data.centerplan: initialViewMode (2d|3d), initialZoom3D, hoverColor3d/Svg, noExtrudeLayerIds[], showLogosOnPlan; data.v0Integration.centerplan
2) GET …/api/wayfinding/floors?centerId= → floors[]: mapSvg (String!), mapImage, shopViewBoxes, mapLocations[], enable3D
Zuerst omitMapSvg=true (zählen); dann floorNumber=0|1|… / Server-Fetch; oder cockpit_center_context include=floors_summary
3) Optional GET …/wayfinding/centerplan?centerId=
4) Routing: GET …/entrances; POST …/wayfinding/routing — endLocationId=mapLocations[].id, startWaypointId=entrances[].id
5) Optional shops?publicWebsite=true&status=Aktiv&limit=5000 für Logos/Namen
Pro Etage: mapSvg leer → 2D+mapImage (CDN cockpitos.b-cdn.net); sonst 3D aus mapSvg. Mapping: mapLocations+shops → svgId→Location, linkableSvgIds.

HYBRID: mapSvg=<image>+path/polygon id="shop-…"; 2D <image> pointer-events:none; shopViewBoxes svgId→viewBox
SVG-IDs: shop-… extrude/klick | service-… dünn | entrance-/mall-floor-/foodcourt- | display- Signage | decorative… skip | logos/nopointer flach | hover-layer transparent | #routes unsichtbar

KLICK/NAV: svgId===DOM-id → shop.id; nie svgId=UUID; nicht per Name. 2D click path/g/polygon. HOVER: svg mousemove→elementFromPoint (nicht mouseover paths); mouseleave→null; hoverColorSvg. Ziele /shops|/services/{slug|id}; Modal intern nie Cockpit-URL; ?shop=uuid. EMBED /embed/centerplan: chrome/show*/embedBg/viewMode/floorNumber/THEME — Controls nicht von außen. A11y: Shop-Liste+Suche parallel; Info-Card Fokus+Esc.

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

3D-PIPELINE (Pflicht wenn 3D): Deps three + @react-three/fiber + drei
1) strip fill/stroke url(#…) — sonst SVGLoader-Crash
2) SVGLoader.parse(mapSvg)→paths; id=node.id ODER parent <g id>
3) classify shop-*|service-*|mall-floor|decorative skip; #routes unsichtbar; logos flach; noExtrude/data-no-extrude flach
4) createShapes→ExtrudeGeometry; Path-Fehler try/catch skip; depth shop60 foodcourt50 floor20 service4; rotateX(-PI/2); scale(1,-1,1); shop GAP 0.95
5) Gruppe scale={[0.01,-0.01,-0.01]} + BBox zentrieren
6) Kamera iso: fov50, dist=max(w,d)/initialZoom3D, pos center+(d,d,d)*0.707
7) Hybrid: <image> NIE extrudieren — nur path/polygon
8) Klick onPointerUp→svgId→mapLocation→shop.id
NICHT mapImage-only fallback wenn mapSvg existiert; NICHT 3D entfernen ohne diese Pipeline; GLB nur wenn mapSvg leer.

FEHLER: mapSvg prüfen (oft "<svg"); kein <img>/dangerouslySetInnerHTML statt SVGLoader; Debug mapSvgLen/shop-/mapLocations; 2D-only nur mapSvg leer ODER keine shop-Polygone ODER enable3D=false.

ROUTEN: POST routing primär; Fallback #routes-Graph. Anker p-shop-… p-service-… pf-stairs… pfb-elevator…
CODE: Server floors+surface; Client Canvas; Performance→Teil E.
VERBOTEN: Demo-Plan | website-config | Error in JSX | #routes sichtbar | decorative/hover als Shops | Cockpit-URL als Detail | mapImage extrudieren | SVG als <img> für 3D
Bei Plan-Klick-Problemen: Chat-Prompt „Centerplan“ unten (kein 11. Instruction-Slot).

Zeichen: 3375 / 5000

Klicks fehlen oder Shop öffnet die falsche Seite? → Centerplan-Klicks im Chat (kein Extra-Slot).

Teil F2 — 3D hakt trotz Teil F

Wann: Teil F ist drin, v0 zeigt trotzdem nur ein flaches Bild statt 3D. Nicht als Instruction speichern.

Genau 10 Slots

A–J sind die zehn Plätze. F2 und die anderen Chat-Texte nie als Extra-Instruction anlegen.

Teil F2 — nur diesen Kasten einmalig in den v0-Chat (nicht als Instruction speichern)

cockpitOS — Chat-Prompt F2 (nur bei Fehlern trotz Teil F — KEIN Instruction-Slot)

Ziel: 2D/3D wie Center-Website. Meist reicht Teil F. NICHT mapImage-only wenn mapSvg existiert; NICHT 3D ohne SVGLoader-Pipeline entfernen.

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

Zeichen: 2499 / 5000 — nur Chat, kein Instruction-Slot

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

Nur wenn mehrere Center dasselbe Layout teilen. Sonst leer lassen.

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

PRINZIP: Mehrere Center teilen EIN v0-Layout (gleiches websiteTemplate). Name/Shops/Hero/Plan/Farben/Fonts pro Center aus API — kein Layout-Duplikat pro Center. Pflicht bei Multi-Center; Einzel-Center: optional (fester Slug Teil A).

VERBOTEN: centerId/Slug in Komponenten hardcoden; HIER_SLUG_EINTRAGEN in Pages; Pro-Center-Kopien von Homepage/Shops; Links immer /{slug}/… auf Custom Domain (/shops ohne Slug).

PFLICHT lib/cockpit/resolve-center.ts → { centerId, slug, websiteTemplate } | null
1) host ohne Port/www 2) Prod (nicht *.vercel.app): GET …/api/centers/by-domain?domain= → center.id 3) Preview/Dev: ?center=slug | DEFAULT_CENTER_SLUG | optional {slug}.vercel.app 4) by-slug → centerId 5) public-visitor-surface → websiteTemplate 6) optional EXPECTED_WEBSITE_TEMPLATE → bei Abweichung 404

ARCHITEKTUR: middleware Host/Query; layout resolveCenter→CenterProvider; Fetch immer mit centerId; neues Center=Domain in Cockpit+Vercel, kein neues v0-Projekt.
LINKS: Custom Domain ohne Slug; Preview /{slug}/… via lib/cockpit/paths.ts.

SEO/RECHT PRO DOMAIN: Metadata/canonical/OG/Favicon/JSON-LD host-basiert; sitemap+robots pro Host (Preview noindex Teil E/J); /impressum+/datenschutz je Center (Teil D) — nie gemeinsamer Text; Design-DNA (Teil C) Farben/Signature aus API.

VERCEL: A) ein Projekt alle Domains (shared) B) mehrere Projekte nur DEFAULT_CENTER_SLUG anders C) Recast/Docker parallel. Pflicht NEXT_PUBLIC_DASHBOARD_URL=https://dashboard.cockpit-os.de. Shared: COCKPIT_VERCEL_PROJECT_MODE=shared, DEFAULT_CENTER_SLUG nur Preview; VERBOTEN Token/Slug als feste Prod-Env. Dedicated: TOKEN+CENTER_SLUG.

ENV-AUDIT bei Setup: Env listen; Modus shared|dedicated; Shared→Token/Slug entfernen; Dedicated→passen; REVALIDATION_SECRET nicht neu wenn Team Shared; vor Vercel-Änderung fragen (Tabelle Ist/Soll).

RECAST (smg-*-template): Dockerfile/compose/GHCR/health/register/revalidate behalten — bei Design nicht löschen; next.config standalone; Push→GHCR; Recast-Env wie Teil J.

DATEN: nach resolveCenter APIs Teil A; Layout JSX identisch. CHAT/FORM: centerId aus resolveCenter; pageContext (Teil C).
ABNAHME: Domain A≠B Inhalte, gleiches Layout; Preview ?center=; keine hardcodierte UUID in app/components; Sitemap nur eigenes Center.

Zeichen: 2308 / 5000

Nur MEC-Templates (Hero-Maße und Sprachen)

Hero-Maße: Canvas 1440×680, Mobil 375 / Bild 322 / Kasten 352. Pro Slide contentBox (512×546, left 35, top 83, opacity 80) und Schriftgrößen 90 / 40. Fehlende Felder füllt das Cockpit. Nicht die alten 516×516 oder 70 % hart einbauen.

Sprachen: Umschalter nur, wenn das Cockpit mehr als eine Sprache liefert (Förde: Deutsch/Dänisch, Oder-Center: Deutsch/Polnisch). Einsprachige Center: keinen Schalter bauen.

Teil H — Stele und Kiosk

Wann: v0 baut eine Stele oder ein Info-Terminal im Center — kein vergrößertes Handy. Die Handy-App dazu ist Teil I.

Mehr: Premium AI / Wayfinding · Kiosk + Companion NOW! · System-Infos 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

Der Plan füllt den Schirm. Bedienung sitzt unten und in den Etagen-Buttons — oben und unten am Gehäuse ist oft tot (nicht klickbar).

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

cockpitOS — Teil H (Kiosk/Stele + AI Premium)

NUR KIOSK/STELE — Companion → Teil I. Signage = H + I (10 Slots, kein K-Slot).

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

TERMINAL-LAYOUT (Pflicht)
1) PLAN = HERO min. 75% — 2D/3D (Teil F)
2) HEADER kompakt ~40–48px — Logo, Uhr — kein Hero-Banner
3) FLOOR-PILLS floating im Plan
4) TOOLBAR unten: Suche, Shops, Route, Handoff-QR, ggf. Begleitung einladen
5) Shop/Route = kompakte Cards — keine Mobile-Sheets
6) Design-DNA Teil C

TOUCH & BEZEL
Bezel tot oben/unten — Taps in Toolbar + Pills. Touch ≥48px, kein Hover-only. Kontrast für Tageslicht. 3D: bottomUIFraction ~0.25.

URLS (v0 entscheidet)
NEXT_PUBLIC_KIOSK_PATH frei. Monorepo /ai-premium nicht Pflicht. Nach Deploy: Kanäle signage+companion (Teil J).

AI PREMIUM — DASHBOARD-STEUERUNG (Pflicht)
GET …/signage-template-options?template=ai-premium → options.kiosk + options.companion
JEDEN enable*-Toggle, Label, Farbe daraus — nie hardcoden (API liefert Defaults gemergt)
enableOffers/Food/Services/Highlights/Nearby/AiSearch/BottomQuickMenu/HandoffQr/GroupSession/A11y/DoohIdle + accentColor, labels, wayfindingSource, idleTimeoutSeconds

QR-TYPEN (nicht verwechseln)
- enableHandoffQr: „Auf Handy weiter" → POST handoff-sessions → sessionKey in Companion-URL (Teil I)
- enableGroupSession: „Begleitung einladen" → POST group-session → joinCode; QR für zweite Person (nicht Route übernehmen)

DATEN & API
1) public-visitor-surface → apiHints (signageConfigGet, signageTemplateOptionsGet, signageDisplayConfigGet, signageNearbyGet, doohSignageActiveGet, hotPicksGet, handoffSessionPost, groupSessionPost, touchscreensGet)
2) signage-config → touchscreens[], URLs
3) Stele: ?touchscreenId= → floorNumber aus API
4) Startup parallel: template-options, display-config?displayId=, hotpicks, shops, services, news, dooh/active
5) Teil A — kein Dummy

KIOSK-BETRIEB
Idle-Reset ~90s: Suche/Route/Chat zurücksetzen — nächster Besucher sieht nichts vom Vorgänger. Kein Cookie-Banner am Terminal. Fehler: Fallback, nie weißer Screen.

CHAT/KI-UI
mallpilot-chat-core (Master /mallpilot-chat-core-kJyZM1UHiHR, Reference-Copy, Teil C): Host /api/chat, MALLPILOT_CHAT_SOURCE=kiosk, enableAiSearch; keine eigene KI-Shell. Legacy bis Migration: visitor-chatbot client=kiosk.

PHASE 1 FEATURES (an enable* koppeln)
Centerplan-Hero, Etage/Stele, Shops+Detail, Slide-ups, KI-Suche, Quick-Menü, A11y, Handoff, Gruppen-QR, DOOH

VERBOTEN
Mobile-Layout | Plan als Mini-Karte | nur Kiosk ohne Companion (Teil I) | Toggles im Code | Session nach Idle-Reset

DOKU (Chat): ai-premium-v0-paritaet-und-dashboard-steuerung.md
v0-Prompt: „Terminal-Kiosk Plan 75%+, AI Premium aus signage-template-options, fehlende Features laut Paritäts-Doku."

Zeichen: 2688 / 5000

Teil I — Handy-App zur Stele (NOW!)

Wann: Zur Stele gehört die Handy-App. Besuchende scannen den QR an der Stele und machen auf dem Smartphone weiter (gleiche Route, gleicher Shop).

Immer zusammen

Stele (Teil H) und Handy-App (Teil I) gehören ins gleiche v0-Projekt — gleiches Aussehen der Daten, zwei verschiedene Oberflächen.

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 — Pfade wählt v0 (Env), nicht Cockpit:
- NEXT_PUBLIC_KIOSK_PATH (z.B. /kiosk) → Stele (Teil H)
- NEXT_PUBLIC_COMPANION_PATH (z.B. /now) → Mobil (Teil I)
Monorepo /companion/t/premium ist Referenz, keine Pflicht.
Handoff-QR: {origin}{COMPANION_PATH}?centerId&sessionKey&shop&screen
Getrennte Deployments: companionPublicUrl aus signage-config

ARCHITEKTUR
Shared: lib/cockpit/, lib/centerplan/ (Teil F), 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 aus mallpilot-chat-core (Teil C: Host /api/chat, MALLPILOT_CHAT_SOURCE=companion); nur companionChatEnabled; Consent 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: 4475 / 5000

Nach dem ersten Live-Gang — Teil J

Nach dem ersten Vercel-Deploy. Redaktion bleibt in v0 — das Cockpit erfährt die Live-Adresse automatisch.

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

Erst nach dem ersten Deploy anlegen. Unverändert kopieren.

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) GEO P0: cockpit_v0_geo_standards — alle anwendbaren p0Blockers im Code (nicht nur gelesen); lib/seo-jsonld.ts; selfVerification+curl-Test; x/10 P0 + fehlende Redaktions-Fakten (Park/ÖPNV) als To-dos melden
2) robots/index: Live-Domain allow + Sitemap; Preview-noindex nur vercel.app; canonical + metadataBase = Custom Domain
3) /impressum + /datenschutz befüllt (page-content, Teil D) + Footer — ohne diese KEIN Go-Live
4) Datenschutz (Teil E): Cookie blockt Umami/Externes bis Opt-in; kein GA/GTM/Vercel-Analytics/Pixel; CSP+HSTS
5) Kontakt-/Vermietungsformular getestet (formRecipients)
6) 404/error gebrandet (Teil B); Favicon + OG-Image
7) A11y (Teil E): Lighthouse A11y≥95/Perf≥90; aria-label; Dialog-Trap; Plan Listen-Alternative (Teil F)
8) GEO-Routen 200 (Teil D); ÖZ/specialDays/Anfahrt/Park-Fakten SSR = Cockpit + JSON-LD match HTML; Sitemap ohne 404
9) Qualitäts-Check Teil C (Impeccable Exit 0) — nicht nur behauptet
10) Deslop (Teil C): Verhalten behalten; Dead Code zuerst löschen

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: Center-Website → 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: 4086 / 5000

Nur für den Chat (nie als Instruction speichern)

Diese Texte nicht in die zehn Instruction-Slots legen. Nur einfügen, wenn etwas hakt — per Copy in den v0-Chat.

ThemaWann
Chatbot-KartenAntwort zeigt keine Angebots-/Shop-Karten
Bot-FeatureChat soll etwas Neues können (Core-Repo)
GEO P0Vor Go-Live: Suche und KI-Antworten vorbereiten
GEO prüfenVor Audit oder zweiter Kontrolle
Centerplan-KlicksPlan sichtbar, Klicks fehlen oder falsche Zielseite
3D nacharbeiten (F2)Nach Teil F nur flaches Bild statt 3D
Stele-Lücken (K)Stele soll Cockpit-Schalter wie AI Premium nutzen

Chatbot — nur in den Chat (kein Slot)

Wann: Chat soll Karten unter der Antwort zeigen (Angebote, Shops). Einmalig in den v0-Chat, nicht als Instruction.

cockpitOS — Chatbot-Verdrahtung (Antwort → Karten)

KONTEXT: Teil C aktiv (mallpilot Host /api/chat + Artifact-Karten aus Core). Legacy 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.

Bot-Feature — nur in den Chat (kein Slot)

Wann: Der Chatbot soll etwas können, das er noch nicht kann. Nicht in der Website nachbauen — zuerst im gemeinsamen Bot-Projekt mallpilot-chat-core. Diesen Kasten in den Chat dort (oder an die IT) geben.

cockpitOS — Bot-Feature mallpilot (Rückspiel Core-Repo)

KONTEXT: Bot-UI nur aus mallpilot-chat-core (0.8.3+, MallPilotSiteShell) — nicht duplizieren. Alle v0-Kanäle: Host /api/chat + MALLPILOT_CHAT_SOURCE. Zuerst Chat-to-Chat Master /mallpilot-chat-core-kJyZM1UHiHR.
Kanal: chatbot | companion | kiosk (Env MALLPILOT_CHAT_SOURCE) | Center: {centerId}

FEHLENDES FEATURE (1 Satz):
{Was soll der Bot können?}

AKZEPTANZ (2–3 Bullets):
- …
- …

COCKPIT/API (nur lesen):
- public-visitor-surface → data.chatbot + data.bots
- public-visitor-surface → apiHints.chatInteractionsPost (Teil C)

AUFGABE FÜR CORE-REPO:
1) Feature in mallpilot-chat-core implementieren (Props/Hook/Widget — kanalübergreifend wenn sinnvoll)
2) Version/Release notieren
3) Website/App: Dependency updaten, nur Config/Branding anpassen — kein Fork der Bot-Logik

VERBOTEN: Chat-Widget, contentBlocks-Renderer oder Consent-Flow im Website-Repo neu bauen.

Suchmaschinen / GEO — nur in den Chat (kein Slot)

Wann: Vor dem Live-Gang, wenn die Seite für Suche und KI-Antworten vorbereitet werden soll. Einmalig in den v0-Chat (MCP nötig). Parken und ÖPNV müssen als echte Texte im Cockpit stehen — sonst bleibt die Bewertung niedriger.

cockpitOS — GEO P0-Umsetzung (KEIN Instruction-Slot)

1) cockpit_v0_geo_standards(centerId) — alle anwendbaren p0Blockers im Code umsetzen (nicht nur lesen).
2) lib/seo-jsonld.ts anlegen — Port apps/center-website/lib/seo-utils.ts.
3) P0-Kern: Center-JSON-LD openingHoursSpecification+email+geo(nur lat/lng); /shops ItemList+href="/shops/{slug}"; Shop-Detail Store+unique title; dynamische sitemap ohne 404; Park/Auto/ÖPNV+Services aus page-content SSR; curl findet ÖZ+Adresse ohne JS.
4) selfVerification aus Tool abhaken; Kurzbericht: x/10 P0 (+ fehlende Redaktions-Fakten als To-dos, nicht halluzinieren).
Referenz: seo-utils.ts, sitemap.ts, docs.cockpit-os.de/center-website/seo-ai-geo

GEO nochmal prüfen — nur in den Chat (kein Slot)

Wann: Vor Go-Live oder vor einem Audit. Platzhalter ersetzen, in den v0-Chat.

GEO Re-Check — Center-Website (SMG-Audit-Simulation). Kein Redesign — Diagnose + Fix-Liste.

Center: [NAME] | Slug: [slug] | centerId: [uuid] | Live-URL: [https://…] | Template: [mec-…]

Schritt 0 (MCP): cockpit_v0_geo_standards, cockpit_center_context (center,shops,news,events,offers,services), cockpit_get_center_website_config, cockpit_public_center_by_slug — Cockpit vs. Live abgleichen.

Schritt 1 (Crawl ohne JS): Pflicht-Routen finden (Nav/Sitemap): /, /oeffnungszeiten, /shops, /gastronomie|Gastro in Shopliste, /parken|/kontakt, /anfahrt, /services, /aktuelles, /kontakt, /faq, /sitemap.xml, /robots.txt — je URL: Status, SSR-Fakten im HTML?, aus Nav oder geraten?

Schritt 2 (Readiness grob): Technical (Routen 200, canonical, SSR) | Structured Data (JSON-LD-Typen, Match HTML) | Content (Shops, Gastro, Parken, ÖZ) | Freshness (Events mit Datum). Robots: GPTBot/AI-Crawler nicht blockiert?

Schritt 3 Ground-Truth: opening_hours, special_opening_hours, address, phone, shops, restaurants, parking_*, services, public_transport, car_directions, events — ≥3 Lücken = kritisch.

Output (strikt): (1) Executive Summary Readiness __/100 + P0-Blocker Ja/Nein (2) P0-Fixes nummeriert (3) Seiten-Matrix (4) Ground-Truth-Gaps (5) Structured-Data-Audit (6) Top-10 Audit-Fragen (7) Cockpit vs. Live (8) Sprint-Roadmap (9) Re-Check: SMG /check Ziel Visibility≥75%, Quality≥65%.

Regeln: JSON-LD muss sichtbarem Text entsprechen; Shop-Namen nur aus DOM-Text; nichts schönreden. Referenz: cockpit_v0_geo_standards, seo-ai-geo.md

Centerplan-Klicks fehlen — nur in den Chat (kein Slot)

Wenn der Plan sichtbar ist, aber Klicks fehlen oder Shops auf die falsche Website gehen: diesen Kasten einmalig in den v0-Chat.

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 K — fehlende Stele-Features

Genau 10 Slots

Die Steuerung der Stele steckt in Teil H. Teil K ist nur ein Chat-Text — nicht als „Cockpit K“ speichern.

Wann: Die Stele soll dieselben Schalter aus dem Cockpit nutzen wie das bisherige AI-Premium-Template. Kein Slot — der letzte Instruction-Kasten ist Teil J (siehe oben).

Ausführlich: AI Premium v0-Parität.

Teil K — nur diesen Kasten einmalig in den v0-Chat (nicht als Instruction speichern)

cockpitOS — Chat-Prompt K (AI Premium Lücken — KEIN Instruction-Slot)

KONTEXT: Teil H + I aktiv. Dashboard-Optionen aus signage-template-options?template=ai-premium.

AUFGABE: Vergleiche unsere v0-Signage-App mit der Cockpit-Doku „AI Premium v0-Parität“ (Checkliste Phase 1).
Implementiere fehlende Kiosk-Features und koppel sie an options.kiosk.enable*.
Companion-Lücken → Teil I. URLs bleiben bei unseren Pfaden (Env). Keine Dummy-Daten.

PRÜFEN: Handoff-QR vs Gruppen-QR getrennt? Jeder enable*-Toggle aus API? dooh/active für Signage?

Zeichen: ~520 / 5000 — nur Chat, kein Instruction-Slot


An die IT übergeben

Was v0 ausgibt (ZIP oder Link) weitergeben. Zwei Sätze mitgeben:

  1. Bitte auf Vercel dieselbe Cockpit-Adresse setzen wie in Teil A.
  2. Bitte die Website nach dem Deploy mit dem Cockpit verbinden (Anleitung A–Z, Abschnitt F3, plus Instruction Teil J).

Ein Center → oft ein eigenes GitHub-Repo und ein Vercel-Projekt (Ordnung).
Mehrere Center, ein Layout → ein Repo und ein Vercel-Projekt mit allen Center-Domains (Multi-Center).

v0-Websites sind zusätzliche Kanäle. Die Daten kommen immer aus dem Dashboard. Ausführlich: Parallele Frontends.


Wenn etwas nicht erscheint

Zuerst die einfachen Checks. Die technischen Details darunter nur, wenn die IT mitliest oder der Plan leer bleibt.

SymptomZuerst prüfen
Keine Shops / leere SeiteStimmt der Kurzname in Teil A? Ist die Website im Cockpit aktiv? Gibt es veröffentlichte Shops?
Keine BilderIT fragen (Medien-Adresse).
Chat fehltIm Cockpit: Chatbot eingeschaltet?
Playlist / DOOH leerPlaylist im Cockpit aktiv und im richtigen Zeitraum? Kurzname exakt wie in DOOH? Website am Center aktiv?
Plan ohne Klicks oder Shop öffnet die falsche SeiteInstruction F neu einspielen. Bei Bedarf den Chat-Text „Centerplan-Klicks“.
Hybrid-Plan wirkt beim Zoomen weichBild groß und scharf hochladen (lange Kante eher 2000–3000 px). Mehr: Hybrid-Centerplan · KI-Masterprompt.
3D fehlt, nur flaches BildTeil F neu einspielen. Wenn es dann noch hakt: Chat-Text F2.
Technik (IT) — Center-ID, Etage, leere Pläne

Die URL …/dashboard/centerplaene/{UUID}/edit enthält die Etagen-ID, nicht die Center-ID. Für die öffentliche API immer die Center-ID aus GET …/centers/by-slug/{slug} → Feld id verwenden. Sonst sind Etagen leer oder ohne Zuordnungen — das wirkt wie „keine Mappings“.

  • Shop-Klick auf dem Plan: Verknüpfung nur über GET …/wayfinding/floorsmapLocations (svgId + Shop), nicht über die reine Shops-Liste.
  • Weg zum Shop: POST …/routing mit Start aus …/entrances und Ziel = mapLocations[].id. Das Liniennetz #routes nicht als sichtbare Mall-Linien zeigen.
  • mapLocations leer, aber in der Datenbank gibt es Zuordnungen: öffentliche API liefert nur aktive Shops, Filialen, Services, Büros. Archivierte Einträge erscheinen nicht.
  • Nur eine Etage in v0: oft sind beide da, der Chat kürzt große Antworten. Zuerst ohne Plan-Zeichnung zählen, dann Etage einzeln laden.
  • 3D aus der Plan-Zeichnung (mapSvg) bauen, nicht aus dem Hintergrundbild (mapImage).
  • Shop-Klick muss auf eure Shop-Seite gehen (/shops/…). Das Cockpit liefert nur die Shop-ID, keine v0-URL.
  • Zusatz-Parameter wie ?includeMapLocations= an der Floors-Route werden ignoriert. Leere Liste = keine aktiven Zuordnungen oder falsche Center-ID.
Technik (IT) — Suche / GEO ohne Extra-Slot

Kein 11. Instruction-Slot. Der Kern steckt in Teil D, E und J. Vor Go-Live den Chat-Text „GEO P0-Umsetzung“, danach bei Bedarf „GEO nochmal prüfen“. Parken und ÖPNV müssen als Fakten im Cockpit stehen — nur das Tool lesen reicht nicht.


Querverweise

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