openapi: 3.0.3
info:
  title: CockpitOS – AgencyOS Integration
  description: |
    AgencyOS (team.cockpit-os.de): magic link handshake and v1 REST endpoints.
    API keys may be organization-scoped (single org + integration-owned centers without org) or user-scoped
    (all centers the connecting Cockpit user may access, across orgs, plus same integration orphans).
    Browser-only routes (session cookie) are documented for completeness; AgencyOS backends typically use
    POST/GET magic-link + Bearer v1 APIs only.
  version: 1.0.0
servers:
  - url: https://dashboard.cockpit-os.de
    description: Example production dashboard
  - url: http://localhost:3000
    description: Local dashboard dev

tags:
  - name: AgencyOS Magic Link
  - name: AgencyOS v1
  - name: Cockpit Browser (Session)

paths:
  /api/agencyos/magic-link:
    post:
      tags: [AgencyOS Magic Link]
      summary: Create magic link (no auth)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [integrationName]
              properties:
                integrationName:
                  type: string
                  example: AgencyOS Production
                returnUrl:
                  type: string
                  format: uri
                  description: Optional https? URL for post-connect browser redirect
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MagicLinkCreateResponse'
        '400':
          description: Bad request
        '500':
          description: Server error
    get:
      tags: [AgencyOS Magic Link]
      summary: Poll magic link status (no auth)
      parameters:
        - in: query
          name: token
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Status (includes apiKey when completed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MagicLinkStatusResponse'
        '400':
          description: Missing token
        '404':
          description: Invalid token
        '410':
          description: Expired (while still pending)

  /api/agencyos/magic-link/complete:
    post:
      tags: [Cockpit Browser (Session)]
      summary: Complete magic link (NextAuth session + JSON body)
      description: |
        Called from the dashboard after login; not used with Bearer from AgencyOS server.
        Either organizationId (org-scoped key) or accessScope "user" (user-scoped key, no organizationId).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [token, organizationId]
                  properties:
                    token:
                      type: string
                    organizationId:
                      type: string
                      format: uuid
                - type: object
                  required: [token, accessScope]
                  properties:
                    token:
                      type: string
                    accessScope:
                      type: string
                      enum: [user]
      responses:
        '200':
          description: Integration created
        '401':
          description: Unauthorized
        '403':
          description: No permission for organization
        '409':
          description: Token already used

  /api/agencyos/organizations:
    get:
      tags: [Cockpit Browser (Session)]
      summary: List organizations user may connect (session)
      responses:
        '200':
          description: OK
        '401':
          description: Unauthorized

  /api/agencyos/v1/centers:
    get:
      tags: [AgencyOS v1]
      summary: List centers allowed for this API key (org or user scope + integration orphans)
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CentersListResponse'
        '401':
          description: Missing or invalid API key
    post:
      tags: [AgencyOS v1]
      summary: Create center (with or without organization)
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CenterCreateBody'
      responses:
        '201':
          description: Created
        '400':
          description: Validation error
        '401':
          description: Missing or invalid API key
        '409':
          description: Slug conflict

  /api/agencyos/v1/centers/{centerId}:
    parameters:
      - in: path
        name: centerId
        required: true
        schema:
          type: string
    get:
      tags: [AgencyOS v1]
      summary: Get one center
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
        '401':
          description: Unauthorized
        '404':
          description: Not found or wrong org
    patch:
      tags: [AgencyOS v1]
      summary: Partial update center
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              properties:
                attachOrganizationId:
                  type: string
                  format: uuid
                  description: >
                    When using a user-scoped API key and assignToKeyedOrganization (or alias) is true,
                    target organization to attach the orphan center to (user must be allowed to link).
              description: >
                Optional fields e.g. name, address, city, postalCode, slug, websiteEnabled.
                assignToKeyedOrganization / assignToOrganization / linkToKeyedOrganization true
                links an integration-owned orphan center to an organization: for organization-scoped keys,
                the keyed organization; for user-scoped keys, attachOrganizationId is required.
      responses:
        '200':
          description: OK
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '404':
          description: Not found
        '409':
          description: Slug conflict

  /api/agencyos/v1/centers/{centerId}/context:
    parameters:
      - in: path
        name: centerId
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [AgencyOS v1]
      summary: Read center context for AI (stammdaten, shops, services, optional editorial)
      description: |
        Same visibility rules as GET /v1/centers/{centerId}. Shops and shop locations use the same filters as
        the public website (status Aktiv + publish windows). Services use websiteServicePublicFilter.
        Optional include=comma-separated center,shops,services,news,events,offers,categories,chains,floors_summary (default center,shops).
        News/events/offers: all rows for this center (any status, incl. drafts), newest updatedAt first;
        each item exposes agencyosPushId and wordpressPushId when present in metadata.
        categories: id, name, slug only (token-efficient; same source as website shop categories).
        chains: id, name, slug only (max 500).
        **floors_summary:** per active MapFloor counts and metadata only — mapSvgChars (length only, no markup), suggestsHybridSvg, hasMapImageUrl, hasShopViewBoxes, shopViewBoxesKeyCount, mapLocationActiveCount. For full SVG + mapLocations use public GET `/api/wayfinding/floors` (or MCP cockpit_public_wayfinding_floors).
        shopLimit default 500 max 2000; serviceLimit default 200 max 500;
        newsLimit/eventsLimit/offersLimit default 80 max 200 each.
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: include
          schema:
            type: string
            example: center,shops,services,news,events,offers,categories,chains,floors_summary
        - in: query
          name: shopLimit
          schema:
            type: integer
            default: 500
        - in: query
          name: serviceLimit
          schema:
            type: integer
            default: 200
        - in: query
          name: newsLimit
          schema:
            type: integer
            default: 80
        - in: query
          name: eventsLimit
          schema:
            type: integer
            default: 80
        - in: query
          name: offersLimit
          schema:
            type: integer
            default: 80
      responses:
        '200':
          description: OK — data.center, data.shops[], optional services/news/events/offers/categories/chains/data.floors_summary[]; meta.counts (incl. floors_summary when requested)
        '401':
          description: Missing or invalid API key
        '404':
          description: Center not found or not allowed for key
        '500':
          description: Server error

  /api/agencyos/v1/content/push/preview:
    post:
      tags: [AgencyOS v1]
      summary: Preview push (dry-run, no DB writes)
      description: |
        Same JSON body and auth as POST /api/agencyos/v1/content/push. Resolves upserts and chain logic
        but does not create or update rows. Returns previewPlan with planned create/update per entity.
        New ShopChain creation is reported as would_create_chain without inserting.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [centerId]
              properties:
                centerId:
                  type: string
                  format: uuid
                shops:
                  type: array
                events:
                  type: array
                news:
                  type: array
                offers:
                  type: array
                services:
                  type: array
      responses:
        '200':
          description: Preview only — data.previewPlan, data.summary, optional data.errors
        '400':
          description: Bad JSON or missing centerId
        '401':
          description: Unauthorized
        '403':
          description: centerId not allowed for key
        '500':
          description: Server error
    get:
      tags: [AgencyOS v1]
      summary: Preview not available via GET
      description: Use POST with JSON body (same as content push).
      responses:
        '405':
          description: Method not allowed — use POST

  /api/agencyos/v1/content/push:
    post:
      tags: [AgencyOS v1]
      summary: Push entity arrays into one center
      description: |
        Same entity shapes as WordPress /api/wordpress/push-content; requires centerId in body.
        Shop items support chain linking and AI-friendly aliases (chainName, kette, agencyosId, …).
        For events, news, offers, and services: use agencyosId / externalId / clientReference (or non-UUID id when source is agencyos)
        for upsert via metadata.agencyosPushId — same idempotency model as shops.
        See cockpit-docs developer-guide api-wordpress-push-content.md and api-agencyos-integration.md.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [centerId]
              properties:
                centerId:
                  type: string
                  format: uuid
                shops:
                  type: array
                  items:
                    $ref: '#/components/schemas/ShopPushItem'
                events:
                  type: array
                  items:
                    type: object
                news:
                  type: array
                  items:
                    type: object
                offers:
                  type: array
                  items:
                    type: object
                services:
                  type: array
                  items:
                    type: object
      responses:
        '200':
          description: Processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushContentResponse'
        '400':
          description: Bad JSON or missing centerId
        '401':
          description: Unauthorized
        '403':
          description: centerId not in key organization
        '500':
          description: Server error

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: AgencyOS integration API key (sk_agencyos_…); organization- or user-scoped (see magic-link polling accessScope)

  schemas:
    MagicLinkCreateResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            magicLink:
              type: string
            token:
              type: string
            expiresAt:
              type: string
              format: date-time
        message:
          type: string

    MagicLinkStatusResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            token:
              type: string
            integrationName:
              type: string
            returnUrl:
              type: string
              nullable: true
            status:
              type: string
              enum: [pending, completed]
            expiresAt:
              type: string
              format: date-time
            organizationId:
              type: string
              nullable: true
            integrationId:
              type: string
              nullable: true
            apiKey:
              type: string
              description: Present only when status is completed
            accessScope:
              type: string
              enum: [organization, user]
              description: Present when status is completed; user = cross-org centers per Cockpit user rights

    CentersListResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            type: object
            additionalProperties: true

    CenterCreateBody:
      type: object
      required: [name, address, city, postalCode]
      properties:
        name:
          type: string
        address:
          type: string
        city:
          type: string
        postalCode:
          type: string
        country:
          type: string
          default: DE
        slug:
          type: string
        description:
          type: string
        phone:
          type: string
        email:
          type: string
        website:
          type: string
        status:
          type: string
          default: active
        withoutOrganization:
          type: boolean
          description: Create center with organizationId null; scoped to this integration
        noOrganization:
          type: boolean
          description: Same as withoutOrganization
        organizationId:
          type: string
          nullable: true
          description: >
            null = without org. Organization-scoped key UUID must match keyed org.
            User-scoped key must send explicit target org UUID (if permitted) or null / withoutOrganization for orphan.

    ShopPushItem:
      type: object
      description: >
        Core fields plus optional ShopChain link and stable external id for AgencyOS/AI.
        Additional keys are ignored.
      properties:
        id:
          type: string
          description: Cockpit shop UUID, wp_* id, or (agencyos) opaque string stored as agencyosPushId
        name:
          type: string
        category:
          type: string
        slug:
          type: string
        description:
          type: string
        floor:
          type: string
        location:
          type: string
        logo:
          type: string
        phone:
          type: string
        email:
          type: string
        website:
          type: string
        openingHours:
          type: string
        status:
          type: string
        shopChainId:
          type: string
          format: uuid
        chainId:
          type: string
          format: uuid
        chainName:
          type: string
        shopChainName:
          type: string
        brandName:
          type: string
        kette:
          type: string
        marke:
          type: string
        chainCategory:
          type: string
        chainCategoryId:
          type: string
          format: uuid
        agencyosId:
          type: string
        externalId:
          type: string
        clientReference:
          type: string
        clearShopChain:
          type: boolean
        unlinkFromChain:
          type: boolean
        standaloneShop:
          type: boolean
        einzelshop:
          type: boolean
        modus:
          type: string
          description: e.g. einzel, standalone, ohne kette

    PushContentResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            summary:
              type: object
              properties:
                shops:
                  type: integer
                events:
                  type: integer
                news:
                  type: integer
                offers:
                  type: integer
                services:
                  type: integer
            errors:
              type: array
              items:
                type: string
        message:
          type: string

    FloorsSummaryItem:
      type: object
      description: One active floor row for AgencyOS context floors_summary include (no mapSvg markup).
      properties:
        floorId:
          type: string
          format: uuid
        name:
          type: string
        floorNumber:
          type: integer
        isDefault:
          type: boolean
        mapSvgChars:
          type: integer
          description: CHARACTER LENGTH(mapSvg); large values often indicate embedded raster or base64 hybrid SVG.
        suggestsHybridSvg:
          type: boolean
          description: Heuristic — mapSvg contains typical hybrid markers.
        hasMapImageUrl:
          type: boolean
        hasShopViewBoxes:
          type: boolean
        shopViewBoxesKeyCount:
          type: integer
          nullable: true
          description: Number of preset viewBox entries if JSON valid; otherwise null.
        mapLocationActiveCount:
          type: integer
