AgencyOS: cockpitOS anbinden (Magic Link & Integration API)
Diese Seite richtet sich an Entwickler:innen von AgencyOS (Team-Produkt unter team.cockpit-os.de). Sie beschreibt, was AgencyOS implementieren muss, um eine Vertrauensstellung mit dem cockpitOS-Dashboard herzustellen und Center sowie Inhalte über die REST-API anzusprechen. Dabei gibt es zwei Geltungsbereiche für den API-Key: eine Organisation (klassisch) oder nutzerweit (alle Center gemäß den Cockpit-Rechten der anbindenden Person, organisationenübergreifend inkl. zugewiesener Center ohne Organisation).
Produkt: AgencyOS · Dashboard-Implementierung (Monorepo): u. a. apps/dashboard/src/app/api/agencyos/…, apps/dashboard/src/app/agencyos/connect/page.tsx
Schlüssel ohne Magic Link (im Dashboard): Eingeloggte Nutzer:innen mit passender Berechtigung können unter Einstellungen → Integrationen eine AgencyOS-Integration anlegen bzw. den API-Key rotieren (GET/POST /api/agencyos/integrations, POST …/rotate) — sinnvoll z. B. für lokale Tools (MCP, Skripte). Vollständiger Key wie gewohnt einmalig in der Antwort, nicht dauerhaft in der UI.
MCP-Tool-Referenz: Alle registrierten Tools mit Parametern und Beispiel-Prompts — MCP-Tool-Referenz.
Remote-MCP (HTTP, Monorepo): Paket packages/mcp-cockpit-remote stellt dieselben MCP-Tools per Streamable HTTP bereit (für Organisationen mit Claude-„Remote MCP“-URL). Nicht Teil der öffentlichen Dashboard-API; Betrieb mit eigenem Secret COCKPIT_MCP_HTTP_BEARER und COCKPIT_AGENCYOS_API_KEY im Server-Env. Siehe Paket-README.
Ersetzen Sie {DASHBOARD_ORIGIN} durch die öffentliche URL Ihrer Dashboard-Instanz (z. B. https://dashboard.cockpit-os.de). Lokal oft http://localhost:3000. Die Variable NEXTAUTH_URL im Dashboard bestimmt die in Magic-Link-Antworten eingebettete Basis-URL.
Überblick: Was AgencyOS tun muss
- Magic Link anfordern (serverseitig, ohne Nutzer-Session im Cockpit):
POST /api/agencyos/magic-linkmitintegrationNameund optionalreturnUrl. - Nutzer:in zum Cockpit leiten: Response enthält
data.magicLink(Pfad/agencyos/connect?token=…). Dort meldet sich eine berechtigte Person an und wählt entweder eine Organisation oder „Alle Center mit meinen Zugriffsrechten“ (nutzerweiter Key). - API-Key per Polling holen: Solange
status === "pending", regelmäßigGET /api/agencyos/magic-link?token=…aufrufen. Sobaldstatus === "completed", liefert die Antwortdata.apiKey(Präfix typischsk_agencyos_) sowiedata.accessScope("organization"oder"user") unddata.organizationId(UUID odernullbei nutzerweitem Key). - Speichern: API-Key sicher in AgencyOS hinterlegen (Geheimnis, nicht in Logs/URLs).
- API nutzen: Alle folgenden Aufrufe mit
Authorization: Bearer <apiKey>gegen/api/agencyos/v1/….
Sicherheit und Redirect
- Den API-Key niemals in die Redirect-URL legen (kein Query-Parameter mit Secret). Die Übernahme erfolgt ausschließlich über das Polling des Magic-Link-Status.
- Wenn ihr eine
returnUrl(z. B. zurück nachhttps://team.cockpit-os.de/...) mitsendet, kann das Cockpit nach erfolgreichem Abschluss dorthin weiterleiten und setzt u. a.agencyos=connectedsowiecockpit_agency=connected– ohne Key. Den Key nur aus der GET-Antwort lesen, wennstatus === "completed".
Unterschied zu WordPress
| Aspekt | WordPress-Plugin | AgencyOS |
|---|---|---|
| Geltungsbereich | ein Center pro Website-Key | Organisation: alle Center dieser Org · Nutzer: alle Center, auf die die anbindende Person in Cockpit Zugriff hat (mehrere Orgs + ohne Org) |
| Verbindungs-UI | /wordpress/connect | /agencyos/connect |
| Content-Push | POST /api/wordpress/push-content (Key = Website) | POST /api/agencyos/v1/content/push mit centerId im JSON |
| Doku Push-Body | WordPress Push-Content | Gleiche Entity-Arrays (shops, events, …); siehe unten |
1. Magic Link erstellen
POST {DASHBOARD_ORIGIN}/api/agencyos/magic-link
- Auth: keine (öffentlich wie beim WordPress-Magic-Link).
- CORS:
Access-Control-Allow-Origin: *,OPTIONSunterstützt.
Body (JSON):
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
integrationName | string | ja | Anzeigename der Integration in Cockpit (z. B. "AgencyOS Produktion") |
returnUrl | string | nein | http:// oder https://; nach Erfolg optional Redirect aus dem Browser |
Erfolg (200):
{
"success": true,
"data": {
"magicLink": "https://…/agencyos/connect?token=mla_…",
"token": "mla_…",
"expiresAt": "2026-04-01T12:00:00.000Z"
},
"message": "Magic Link erstellt"
}
Hinweis: Token-Gültigkeit 15 Minuten ab Erstellung (sofern nicht vorher abgeschlossen).
2. Magic-Link-Status abfragen (Polling)
GET {DASHBOARD_ORIGIN}/api/agencyos/magic-link?token=<token>
- Auth: keine.
Solange die Verbindung aussteht, ist data.status typischerweise "pending". Nach Abschluss im Browser:
data.status === "completed"data.apiKeygesetztdata.integrationIdgesetztdata.accessScope:"organization"oder"user"data.organizationId: UUID der verbundenen Organisation odernull, wennaccessScope === "user"
AgencyOS-Implementierung: Nicht von einem festen organizationId im Key ausgehen. Bei accessScope === "user" ist organizationId absichtlich null; die erlaubten Center ergeben sich aus den Cockpit-Rechten des Nutzers (siehe GET /v1/centers).
Fehler: u. a. 404 ungültiger Token, 410 abgelaufen (bei noch pending).
3. Verbindung im Browser abschließen (nicht von AgencyOS-Server)
Dieser Schritt läuft im Cockpit mit NextAuth-Session; AgencyOS ruft ihn normalerweise nicht per Server-to-Server auf.
- UI:
GET /agencyos/connect?token=… - Organisationen laden (Session):
GET /api/agencyos/organizations(für die klassische Variante; nutzerweite Option ist auch ohne Einträge in der Liste möglich) - Abschluss:
POST /api/agencyos/magic-link/completemit JSON:- Organisations-Key:
{ "token": "…", "organizationId": "<uuid>" } - Nutzer-Key:
{ "token": "…", "accessScope": "user" }(keinorganizationIdnötig)
- Organisations-Key:
Die Response enthält u. a. apiKey, accessScope, organizationId (nullable) für die sofortige Anzeige im Browser – für AgencyOS ist weiterhin das Polling die Quelle der Wahrheit, damit der Backend-Prozess den Key zuverlässig erhält.
4. AgencyOS API v1 (Bearer API-Key)
Alle Endpunkte unter /api/agencyos/v1/ erwarten:
Authorization: Bearer <apiKey>
apiKey ist der aus Schritt 2 übernommene AgencyOS-Integration-Key (sk_agencyos_…) – entweder an eine Organisation oder nutzerweit gebunden (accessScope aus der Polling-Antwort).
CORS: Access-Control-Allow-Origin: * (u. a. für GET, POST, PATCH, OPTIONS).
Medien-Upload zu Bunny (Bilder & Videos)
POST {DASHBOARD_ORIGIN}/api/agencyos/v1/media/upload
- Auth:
Authorization: Bearer <apiKey> - JSON (Variante A):
{ "url": "https://…", "folder?": "agencyos/uploads" }— Ressource wird geladen und nach BunnyCDN kopiert (Bild oder Video). - JSON (Variante B):
{ "base64": "…", "mimeType?": "video/mp4", "filename?": "…", "folder?": "…" }— optional mitdata:mime;base64,-Präfix. - Raw Body:
Content-Type: image/*odervideo/*oderapplication/octet-stream; Query?folder=&filename=— Rohbytes direkt nach Bunny. - Größenlimits: in etwa 10 MB für typische Bilder, 100 MB für Video (Implementierung in
route.ts). - Antwort:
{ "success": true, "bunnyUrl": "https://…b-cdn.net/…" }(bereits Bunny-URLs werden unverändert bestätigt).
MCP: Paket @mall-os/mcp-cockpit-os, Tool cockpit_upload_media — Parameter url oder base64 (plus optional mimeType, filename, folder).
Mediathek listen: GET {DASHBOARD_ORIGIN}/api/agencyos/v1/media?centerId=<uuid> — Filter type, entityType, q, includeShared, limit, offset. MCP: cockpit_list_media.
UI (Redaktion): Video wie Bilder über FileUpload → POST /api/upload (Multipart, Bunny-Pfad z. B. centers/{centerId}/{entityType}/video/…) und „Aus Mediathek“; nicht zu verwechseln mit dieser AgencyOS-JSON-Route.
4.1 Shopping Center auflisten
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers
Response (200): { "success": true, "data": [ { "id", "name", "slug", "address", "city", "postalCode", "country", "status", "websiteEnabled", "organizationId", "agencyIntegrationId", "updatedAt" }, … ] }
Max. 500 Einträge, sortiert nach Name.
Bei accessScope === "organization" (Standard):
- alle Center mit
organizationId= Organisation des API-Keys, und - Center ohne Organisation (
organizationId: null), die von genau dieser Agency-Integration angelegt wurden (agencyIntegrationId= Integration des Keys).
Bei accessScope === "user":
- alle Shopping Center, auf die der anbindende Cockpit-Nutzer Zugriff hat (z. B. über
UserCenterAssignment, Heimat-Organisation, Super-Rollen – wie in Cockpit definiert), und - dieselben integrationsgebundenen „Waisen“-Center wie oben (ohne Organisation, aber
agencyIntegrationIddieser Integration).
So erscheinen „freie“ Center nur im Kontext der eigenen Integration, ohne fremde ungebundene Center zu leaken.
4.2 Shopping Center anlegen
POST {DASHBOARD_ORIGIN}/api/agencyos/v1/centers
Body (JSON) – Pflichtfelder:
| Feld | Typ | Beschreibung |
|---|---|---|
name | string | |
address | string | |
city | string | |
postalCode | string |
Optional: country (Default "DE"), slug (sonst automatisch aus Name, global eindeutig), description, phone, email, website, status (Default "active").
Organisation (wie im Cockpit):
| Feld | Typ | Beschreibung |
|---|---|---|
| Standard (Organisations-Key) | — | Ohne die folgenden Felder wird organizationId auf die Organisation des API-Keys gesetzt. |
| Standard (Nutzer-Key) | — | Nicht ohne Ziel-Org: Es muss entweder withoutOrganization/noOrganization/organizationId: null oder eine explizite organizationId (UUID) gesendet werden, für die der anbindende Nutzer in Cockpit berechtigt ist; sonst 400/403. |
withoutOrganization / noOrganization | boolean true | Center wird ohne Organisation angelegt (organizationId: null), bleibt aber dieser Integration zugeordnet (intern agencyIntegrationId), damit sie es in GET/Push weiter nutzen kann. |
organizationId | null | Gleiche Bedeutung wie withoutOrganization: true. |
organizationId | string (UUID) | Organisations-Key: nur erlaubt, wenn der Wert exakt der Organisation des Keys entspricht; sonst 403. Nutzer-Key: erlaubt, wenn der Nutzer diese Organisation verknüpfen darf (wie im Cockpit); sonst 403. |
Erfolg: HTTP 201, data enthält u. a. organizationId und agencyIntegrationId.
Fehler: 409 wenn slug bereits vergeben.
4.3 Einzelnes Center lesen / aktualisieren
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}
PATCH {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}
Zugriff, wenn das Center für diesen Key erlaubt ist (Organisations-Key: gleiche Organisation oder integrationsgebundenes Waisen-Center; Nutzer-Key: gemäß Nutzerrechten oder integrationsgebundenes Waisen-Center); sonst 404.
PATCH: nur gesendete Felder werden geändert. Erlaubt u. a. name, address, city, postalCode, country, phone, email, website, description, openingHours, status, slug, websiteEnabled. Pflichtfelder dürfen nicht auf null gesetzt werden.
Später an eine Organisation hängen: assignToKeyedOrganization, assignToOrganization oder linkToKeyedOrganization mit true – nur wenn das Center aktuell ohne Organisation ist und von dieser Integration stammt.
- Organisations-Key: Es wird die Organisation des Keys verbunden (
agencyIntegrationIdwird entfernt). - Nutzer-Key: Zusätzlich
attachOrganizationId(UUID) im JSON-Pflicht – Zielorganisation, an die gehängt werden soll; nur wenn der anbindende Nutzer dafür in Cockpit berechtigt ist; sonst 403. (agencyIntegrationIdwird entfernt.)
Website-Konfiguration (GET + partial PUT)
Lesen: GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/website-config
Liefert designConfig, seoConfig, contentConfig, legalConfig, parkingConfig, analyticsConfig, centerplanConfig, pagesConfig, templateContent (Template-Reiter).
Schreiben: PUT {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/website-config
Partial-Update; templateContent wird deep-merged.
Reiter-Index (Schema): GET {DASHBOARD_ORIGIN}/api/agencyos/v1/website-config-schema?websiteTemplate=ilg
oder ?centerId={uuid} — Dashboard-Reiter mit Speicherort, fields[] (exakte JSON-Pfade), v0PublicRead (öffentliche Live-Site) und MCP-Hinweisen für alle Website-Templates.
v0 / Claude Live-Website (LESEN, ohne Auth):
GET …/api/centers/{centerId}/public-visitor-surface → data.templatePublicContent (Template-Reiter: Hero, Footer, …) + data.apiHints.pageContentGet / homepageTilesGet.
Nicht GET …/website-config im Browser — 401. Schreiben bleibt MCP: cockpit_*. Antwort enthält v0Integration (Lesen-vs-Schreiben-Guide).
Page Content (Hero, SEO, customContent): POST …/page-content — beim Update werden nur mitgesendete Scalar-Felder geändert; customContent wird deep-gemerged (z. B. customContent.ilg.anfahrtBoxes ohne andere Seitenfelder zu löschen).
Empfohlener MCP-Workflow (ILG/RGW):
cockpit_website_config_schema(mitwebsiteTemplateodercenterId)- Reiter aus
tabs[]wählen →fields[]/templateContentPath/customContentPathlesen - Bestehende Werte per GET (
get_center_website_config/page_content) - Partial PUT/upsert nur mit geänderten Keys
- Revalidate (automatisch in API-Response, sofern konfiguriert)
MCP:
| Tool | Funktion |
|---|---|
cockpit_get_center_website_config | Volle Config lesen |
cockpit_update_center_website_config | Partial Update |
cockpit_website_config_schema | Reiter + Feldpfade pro Template |
cockpit_mcp_discover_tools | Tool-Index wenn Claude tool_search scheitert |
contentConfig.specialDays (Sonderöffnungszeiten): JSON-Array oder dasselbe als JSON-String. Pro Eintrag z. B. { "date": "2026-12-24", "label": "Heiligabend", "hours": { "open": "10:00", "close": "14:00" } } — geschlossen mit "hours": null. Optional "image" (URL).
Die Route schreibt in ShoppingCenter.specialDays und spiegelt bei strukturiertem openingHours (mit regularHours) zusätzlich openingHours.specialDays — analog zum Speichern im Cockpit „Center bearbeiten“.
4.3a Center-Kontext für KI lesen (Shops / Services)
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/context
Zugriff wie GET /v1/centers/{centerId} (Bearer-Key, sonst 404).
Liefert gebündelte Lesedaten für AgencyOS/KI, ohne die öffentliche Website-API zu nutzen:
| Query | Default | Max | Beschreibung |
|---|---|---|---|
include | center,shops | — | Komma-getrennt: center, shops, services, news, events, offers, categories, chains, floors_summary |
shopLimit | 500 | 2000 | Max. Anzahl kombinierter Einträge (Einzelshops + Filialen), sortiert nach Name |
serviceLimit | 200 | 500 | Nur wenn services in include |
newsLimit | 80 | 200 | Nur wenn news in include |
eventsLimit | 80 | 200 | Nur wenn events in include |
offersLimit | 80 | 200 | Nur wenn offers in include |
Filter: Shops und Filialen wie GET …/centers/{centerId}/shops?publicWebsite=true&status=Aktiv (inkl. Veröffentlichungsfenster). Services wie websiteServicePublicFilter (Center-Website-SSR).
News / Events / Angebote: Alle Datensätze dieses Centers (jeder Status, inkl. Entwurf), sortiert nach updatedAt absteigend — für Kontext, Dedupe und Abgleich mit AgencyOS. Keine Volltexte; pro Zeile u. a. id, title, slug, status, Daten, source, sowie agencyosPushId und wordpressPushId aus metadata, falls gesetzt.
categories: Wie getWebsiteShopCategories (Center + Global, minShopCount: 0, Status Aktiv für Zählung) — nur Referenzen zur Token-Optimierung: je Eintrag id, name, slug (keine Zähler/Icons/Farben im Kontext).
chains: Alle ShopChain, die über Shops oder Filialen (ShopLocation) an dieses Center angebunden sind (max. 500, nach Name) — nur id, name, slug.
floors_summary: Für jede aktive Etage ein kompaktes Objekt — u. a. floorId, name, floorNumber, mapSvgChars (Zeichenzahl von mapSvg ohne Auslieferung des Strings), suggestsHybridSvg (Heuristik: typische Hybrid-Marker im Markup), hasMapImageUrl, hasShopViewBoxes, shopViewBoxesKeyCount, mapLocationActiveCount. Für Svg-Markup, mapLocations und Routenplanung weiter die öffentliche Route GET /api/wayfinding/floors?centerId= oder das MCP-Tool cockpit_public_wayfinding_floors.
Response (200): { success: true, data: { center?, shops?, services?, news?, events?, offers?, categories?, chains?, floors_summary? }, meta: { …limits, counts } }
data.shops[]: kompakte Objekte u. a. id, name, category, slug, floor, location, status, isShopLocation, chain, logo, coverImage, displayId.
4.3b Wartung: Duplikate, Ketten-Merge, Domains, Centerplan
Zusätzliche AgencyOS v1-Routen (Bearer, Center-Zugriff wie bei anderen /v1/centers/{centerId}-Endpunkten):
| Route | Methode | Zweck |
|---|---|---|
…/centers/{centerId}/shop-duplicates | GET | Potenzielle Shop-Duplikate im Center (Namens-Ähnlichkeit, Query threshold 0.5–1, optional includeArchived). Antwort: Gruppen mit suggestedKeepId und archiveCandidateIds. |
…/chains/duplicates | GET | Potenzielle ShopChain-Duplikate global (Query threshold, Default 0.8). |
…/chains/merge | POST | Ketten zusammenführen (Quelle → Ziel): Filialen/Einzelshops umhängen, Quell-Kette löschen. Body: sourceChainId + targetChainId, optional dryRun, oder Bulk pairs[] (max. 25). |
…/centers/{centerId}/verify-domains | POST | DNS/HTTPS für Custom Domains prüfen und domainStatus / sslStatus in der DB aktualisieren. |
…/centers/{centerId}/wayfinding/floors | GET | Kompakte Etagen-Liste (IDs, Metriken, Zähler) ohne mapSvg-Body — für MCP/KI; volles SVG weiter GET /api/wayfinding/floors. |
…/centers/{centerId}/map-locations | GET, POST | MapLocations listen (floorId, activeOnly) bzw. anlegen. |
…/centers/{centerId}/map-locations/assign | POST | Shop/Service an SVG-Fläche zuordnen (floorId, svgId, upsert). |
…/map-locations/{locationId} | PATCH | MapLocation aktualisieren (Zuordnung, Geo, aktiv/inaktiv). |
Hilfslogik: apps/dashboard/src/lib/integration/ (find-shop-duplicates, find-chain-duplicates, merge-shop-chains, shop-name-similarity, agency-map-location). Duplikat-Archivierung kann über POST …/content/bulk-archive mit contentType: "shop" erfolgen.
4.4 Inhalte ins Center pushen
POST {DASHBOARD_ORIGIN}/api/agencyos/v1/content/push
Gleiches Konzept wie WordPress Push-Content: Body mit optionalen Arrays shops, events, news, offers, services.
Zusätzlich Pflicht:
| Feld | Typ | Beschreibung |
|---|---|---|
centerId | string | UUID des Centers; zulässig unter denselben Regeln wie GET /v1/centers/{centerId} (Organisations-Key vs. Nutzer-Key inkl. Waisen dieser Integration) |
Hinweise:
- Es gibt keine WordPress-
pages-Speicherung inWordPressWebsite; Fokus liegt auf den Entitäts-Arrays. - In Cockpit werden betroffene Inhalte mit
sourceagencyosmarkiert (analogwordpressbeim WP-Endpunkt).
Idempotenz (Events, News, Angebote, Services — wie Shops)
Für events[], news[], offers[], services[] gilt dieselbe externe Referenz wie bei Shops:
agencyosId,externalId,clientReferenceoderredaktionsReferenz, alternativ bei AgencyOS-Push einid, das keine Cockpit-UUID und keinwp_*ist.- Cockpit speichert den Wert unter
metadata.agencyosPushIdund findet beim nächsten Push dieselbe Zeile zum Update (kein Duplikat). - Reihenfolge der Zuordnung: Cockpit-UUID →
agencyosPushId→ WordPress-wordpressPushId→ slug → (nur WordPress) Legacy nach Titel.
Zusätzlich: publishDate bei News akzeptiert auch Alias publishedAt oder date.
Shops: Ketten & KI-Redaktion
Redakteure arbeiten in AgencyOS oft per natürlicher Sprache mit einer KI. Die API ist so ausgelegt, dass die KI nicht zwingend Cockpit-UUIDs kennen muss:
| Ziel | Empfohlene Felder im shops[]-Eintrag |
|---|---|
| Dieselbe Marke nicht hundertmal anlegen | Pro logischem Shop eine stabile Referenz: agencyosId oder externalId (oder bei source-Push id als freier String, keine UUID) – wird im Cockpit unter metadata.agencyosPushId gespeichert und beim nächsten Push zum Update verwendet. Zusätzlich slug pro Center, falls vorhanden. |
| „Deichmann ist eine Kette“ | kette, marke, chainName, brandName oder shopChainName mit dem Markennamen → Cockpit verbindet den Shop mit dieser ShopChain (existiert sie nicht, wird sie angelegt; nur neue Kettendaten, keine Löschungen). |
| Bereits bekannte Kette (UUID aus Cockpit) | shopChainId oder chainId setzen. |
| Nur Einzelshop ohne Kette | standaloneShop / einzelshop / clearShopChain: true oder modus: "einzel" / "standalone". |
Fachlicher Hinweis: Im Cockpit gibt es zusätzlich Filialen als ShopLocation (Kette + Center). Der Push landet zunächst auf dem Shop inkl. shopChainId-Verknüpfung; eine vollständige Filialen-Spiegelung ist davon getrennt und kann bei Bedarf später ergänzt werden.
Filialen: Branche pro Standort (nicht Kette)
Wenn dieselbe Kette in mehreren Centern unterschiedliche Branchen-Namen braucht (z. B. Aufräumen doppelter Kategorie-Labels nur in einem Center):
| Schritt | API / MCP |
|---|---|
| Kategorien lesen | GET …/categories?centerId=… oder cockpit_list_categories → id der Ziel-Kategorie |
| Filiale finden | cockpit_list_shop_locations (centerId, ggf. chainSlug) → locationId |
| Override setzen | PUT …/shop-locations/{locationId} mit { "categoryId": "<uuid>" } oder MCP cockpit_update_shop_location |
| Wieder Kette erben | { "categoryId": null } |
Nicht cockpit_update_chain / Ketten-category ändern, wenn andere Center die Ketten-Branche unverändert lassen sollen. Die öffentliche Website wertet ShopLocation.categoryRef vor ShopChain.category aus.
Ausführliche Feldliste (inkl. WordPress): WordPress Push-Content – shops.
Response: analog WordPress: success, data.summary, optional data.errors.
4.4a Push-Vorschau (keine DB-Änderung)
POST {DASHBOARD_ORIGIN}/api/agencyos/v1/content/push/preview
- Gleicher JSON-Body wie
POST …/content/push(inkl.centerId), gleicher Bearer-API-Key und Center-Zugriff. - Keine
create/update-Schreiboperationen in der Datenbank — geeignet für Freigabe-Workflows (z. B. AgencyOS zeigt die geplanten Änderungen, danach echter Push). - Response (200):
success,message,data.previewPlan(Liste geplanter Schritte mitentity,actioncreate/update,matchbei Zuordnung,existingId,planned, optionalnotes),data.summary, optionaldata.errors(Validierungs-/Ketten-Fehler wie beim echten Push). - Shop-Ketten: Würde eine neue
ShopChainangelegt (namensbasierte Auflösung), erscheint das inplanned.shopChainalswould_create_chainohne die Kette anzulegen; Hinweis ggf. innotes. GET …/preview: wird nicht unterstützt (405) — JSON-Payload gehört in den POST-Body.
Content-Entwürfe (Workflow) — Lesen, Freigabe, Kundenkontakt
Dieselbe ContentDraft-Logik wie unter Workflow & Freigaben im Dashboard, per AgencyOS-Key:
| Methode | Pfad | Kurzbeschreibung |
|---|---|---|
| GET | /api/agencyos/v1/drafts | Liste; Query wie bisher (status, contentType, centerId, campaignLabel, limit). Neu: includeData=1 — pro Eintrag geparstes data (Längen serverseitig begrenzt), u. a. customerCommunication, dispatchItemId, Inhaltsfelder. |
| GET | /api/agencyos/v1/drafts/{draftId} | Ein Entwurf; optional ?includeData=1. |
| PUT | /api/agencyos/v1/drafts/{draftId} | approve / reject (unverändert). |
| POST | /api/agencyos/v1/drafts/{draftId}/customer-touchpoint-suggestion | Body: { "scenario": "workflow_approve" | "workflow_reject" | "workflow_pending", "rejectionReason"?: string } — gleiche KI-Vorschläge wie im Dashboard (Betreff, E-Mail-Entwurf, interne Hinweise). Nutzt die globale Cockpit-KI-Konfiguration. |
MCP/Claude: cockpit_list_drafts (optional includeData: true), cockpit_get_draft, cockpit_draft_customer_touchpoint, cockpit_update_draft. Pro Entwurf: createdBy, createdByName, source, createdAt.
Audit-Log (Provenance)
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/audit-logs
Bearer-API-Key; Center-Zugriff wie bei anderen v1-Routen.
| Query | Beschreibung |
|---|---|
centerId | Letzte Änderungen im Center (Tagesüberblick) — oder |
entityType + entityId | Historie eines Eintrags (offer, news, shop, event, service, job, center, shop-location) |
limit | Default 50, max 100 |
includeValues | 1 / true — oldValues / newValues mitliefern (Feldänderungen) |
Response (200): success, centerId, count, data[] mit u. a. action (CREATE/UPDATE/DELETE), entityType, entityId, entityLabel, actorDisplay (primary, secondary, isIntegration), userName, userId, timestamp, optional metadata.
MCP: cockpit_audit_logs — z. B. „Wer hat dieses Angebot zuletzt bearbeitet?“ (entityType + entityId aus search_content). Mit summary: true / Query summary=1: Rangliste byUser (ideal für „wer war am aktivsten?“). Ab Deploy: auch MCP-Push, Uploads und Center-Anlage via AgencyOS werden protokolliert (metadata.integrationName, channel: agencyos).
Center-Team (Zugriff & Rollen)
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/team
Bearer-API-Key; Center-Zugriff wie bei anderen v1-Routen.
Response (200): center, count, data[] mit zugewiesenen Nutzer:innen (user.name, user.email), centerRole, Content-/Shop-Berechtigungen, assignedAt.
MCP: cockpit_center_team — z. B. „Wie viele Personen haben Zugriff auf Center X?“
Website-Cache (Revalidate)
Nach POST …/content/push (Direkt-Push, keine Drafts) invalidiert das Dashboard automatisch den Next.js-Cache auf allen in CENTER_WEBSITE_URLS / CENTER_WEBSITE_URL eingetragenen Instanzen — wenn REVALIDATION_SECRET gesetzt ist. Die Push-Antwort kann data.revalidation enthalten (instances[] pro URL).
Manuell: POST {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/revalidate (Bearer-Key, Body optional { "fullRevalidation": true }).
MCP: cockpit_revalidate_website
v0/Vercel: Live-Seite braucht app/api/revalidate/route.ts, REVALIDATION_SECRET auf Vercel (identisch zum Dashboard) und websitePublicUrl im Center (automatisch via Deploy-Registrierung oder manuell im Dashboard). Legacy: globale CENTER_WEBSITE_URLS. Ohne Revalidate-Secret: Daten in Cockpit aktuell, Vercel kann ISR-Cache zeigen.
Frontend-Kanäle lesen (Cockpit vs. v0)
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/frontend-channels
Bearer-API-Key; gleiche Übersicht wie Dashboard Website-Management → Frontend-Kanäle (v0).
Response (200): success, data mit u. a.:
| Feld | Bedeutung |
|---|---|
channels.website.cockpitPreview | {slug}.cockpit-os.de |
channels.website.v0VercelStaging / v0VercelProduction | Registrierte v0/Vercel-URLs |
resolvedUrls.website | Effektiver Dashboard-Link (v0 wenn gesetzt, sonst Cockpit) |
frontendDeploymentMeta | Letzte Meldung pro Kanal (registeredAt, origin, …) |
interpretation.isV0WebsiteConnected | Kurz: v0 schon gemeldet? |
websiteStackStatus | Cockpit / Vercel / Live-Status |
apiHints | u. a. qrResolveGet, qrScanPost, qrTrackPost, companionQrPath, websiteQrPath (Standort-QRs, siehe API-Vertrag) |
MCP: cockpit_frontend_channels — nach Deploy prüfen; fehlt Meldung → cockpit_register_frontend_deployment. data.apiHints enthält Standort-QR-Vertrag (qrResolveGet, qrScanPost, …).
Standort-QRs (MCP): cockpit_qr_codes — action=list|get|create|resolve; öffentliche Auflösung wie v0 /companion/qr/[qrCodeId]. Nicht Handoff (/api/public/handoff-sessions).
Hinweis: Keine Route-Registry pro Pfad — nur Kanal-URLs (Cockpit-Subdomain vs. Vercel vs. Custom Domain).
Website-Live-Inventar (Bulk)
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/website-live-inventory
Bearer-API-Key; liefert für alle zugänglichen Center (max. 500) den Website-Live-Status in einem Aufruf — statt pro Center frontend-channels zu pollen.
Query-Parameter (optional):
| Parameter | Beschreibung |
|---|---|
chainSlug / chainName | Wie bei GET …/centers — nur Center mit Filiale dieser Kette |
organizationId / organizationName | Nur Center einer Organisation |
websiteEnabledOnly=true | Nur Center mit aktivierter Website |
excludeTestLike=true | Test-/Demo-Center (Name/Slug) ausblenden |
Response (200): success, data.items[], data.summary, meta.
Pro Eintrag in items:
| Feld | Bedeutung |
|---|---|
bucket | off | cockpit_only | v0_preview | v0_production | custom_domain_live |
isV0WebsiteConnected | v0/Vercel-Deploy gemeldet? |
urls.cockpitPreview / v0Staging / v0Production / customDomain | Kanal-URLs |
urls.activeSurface | Effektive Besucher-Oberfläche (Priorität: Production → Domain → Staging → Cockpit) |
domainStatus / domainVerified | DNS/Domain-Stand |
websiteStackOverallLevel | off | configured | pending | active | warning |
summary.byBucket zählt Center pro Klasse; summary.v0ConnectedCount und customDomainActiveCount für schnelle KPIs.
MCP: cockpit_website_live_inventory — gleiche Filter als Tool-Parameter.
Hinweis: Kein HTTP-Ping auf Live-URLs — Klassifikation aus Cockpit-Daten (wie Dashboard „Frontend-Kanäle“). Für einen einzelnen Center-Detailstand weiterhin cockpit_frontend_channels oder cockpit_dns_status.
Outstand-Kanal-Inventar (Bulk)
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/social/center-accounts
Bearer-API-Key; liefert für alle zugänglichen Center (max. 500) die Outstand.io-Kanal-Verknüpfungen in einem Aufruf — statt pro Center website-config zu lesen (dort sind socialMedia nur Website-Links, kein Outstand).
Query-Parameter (optional):
| Parameter | Beschreibung |
|---|---|
chainSlug / chainName | Wie bei GET …/centers — nur Center mit Filiale dieser Kette |
organizationId / organizationName | Nur Center einer Organisation |
centerId | Einzelnes Center (UUID) |
connectedOnly=true | Nur Center mit mindestens einem aktiven Outstand-Kanal |
activeAccountsOnly=true | In accounts nur aktive Mappings (isActive=true) |
excludeTestLike=true | Test-/Demo-Center (Name/Slug) ausblenden |
Response (200): success, data.items[], data.summary, meta.
Pro Eintrag in items:
| Feld | Bedeutung |
|---|---|
outstandConnected | Mindestens ein aktiver Kanal verknüpft? |
activeAccountCount / totalAccountCount | Anzahl aktiver bzw. aller Mappings |
accounts[] | outstandAccountId, network, username, isActive, … |
summary.centersWithOutstand / centersWithoutOutstand für schnelle KPIs.
MCP: cockpit_list_outstand_centers — gleiche Filter als Tool-Parameter.
Hinweis: Liefert Verknüpfungsstand aus center_social_accounts, keine Outstand-Performance-Metriken (Reichweite, Follower). Dafür weiterhin Dashboard Social Reporting.
Frontend-Deploy registrieren (v0 → Cockpit)
POST {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/frontend-deployments/register
Bearer-API-Key; Center-Zugriff wie bei anderen v1-Routen.
Body (JSON):
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
channel | string | ja | website | signage | companion |
origin | string | ja | Deploy-Origin ohne Pfad, z. B. https://xyz.vercel.app |
source | string | nein | z. B. vercel, v0, mcp |
Response (200): success, data mit origin, updated, editorMessage, optional revalidation.
Öffentlich (Vercel Deploy Hook): POST {DASHBOARD_ORIGIN}/api/public/frontend-deployments/register mit Header X-Cockpit-Register-Token: frt_… (pro Center im Dashboard erzeugt) — kein centerId nötig, Center wird am Token erkannt.
MCP: cockpit_register_frontend_deployment — vorher optional cockpit_frontend_channels zur Verifikation.
Datensicherheit: Aktualisiert nur die URL des gewählten Kanals (websitePublicUrl / …) und frontendDeploymentMeta — löscht keine Inhalte und ersetzt keine Custom Domains.
Siehe v0 Deploy ans Cockpit melden.
Homepage-Kacheln & Seiten-Inhalte (MCP-Schreiben)
Bisher nur Dashboard oder öffentliches GET — ab AgencyOS v1 auch Schreiben per API-Key (mit Audit + Revalidate):
| Ressource | Lesen | Schreiben |
|---|---|---|
| Homepage-Kacheln | GET …/centers/{centerId}/homepage-tiles | POST (neu), PATCH …/homepage-tiles/{tileId}, DELETE …/homepage-tiles/{tileId} |
| Page Content | GET …/centers/{centerId}/page-content (optional ?pageType=) | POST (Upsert pro pageType) |
MCP: cockpit_homepage_tiles (action: list/create/update/delete), cockpit_page_content (action: list/get/upsert). Öffentliche Vorschau weiterhin cockpit_public_homepage_tiles / cockpit_public_page_content.
Gültige pageType-Werte: u. a. ueber-uns, kontakt, anfahrt, datenschutz, impressum, shops, jobs — vollständige Liste in apps/dashboard/src/lib/page-content-api.ts.
Suche (Jobs & Offices)
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/search — contentType unterstützt jetzt auch job und office (Komma-getrennt). MCP: cockpit_search_content mit gleichen Parametern.
Büros & Praxen (AgencyOS + MCP)
Was: CRUD für Offices/Praxen über AgencyOS v1 — Schreibfelder aligned mit Dashboard /api/offices (Partial-Update).
Warum: MCP/AgencyOS kannten bisher nur Basis-Felder; Logo, Bilder, Qualifikationen und Veröffentlichungszeiten fehlten.
Wer ist betroffen: Redaktion (Dashboard unverändert), Dev/Agenten (AgencyOS, MCP).
| Methode | Pfad | Beschreibung |
|---|---|---|
GET | /api/agencyos/v1/offices | Liste (centerId, q, type, status, limit) |
POST | /api/agencyos/v1/offices | Anlegen — centerId, name, type oder officeTypeId |
GET | /api/agencyos/v1/offices/{officeId} | Detail inkl. officeType, Center |
PUT | /api/agencyos/v1/offices/{officeId} | Partial-Update — nur gesetzte Felder |
DELETE | /api/agencyos/v1/offices/{officeId} | Archivieren (status: Archiviert) |
Schreibbare Felder (Auszug): Stammdaten (name, type, status, specialty, description, …), Kontakt (phone, email, website, bookingUrl), Medien (logo, coverImage, teamPhoto, images), Team (owner, teamSize), JSON-Felder (openingHours, services, languages, qualifications, certifications, insuranceTypes, appointmentType, onlineServices), Barrierefreiheit/Parkplatz/Notfall, Flags (featured, verified), officeTypeId, publishStartDate, publishEndDate, displayId.
JSON-Felder: Text, JSON-String oder Array/Objekt — Serialisierung wie im Dashboard (apps/dashboard/src/lib/agencyos-office-fields.ts).
Workflow-Entwurf: POST mit submitToWorkflow: true → Content-Draft statt Direktanlage (wie bisher).
MCP: cockpit_list_offices, cockpit_get_office, cockpit_create_office, cockpit_update_office, cockpit_archive_office
So testen:
GET /api/agencyos/v1/offices?centerId=…mit Bearer-Key.POSTmitname,type, optionallogoundqualifications: ["Facharzt"].PUT …/{officeId}mitfeatured: true→updatedFieldsin der Antwort prüfen.- MCP
cockpit_get_officemit derselbenofficeId.
Teams-Benachrichtigungen (cockpitOS → AgencyOS, Phase 2)
Was: Wenn im Cockpit eine Social-Freigabe angefordert wird, kann das Dashboard optional einen HTTP-Hook an AgencyOS senden. AgencyOS liefert dann proaktive Teams-DMs oder Adaptive Cards an die gewählten Freigeber (Notification-Hub: team.cockpit-os.de).
Warum: Redaktion arbeitet im Cockpit; der Teams-Bot lebt in AgencyOS. Das CMS bleibt Content-Quelle und meldet Events — keine eigene Bot-Logik im Monorepo.
Wer ist betroffen: Redaktion/Freigeber (Teams), Dev/Betrieb (Env auf beiden Seiten).
Ablauf
cockpitOS (dieses Repo) — bereits vorbereitet
| Thema | Pfad |
|---|---|
| HTTP-Client | apps/dashboard/src/lib/integration/agencyos-teams-notify.ts |
| Hook nach Freigabe-Anfrage | apps/dashboard/src/lib/integration/social-review-request-notify.ts |
| Auslöser | applySubmitReview in social-approval-actions.ts |
Env (Dashboard, nur Server):
| Variable | Pflicht | Beschreibung |
|---|---|---|
AGENCYOS_TEAMS_NOTIFY_URL | ja (für Aktivierung) | Vollständige URL, z. B. https://team.cockpit-os.de/api/integrations/notifications/social-review |
AGENCYOS_TEAMS_NOTIFY_SECRET | ja (für Aktivierung) | Gemeinsames Geheimnis; Header Authorization: Bearer … |
Ohne beide Variablen: kein Aufruf — Dashboard-Glocke, Desktop-Push und E-Mail laufen unverändert.
AgencyOS — zu implementieren (Notification-API)
POST {AGENCYOS_ORIGIN}/api/integrations/notifications/social-review
- Auth:
Authorization: Bearer <AGENCYOS_TEAMS_NOTIFY_SECRET>(gleicher Wert wie in Cockpit-Env) - Content-Type:
application/json
Body (von Cockpit gesendet):
{
"source": "cockpitos",
"draftId": "cuid",
"centerId": "uuid",
"centerName": "Center XY",
"caption": "Post-Text…",
"requester": {
"userId": "cockpit-user-uuid",
"name": "Max Mustermann",
"email": "max@example.com"
},
"approvers": [
{ "userId": "uuid", "name": "Anna", "email": "anna@example.com" }
],
"approvalMode": "all",
"reviewUrl": "https://dashboard.cockpit-os.de/dashboard/social/approvals?highlight=…",
"guestReviewUrl": "https://dashboard.cockpit-os.de/freigabe/social/token (optional)"
}
| Feld | Typ | Beschreibung |
|---|---|---|
source | "cockpitos" | Herkunftssystem |
draftId | string | SocialPostDraft.id im Cockpit |
approvalMode | "all" | "one" | Entspricht Cockpit-Freigaberegel |
reviewUrl | string | Deep-Link ins Cockpit-Freigabe-Board |
guestReviewUrl | string | null | Optionaler Gast-Link ohne Login |
User-Mapping: Cockpit-userId und AgencyOS-userId sind nicht identisch. AgencyOS soll Freigeber primär über email (oder teamsUserId / Entra aadObjectId) auflösen.
Erfolg (200):
{
"success": true,
"sent": { "teams": 2, "inApp": 1, "skipped": 0 }
}
Fehler: 401 ungültiges Secret, 422 fehlende Pflichtfelder.
Erwartetes Verhalten in AgencyOS:
- Pro Approver mit
teamsConversationRef: Adaptive Card (Caption, Center, Freigeben-Link zum Cockpit) - Ohne Bot-Kontakt: In-App-Notification + PWA-Push in AgencyOS (falls User dort existiert)
- Kein Blockieren des Cockpit-Workflows bei AgencyOS-Ausfall (Cockpit loggt nur)
So testen (End-to-End)
Schnellster Weg (Smoke-Test, ~30 Sekunden):
./scripts/smoke-agencyos-teams-notify.sh
Das Skript fragt Secret und Freigeber-E-Mail ab (oder aus Env / --load-env aus apps/dashboard/.env.local). Es prüft Auth (401) und sendet eine Test-Card an AgencyOS → Teams.
| Variable | Was du brauchst |
|---|---|
AGENCYOS_TEAMS_NOTIFY_SECRET | Gleicher Wert in Cockpit und AgencyOS (Render) |
TEST_APPROVER_EMAIL | E-Mail einer Person, die in AgencyOS existiert und den Teams-Bot einmal geöffnet hat |
Vollständiger Flow (Cockpit-UI):
- AgencyOS: Notification-Endpunkt deployen + Secret setzen
- Cockpit Render:
AGENCYOS_TEAMS_NOTIFY_URL+AGENCYOS_TEAMS_NOTIFY_SECRETsetzen → Redeploy - Freigeber: Teams-Bot in AgencyOS mindestens einmal öffnen (
teamsConversationRef) - Cockpit: Social-Post → Freigabe anfordern mit Test-Prüfer
- Prüfer erhält Teams-Nachricht; Cockpit-Glocke/E-Mail weiterhin wie bisher
Abgrenzung
| System | Social-Freigaben | Teams |
|---|---|---|
| cockpitOS | SocialPostDraft, /dashboard/social/approvals | Hook nur wenn Env gesetzt |
| AgencyOS | eigener SocialPost / Redaktionsplan | Bot, Crons, Cards (MVP dort) |
Social-Review-Outcome (Phase 2b)
Was: Nach Freigabe/Ablehnung/Publish-Fehler meldet Cockpit das Ergebnis an AgencyOS → Teams-DM an den Ersteller (nicht den Freigeber).
Warum: Der Anfragende erhält bewusst keine Freigabe-Anfrage-Benachrichtigung; das Outcome schließt den Kreis im Teams-Bot.
| Thema | Pfad |
|---|---|
| HTTP-Client | apps/dashboard/src/lib/integration/agencyos-teams-notify.ts (notifyAgencyOsTeamsSocialReviewOutcome) |
| Auslöser | notifySocialReviewOutcome in social-review-notify.ts (nach Dashboard + E-Mail) |
URL: Aus AGENCYOS_TEAMS_NOTIFY_URL abgeleitet (…/social-review → …/social-review-outcome) oder explizit AGENCYOS_TEAMS_NOTIFY_OUTCOME_URL.
POST {AGENCYOS_ORIGIN}/api/integrations/notifications/social-review-outcome
- Auth: gleiches
AGENCYOS_TEAMS_NOTIFY_SECRET - Body:
{
"source": "cockpitos",
"draftId": "cuid",
"centerId": "uuid",
"centerName": "Center XY",
"caption": "Post-Text…",
"outcome": "approved",
"requester": { "userId": "…", "name": "…", "email": "sb@…" },
"reviewer": { "userId": "…", "name": "…", "email": "julia@…" },
"rejectionReason": null,
"reviewUrl": "https://dashboard.cockpit-os.de/dashboard/social/approvals?highlight=…"
}
outcome | Bedeutung |
|---|---|
approved | Freigegeben und Publish erfolgreich (oder geplant) |
rejected | Abgelehnt — rejectionReason gesetzt |
publish_failed | Freigegeben, Outstand-Publish fehlgeschlagen |
AgencyOS: Endpunkt + Card an requester.email (User-Match per E-Mail).
Social-Review-Digest (AgencyOS v1)
Was: Offene Cockpit-SocialPostDraft mit status=in_review, gruppiert nach Freigeber-E-Mail — für täglichen Digest-Cron in AgencyOS (neben native AgencyOS-Posts).
| Methode | Pfad |
|---|---|
GET | /api/agencyos/v1/social-review-digest |
GET | /api/agencyos/v1/social-review-digest?approverEmail=julia@… |
Auth: Agency-API-Key. MCP: cockpit_social_review_digest.
Response (Auszug):
{
"success": true,
"generatedAt": "2026-07-10T09:00:00.000Z",
"recipientCount": 3,
"totalOpenDrafts": 5,
"recipients": [
{
"userId": "uuid",
"name": "Julia",
"email": "julia@example.com",
"openCount": 2,
"items": [
{
"draftId": "cuid",
"centerName": "Burgaupark Jena",
"captionExcerpt": "Sommer-Sale…",
"requesterName": "Saad",
"approvalMode": "any",
"reviewUrl": "https://dashboard.cockpit-os.de/dashboard/social/approvals?highlight=…",
"guestReviewUrl": null
}
]
}
]
}
Smoke:
COCKPIT_AGENCYOS_API_KEY=sk_agencyos_… ./scripts/smoke-agencyos-social-review-digest.sh
In-Chat-Freigabe (AgencyOS Proxy)
Was: AgencyOS kann Freigabe/Ablehnung direkt aus der Teams-Card auslösen — Proxy zu Cockpit.
| Methode | Pfad |
|---|---|
POST | /api/agencyos/v1/social/drafts/{draftId}/review |
Body:
{
"action": "approve",
"approverEmail": "julia@example.com",
"reason": "optional bei reject"
}
- Freigeber wird per E-Mail im Cockpit aufgelöst (muss aktiv sein und in
assignedApproverIdsstehen). - Gleiche Regeln wie Dashboard (
all/any, Multi-Freigabe, Publish). - Kein MCP-Tool — nur serverseitiger AgencyOS-Proxy mit API-Key.
Implementierung: load-agencyos-social-review-digest.ts, social-review-digest/route.ts, social/drafts/[draftId]/review/route.ts.
Organization Accountability (ILG/HBB, „Wer ist zuständig?“)
Was: Aggregiert KAM-Zuständigkeiten und Org-Kontakte pro Organisation aus Cockpit — für Bot-Fragen wie „Wer ist für ILG zuständig?“.
| Methode | Pfad |
|---|---|
GET | /api/agencyos/v1/organizations/{slugOrId}/accountability |
slugOrId: Slug (ilg, hbb), Name oder UUID.
MCP: cockpit_organization_accountability
Response (Auszug):
{
"success": true,
"data": {
"organization": { "id": "…", "name": "ILG", "slug": "ilg" },
"centerCount": 27,
"centersWithKam": 1,
"keyAccountManagers": [
{
"userId": "…",
"name": "Julia Warns",
"email": "juw@schickma.de",
"assignmentKind": "primary",
"centerCount": 1,
"centers": [{ "centerId": "…", "centerName": "Burgaupark Jena" }]
}
],
"hasAccountability": true,
"hints": ["Haupt-KAM laut Cockpit: Julia Warns (juw@schickma.de)", "26 von 27 Centern ohne KAM-Zuweisung im Cockpit"]
}
}
Hinweis: AgencyOS search_centers nutzt heute nur Customer.accountManagerId — ohne Cockpit-Fallback bleibt die Bot-Antwort leer, obwohl Cockpit-Daten existieren können. AgencyOS soll bei leerem accountManager diese API nachladen.
Pfad Cockpit: load-organization-accountability.ts, organizations/[slugOrId]/accountability/route.ts
Organisations-KAM mit Vererbung (2026-07-10)
Was: KAMs können einmal pro Organisation gesetzt werden und gelten für alle Center — außer ein Center hat eigene KAM-Zuweisung (Abweichung).
| Ebene | Dashboard | API |
|---|---|---|
| Organisation | /dashboard/organizations/{slug} — KAM-Karte | GET/PUT /api/organizations/{slug}/key-account-managers |
| Center (Abweichung) | Center-Profil → Tab Team | GET/PUT /api/centers/{id}/key-account-managers — revertToOrganization: true setzt Vererbung zurück |
| AgencyOS | — | GET/PUT …/organizations/{slugOrId}/key-account-managers, GET/PUT …/centers/{centerId}/key-account-managers (liefert source, hasCenterOverride) |
Vererbungsregel: Keine Center-Zeilen → Organisations-KAMs. Mindestens eine Center-Zeile → nur Center-KAMs.
MCP lesen: cockpit_center_key_account_managers, cockpit_organization_key_account_managers, cockpit_organization_accountability, cockpit_kam_briefing_index, cockpit_kam_quality_watch_index
MCP schreiben: cockpit_set_center_key_account_managers, cockpit_set_organization_key_account_managers — Body wie Dashboard-PUT (userIds[], assignments[], Center: revertToOrganization: true). User-UUIDs über cockpit_center_team oder Nutzer-Verwaltung.
Noch ohne AgencyOS/MCP: KAM-Zuweisung aus der Nutzer-Bearbeitung (kamCenterAssignments am User-Endpoint).
Externe Ansprechpartner (Center)
Was: Agentur, Haustechnik, Sicherheit usw. pro Center — Tab Team & Ansprechpartner im Dashboard; Speicherung in CenterContact mit metadata.contactKind = external.
Warum: KI-Kontext, Notfall-Listen und AgencyOS-Automation brauchen strukturierte externe Kontakte — bisher nur Dashboard ohne MCP.
| Methode | Pfad | Beschreibung |
|---|---|---|
GET | /api/agencyos/v1/centers/{centerId}/external-contacts | Liste |
POST | /api/agencyos/v1/centers/{centerId}/external-contacts | Anlegen |
PUT | /api/agencyos/v1/centers/{centerId}/external-contacts/{contactId} | Aktualisieren |
DELETE | /api/agencyos/v1/centers/{centerId}/external-contacts/{contactId} | Löschen |
Body (POST/PUT): name (Pflicht), optional company, role, email, phone, type (Haustechnik, …), emergency, responseTime, notes.
MCP: cockpit_center_external_contacts, cockpit_create_center_external_contact, cockpit_update_center_external_contact, cockpit_delete_center_external_contact
Migration: packages/database/migrations/20260722_center_contact_metadata_SAFE.sql — Spalte metadata (JSONB), email nullable.
So testen: Dashboard Center → Team → Kontakt anlegen → MCP GET mit gleicher centerId → Eintrag in data[].
Website-Ansprechpartner (Center Website → Kontakte)
Was: Center Manager und weitere Ansprechpartner für Kontaktseite / Impressum — Tab Center Website → Grundeinstellungen → Kontakte (CenterContact mit metadata.contactKind = website).
Abgrenzung: Nicht externe Team-Kontakte (Agentur, Haustechnik) — die liegen unter Center → Team & Ansprechpartner (external-contacts).
| Methode | Pfad | Beschreibung |
|---|---|---|
GET | /api/agencyos/v1/centers/{centerId}/contacts | Liste (Auth) |
POST | /api/agencyos/v1/centers/{centerId}/contacts | Anlegen |
PUT | /api/agencyos/v1/centers/{centerId}/contacts/{contactId} | Aktualisieren |
DELETE | /api/agencyos/v1/centers/{centerId}/contacts/{contactId} | Löschen |
GET | /api/centers/{centerId}/contacts/public | Öffentlich (v0, nur aktive) |
Body (POST/PUT): role + name (Pflicht), optional email, phone, department, photoUrl, displayOrder, isActive.
MCP lesen: cockpit_center_website_contacts, cockpit_public_center_contacts (v0)
MCP schreiben: cockpit_create_center_website_contact, cockpit_update_center_website_contact, cockpit_delete_center_website_contact
v0: GET …/public-visitor-surface → apiHints.contactsPublicGet
So testen: Dashboard Center Website → Kontakt anlegen → cockpit_center_website_contacts → gleicher Eintrag; v0: cockpit_public_center_contacts.
Operational Briefing (Center-KAM-Daily, Phase 2)
Was: Liefert pro Center die operativen To-dos aus dem Cockpit — offene Social-Postings, Google-Rezensionen ohne Antwort, Reporting-Erinnerungen, Website-Workflow-Entwürfe. AgencyOS nutzt die API für tägliche Teams-DMs an KAMs (Cron center-kam-briefing).
Warum: KAM-Daten (Tasks) liegen in AgencyOS; Social/Reviews/Reporting in Cockpit — eine Aggregat-API vermeidet Duplikate.
Endpunkte
| Methode | Pfad | Beschreibung |
|---|---|---|
GET | /api/agencyos/v1/centers/{centerId}/operational-briefing | Ein Center |
GET | /api/agencyos/v1/operational-briefing?centerIds=id1,id2 | Bulk (max. 30, nur erlaubte Center) |
Auth: Authorization: Bearer <sk_agencyos_…> (gleicher Key wie andere v1-Routen)
MCP: cockpit_operational_briefing — Parameter centerId oder centerIds (kommagetrennt).
Beispiel-Response (data)
{
"centerId": "uuid",
"centerName": "Rathaus-Galerie Wuppertal",
"generatedAt": "2026-07-08T20:00:00.000Z",
"social": {
"pending": 1,
"inReview": 2,
"totalOpen": 3,
"href": "https://dashboard.cockpit-os.de/dashboard/social/approvals?status=in_review"
},
"workflow": { "openDrafts": 0, "href": "https://dashboard.cockpit-os.de/dashboard/workflow" },
"reviews": {
"configured": true,
"available": true,
"unanswered": 3,
"responseNeeded": 1,
"href": "https://dashboard.cockpit-os.de/dashboard/analytics/reviews?centerId=…"
},
"reporting": {
"configured": true,
"reminderMonth": "2026-06",
"monthNotSent": true,
"quarterReminder": false,
"href": "https://dashboard.cockpit-os.de/dashboard/analytics/client-report?centerId=…"
},
"hints": [
"Social: 2 in Prüfung, 1 ausstehend",
"Google: 3 Rezensionen ohne Antwort (1 mit Handlungsbedarf)",
"Reporting: Monatsbericht Juni 2026 noch nicht versendet"
],
"hasActionItems": true
}
Reporting-Regeln (Cockpit)
| Flag | Bedeutung |
|---|---|
monthNotSent | Vormonat (reminderMonth) hat keinen Eintrag in client_report_notify_log und Center hat clientReportNotifyEmail |
quarterReminder | Erste 14 Tage von Jan/Apr/Jul/Okt und monthNotSent |
Smoke-Test (Cockpit-Repo)
COCKPIT_AGENCYOS_API_KEY=sk_agencyos_… CENTER_ID=uuid ./scripts/smoke-agencyos-operational-briefing.sh
Implementierung (Cockpit)
| Pfad | Rolle |
|---|---|
apps/dashboard/src/lib/integration/load-agencyos-operational-briefing.ts | Aggregat-Logik |
…/centers/[centerId]/operational-briefing/route.ts | Einzel-Center |
…/operational-briefing/route.ts | Bulk |
AgencyOS implementiert Cron + Teams-Nachricht — siehe Prompt in Repo-Doku / separater Agent-Chat.
KAM-Zuordnung (Cockpit) & Briefing-Index
Was: Key Account Manager werden im Cockpit pro Center gepflegt — mehrere Personen möglich (z. B. Urlaubsvertretung). AgencyOS nutzt die E-Mail-Adresse für Teams-DMs; die operative Datenaggregation bleibt in operational-briefing.
Warum: KAM war bisher nur in AgencyOS (keyAccountManagerId, ein User). Cockpit ist die führende Quelle für Center-Stammdaten und operative To-dos.
Dashboard (Redaktion / Betrieb)
| Wo | Pfad |
|---|---|
| UI | Center-Detail → Tab Team & Ansprechpartner → Karte Key Account Manager (KAM) |
| API (Session) | GET/PUT /api/centers/{centerId}/key-account-managers — Body PUT siehe JSON-Beispiele unten |
Body PUT (Session-API):
{ "userIds": ["uuid-1", "uuid-2"] }
{ "assignments": [{ "userId": "uuid-1", "assignmentKind": "primary" }] }
{ "revertToOrganization": true }
Wer: SUPER_ADMIN, CENTER_ADMIN, ORG_MARKETING_MANAGER (Schreiben); Lesen für alle mit Center-Zugang.
MCP: Lesen cockpit_center_key_account_managers · Schreiben cockpit_set_center_key_account_managers · Org: cockpit_organization_key_account_managers / cockpit_set_organization_key_account_managers
So testen:
- Center öffnen → Profil → Team.
- Einen oder mehrere aktive Cockpit-User als KAM hinzufügen.
GET /api/agencyos/v1/centers/{centerId}/key-account-managersmit AgencyOS-Key — gleiche Liste.PUTmit gleichem Key +userIdsoder MCPcockpit_set_center_key_account_managers.GET /api/agencyos/v1/kam-briefing-index— Empfänger gruppiert nach KAM inkl. Briefings.
AgencyOS-v1-Endpunkte
| Methode | Pfad | Beschreibung |
|---|---|---|
GET | /api/agencyos/v1/centers/{centerId}/key-account-managers | Effektive KAM-Liste (inkl. Vererbung) |
PUT | /api/agencyos/v1/centers/{centerId}/key-account-managers | Center-KAMs setzen oder revertToOrganization: true |
GET | /api/agencyos/v1/organizations/{slugOrId}/key-account-managers | Organisations-KAMs |
PUT | /api/agencyos/v1/organizations/{slugOrId}/key-account-managers | Organisations-KAMs setzen |
GET | /api/agencyos/v1/organizations/{slugOrId}/hosting | Shared Vercel: vercelProjectsByTemplate, Fallback vercelSharedProjectId |
PATCH | /api/agencyos/v1/organizations/{slugOrId}/hosting | Map pflegen (nur Global-API-Key); Body: vercelProjectsByTemplate, optional vercelSharedProjectId |
GET | /api/agencyos/v1/organizations/{slugOrId}/bulk-center-hosting?websiteTemplate=… | Übersicht Hosting aller Center (optional Template-Filter) |
POST | /api/agencyos/v1/organizations/{slugOrId}/bulk-center-hosting | Bulk: Center websiteHostingChannel + vercelProjectMode + optional Org-Map merge |
PATCH | /api/agencyos/v1/centers/{centerId}/hosting | Einzelnes Center: Hosting-Kanal + Vercel-Modus |
GET | /api/agencyos/v1/kam-briefing-index | Alle KAM-Empfänger + Briefings (gruppiert nach User) |
GET | /api/agencyos/v1/kam-briefing-index?onlyWithActionItems=true | Nur Empfänger mit mindestens einem Center mit hasActionItems |
operational-briefing enthält zusätzlich kams[] pro Center:
"kams": [
{
"userId": "uuid",
"name": "Max Mustermann",
"email": "max@example.com",
"sortOrder": 0,
"assignmentKind": "primary"
},
{
"userId": "uuid2",
"name": "Julia Vertretung",
"email": "julia@agency.de",
"sortOrder": 1,
"assignmentKind": "substitute"
}
]
Smoke-Tests (Cockpit-Repo)
COCKPIT_AGENCYOS_API_KEY=sk_agencyos_… ./scripts/smoke-agencyos-kam-briefing-index.sh
ONLY_WITH_ACTION_ITEMS=true COCKPIT_AGENCYOS_API_KEY=… ./scripts/smoke-agencyos-kam-briefing-index.sh
Implementierung (Cockpit)
| Pfad | Rolle |
|---|---|
packages/database/migrations/20260708233000_add_center_key_account_managers_SAFE.sql | Tabelle |
apps/dashboard/src/lib/center-key-account-managers.ts | Lade-/Speicher-Logik, Briefing-Index |
…/api/centers/[centerId]/key-account-managers/route.ts | Dashboard-API |
…/api/agencyos/v1/centers/[centerId]/key-account-managers/route.ts | AgencyOS-Read |
…/api/agencyos/v1/kam-briefing-index/route.ts | Cron-Einstieg für Teams-DMs |
apps/dashboard/src/components/center-kam-assignments-card.tsx | UI |
AgencyOS: Cron center-kam-briefing soll kam-briefing-index konsumieren (nicht mehr ShoppingCenter.keyAccountManagerId als Primärquelle).
MCP: cockpit_center_key_account_managers, cockpit_kam_briefing_index, cockpit_operational_briefing (ein Center oder Bulk centerIds).
Content-Quality-Watch (Variante A)
Was: Liefert pro Center Qualitäts-Hinweise für den AgencyOS Teams-Bot — fehlende Center-/Shop-Stammdaten, Inhalte ohne Titelbild, Inhaltslücken (Mengen) und lange Inaktivität ohne Veröffentlichung. Getrennt vom operativen Tages-Briefing.
Warum: Proaktive KAM-Nachrichten wie „8 Shops ohne Logo“ oder „seit 6 Wochen nichts veröffentlicht“ brauchen eine Aggregat-API — nicht 30 Einzelabfragen pro Center.
Endpunkte
| Methode | Pfad | Beschreibung |
|---|---|---|
GET | /api/agencyos/v1/centers/{centerId}/content-quality-watch | Ein Center |
GET | /api/agencyos/v1/content-quality-watch?centerIds=id1,id2 | Bulk (max. 30) |
GET | /api/agencyos/v1/content-quality-watch?…&onlyWithIssues=true | Bulk nur mit hasQualityIssues |
GET | /api/agencyos/v1/kam-quality-watch-index | Nach KAM gruppiert |
GET | /api/agencyos/v1/kam-quality-watch-index?onlyWithIssues=true | Nur KAMs mit Qualitätsproblemen |
Query-Parameter (optional): inactiveDays (21), minOffers (3), minEvents (1), minNews (1), minJobs (0), maxShopSamples (5)
Auth: Authorization: Bearer <sk_agencyos_…>
MCP: cockpit_content_quality_watch, cockpit_kam_quality_watch_index
Beispiel-Response (data)
{
"centerId": "uuid",
"centerName": "Rathaus-Galerie Wuppertal",
"hasQualityIssues": true,
"qualityScore": 62,
"qualityHints": [
"Stammdaten: Center ohne Öffnungszeiten",
"Shops: 8 von 142 ohne Logo (u.a. H&M, Zara, …)",
"Inhalte: 3 aktive Angebote ohne Titelbild",
"Aktivität: seit 42 Tagen nichts veröffentlicht"
],
"issues": {
"center": { "missingLogo": false, "missingCoverImage": false, "missingHeroImage": true, "missingOpeningHours": true },
"shops": { "activeTotal": 142, "withoutLogo": 8, "withoutCoverImage": 12, "withoutOpeningHours": 5, "samples": [] },
"content": { "activeOffersWithoutImage": 3, "activeEventsWithoutImage": 0, "publishedNewsWithoutImage": 1, "gaps": [] },
"activity": { "lastPublishedAt": "2026-05-28T10:00:00.000Z", "daysSinceLastPublish": 42, "inactiveThresholdDays": 21, "isInactive": true }
},
"links": {
"shops": "https://dashboard.cockpit-os.de/dashboard/content/centers/…?tab=shops",
"centerEdit": "https://dashboard.cockpit-os.de/dashboard/content/centers/…?tab=team",
"content": "https://dashboard.cockpit-os.de/dashboard/content/centers/…"
},
"kams": [{ "userId": "…", "name": "…", "email": "…", "assignmentKind": "primary" }]
}
Smoke-Test (Cockpit-Repo)
COCKPIT_AGENCYOS_API_KEY=sk_agencyos_… CENTER_ID=uuid ./scripts/smoke-agencyos-content-quality-watch.sh
Der Smoke-Test prüft Einzel-Center und kam-quality-watch-index?onlyWithIssues=true.
Implementierung (Cockpit)
| Pfad | Rolle |
|---|---|
apps/dashboard/src/lib/integration/load-agencyos-content-quality-watch.ts | Aggregat-Logik |
…/centers/[centerId]/content-quality-watch/route.ts | Einzel-Center |
…/content-quality-watch/route.ts | Bulk |
…/kam-quality-watch-index/route.ts | KAM-Index für Cron |
apps/dashboard/src/lib/center-key-account-managers.ts | loadKamQualityWatchIndex |
AgencyOS: Cron center-quality-watch + Bot-Tools — separater Agent-Chat im SMG-AgencyOS-Repo.
OpenAPI (Swagger) – optional nutzen
Im Repository liegt eine maschinenlesbare Spezifikation:
- Dateien:
/openapi/agencyos-integration.yaml(AgencyOS v1 — inkl.GET …/contextmit optionalemfloors_summary),/openapi/public-wayfinding-read.yaml(öffentlicher Wayfinding-Read ohne Key:floors,centerplan)
Empfehlung:
- Markdown (diese Seite) bleibt die verständliche Anleitung inkl. Ablauf und Sicherheit.
- OpenAPI lohnt sich, wenn ihr Client-Code generieren, Contract-Tests oder Swagger UI (z. B. editor.swagger.io mit Import-URL) nutzen wollt.
- Nachteil: Zwei Quellen – bei API-Änderungen OpenAPI und diese Seite pflegen, oder langfristig die Beschreibung aus OpenAPI in die Docs einbinden (Plugin/Aufwand).
Kurz: Swagger/OpenAPI ist sinnvoll, aber nicht Pflicht. Für AgencyOS reicht zunächst diese Doku; OpenAPI ist ein komfortables Zusatzangebot.
Implementierung im Repo (Referenz)
| Thema | Pfad |
|---|---|
| Magic Link GET/POST | apps/dashboard/src/app/api/agencyos/magic-link/route.ts |
| Complete | apps/dashboard/src/app/api/agencyos/magic-link/complete/route.ts |
| Organisationen (Session) | apps/dashboard/src/app/api/agencyos/organizations/route.ts |
| Connect-UI | apps/dashboard/src/app/agencyos/connect/page.tsx |
| v1 Centers | apps/dashboard/src/app/api/agencyos/v1/centers/route.ts, …/v1/centers/[centerId]/route.ts |
| v1 Center-Kontext (KI) | apps/dashboard/src/app/api/agencyos/v1/centers/[centerId]/context/route.ts |
| Ladelogik Kontext (Shops/Services) | apps/dashboard/src/lib/integration/load-agencyos-center-context.ts |
| v1 Content Push | apps/dashboard/src/app/api/agencyos/v1/content/push/route.ts |
| v1 Push-Vorschau (dry-run) | apps/dashboard/src/app/api/agencyos/v1/content/push/preview/route.ts |
| v1 Content-Entwürfe (Liste, Detail, Touchpoint) | apps/dashboard/src/app/api/agencyos/v1/drafts/route.ts, …/drafts/[draftId]/route.ts, …/drafts/[draftId]/customer-touchpoint-suggestion/route.ts |
| v1 Audit-Log | apps/dashboard/src/app/api/agencyos/v1/audit-logs/route.ts |
| v1 Center-Team | apps/dashboard/src/app/api/agencyos/v1/centers/[centerId]/team/route.ts |
| v1 Website-Config | …/centers/[centerId]/website-config/route.ts (GET + PUT) |
| v1 Outstand-Kanal-Inventar (Bulk) | apps/dashboard/src/app/api/agencyos/v1/social/center-accounts/route.ts, outstand-center-inventory.ts |
| v1 Website-Reiter-Schema | …/website-config-schema/route.ts, website-config-mcp-schema.ts, mcp-tab-hints-shared.ts, mcp-tab-hints-all-templates.ts, ilg-rgw-mcp-tab-fields.ts |
| v1 Homepage-Kacheln | …/centers/[centerId]/homepage-tiles/route.ts, …/homepage-tiles/[tileId]/route.ts |
| v1 Page Content | …/centers/[centerId]/page-content/route.ts |
| v1 Mediathek (Liste) | apps/dashboard/src/app/api/agencyos/v1/media/route.ts |
| v1 Suche | apps/dashboard/src/app/api/agencyos/v1/search/route.ts |
| Push → Revalidate | apps/dashboard/src/lib/integration/agencyos-content-revalidate.ts |
| Push-Audit (Integration) | apps/dashboard/src/lib/integration/integration-audit.ts, process-center-entity-push.ts |
| Gemeinsame Push-Logik (mit WordPress geteilt) | apps/dashboard/src/lib/integration/process-center-entity-push.ts |
| Teams-Hook Social-Freigabe (Phase 2) | apps/dashboard/src/lib/integration/agencyos-teams-notify.ts, social-review-request-notify.ts |
| Operational Briefing (KAM-Daily) | load-agencyos-operational-briefing.ts, …/operational-briefing/route.ts |
| KAM-Zuordnung & Briefing-Index | center-key-account-managers.ts, …/key-account-managers/route.ts, …/kam-briefing-index/route.ts |
Verwandte Dokumentation
- KI-Website-Bau & Cockpit-Sync (Konzept & Phasen) – Zielbild Greenfield/Brownfield, bestehende Bausteine, Roadmap ohne Datenverlust
- WordPress Push-Content – Detaillierte Feldlisten für
shops/events/news/offers/services - Centerplan WordPress-Integration – Kontext WordPress (nicht AgencyOS)
Nutzungsstatistik: Seitenaufrufe werden anonymisiert erfasst. Im Umami-Dashboard nach diesem Pfad filtern: /developer-guide/api-agencyos-integration