Website mit v0 und Cockpit — nur Schritte
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: 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, …).
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.
10 Slots (alphabetisch in v0 — A … J):
| Slot | Instruction | Wann |
|---|---|---|
| 1 | Cockpit A | immer (Routen) |
| 2 | Cockpit B | immer (JSON/API) |
| 3 | Cockpit C | immer (Medien, Chat, Design, Vercel) |
| 4 | Cockpit D | immer (Workflow — Pflicht) |
| 5 | Cockpit E | immer (Performance & Resilienz) |
| 6 | Cockpit F | Centerplan / Wegfindung |
| 7 | Cockpit G | nur Multi-Center (sonst frei) |
| 8 | Cockpit H | Signage/Kiosk (mit I) |
| 9 | Cockpit I | Companion (mit H) |
| 10 | Cockpit J | nach erstem Vercel-Deploy |
Nicht als Instruction: Teil F2 (3D-Feintuning) → bei Bedarf in den v0-Chat (spart Slot 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 Cockpit → Templates.
1. Kurz vorbereiten
- v0 öffnen (v0.dev).
- Im Cockpit den Slug deines Centers aufschreiben (kurzer Name).
2. Instructions in v0 anlegen (Kern + optional)
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.
- In v0: Instructions öffnen.
- Alphabetisch A → J anlegen (Cockpit A … Cockpit J). Je Teil den Kasten unten (Reihenfolge A–J auf dieser Seite) kopieren → einfügen → speichern.
- Cockpit D heißt in v0 Cockpit Workflow (Pflicht).
- Cockpit J erst nach erstem Vercel-Deploy (enthält Vercel-Env Team Shared vs. Projekt).
- Optional: F (Plan), G (Multi-Center), H+I (Signage). F2 nur Chat, nicht als 11. Instruction.
- Nach Team-Shared-Umstellung: Teil A, C, J in bestehenden v0-Projekten ersetzen (nicht nur anhängen).
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:
- Bitte auf Vercel
NEXT_PUBLIC_DASHBOARD_URLsetzen — gleiche Adresse wie Teil A. - Bitte Deploy-Hook ans Cockpit (Anleitung A–Z Abschnitt F3 + Instruction Teil J).
- Bitte auf Vercel
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/floors→mapLocations(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.
- Wegfindung: Entweder
POST …/routingmitstartWaypointId(aus…/entrances) undendLocationId(=mapLocations[].id) — oder Pfad ausmapSvg/#routesselbst berechnen;#routesnicht als sichtbare Mall-Linien für Besucher übernehmen. - Keine mapLocations / leere floors:
centerIdwirklich die Center-UUID aus by-slug? Nicht die UUID aus der Centerplan-Edit-URL (das istfloorId). - 3D „kaputt“ / v0 will nur 2D:
floors[].mapSvgprüfen (String mit<svgundshop-?). 3D aus mapSvg mit SVGLoader — nicht ausmapImage. Instruction Teil F (Abschnitt „V0-Typische Fehler“) neu einspielen. - mapLocations leer trotz
?includeMapLocations=…: Diese Route wertet nurcenterIdaus — Zusatz-Parameter werden ignoriert;mapLocationssind immer enthalten, wenn sie in der DB existieren. Leere Arrays = keine gepflegten Markierungen 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.
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 (IT setzt auf Vercel — v0 liest das nicht selbst):
- Team Shared (einmal, jedes Projekt verlinken + Redeploy): COCKPIT_DASHBOARD_URL, REVALIDATION_SECRET, COCKPIT_FRONTEND_CHANNEL=website
- Nur dieses Projekt: NEXT_PUBLIC_DASHBOARD_URL (= API-Basis oben), COCKPIT_REGISTER_TOKEN (frt_…), COCKPIT_CENTER_SLUG (= HIER_SLUG_EINTRAGEN)
- Gleicher Key auf Projekt- und Team-Ebene: Projekt-Env gewinnt — keine widersprüchlichen Doppel.
Next.js öffentliche Site: Server Components wo möglich; alle URLs mit dieser Basis.
WARNUNG UUID: …/dashboard/centerplaene/XXXXXXXX/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, templatePublicContent, Chatbot-UI, 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 — Cockpit-Zeitfenster wie Aktuelles-Bundle; Filter z. B. contentCategorySlug=kinowerbung; ohne published=nur Legacy; forToGo=true optional
- Bundle …/aktuelles-bundle (News+Events+Offers+Jobs zusammen)
- Hot Picks …/hotpicks (≠ homepage-tiles …/homepage-tiles)
- Baustellen …/news?constructionDiary=true
- Einzel-Angebot …/offers?offerId={uuid|slug}
- Events …/events — wie Bundle (publish-Fenster; status default Aktiv; forToGo optional)
- Offers/Jobs …/offers | …/jobs — Zeitfenster wie Bundle
- Services …/services?publicWebsite=true&limit=5000
- Büros …/api/offices?centerId={centerId}&status=Aktiv
- CMS …/page-content (Seiten-Reiter: pageType filtern)
- 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)
- Homepage-Kacheln …/homepage-tiles
- Centerplan …/api/wayfinding/centerplan?centerId={centerId}
- Floors …/api/wayfinding/floors?centerId={centerId} — mapLocations dabei; Extra-Query-Flags ignorieren
- 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, nicht 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 im v0-Projekt: 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 an Dashboard:
POST …/api/centers/{centerId}/send-contact-inquiry
POST …/api/centers/{centerId}/send-rental-inquiry (zusätzlich company?, spaceInterest?)
- Pflichtfelder: name, email, message. Antwort: { success: true, recipient? } oder { success:false, error }.
- Anderer Empfänger als Standard-Kontakt (z. B. Seifenkisten → mu@…): NICHT beliebig im Body — zuerst in Cockpit freigeben:
templateContent.formRecipients.seifenkisten = "mu@schickma.de" (MCP: update_center_website_config), dann Proxy-Body formKey: "seifenkisten".
Alternativ recipientEmail nur wenn diese Adresse bereits in Cockpit konfiguriert ist (Allowlist). customRecipient = Alias für recipientEmail.
- Kein RESEND_API_KEY in v0/Vercel. Standard-Empfänger: Center-Stammdaten email oder Website-Reiter Kontakt (Template).
Teil B — nur diesen Kasten in Instruction „Cockpit B“
CockpitOS — Teil B (Antworten & Öffnungszeiten)
JSON-Formen:
- GET by-slug: flaches Objekt OHNE success/data — u. a. 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: data.center. Template (Hero/Footer): data.templatePublicContent.content (rootKey z. B. rgw). Chat-UI: data.chatbot. apiHints: u. a. pageContentGet, homepageTilesGet, aktuellesBundleGet, hotPicksGet, …
- Hot Picks: GET …/hotpicks → { success, hotPicks: [ { id, contentType, type, title, description, image, priority, position, startDate, endDate, channels, content, … } ] }. „Hot Pick“ = im Cockpit gesetztes Highlight mit Verweis auf Inhalt (Shop/Event/News/Angebot/…); Layout (Karten, Slider, Liste) ist Sache des Frontends. **Verwechslung vermeiden:** `GET …/homepage-tiles` sind die konfigurierbaren Startseiten-Kacheln — andere Datenstruktur, anderer Zweck als `hotpicks`.
- DOOH public: GET …/dooh/public/playlists → { success, playlists }. GET …/playlist?slug= → { success, playlist | null } (null = inaktiv oder außerhalb Gültigkeit). Items: type VIDEO|IMAGE|SLIDESHOW|IFRAME, payload mit 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 — für Karte: Floors-API oder `?includeWayfindingLinkages=true` → `wayfindingLinkages[]` (svgId, floorName, mapLocationId).
- GET wayfinding/floors?centerId=: { success, floors: [ … ] }. Der Query-Parameter centerId muss die **Shopping-Center-UUID** sein (by-slug → id), nicht die UUID aus …/centerplaene/…/edit (das ist floorId). Pro Etage u. a. **mapSvg** (kann Hybrid sein: `<image>` + Vektor-Layer), **mapImage** (CDN), **shopViewBoxes** (optional JSON-String: svgId→viewBox), mapLocations[], optional shopSvgIdsWithOffers. mapLocations: svgId (= id im SVG, z. B. shop-56), type (z. B. shop, service), shop / shopLocation / service / office mit UUID und Anzeigedaten. Ablauf bei Klick auf SVG: mapLocation mit passendem svgId suchen → shop.id (oder andere Entität) für Detail-Link. Fehlendes svgId oder fehlender Shop = Zuordnung im Cockpit noch nicht gepflegt — nicht per Namen erraten.
- GET entrances: { success, entrances: [ { id, floorId, position, floor, … } ] } — id typischerweise als startWaypointId für POST routing.
- POST …/wayfinding/routing: Mehrstufige Route über Waypoints in der DB; Ziel-Endpunkt über endLocationId = **UUID der MapLocation** (mapLocations[].id), nicht svgId. Ohne gepflegte Waypoints/Verbindungen im Cockpit kann die API leer/fehlschlagen — dann SVG-#routes-Ansatz oder Hinweis „Weg nicht verfügbar“.
- SVG #routes: Die Gruppe ist **Datenlayer** (sichtbare „Original-Linien“ der Mall-Zeichnung nicht 1:1 zeigen); Besucher sehen nur die berechnete Route als Overlay. Referenz-Logik im Monorepo: @mall-os/wayfinding (parseSvgRoutes, findPathThroughNetwork); in reinem v0-Export ggf. vereinfacht nachbauen.
- Andere Reads: oft { success, data } — bei Unsicherheit Struktur prüfen.
Öffnungszeiten (openingHours):
- null, String oder Objekt. Objekt-Keys: monday … sunday (englisch).
- Geöffnet: z. B. { "hours": "10:00 - 20:00" }. Geschlossen: z. B. { "open": "closed", "close": "closed" }.
- Beispiel:
{"monday":{"hours":"10:00 - 20:00"},"tuesday":{"hours":"10:00 - 20:00"},"wednesday":{"hours":"10:00 - 20:00"},"thursday":{"hours":"10:00 - 20:00"},"friday":{"hours":"10:00 - 20:00"},"saturday":{"hours":"10:00 - 20:00"},"sunday":{"open":"closed","close":"closed"}}
- Niemals Roh-JSON für Besucher anzeigen — immer lesbar (Tabelle/Liste, deutsche Wochentage). String: JSON.parse mit try/catch, sonst Fallback-Text.
Öffentliche GETs: CORS *. POST page-content ohne Auth nicht von fremden Websites. Chatbot- und Routing-POST können 429 liefern — Nutzer freundlich informieren.
Fehler-Handling: if (!res.ok) → leeres Array/null/Defaults — nie Error-Objekt in JSX (React #418). Kein GET website-config/agencyos ohne Auth.
Code-Struktur (Pflicht):
- App Router: schlanke `page.tsx`; Daten in **Server Components**; `use client` nur in kleinen UI-Blöcken.
- `components/` **nach Feature** (shops, layout, wayfinding …), ggf. `ui/`. Nicht 1000+ Zeilen/Datei — Logik, View, API trennen.
- `lib/api/cockpit` (eine **Basis-URL**), **eine Funktion pro Ressource** + **Typen** (`lib/types/…`); Fehler zentral, nicht pro Seite duplizieren.
- `lib/utils/`, `lib/media` f. CDN (s. C); Konstanten zentral. `app/layout` + Header/Footer **getrennt**; `error`/`loading` sinnvoll.
- Ziel: wartbar, auffindbare Fehler, kein Monolith.
Teil C — nur diesen Kasten in Instruction „Cockpit C“
CockpitOS — Teil C (Bilder, Chatbot, Deployment)
Bilder / Medien:
- API liefert oft relative Pfade: /uploads/…, centers/…, global/… oder volle URLs.
- Relative Pfade mit CDN-Basis prefixen. Default-Produktion: https://cockpitos.b-cdn.net
Beispiel: /uploads/x.png → https://cockpitos.b-cdn.net/uploads/x.png
- Env optional: NEXT_PUBLIC_BUNNY_CDN_URL (sonst Default wie oben).
Besucher-Chatbot:
- Zuerst public-visitor-surface → data.chatbot. Wenn enabled=false → kein Chat-Widget.
- Vor erster Nachricht: Zustimmung (consentTitle, consentDescription, privacyPolicyUrl).
- POST /api/ai/visitor-chatbot mit centerId (UUID) und messages [{role, content}]. Keine OpenAI-Keys im Client.
- Optional stream: true (SSE). Bei 429 kurzen Hinweis, kein Technik-Jargon.
Auth / Grenzen:
- Kein website-config / agencyos für anonyme Browser-Clients (401). Keine Secrets im Frontend.
- Template-Inhalte (Hero, Footer, Optionen, alle Templates): public-visitor-surface → data.templatePublicContent.content (rootKey z. B. rgw). Gleiche Antwort: data.v0Integration (Lesen-vs-Schreiben).
- Nie getWebsiteConfig(), getHeroSlides() o. Ä. gegen geschützte Config-URLs — bei !res.ok Fallback, nie Error in JSX (#418).
- Kontakt/Vermietung: kein Resend in v0 — Proxy app/api/kontakt → Dashboard send-contact-inquiry (s. Teil A Schritt 5).
Umami Analytics — korrekte Implementierung:
Analytics-Config kommt von GET {NEXT_PUBLIC_DASHBOARD_URL}/api/centers/{centerId}/public-visitor-surface → data.tracking
NICHT /api/centers/{centerId}/public-config (existiert nicht), NICHT /api/agencyos/... (braucht Auth).
Referenz-Implementierung (Server Component, kein useEffect nötig):
const res = await fetch(`${process.env.NEXT_PUBLIC_DASHBOARD_URL}/api/centers/${centerId}/public-visitor-surface`)
const json = await res.json()
const tracking = json?.data?.tracking
if (!tracking?.analyticsEnabled || !tracking?.umamiWebsiteId) return null
return (
<Script
src={`${tracking.umamiUrl || 'https://analytics.cockpit-os.de'}/script.js`}
data-website-id={tracking.umamiWebsiteId}
strategy="afterInteractive"
/>
)
Wichtig: tracking.umamiUrl ist die Basis-URL des Umami-Servers (default: https://analytics.cockpit-os.de).
Vercel Env (IT — Details Teil A + Teil J):
- Team Shared verlinken: COCKPIT_DASHBOARD_URL, REVALIDATION_SECRET, COCKPIT_FRONTEND_CHANNEL
- Nur Projekt: NEXT_PUBLIC_DASHBOARD_URL (= Teil A), COCKPIT_REGISTER_TOKEN, COCKPIT_CENTER_SLUG — Redeploy nach Änderung
- .env.example mit Variablennamen (ohne Secrets) mitliefern
- Revalidate: app/api/revalidate/route.ts + REVALIDATION_SECRET (Shared); MCP cockpit_agencyos_revalidate_website nach großen Pushes
Vollständiger Referenz-Text (alle Teile zusammen): Cockpit-Doku Developer Guide → public-center-website-api-vertrag → Abschnitt Copy-Paste.
Design & UI-Qualität (Pflicht — keine generische „AI-Optik“ / AI slop):
- Vorher klären: **Zweck, Zielgruppe, Tonalität** — eine **deutliche** Richtung wählen und durchziehen: z. B. bewusst minimal, editorial/Magazin, retro-futuristisch, luxuriös, verspielt, brutalist, soft/pastel, industrial, art deco, organic — nicht immer „beliebig modern“. Eine Sache, die hängen bleibt, **bewusst** setzen; Intention schlägt Lautstärke.
- **Typografie:** Kein Einheitslook — **zwei klare Ebenen** (Display + Body), **unverwechselbare** Webfonts. Vermeiden: Inter, Roboto, Arial, System-UI-Silofonts, **Space Grotesk** (inzwischen abgenutzt) und andere „Jeder-Generator“-Kombis.
- **Farbe & Theme:** CSS-Variablen, **konsistente** Palette; **dominierende** Farbe(n) + scharfe Akzente, nicht fahles „alles grau“ und nicht das Klischee **Lila-Gradient auf Weiß**.
- **Layout & Raum:** bewusstes Layout: **Asymmetrie, Überlappung, diagonale Linien, Grid-Brüche** oder kontrolliert dichte Raster — **nicht** ständig zentrierte Card-Stacks nach Schema F.
- **Motion:** sinnvoll, nicht wahllos — CSS-Animationen wo reicht, ggf. Motion/Framer; **eine** gut orchestrierte Einstiegs-Sequenz (z. B. `animation-delay` staffeln) besser als viele sinnlose Micro-Hover-Effekte. Scroll/Hover, die zur Richtung passen. **Bei Ruckeln oder schwerer UI:** Teil **E** (Performance) hat Vorrang.
- **Hintergründe & Atmosphäre:** nicht nur Fläche — feine **Textur, Noise/Korn, Gradient-Mesh, Schicht-Transparenzen, Schatten, dekorative Ränder**, passend zur Richtung.
- **Komplexität = Vision:** Maximalistisch = mehr Deko/Animation/Detail; Minimal = Zurückhaltung, viel Weißraum, **typografische** Präzision.
- Niemals: austauschbare, kontextlose Oberfläche — **Jedes** Projekt soll sich **eigen** fühlen; zwischen hell/dunkel, Stilen und Stimmungen **variieren**, nicht in dieselben drei Fonts/Paletten laufen.
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, muss aus der CockpitOS-API kommen.
VERBOTENE MUSTER (niemals tun):
- getWebsiteConfig() / getHeroSlides() gegen /api/agencyos/…/website-config oder /api/centers/…/website-config ohne Auth (401)
- const shops = [ { name: "H&M", ... } ] // hardcodierte Shops
- const news = [ { title: "Summer Sale", ... } ] // hardcodierte News
- Platzhalter-Arrays oder Mock-Objekte für echte Inhalte
- Eigene API-Endpunkte erfinden die in der Doku nicht aufgeführt sind (z.B. /public-config, /analytics-config o.ä.)
- Bei API-Fehlern Error-Objekte rendern — immer Fallback (leeres Array, Defaults)
- Resend oder RESEND_API_KEY in v0/Vercel — E-Mail geht über Dashboard send-contact-inquiry / send-rental-inquiry (v0 baut nur app/api/kontakt Proxy)
- Für Analytics-Config einen neuen Endpunkt bauen: die Daten kommen von GET …/public-visitor-surface → data.tracking
PROJEKT-START: Neues Center-Website-Projekt immer so beginnen:
1. cockpit_center_project_init aufrufen (center_slug = Slug des Centers, api_base_url = API-Basis)
→ gibt centerId, Checkliste und Quick-Reference zurück
2. cockpit_agencyos_update_center_website_config aufrufen, wenn Website-Config (Theme, Analytics, Domain) gesetzt/geändert werden soll
3. Inhalte lesen: cockpit_agencyos_center_context (centerId)
4. Fehlende Inhalte direkt ins CockpitOS schreiben: cockpit_agencyos_content_push
5. Nie direkt in der Datenbank ändern — immer über die offiziellen MCP-Tools oder API
MCP-KERNTOOLS (Schreiben/Lesen):
- Start: cockpit_center_project_init(centerSlug | centerId) → centerId, Checkliste
- Lesen: cockpit_agencyos_center_context(centerId, include="shops,news,events,offers,…")
- Schreiben: cockpit_agencyos_content_push({ centerId, shops?, news?, events?, offers?, … })
- Config/Theme/Analytics: cockpit_agencyos_get_center_website_config + update_center_website_config
- Deploy melden: cockpit_agencyos_register_frontend_deployment (Teil J)
- Cache: cockpit_agencyos_revalidate_website nach größeren Pushes
- Tool unbekannt: cockpit_mcp_discover_tools — nie raten, nie DB direkt
CONTENT WORKFLOW — PROAKTIV ANBIETEN:
- Wann immer ein Redakteur einen Inhalt beschreibt (Shop, News, Event, Angebot), sofort anbieten ihn direkt ins CockpitOS zu schreiben.
- Nicht warten bis der Nutzer explizit fragt. Immer fragen: "Soll ich das direkt ins CockpitOS eintragen?"
- cockpit_agencyos_content_push direkt verwenden (kein Zwischenschritt nötig, Live-Push ist aktiviert).
- cockpit_agencyos_content_push_preview ist optional — nur aufrufen wenn der Nutzer explizit eine Vorschau möchte oder bei komplexen/mehrteiligen Aktionen.
- Nach dem Push: cockpit_agencyos_center_context aufrufen und kurz bestätigen dass der Inhalt angekommen ist.
VERÖFFENTLICHUNGSZEITFENSTER (wie im Cockpit „Sichtbar ab / bis“) — beim MCP-Schreiben mitliefern, nicht erwarten dass es „von selbst“ passiert:
- News in content_push: publishDate (+ optional publishEndDate).
- Events: startDate/endDate = Termin; zusätzlich publishStartDate/publishEndDate wenn die Website andere Sichtbarkeit als den Termin braucht.
- Angebote: startDate/endDate = Aktionszeitraum; zusätzlich publishStartDate/publishEndDate für die Website-Laufzeit. Hinweis: cockpit_agencyos_update_offer ohne Publish-Felder — für genau diese Fenster weiter content_push oder API-PUT nutzen.
- Öffentliche GETs **`…/news?published=true`**, **`…/events`**, **`…/jobs`**, **`aktuelles-bundle`**: verschwindet auf der Website **ohne zusätzliche Client-Logik**, sobald Cockpit-Ende erreicht ist (serverseitig wie Bundle). Beim **Anlegen/Schreiben** trotzdem dieselben Daten setzen wenn die Redaktion Zeitfenster nennt.
LIVE-BETRIEB & DEPLOY:
- Inhalte parallel im v0-Chat oder Cockpit — gleiche DB (content_push).
- Nach Vercel-Deploy: Instruction Teil J (register_frontend_deployment oder IT /api/cockpit-register).
- Status: Website-Management → Frontend-Kanäle (v0). REVALIDATION_SECRET auf Vercel für Auto-Revalidate.
ZIEL: Der Redakteur soll so wenig wie möglich manuell eingreifen müssen. Der Assistent übernimmt das Schreiben ins CockpitOS aktiv und proaktiv.
ANALYTICS (Umami):
- Website-Config über cockpit_agencyos_update_center_website_config setzen: analyticsConfig mit umamiWebsiteId + umamiUrl
- Den Tracking-Code niemals selbst in den Website-Code hartcoden — das Dashboard-System steuert Analytics über die Config
- Nach dem Setzen: GET /api/agencyos/v1/centers/{centerId}/website-config zur Verifikation
WENN INHALTE FEHLEN:
- Nicht erfinden, nicht mocken
- Sofort anbieten den Inhalt direkt über cockpit_agencyos_content_push anzulegen.
- Beispiel: "Ich sehe, dass noch kein Angebot vorhanden ist. Soll ich direkt eines im CockpitOS anlegen?"
Teil E — nur diesen Kasten in Instruction „Cockpit E“
CockpitOS — Teil E (Performance, Resilienz & flüssige UI)
Ziel: Seiten ohne Ruckeln (60fps), kleiner JS-Bundle, schnelle Interaktion (INP). Schönheit ja — aber keine Deko-Animation um jeden Preis.
ARCHITEKTUR:
- Standard: Server Components; "use client" nur für echte Interaktion (Slider, Overlay, Formular-State).
- Keine ganze page.tsx als Client nur wegen einem Button.
- Schwere Client-Bausteine: dynamic(() => import(...), { ssr: false }) + loading-Fallback.
- Keine vollständige shadcn/ui-Bibliothek — nur genutzte Komponenten; keine komplette Icon-Library (einzelne Icons importieren).
REACT (nur bei Listen/Slidern mit Ruckeln):
- Keine Inline-Objekte/Funktionen in großen Listen: style={{…}}, onClick={() => …} pro Item vermeiden.
- useMemo/useCallback nur bei nachgewiesenem Re-Render-Problem, nicht pauschal.
ANIMATIONEN (Pflicht):
- Nur transform und opacity (translate, scale, rotate).
- 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 abschalten oder stark verkürzen.
- Slider: kein Layout-Shift beim Bildwechsel; Intervall ≥ 5s; bei document.hidden pausieren.
SCROLL & EVENTS:
- scroll/mousemove/resize: 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 wie Teil C.
- Autoplay-Video nur muted loop, kurz und komprimiert.
BUNDLE:
- Eine Animations-Lib (CSS oft genug; Framer Motion nur wenn nötig).
- Keine ungenutzten Chart-/Map-/UI-Pakete.
ABNAHME (IT):
- Chrome DevTools Performance: Scroll + Klick, Long Tasks > 50ms reduzieren.
- React Profiler: Layout/Seite nicht bei jedem kleinen State neu rendern.
- Optional: ANALYZE=true npm run build (Bundle Analyzer).
Bei Konflikt mit Teil C „Motion“: Performance-Regeln hier haben Vorrang (z. B. kein backdrop-blur über vollem Hero).
RESILIENZ (Fallback wenn Dashboard/API kurz down — Pflicht):
Strategie: ISR + statisches JSON-Fallback + Try-Catch. Nie leere Dummy-Shops/News bei API-Fehler.
1) ISR (Incremental Static Regeneration):
export const revalidate = 300 (5 Min) auf Seiten/Segments mit Cockpit-fetch.
Vercel cached API-Antworten zwischen Requests.
2) Statisches JSON-Fallback (Build):
Skript vor next build (z. B. scripts/generate-fallback-json.mjs):
Holt Shops/News/Events/Aktuelles von NEXT_PUBLIC_DASHBOARD_URL + Center-Slug (Teil A).
Schreibt public/fallback/shops.json, news.json, events.json, aktuelles-bundle.json (+ optional surface.json).
package.json: "prebuild": "node scripts/generate-fallback-json.mjs" (oder in postbuild vor deploy).
3) Try-Catch zur Laufzeit:
async function loadShops() { try { const res = await fetch(…); if (!res.ok) throw new Error(); return await res.json(); }
catch { return JSON.parse(await readFile('public/fallback/shops.json','utf8')); } }
Gleiches Muster für news/events/bundle. Optional kleines Banner „Offline-Modus“ wenn Fallback aktiv.
ISR / CACHE (Publish & Live auf Vercel):
- Zusätzlich on-demand: POST /api/revalidate + REVALIDATION_SECRET (Route aus Monorepo-Vorlage)
- Nach MCP content_push invalidiert Dashboard; MCP cockpit_agencyos_revalidate_website nach großen Pushes
- Zeitfenster News/Events/Angebote: serverseitige Filter + Revalidate — nicht stale-forever
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
2) GET …/api/wayfinding/floors?centerId= → floors[]:
mapSvg (String!), mapImage, shopViewBoxes (JSON-String), mapLocations[],
enable3D, model3D?, shopSvgIdsWithOffers[]
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 — nie Original-Linien zeigen
KLICK & NAVIGATION
mapLocations: svgId === DOM-id → Detail-Link shop.id / service.id
Niemals svgId als Shop-UUID. Kein Mapping ≠ per Name erraten.
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 aus public-visitor-surface respektieren
- noExtrudeLayerIds + SVG data-no-extrude="true" → nicht extrudieren
- #routes in 3D-Szene 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
- Die API liefert 3D-taugliche Daten: floors[].mapSvg = SVG-String (oft beginnt mit "<svg")
- NICHT behaupten „kein verwertbares Format“ ohne mapSvg geprüft zu haben
- NICHT mapImage für Extrusion — mapImage = nur 2D-Hintergrund; 3D aus mapSvg-Vektoren
- NICHT mapSvg als JSON/XML-API missverstehen; NICHT <img> oder 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> in SVG nicht extrudieren — nur path/polygon mit id shop-… / service-…
- Einzelne Paths können ExtrudeGeometry werfen → Path überspringen, Rest rendern (try/catch)
- 2D-only (mapImage) 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
Center-UUID aus by-slug — nicht floorId aus Centerplan-Edit-URL.
Doku: centerplan/svg-struktur.md, hybrid-png-mit-polygonen.md (Cockpit-Docs).
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.
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
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.
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, …) kommen pro Center aus der API — nicht aus
dupliziertem Code. Welches Template (ilg, mec-shopbox, rgw, …) ist egal — Teil G gilt
für jede Template-Familie mit mehreren Centern.
ZIEL
Ein v0-Projekt pro Layout/Template-Familie — KEIN Duplizieren des Layouts pro Center-ID.
WANN EINSCHALTEN
Pflicht wenn mehrere Center dasselbe Layout nutzen (gleiches websiteTemplate).
Einzel-Center: Teil G optional — dann reicht fester Slug in Teil A.
VERBOTEN
- centerId oder Center-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 oder localhost):
a) Query ?center={slug}, ODER
b) process.env.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={cockpit websiteTemplate-Wert}:
Abweichung → 404/Fehlerseite, nicht 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)
- Kein zweites v0-Projekt pro Center — neues Center = Domain in Vercel + Cockpit pflegen
LINKS & ROUTEN
- Custom Domain: Pfade OHNE Slug (/shops, /aktuelles)
- Preview/Slug-Modus: /{slug}/shops — lib/cockpit/paths.ts generatePath(path, { slug, isCustomDomain })
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
Pflicht: NEXT_PUBLIC_DASHBOARD_URL=https://dashboard.cockpit-os.de (ohne Slash)
Optional: EXPECTED_WEBSITE_TEMPLATE=<websiteTemplate aus Cockpit>
Optional Preview: DEFAULT_CENTER_SLUG=beispiel-center-slug
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.
ABNAHME
- Domain A → Logo/Shops von Center A; Domain B → Center B, gleiches Layout
- v0-Preview: ?center=slug funktioniert
- grep: keine hardcodierte Center-UUID in components/ oder app/
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-Terminal — kein 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 = 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 — nicht 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
TOUCH & BEZEL (physische Stele)
- Oben/unten Bezel oft tot — Haupt-Taps in unterer Toolbar + Floor-Pills (greifbar)
- Touch-Targets min. 48px in Toolbar/Pills; kein Hover-only
- Handoff-QR in Toolbar → Companion (Teil I)
- 3D: bottomUIFraction ~0.25 damit Ziel-Shop über Toolbar sichtbar
DATEN
Teil A: floors/mapSvg, shops, surface — kein Dummy
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
v0-Prompt: „Kiosk Terminal-Style: Plan 75%+, kompakter Header, Floating Floor-Pills, schmale Toolbar unten.“
REFERENZ-DOKU (Cockpit-Docs):
- premium-ai-template.md (Premium AI / Wayfinding)
- kiosk-companion-now-app.md (Kiosk + Companion NOW!)
- kiosk-companion-system-info-for-v0.md (Feature-Liste für v0)
HANDOFF:
- Toolbar: QR/Button „Auf Handy weiter“ → POST /api/public/handoff-sessions → Companion (Teil I)
- Gleiche Origin für Kiosk + Companion (ein v0-Projekt / eine Vercel-URL)
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).
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
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 (Zone 62–86%, Teil H)
2) Session: POST /api/public/handoff-sessions { centerId, touchscreenId?, location? } → sessionKey
(v0: Proxy app/api/qr-sessions → Dashboard; Legacy: /api/qr-sessions auf Signage)
3) QR/Link: /companion?centerId=UUID&sessionKey=…&shop=…&screen=companion-home|wayfinding
4) Companion: URL-Params lesen → Ziel-Shop/Route setzen, optional Handoff-Banner
COMPANION UI (MOBIL)
- mobile-first Portrait; env(safe-area-inset-*)
- Fix oben: Logo + „NOW!“ | Fix unten: Concierge/Chat (Teil C visitor-chatbot)
- 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
ROUTEN COMPANION
/companion — Home (Karte + Highlights)
/companion/shops, /companion/shops/[id]
/companion/wayfinding (optional wenn Home schon Plan hat)
Menü: News, Events, Angebote → Cockpit GETs (Teil A)
DATEN
Wie Center-Website: public-visitor-surface, floors, shops, aktuelles-bundle, hotpicks
Kein website-config ohne Auth. centerId aus URL/Handoff.
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
QR-Sessions-Tab im Dashboard = nur Cockpit-Signage-Host — v0-Sessions nicht automatisch dort
DESIGN
Companion ≠ verkleinerter Kiosk — eigene Handy-Optik (Cards, Bottom-Sheets, Daumenzone unten)
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-62%-Zonen-Layout
- Hardcodierte Session ohne sessionKey in URL
- Dummy-Shops | getWebsiteConfig ohne Auth
Siehe auch: kiosk-companion-system-info-for-v0.md (Cockpit-Docs)
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 ans Cockpit melden)
WANN: Nach erstem Vercel-Deploy oder wenn sich die Deploy-URL ändert (neues Vercel-Projekt, neue Preview-Production-URL).
ZIEL: Cockpit soll die Live-Adresse kennen — Redaktion muss nicht ins Dashboard, um URLs einzutragen. Single Source of Truth bleibt Cockpit für Inhalte; diese Meldung betrifft nur die Frontend-Adresse.
AUTOMATISCH (bevorzugt, IT richtet einmal ein):
- Route app/api/cockpit-register + optional postbuild scripts/cockpit-register-after-deploy.mjs
- Vercel Env — zwei Ebenen (v0/Chat kann das nicht prüfen — nur Vercel Dashboard):
A) Team Shared (einmal, jedes Projekt: Settings → Env → Link Shared → Redeploy):
COCKPIT_DASHBOARD_URL, REVALIDATION_SECRET, COCKPIT_FRONTEND_CHANNEL (website|signage|companion)
B) Nur dieses Projekt (pro Center):
COCKPIT_REGISTER_TOKEN (frt_…), COCKPIT_CENTER_SLUG (Cockpit-Slug, muss zum Token passen),
NEXT_PUBLIC_DASHBOARD_URL (= gleiche Basis wie Teil A)
Optional Projekt: VERCEL_PROJECT_ID=prj_…, COCKPIT_VERCEL_PROJECT_MODE=dedicated|shared
Bestehende Projekte: Shared greift erst nach Verlinken + Redeploy — nicht automatisch rückwirkend.
MANUELL IM CHAT (wenn kein Hook):
1. Deploy-Origin ermitteln (https://…vercel.app ohne Pfad)
2. cockpit_agencyos_register_frontend_deployment aufrufen:
- centerId aus cockpit_center_project_init / by-slug
- channel: website (Center-Website) | signage (Kiosk) | companion (NOW!)
- origin: die Vercel-URL
- optional: vercelProjectId (prj_…), vercelProjectMode (dedicated|shared)
3. Kurz bestätigen: „Mit Cockpit verbunden — Änderungen erscheinen nach Publish automatisch.“
GO-LIVE CUSTOM DOMAIN (nur IT / mit Freigabe — nicht für jeden Chat):
- Nach Custom Domain im Cockpit: cockpit_agencyos_dns_status (Vorschau)
- Dann cockpit_agencyos_go_live_domain mit dryRun=true, danach dryRun=false
- Voraussetzung: Dashboard-Env VERCEL_API_TOKEN, UD_RESELLING_* (IT)
STATUS IM COCKPIT (nur ansehen, kein Pflicht-Schritt für Redaktion):
Website-Management → Frontend-Kanäle (v0) — Verbindung prüfen / „Zuletzt von v0 gemeldet“
LIVE-BETRIEB: Inhalte weiter über Teil D (content_push) — egal ob Nutzer im v0-Chat oder im Cockpit editiert. Nach Schreiben: Revalidate läuft automatisch, wenn REVALIDATION_SECRET auf Vercel gesetzt ist.
NIEMALS: Register-Token (frt_…) oder AgencyOS-Keys in Client-Code oder öffentliche Repos — nur Server/Vercel-Env.
TOKEN BESCHAFFEN (IT / MCP):
- Dashboard: Center → Website oder Website-Management → Frontend-Kanäle (v0) → Register-Token erzeugen (frt_…)
- MCP: cockpit_center_project_init liefert centerId; Deploy-Meldung mit register_frontend_deployment
KANÄLE:
- website = Center-Website (v0-Layout)
- signage = Kiosk/Stele (Teil H)
- companion = NOW!-App (Teil I) — oft gleiches Repo, eigener channel wenn separate Vercel-URL
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)
| Thema | Kurz |
|---|---|
| Daten/API | Immer Dashboard — CORS erlaubt Vercel |
| v0 verbinden | Website-Management → Frontend-Kanäle (v0) — ein Ort; nach Deploy oft automatisch (Teil J) |
| Handoff-QR | Kiosk + Companion gleiche Origin (ein v0-Projekt); POST /api/public/handoff-sessions |
| Companion → QR-Sessions | Handoff-Sessions aus v0 sichtbar wenn POST /api/public/handoff-sessions genutzt wird |
| Statistik sichtbar machen | v0: Umami (/api/umami-app-config?app=companion) + optional POST /api/analytics/usage-events mit centerId |
Für IT (JSON-Beispiele, Feldlisten): Beispiele & Medien
Nutzungsstatistik: Seitenaufrufe werden anonymisiert erfasst. Im Umami-Dashboard nach diesem Pfad filtern: /en/content-creator-handbuch/center-website-v0-api-ready-to-go