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 Automationen. Vollständiger Key wie gewohnt einmalig in der Antwort, nicht dauerhaft in der UI.
Persönliches MCP-Login (zusätzlich, ersetzt den Key nicht): Nach cockpitOS-Login unter Mein Konto → MCP verbinden ein Token sk_mcp_… erzeugen (GET/POST /api/mcp/tokens, DELETE /api/mcp/tokens/{id}). Derselbe AgencyOS-v1-Bearer-Platz: Authorization: Bearer sk_mcp_…. Rechte = Cockpit-Rechte dieser Person. Journal/userId und Audit zeigen den User. MCP-Env: COCKPIT_MCP_USER_TOKEN hat Vorrang vor COCKPIT_AGENCYOS_API_KEY. GET /api/agencyos/v1/me bzw. MCP cockpit_whoami zeigt authKind.
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.
Partner-Medien-Einreichungen (KAM-Warteschlange)
Center-Ansprechpartner reichen Fotos/Videos über die Partner-App (v0) oder Token-Links ein; Redaktion bearbeitet die Warteschlange im Cockpit (Center-Reiter Medien-Einreichungen). Nutzer-Doku: Partner-Medien-Einreichungen.
AgencyOS (Bearer API-Key):
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/agencyos/v1/centers/{centerId}/media-intake | Liste; Query status, limit, offset |
| POST | /api/agencyos/v1/centers/{centerId}/media-intake | Einreichung — Body: topic, description?, conditions?, contributionType?, eventDate?, shopId?, mediaItems[] |
| PATCH | /api/agencyos/v1/centers/{centerId}/media-intake/{submissionId} | Status/Notiz — Body: status (draft | new | in_review | accepted | rejected | published), notes? |
| GET | /api/agencyos/v1/centers/{centerId}/partner-tasks | Partner-Aufgaben listen |
| POST | /api/agencyos/v1/centers/{centerId}/partner-tasks | Aufgabe anlegen — Body: title, description?, dueDate?, shopId?, requiresAck? |
MCP: cockpit_list_media_intake, cockpit_create_media_intake, cockpit_update_media_intake_status, cockpit_create_partner_task.
Partner-API (Manager-App / MallCrew): Vollständige Referenz → Partner-API v1.
Basis /api/partner/v1/ — Login POST …/auth/login → partnerToken; Shops GET …/centers/{centerId}/shops; Upload POST …/media/upload?centerId=; Einreichung GET/POST …/centers/{centerId}/media-intake; Entwürfe/Kommentare/KI/Aufgaben ab 0.1.213 (siehe Partner-API-Doku).
Gast ohne Login: GET/POST …/intake/submit + POST …/intake/upload mit X-Intake-Token: mi_…. Manager-App: Proxy + HttpOnly-Cookie — Details: docs/v0-partner-media-intake-prompt.md, docs/v0-mallcrew-prompt.md.
AgencyOS Partner-Aufgaben: GET/POST /api/agencyos/v1/centers/{centerId}/partner-tasks — MCP: cockpit_create_partner_task.
Migrationen:
| Skript | Migration |
|---|---|
pnpm db:migrate:media-intake | 20260828130000_add_media_intake_SAFE.sql |
pnpm db:migrate:mallcrew-extensions | 20260828160000_add_mallcrew_extensions_SAFE.sql |
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, latitude, longitude, 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}
GET enthält websiteUrls: cockpitPreview = https://preview.cockpit-os.de/{slug} (Cockpit-Layout testen), liveSubdomain = https://{slug}.cockpit-os.de (Live-Standard). recommendedPreview für Vorschau-Links nutzen — nicht die Subdomain.
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.
PUT: nur gesendete Felder werden geändert. Erlaubt u. a. name, address, city, postalCode, country, phone, email, website, description, openingHours, latitude, longitude, status, slug, websiteEnabled, brandDna (JSON-Partial: toneOfVoice, targetAudience, usps, tabooWords, examplePost, visualStyle, …), brandGuidelines (Freitext). GET liefert dieselben Felder. MCP: cockpit_update_center. Dashboard: Center → Reiter Design (?tab=design). Pflichtfelder dürfen nicht auf null gesetzt werden. Koordinaten: gültige Zahlen (latitude −90…90, longitude −180…180); null oder leerer String löscht den Wert. Öffentlich lesbar für v0 unter GET …/public-visitor-surface → data.wayfindingMap.
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, chatbotConfig, comingSoonEnabled / comingSoonHeadline / comingSoonConfig, themeConfig, qrCode, templateContent (Template-Reiter).
Schreiben: PUT {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/website-config
Partial-Update; templateContent wird deep-merged.
chatbotConfig (Besucher-Chatbot): Aktivierung und UI-Einstellungen wie im Dashboard-Reiter Website → Chatbot. Persistenz in AIConfiguration (level=center, appType=center_website) plus themeOverrides.chatbotEnabledFallback. Minimal zum Einschalten: { "chatbotConfig": { "enabled": true } }. API-Keys in Antworten maskiert (••••); beim PUT werden maskierte Keys nicht überschrieben.
comingSoonEnabled / comingSoonHeadline / comingSoonConfig (Vorschaltseite): Wie Dashboard-Reiter Vorschaltseite. comingSoonConfig ist ein JSON-Objekt (z. B. Hintergrund, Zusatztexte). Partial PUT — nur gesendete Keys werden geändert.
themeConfig (Theme-Vererbung): { "inheritFromOrganization": true } setzt websiteTheme=null (Organisations-Theme erben). { "inheritFromOrganization": false } ohne customTheme.id → design-only (nur Design-Farben). Mit customTheme.id → explizites Center-Theme.
qrCode (Wayfinding-QR-Modal): Texte für das QR-Modal im Centerplan (title, description, buttonText). Persistenz in themeOverrides.qrCodeConfig — analog Dashboard-Reiter QR-Code.
MCP-Beispiel (Chatbot):
cockpit_update_center_website_config {
"centerId": "…",
"chatbotConfig": {
"enabled": true,
"greetingMessage": "Willkommen! Wie kann ich helfen?",
"aiAssistantPageTemplate": "mally"
}
}
Aggregierte Bot-Konfiguration (bots-profile): Bots & Assistenten Hub. Reads: GET …/bots-profile, MCP cockpit_get_bots_profile, Public data.bots. Writes: PATCH …/bots-profile, MCP cockpit_update_bots_profile. Signage Template-Optionen only: MCP cockpit_update_signage_template_options (Lesen: cockpit_signage_template_options). Bot-UI: github.com/sawmuedev/mallpilot-chat-core (v0 importiert — siehe V0_INSTRUCTION_BOT_CORE_PATCH.md).
MCP-Beispiel (QR-Modal-Texte):
cockpit_update_center_website_config {
"centerId": "…",
"qrCode": {
"title": "QR-Code scannen",
"description": "Scanne den Code, um auf dem Handy weiterzumachen.",
"buttonText": "Verstanden"
}
}
legalConfig.cookieConsentTitleTemplate und legalConfig.cookieConsentDescription werden in themeOverrides persistiert (wie Dashboard-Reiter Legal). Öffentliche Website: GET …/public-visitor-surface → visitorPrivacy.cookieBannerTitleTemplate.
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). |
…/centers/{centerId}/signage-template-options | PUT | Signage Template-Optionen (Partial): { templateId, kiosk?, companion? } → signageTemplateOptions[templateId]. MCP: cockpit_update_signage_template_options. |
…/centers/{centerId}/signage-global-options | GET | Signage global (öffentlich): Wartungsmodus + centerLogoUrl — alle Templates. MCP: cockpit_signage_global_options. |
…/centers/{centerId}/signage-global-options | PUT | Signage global schreiben: { maintenanceModeEnabled?, maintenanceMessage? } → signageTemplateOptions._global. MCP: cockpit_update_signage_global_options. |
…/wayfinding/floors/{floorId} | PATCH | Centerplan-Etage aktualisieren (name, floorNumber, isDefault, isActive, color) — kein SVG-Upload. |
…/centers/{centerId}/touchscreens | GET, POST | Touchscreen-/Stele-Standorte listen bzw. anlegen (floorId, position, screenOrientation, optional chromeOsSerialNumber, annotatedAssetId, annotatedLocation). |
…/touchscreens/{touchscreenId} | PATCH, DELETE | Touchscreen aktualisieren (inkl. floorId, Google-Admin-Felder) bzw. deaktivieren (soft delete: isActive=false, Plan-Zuordnung entfernt). Bei Etagenwechsel wird eine MapLocation-Zuordnung auf anderer Etage automatisch gelöscht. |
…/touchscreens/{touchscreenId}/assign | POST, DELETE | Touchscreen einer MapLocation zuordnen bzw. Zuordnung entfernen. |
…/signage-screens/search?q={term} | GET | Signage-Screen-Suche (SN, Asset-ID, Stele-/Center-Name) — nur Center im Agency-Key-Scope; optional centerId. |
Intern (kioskOS, nicht öffentlich): GET /api/internal/kioskos/screens?q={term} — Bearer KIOSKOS_SCREENS_SECRET; Suche über Center-Name/Slug, Asset-ID, Seriennummer.
MCP-Tools (AgencyOS-Key): cockpit_list_wayfinding_floors, cockpit_update_wayfinding_floor, cockpit_list_map_locations, cockpit_create_map_location, cockpit_assign_map_location, cockpit_update_map_location, cockpit_list_touchscreens, cockpit_create_touchscreen, cockpit_update_touchscreen, cockpit_search_signage_screens, cockpit_assign_touchscreen, cockpit_unassign_touchscreen, cockpit_delete_touchscreen.
Typischer Agent-Workflow (Centerplan-Mapping):
cockpit_public_center_by_slug→centerIdcockpit_list_wayfinding_floors→floorIdpro Etagecockpit_public_wayfinding_floors(omitMapSvg/floorNumber) odercockpit_list_map_locations→ bestehendesvgId/Zuordnungen prüfencockpit_assign_map_locationmitfloorId,svgId,shopId(oderserviceId)- Optional Stele:
cockpit_create_touchscreen+cockpit_assign_touchscreen - Google-Admin-Zuordnung:
cockpit_search_signage_screens→cockpit_update_touchscreen(SN, Asset-ID)
Hinweis Eingänge/Routing: Waypoints/Eingänge (GET /api/centers/{centerId}/entrances) sind weiterhin öffentlich lesbar; Schreiben erfolgt im Dashboard (Centerplan-Editor) — noch kein AgencyOS/MCP-Schreibtool.
Hilfslogik: apps/dashboard/src/lib/integration/ (find-shop-duplicates, find-chain-duplicates, merge-shop-chains, shop-name-similarity, agency-map-location).
Inhalte archivieren (Soft-Delete, kein Hard-Delete)
CockpitOS löscht Shops/News/Events/Angebote/Services nicht physisch aus der Datenbank — sie werden auf status: "Archiviert" gesetzt (ggf. isActive: false). Grund: Verknüpfungen (Angebote/Events/Jobs an Shops), Audit, Wiederherstellung im Dashboard.
| Weg | Endpoint / MCP |
|---|---|
| Einzeln | PUT …/agencyos/v1/shops|events|news|offers/{id} mit { "status": "Archiviert" } — MCP: cockpit_archive_content |
| Mehrere | POST …/content/bulk-archive — contentType: offer, event, news, shop, service; ids[] oder (nur Angebote/Events/News) autoExpired: true |
| Push-Alternative | POST …/content/push mit z. B. shops: [{ "id": "…", "status": "Archiviert" }] |
MCP: cockpit_archive_content, cockpit_bulk_archive. Kein cockpit_delete_shop — absichtlich nur Archivierung.
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.
Kategorie-Kacheln auf der Website (categoryIcon / cardSettings)
Globale Defaults (alle Kategorien ohne eigenes cardSettings) liegen im Dashboard-Reiter Webseiten → Design unter designConfig.categoryCardSettings — u. a. showElements.categoryIcon.
| Ziel | API / MCP |
|---|---|
| Global für Center (Icon ein/aus für alle Kategorien) | PUT …/centers/{centerId}/website-config mit designConfig.categoryCardSettings.showElements.categoryIcon oder MCP cockpit_update_center_website_config |
| Nur eine Kategorie (Center-Override bei globaler Kategorie) | PUT …/categories/{categoryId} mit { "centerId": "…", "cardSettings": …, "icon": "…", "coverImage": "…" } oder MCP cockpit_update_category |
| Lesen | GET …/categories liefert cardSettings; öffentlich: public-visitor-surface → design.designConfig.categoryCardSettings |
Bei globalen Kategorien ist centerId im PUT Pflicht (Center-spezifische Override-Kopie, wie im Dashboard).
Website-Config Lesen/Schreiben (Agenten): cockpit_get_center_website_config / cockpit_update_center_website_config — Reiter-Feldpfade via cockpit_website_config_schema. Wichtig für Base-Template:
| Setting | Pfad |
|---|---|
| Kategorie-Icon global | designConfig.categoryCardSettings.showElements.categoryIcon |
| NOW! Hero/Titel | designConfig.toGoHeroImage, toGoGreeting, toGoTitle, toGoSubtitle, toGoBackgroundImage |
| Center-Inhalte | contentConfig.shortName, tagline, socialMedia, specialDays, … |
| Shops-Filter / Shop-Bilder | contentConfig.features.showCategoryFilter, showShopImages, … |
| Centerplan-Hintergrund | centerplanConfig.showMapBackground |
| Centerplan-Renderer/Layout | centerplanConfig.mapRenderer, layoutMode, overlayGeoBounds, … — vollständige Feldliste: cockpit_website_config_schema → Tab centerplan |
| Parken-Seite | parkingConfig.parkingHeroTitle, parkingContentBoxes, … — Tab parking im Schema |
| Seiten ein/aus | pagesConfig.shops, pagesConfig.wayfinding, … — Tab pages im Schema |
| Themenwelten (Gastro/Kategorie/Büro) | GET/POST …/centers/{centerId}/theme-worlds?kind=… · MCP cockpit_list_center_theme_worlds, cockpit_upsert_center_theme_world |
contentConfig.features und designConfig.categoryCardSettings werden partial gemerged (nur gesendete Keys überschreiben).
Themenwelten (Base-Template)
| Aktion | REST | MCP |
|---|---|---|
| Liste | GET …/centers/{centerId}/theme-worlds?kind=gastronomy|category|office | cockpit_list_center_theme_worlds |
| Anlegen | POST …/theme-worlds Body { kind, name, slug, … } | cockpit_upsert_center_theme_world (ohne themeId) |
| Aktualisieren | PUT …/theme-worlds/{themeId} Body { kind, … } | cockpit_upsert_center_theme_world (mit themeId) |
| Löschen | DELETE …/theme-worlds/{themeId}?kind=… | cockpit_delete_center_theme_world |
shopIds / shopLocationIds beim Update ersetzen die Shop-Zuordnung (wie Dashboard). officeIds nur bei kind=office.
Shops/Filialen: Vorübergehend geschlossen (sichtbar + Badge)
Shop oder Filiale bleibt status=Aktiv und auf der Website in Liste/Suche sichtbar; Besucher sehen Badge/Hinweis.
| Feld | Typ | Beschreibung |
|---|---|---|
temporarilyClosed | boolean | true = Hinweis aktiv |
temporarilyClosedNote | string | null | Optionaler Badge-Text; leer = „Vorübergehend geschlossen“ |
temporarilyClosedUntil | ISO 8601 | null | Optional; nach diesem Datum wird der Hinweis beim Rendern ausgeblendet |
API: PUT …/shops/{shopId} bzw. PUT …/shop-locations/{locationId} · MCP: cockpit_update_shop / cockpit_update_shop_location. Nutzer-Doku: Shops vorübergehend geschlossen.
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. Antwort enthält guestReviewUrl / guestReviewExpiresAt, wenn ein gültiger Token existiert (ohne Roh-Token). |
| PUT | /api/agencyos/v1/drafts/{draftId} | { "action": "approve" | "reject", "reason"?: string } — Freigabe führt executeContentDraft aus (wie Dashboard). |
| GET/POST | /api/agencyos/v1/drafts/{draftId}/review-link | Gast-Freigabe-Link erzeugen (POST) oder gültigen Link lesen (GET). Antwort: url, guestReviewUrl, expiresAt. |
| 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. |
Gast-Freigabe (Website-Entwürfe, ohne Login)
Analog Social (/freigabe/social/{token}):
| Öffentliche UI | {DASHBOARD_ORIGIN}/freigabe/content/{token} |
| Öffentliche API | GET/POST {DASHBOARD_ORIGIN}/api/content/review/{token} |
| Token-Prefix | cr_… (30 Tage Gültigkeit beim Erzeugen) |
| Dashboard-Link erzeugen | POST /api/content/drafts/{draftId}/review-link (Session) oder AgencyOS-Route oben |
| POST Body (Gast) | { "action": "approve" | "reject", "reviewerName"?: string, "reason"?: string, "comment"?: string } — bei Ablehnung landen reason/comment in rejectionReason und feedback |
AgencyOS Cutover: Eigene Route /review/{token} → 302 auf {DASHBOARD_ORIGIN}/freigabe/content/{token} (Token 1:1 übernehmen, wenn Cockpit-Link; sonst neuen Link per POST …/review-link erzeugen). Keine eigene Content-Freigabe-UI in AgencyOS mehr pflegen.
MCP/Claude: cockpit_list_drafts (optional includeData: true), cockpit_get_draft (liefert guestReviewUrl), cockpit_content_draft_review_link (action: create|get), cockpit_draft_customer_touchpoint, cockpit_update_draft. Pro Entwurf: createdBy, createdByName, source, createdAt.
Product-Feedback (Dashboard-Inbox)
Getrennt von Besucher-Feedback. Nur globaler AgencyOS-Key (accessScope=global) oder COCKPIT_FEEDBACK_AGENT_SECRET.
| Methode | Pfad | Zweck |
|---|---|---|
| GET | /api/agencyos/v1/product-feedback | Inbox listen (status, type, limit) |
| POST | /api/agencyos/v1/product-feedback | Anlegen (type + message, optional centerId/pageLabel) — Melder cockpitOS-Assistent. Oder Claim (id / referenceCode) |
| GET | /api/agencyos/v1/product-feedback/{id} | Ein Ticket (UUID oder FB-…) |
| POST | /api/agencyos/v1/product-feedback/{id}/dispatch | Bestehendes Ticket nachschicken (nur bug/change + Status new) |
| PATCH | /api/agencyos/v1/product-feedback/{id} | status, resolutionNote, cursorPrUrl, messageAppend. done nur mit Antwort und grünem /api/health |
MCP: cockpit_support_intake (Pflicht-Einstieg: Alltagsbeschreibung reicht, zuerst nachschauen), danach cockpit_product_feedback_list, cockpit_product_feedback_create, cockpit_product_feedback_claim, cockpit_product_feedback_dispatch, cockpit_product_feedback_update.
Faustregel für Agenten: Die Beschreibung der Person ist der Prompt. Nicht nach Fachbegriffen oder der technischen Ursache fragen. Nicht an Menschen weiterleiten, bevor im Cockpit nachgeschaut wurde. Ticket-message = ihr Wortlaut.
Nutzer-Doku: Feedback an cockpitOS.
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. Persönliches MCP-Token: userId = Cockpit-User, channel: mcp_user. AgencyOS-Key: weiter channel: agencyos. GET /api/agencyos/v1/me / cockpit_whoami zeigt authKind. Journal-Liste enthält actor (Name, E-Mail).
MCP-Aktionen rückgängig machen (Journal)
Was: Jeder schreibende AgencyOS-Aufruf (POST/PUT/PATCH/DELETE) speichert den Vorher-Stand. Agenten können die letzten Aktionen listen und eine Aktion zurücksetzen — Inhalte, Website-Config, DNS, nicht nur ein einzelner Endpunkt.
Warum: Audit zeigt nur „wer hat geändert“. Undo braucht den konkreten Vorher-Stand.
Wer: MCP/Claude, AgencyOS-Integrationen. Redaktion sieht den Effekt in den betroffenen Modulen.
Wo: MCP cockpit_undo_mcp_actions. API:
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/agencyos/v1/actions | Liste (Query: hours default 24 max 72, limit, centerId) |
| POST | /api/agencyos/v1/actions | { action: "list" | "undo", actionId?, last?, dryRun?, hours? } — Undo zuerst mit dryRun: true |
So testen
- Eine unkritische Änderung schreiben (z. B. Shop-Name oder DNS-Dry-Run danach
dryRun=false). cockpit_undo_mcp_actionsmitaction=list— die Aktion erscheint mitundoable: true.- Dieselbe ID mit
action=undounddryRun=true— Vorschau der Schritte. - Denselben Aufruf mit
dryRun=false— Daten bzw. DNS sind wieder wie vorher.
Betrieb: Neue Tabelle agency_os_actions (additive Migration). Keine Secrets im Journal (Passwort, API-Key, Token). Nicht rückgängig: Benutzer löschen, Passwort-Reset, Einladung, Medien-Upload, Social-Engage, Domain kaufen/transferieren, Zone löschen.
MCP: cockpit_undo_mcp_actions — „Mach die letzte Änderung rückgängig“ → action=undo, last=true, zuerst dryRun=true.
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?“
Benutzer-Verwaltung (AgencyOS / MCP)
Vollständige Benutzerverwaltung im Scope des API-Keys (Organisation, nutzerweit oder global) — analog zur Dashboard-Nutzer-Verwaltung, ohne SUPER_ADMIN-Rolle.
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/agencyos/v1/users | Listen (Filter: organizationId, role, email, centerId, limit, offset) |
| POST | /api/agencyos/v1/users | Anlegen (name, email, role; optional organizationId, centerAssignments, organizationAssignments, invite) |
| GET | /api/agencyos/v1/users/{userId} | Einzelner Benutzer inkl. Zuweisungen |
| PUT | /api/agencyos/v1/users/{userId} | Aktualisieren (isActive, Rolle, Zuweisungen; newPassword: true = Reset-Link) |
| DELETE | /api/agencyos/v1/users/{userId} | Löschen |
| POST | /api/agencyos/v1/users/{userId}/invite | Einladungs-E-Mail erneut senden |
| POST | /api/agencyos/v1/users/{userId}/reset-password | Passwort-Reset-Link per E-Mail |
Scope: Organisations-Key verwaltet nur Benutzer der eigenen Organisation (bzw. Center in dieser Org). Nutzerweiter Key: Org/Center gemäß Cockpit-Rechten des anbindenden Users. Global-Key: alle Benutzer außer Super-Admins.
Center-Zuweisungen: Nur Center, die der Key ohnehin lesen/schreiben darf (centerAccessibleByAgencyIntegration).
MCP: cockpit_list_users, cockpit_get_user, cockpit_create_user, cockpit_update_user, cockpit_delete_user, cockpit_invite_user, cockpit_reset_user_password
Code: apps/dashboard/src/lib/integration/agencyos-user-admin.ts, Routen unter apps/dashboard/src/app/api/agencyos/v1/users/
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.
United-Domains-Konten
Was / Warum: Die UD-APIs (Reseller + Retail-DNS) sind das Konto — nicht das Cockpit. Agenten sollen alle Domains dort listen, einzelne Einträge inkl. DNS lesen und Records setzen können (Parked, Redirects, Altbestand, DNS-only). Der Cockpit-Abgleich ist optional. cockpit_website_live_inventory bleibt nur für Live-URLs im Cockpit; cockpit_dns_status nur für ein Center.
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/domains/ud-inventory
Bearer-API-Key; liest direkt aus den United-Domains-Konten: Reseller QueryDomainList + QueryDNSZoneList + Retail-Zonen. centers[] ist nur der Abgleich mit Cockpit-Centern.
Query-Parameter (optional):
| Parameter | Beschreibung |
|---|---|
q | Filter: Domain oder Center-Name (Teilstring) |
organizationId / organizationName | Nur Treffer zu Centern dieser Organisation (z. B. ILG) |
unmatchedOnly=true | Nur UD-Domains ohne passendes Center — nur Global-Key, ohne Org-Filter |
Mandantentrennung: Org- und User-Keys sehen nur Domains, die zu ihren Centern passen. Domains im UD-Konto ohne Cockpit-Treffer (andere Kunden, Parked, Altbestand) nur Super-Admin bzw. Agency-Key mit accessScope=global.
Response (200): success, data.configured, data.warnings, data.summary, data.items[], data.cockpitOnly[].
Pro Eintrag in items:
| Feld | Bedeutung |
|---|---|
domain / apex | Domain aus UD, normalisiert |
sources | reseller und/oder retail |
status / expiration | Wenn Reseller wide=1 liefert |
centers | Passende Cockpit-Center (leer = nur UD) |
cockpitOnly: Center-Domains, die nicht im UD-Konto liegen (anderer Registrar/Nameserver).
Eine Domain (Status + DNS):
GET {DASHBOARD_ORIGIN}/api/agencyos/v1/domains/ud-domain?domain=example.de
Registrierung (StatusDomain), Ablauf, Nameserver, Zone/SOA (StatusDNSZone), Ist-Records, Web-Weiterleitungen (QueryWebFwdList, wenn die API sie liefert). Kein centerId. Authinfo/EPP werden nicht zurückgegeben. capabilities sagt, welche Record-Typen schreibbar sind.
DNS setzen:
POST {DASHBOARD_ORIGIN}/api/agencyos/v1/domains/ud-dns
Body: { "domain": "example.de", "dryRun": true, "createZoneIfMissing": false, "records": [{ "op": "set", "type": "A", "name": "@", "value": "216.24.57.1", "ttl": 300 }] }.
| Feld | Bedeutung |
|---|---|
dryRun | Default true — nur Plan/Vorschau. Schreiben nur mit false. |
createZoneIfMissing | Reseller: Zone anlegen, falls keine existiert. Retail: Domain muss schon im Portfolio liegen. |
records[].op | add (dazu), set (gleicher Name+Typ ersetzen), delete |
records[].type | Nur A, AAAA, CNAME, MX, TXT, SRV, CAA |
records[].value | MX: "10 mail.example.de." (Priorität + Ziel). SRV: "0 1 443 mail.example.de." (Priorität + Gewicht + Port + Ziel). Alternativ bei SRV getrennte Felder priority, weight, port, target — werden zu einem Wert zusammengeführt. |
name ist @, www, eine Subdomain oder * (Zonen-Wildcard, auch als *.example.de). Beim Abgleich gilt * gleich *.<apex> — sonst bleibt matchedExisting bei Leichen-Wildcards 0.
Nicht möglich (bewusst): NS/SOA ändern, Zone löschen, Nameserver, Authinfo, Domain kaufen/transferieren. CNAME auf @ wird abgelehnt. Max. 20 Records pro Aufruf. Kill-Switch: COCKPIT_DISABLE_DNS_PROVIDER_WRITE=1.
Mandantentrennung (Schreiben): wie Lesen — Org-/User-Keys nur, wenn die Domain zu einem ihrer Center passt. Global-Key: jede Domain im Konto.
Dashboard: Domains & Go-Live — Filter Alle / Mit Center / Nur UD, Button Details. Session: GET /api/domains/ud-inventory, GET /api/domains/ud-domain?domain=, POST /api/domains/ud-dns.
MCP: cockpit_ud_account (list / inspect / update-dns) · cockpit_ud_dns · cockpit_list_ud_domains · cockpit_ud_domain
So testen
inspectauf eine Domain —dns.recordsundcapabilities.writableTypesprüfen.update-dns/POST …/ud-dnsmitdryRun=true— Response enthältplanundnextStep, UD bleibt unverändert.- Denselben Body mit
dryRun=false— Record erscheint ininspect. - Org-Key auf eine fremde Domain: 403.
Betrieb: Dashboard-Env UD_RESELLING_LOGIN / UD_RESELLING_PASSWORD und/oder UD_DNS_API_KEY. Vollständige Kontoliste nur Super-Admin / Developer bzw. Agency-Key mit accessScope=global.
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). - MCP:
cockpit_social_review(draftId,action,approverEmail, optionalreason).
Implementierung: load-agencyos-social-review-digest.ts, social-review-digest/route.ts, social/drafts/[draftId]/review/route.ts.
Social-Entwürfe, Community, Performance (AgencyOS v1)
Was: Agenten können Social-Entwürfe listen, anlegen, bearbeiten, Freigabe auslösen, Community lesen und moderieren sowie Social-Performance auswerten. Nicht über AgencyOS/MCP: KI-Bild, Kanal-OAuth, PDF-Export.
| Methode | Pfad | MCP |
|---|---|---|
GET | /api/agencyos/v1/social/drafts?centerId=&status= | cockpit_list_social_drafts |
POST | /api/agencyos/v1/social/drafts | cockpit_create_social_draft |
GET | /api/agencyos/v1/social/drafts/{draftId} | cockpit_get_social_draft |
PATCH | /api/agencyos/v1/social/drafts/{draftId} | cockpit_update_social_draft |
DELETE | /api/agencyos/v1/social/drafts/{draftId} | cockpit_delete_social_draft |
POST | /api/agencyos/v1/social/drafts/{draftId}/review | cockpit_social_review |
GET | /api/agencyos/v1/social/drafts/{draftId}/quality-check | cockpit_social_draft_quality_check (optional forSchedule) |
GET | /api/agencyos/v1/social/engage?centerId=&engagePostId=&days=&includeMetrics= | cockpit_social_engage |
POST | /api/agencyos/v1/social/engage | cockpit_social_engage_action |
GET | /api/agencyos/v1/social/reporting?centerId=&days=&includeAds= | cockpit_social_reporting |
GET | /api/agencyos/v1/social/analytics?centerId=&days=&postLimit= | cockpit_social_analytics |
POST | /api/agencyos/v1/social/sync-metrics | cockpit_social_sync_metrics |
GET | /api/agencyos/v1/social/center-accounts | cockpit_list_outstand_centers |
GET | /api/agencyos/v1/centers/{centerId}/social-templates | cockpit_social_templates action=list |
POST | /api/agencyos/v1/centers/{centerId}/social-templates | cockpit_social_templates action=create |
PATCH | /api/agencyos/v1/centers/{centerId}/social-templates | cockpit_social_templates action=set_required (required) |
PATCH | /api/agencyos/v1/centers/{centerId}/social-templates/{templateId} | cockpit_social_templates action=update |
DELETE | /api/agencyos/v1/centers/{centerId}/social-templates/{templateId} | cockpit_social_templates action=delete |
Social-Bildvorlagen (FB-Y4JZXXR3): PNG/WebP-Rahmen pro Center, optionaler Canva-Link. Kein Canva-OAuth. required=true blockiert Freigabe/Publish ohne containers[0].options.socialTemplateId aus demselben Center. Default: nicht Pflicht.
POST Entwurf anlegen (/social/drafts):
{
"centerId": "uuid",
"caption": "Sommer-Sale ab morgen!",
"scheduledAt": "2026-09-01T10:00:00.000Z",
"postType": "feed",
"mediaUrls": ["https://….b-cdn.net/…/bild.jpg"],
"firstComment": "Link in Bio",
"source": "karma-migration",
"action": "submit_review",
"assignedApproverEmails": ["juw@schickma.de"],
"approvalMode": "any"
}
containers[]= volles Medien-/Options-JSON;mediaUrls[](MCP-Shortcut) wird serverseitig zucontainersgemappt.- Optional:
centerIds,linkUrl,location,socialAccountIds,campaignLabel,revisionNotes. - Ohne
action: Statuspending(Entwurf speichern). action: submit_review: Freigabe anfordern (+ optional Teams-Hook).action: publish_direct: Sofort publishen/planen — nur wenn der Integration-Nutzer (Magic-Link) Approver-Rolle hat.assignedApproverEmails: Robi/Teams-freundlich — E-Mails aktiver Cockpit-Nutzer.
PATCH Entwurf (/social/drafts/{draftId}): Felder wie oben; action: cancel_schedule für geplante Posts; scheduledAt, Caption und Medien sind auch bei Status scheduled bzw. approved mit Termin änderbar. Mit Outstand-ID synchronisiert der bestehende Update-Pfad; ohne ID nur Cockpit-Speicher. Vertrag der Felder unverändert.
Reel: postType: "reel" braucht mindestens ein Video in containers[].media (MP4/MOV/WEBM oder Dateiname mit Video-Endung). Bild allein → 400 vor Outstand. Geplante Posts laufen über Outstand-scheduledAt; cockpitOS gleicht überfällige Termine per Cron/Webhook ab, ohne Force-Publish.
DELETE Entwurf (/social/drafts/{draftId}): Entwürfe, Zur Freigabe, Überarbeitung und geplante Posts löschen (geplant inkl. Outstand). Live-Posts: 409. MCP: cockpit_delete_social_draft.
centerIdist bei Engage, Reporting, Analytics und Sync Pflicht.- Engage lesen: Kommentartexte mit
engagePostId+includeComments=true; Metriken mitincludeMetrics=true. Response enthältplatformCommentId,platformPostId,zernioAccountIdfür Aktionen. - Reporting mit
includeAds=true: Meta/Google Ads-KPIs (Zernio-Snapshots) neben organischem Überblick. - Engage schreiben (
POST):action=reply|like|hide|delete|deleteRemote|repost.delete= Kommentar IG/Threads.deleteRemote= Live-Post auf Facebook/X/LinkedIn/Bluesky.repost= erneut teilen (optionalquote). Body-Felder wie Dashboard/api/social/engage. - Analytics liefert Plattform-KPIs (IG/FB/…), Posts mit Metriken, Follower-Trend, Engagement — ideal für Claude-Auswertungen.
- Reporting = KPI-Überblick ohne Post-Liste; Sync holt Live-Daten von Outstand (optional Zernio).
Typischer Agent-Flow:
cockpit_list_outstand_centers— welche Kanäle sind verbunden?cockpit_social_sync_metrics— frische Zahlen (bei leeren KPIs)cockpit_social_analytics— Auswertung (Plattformvergleich, Top-Posts)cockpit_social_engage— Community/Kommentare vertiefen (engagePostId,includeComments=true)cockpit_social_engage_action— Antwort/Like/Hide mit IDs aus Schritt 4
Implementierung: load-social-analytics.ts, load-agencyos-social-cockpit.ts, social-engage-actions.ts, social/analytics/route.ts, social/sync-metrics/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.
Center-Mail (Mailbox & Alias, Phase 0–1)
Was: Kunden-Postfächer und Weiterleitungen. Inventar in cockpitOS (center_mailboxes, center_mail_aliases); der Mail-Anbieter steckt hinter MailProvider.
Hinweis: Kein öffentlicher Endpunkt. Passwort nur einmal in der POST-Response, nie in der Liste. Phase 0b: aktive Mailbox-/Alias-Adressen sind Empfänger für POST /api/centers/{centerId}/send-contact-inquiry (Resend).
| Methode | Pfad | Beschreibung |
|---|---|---|
GET | /api/agencyos/v1/centers/{centerId}/mailboxes | Liste |
POST | /api/agencyos/v1/centers/{centerId}/mailboxes | Anlegen (localPart, optional domain, quotaMb, name) |
PATCH | /api/agencyos/v1/centers/{centerId}/mailboxes/{mailboxId} | Quota / aktiv / Name |
POST | /api/agencyos/v1/centers/{centerId}/mailboxes/{mailboxId}/password | Neues Passwort (confirm=true) |
DELETE | /api/agencyos/v1/centers/{centerId}/mailboxes/{mailboxId} | Löschen |
GET | /api/agencyos/v1/centers/{centerId}/mail-aliases | Weiterleitungen |
POST | /api/agencyos/v1/centers/{centerId}/mail-aliases | Anlegen (localPart, target, optional domain). Extern: verificationRequired |
POST | /api/agencyos/v1/centers/{centerId}/mail-aliases/{aliasId}/verify | Bestätigung erneut senden |
DELETE | /api/agencyos/v1/centers/{centerId}/mail-aliases/{aliasId} | Löschen |
GET | /api/agencyos/v1/centers/{centerId}/mail-dns | Soll-DNS + Diff (nichts schreiben) |
POST | /api/agencyos/v1/centers/{centerId}/mail-dns | dryRun Standard true; Schreiben nur mit dryRun=false und confirm=true |
GET POST | /api/agencyos/v1/mailboxes | Ohne Center: Plattform/eigenständig. POST: localPart, optional domain/centerId. Nur Super-Admin-Key ohne Center |
PATCH DELETE | /api/agencyos/v1/mailboxes/{mailboxId} | Quota / aktiv / Name bzw. löschen |
POST | /api/agencyos/v1/mailboxes/{mailboxId}/password | Neues Passwort (confirm=true) |
GET POST | /api/agencyos/v1/mail-aliases | Weiterleitungen ohne Pflicht-Center |
POST | /api/agencyos/v1/mail-aliases/{aliasId}/verify | Bestätigung erneut senden |
DELETE | /api/agencyos/v1/mail-aliases/{aliasId} | Löschen |
GET | /api/agencyos/v1/mailcow/ui | Capabilities (UI-Texte/App-Links ja; Logo/SOGo nein). Nur globaler oder persönlicher Super-Admin-Key |
POST | /api/agencyos/v1/mailcow/ui | confirm=true Pflicht. action=ui (Titel/Hilfe/Footer) oder action=appLinks (links[]) |
MCP: cockpit_list_mailboxes, cockpit_create_mailbox, cockpit_update_mailbox, cockpit_reset_mailbox_password, cockpit_delete_mailbox, cockpit_list_mail_aliases, cockpit_create_mail_alias, cockpit_resend_mail_alias_verification, cockpit_delete_mail_alias, cockpit_mail_dns_plan, cockpit_mail_dns_apply, cockpit_mailcow_admin_status, cockpit_mailcow_set_ui, cockpit_mailcow_set_app_links
Dashboard-Session (gleiche Services, kein Bearer): /api/centers/{centerId}/mailboxes, mail-aliases, mail-dns. Reiter E-Mail am Center. Öffentliche Bestätigung: POST /api/mail-alias/verify (Token, kein Bearer). Super-Admin: GET/POST /api/mailboxes, /api/mailcow/ui.
Migration: packages/database/migrations/20260903160000_add_center_mail_SAFE.sql, 20260909090000_add_mail_alias_verify_SAFE.sql, 20260909100000_add_mail_owner_kind_SAFE.sql
So testen: Center-Mail Provisioning
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 | Organisationen listen (Global: alle; Org-Key: eigene) |
POST | /api/agencyos/v1/organizations | Organisation anlegen (nur Global-API-Key); Body: name (Pflicht), optional slug, type (SINGLE_MALL | MULTI_MALL | PORTFOLIO), description, website, Kontaktfelder |
GET | /api/agencyos/v1/organizations/{slugOrId} | Organisation lesen (Slug, UUID oder Name) |
PATCH | /api/agencyos/v1/organizations/{slugOrId} | Organisation aktualisieren (Partial Update; Org-Key: Stammdaten; Global-Key zusätzlich isActive, maxCenters, maxUsers) |
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: Website-Hosting-Kanal + Vercel-Modus |
PATCH | /api/agencyos/v1/centers/{centerId}/signage-hosting | Signage/Companion: Hosting-Kanal, signageVercelProjectMode/Id, signagePublicUrl |
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, Gast-Link) | apps/dashboard/src/app/api/agencyos/v1/drafts/route.ts, …/drafts/[draftId]/route.ts, …/drafts/[draftId]/review-link/route.ts, …/drafts/[draftId]/customer-touchpoint-suggestion/route.ts |
| Content-Gast-Freigabe (öffentlich) | apps/dashboard/src/app/api/content/review/[token]/route.ts, apps/dashboard/src/app/freigabe/content/[token]/page.tsx |
| v1 Audit-Log | apps/dashboard/src/app/api/agencyos/v1/audit-logs/route.ts |
| v1 Aktionsjournal / Undo | apps/dashboard/src/app/api/agencyos/v1/actions/route.ts, agency-os-action-journal.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 Social-Entwürfe / Engage / Reporting | social/drafts/route.ts, social/drafts/[draftId]/route.ts, social/drafts/[draftId]/quality-check/route.ts, social/engage/route.ts, social/reporting/route.ts, load-agencyos-social-cockpit.ts |
| Social Pre-Flight-QA (Dashboard) | apps/dashboard/src/lib/integration/social-preflight-qa.ts, GET /api/social/drafts/[draftId]/quality-check |
| 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