openapi: 3.1.0

info:
  title: SPRLST API
  version: "1"
  description: |
    REST-API für SPRLST. Alle Endpunkte sind unter `/api/v1` erreichbar.

    ## MCP-Server (für KI-Agenten)

    Neben der REST-API steht ein MCP-Server (Model Context Protocol) bereit:

    **URL:** `POST https://sprlst.de/mcp`

    Der MCP-Server exponiert dieselben Operationen als **Tools**, die KI-Agenten
    (Claude, Cursor, etc.) direkt aufrufen können – ohne HTTP-Routing-Wissen.

    ### Verfügbare Tools

    | Tool | Ability | Beschreibung |
    |---|---|---|
    | `list-nodes` | read | Alle zugänglichen Root-Nodes |
    | `get-node` | read | Einzelnen Node per ID |
    | `get-children` | read | Direkte Kinder eines Nodes |
    | `get-subtree` | read | Node + Nachfahren als Baum (max_depth 1–10) |
    | `get-ancestors` | read | Pfad von Root bis zum Node |
    | `list-comments` | read | Kommentare eines Nodes |
    | `create-node` | write | Neuen Node anlegen |
    | `update-node` | write | Name, Beschreibung, Status, Position ändern |
    | `move-node` | write | Node in anderen Teilbaum verschieben |
    | `create-tree` | write | Ganzen Teilbaum auf einmal anlegen (max 50 Nodes, 5 Ebenen) |
    | `add-comment` | write | Kommentar hinzufügen |
    | `delete-node` | delete | Node und alle Nachfahren löschen |

    ### Claude Code einbinden

    In `~/.claude/settings.json` (oder lokal `.claude/settings.json`):

    ```json
    {
      "mcpServers": {
        "sprlst": {
          "type": "http",
          "url": "https://sprlst.de/mcp",
          "headers": {
            "Authorization": "Bearer <token>"
          }
        }
      }
    }
    ```

    ## Authentifizierung

    Alle Endpunkte erfordern einen Bearer-Token im `Authorization`-Header:

    ```
    Authorization: Bearer <token>
    ```

    Tokens werden über `POST /api/v1/tokens` erzeugt. Jeder Token trägt
    **Abilities** (Berechtigungsscopes) und optional eine
    **Node-Beschränkung** (`node_ids`), die den Zugriff auf einen Teilbaum
    einschränkt.

    ### Abilities / Scopes

    | Ability         | Bedeutung                              |
    |-----------------|----------------------------------------|
    | `nodes:read`    | Nodes, Kommentare und Subtrees lesen   |
    | `nodes:write`   | Nodes und Kommentare erstellen/ändern  |
    | `nodes:delete`  | Nodes und Kommentare löschen           |
    | `nodes:admin`   | Berechtigungen verwalten               |

    ### Token-Presets

    | Preset        | Abilities                                              |
    |---------------|--------------------------------------------------------|
    | `read-only`   | `nodes:read`                                           |
    | `read-write`  | `nodes:read`, `nodes:write`, `nodes:delete`            |
    | `ai-agent`    | `nodes:read`, `nodes:write`                            |
    | `full-access` | `nodes:read`, `nodes:write`, `nodes:delete`, `nodes:admin` |

    ### Node-Beschränkungen (`node_ids`)

    Wird ein Token mit `node_ids` erstellt, darf er nur auf Nodes zugreifen,
    die sich innerhalb des angegebenen Teilbaums befinden (d. h. der Node selbst
    oder einer seiner Nachfahren). Ein leeres Array (`[]`) verweigert den
    Zugriff auf alle Nodes.

    ## Fehlerantworten

    | Code | Bedeutung                                   |
    |------|---------------------------------------------|
    | 401  | Kein oder ungültiger Token                  |
    | 403  | Fehlende Ability oder Node-Berechtigung     |
    | 404  | Ressource nicht gefunden                    |
    | 422  | Validierungsfehler                          |

servers:
  - url: https://sprlst.de/api/v1
    description: Produktion

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer

  schemas:
    Node:
      type: object
      properties:
        id:
          type: string
          description: UUID des Nodes
        name:
          type: string
        description:
          type: string
          nullable: true
        is_completed:
          type: boolean
        position:
          type: integer
          description: Sortierposition innerhalb der Geschwister (0-basiert)
        parent_id:
          type: string
          nullable: true
          description: UUID des Eltern-Nodes; null bei Root-Nodes
        user_id:
          type: string
          description: Besitzer des Nodes
        created_by:
          type: string
          description: User-ID, die den Node angelegt hat
        created_via_token_id:
          type: string
          nullable: true
        created_via_token_name:
          type: string
          nullable: true
        children_count:
          type: integer
          description: Anzahl direkter Kinder (nur bei Listenendpunkten)
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    NodeSubtree:
      type: object
      description: Node mit rekursiv eingebetteten Kindern
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        is_completed:
          type: boolean
        position:
          type: integer
        parent_id:
          type: string
          nullable: true
        user_id:
          type: string
        created_by:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        children:
          type: array
          items:
            $ref: '#/components/schemas/NodeSubtree'

    Comment:
      type: object
      properties:
        id:
          type: string
        node_id:
          type: string
        user_id:
          type: string
        content:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Permission:
      type: object
      properties:
        user_id:
          type: integer
        user_name:
          type: string
          nullable: true
        user_email:
          type: string
          nullable: true
        level:
          type: string
          enum: [read, write, admin]
        granted_by:
          type: integer

    Token:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        abilities:
          type: array
          items:
            type: string
        node_restrictions:
          type: array
          nullable: true
          items:
            type: string
          description: Liste von Node-UUIDs; null = unbeschränkt
        last_used_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
        role:
          type: string

paths:

  # ── Auth / User ──────────────────────────────────────────────────────────────

  /user:
    get:
      summary: Eingeloggten Nutzer abrufen
      description: Gibt die Basisdaten des Nutzers zurück, dem der Token gehört.
      tags: [Auth]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/User'

  # ── Token-Verwaltung ─────────────────────────────────────────────────────────

  /tokens:
    get:
      summary: Alle eigenen Tokens auflisten
      tags: [Tokens]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Token'

    post:
      summary: Neuen Token erstellen
      description: |
        Erstellt einen neuen API-Token. Der Klartext-Token (`token`) wird nur
        einmalig in der Antwort zurückgegeben – danach nicht mehr abrufbar.

        Abilities können entweder direkt als Array oder über ein `preset` angegeben
        werden. Bei Angabe von `node_ids` ist der Token auf Nodes innerhalb dieser
        Teilbäume beschränkt.
      tags: [Tokens]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 255
                  example: "Mein Automatisierungs-Token"
                preset:
                  type: string
                  enum: [read-only, read-write, ai-agent, full-access]
                  description: Vordefinierte Ability-Kombination (überschreibt `abilities`)
                abilities:
                  type: array
                  items:
                    type: string
                    enum: [nodes:read, nodes:write, nodes:delete, nodes:admin]
                  description: Ignoriert, wenn `preset` gesetzt ist. Standard: [nodes:read]
                expires_at:
                  type: string
                  format: date-time
                  nullable: true
                  description: Ablaufzeitpunkt (muss in der Zukunft liegen)
                node_ids:
                  type: array
                  nullable: true
                  items:
                    type: string
                  description: Node-UUIDs, auf die der Token beschränkt ist
            examples:
              read-only:
                summary: Lesender Token ohne Ablauf
                value:
                  name: "Readonly"
                  preset: "read-only"
              ai-agent:
                summary: KI-Agent mit Schreibzugriff auf einen Teilbaum
                value:
                  name: "AI Agent"
                  preset: "ai-agent"
                  node_ids: ["abc-123"]
      responses:
        "201":
          description: Token erstellt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: '#/components/schemas/Token'
                      - type: object
                        properties:
                          token:
                            type: string
                            description: Klartext-Token (nur einmalig sichtbar)

    delete:
      summary: Alle eigenen Tokens löschen
      tags: [Tokens]
      responses:
        "204":
          description: Alle Tokens gelöscht

  /tokens/{tokenId}:
    delete:
      summary: Einzelnen Token löschen
      tags: [Tokens]
      parameters:
        - name: tokenId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "204":
          description: Token gelöscht
        "404":
          description: Token nicht gefunden

  # ── Nodes – Lesen ────────────────────────────────────────────────────────────

  /nodes:
    get:
      summary: Root-Nodes auflisten
      description: |
        Gibt alle Root-Nodes (ohne Eltern) des eingeloggten Nutzers zurück,
        ergänzt um geteilte Root-Nodes anderer Nutzer. Bei Tokens mit
        `node_ids` werden nur Nodes zurückgegeben, auf die der Token Zugriff hat.
      tags: [Nodes]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Node'

    post:
      summary: Node erstellen
      description: |
        Erstellt einen neuen Node. Wird `parent_id` angegeben, muss der Nutzer
        Schreibzugriff auf den Eltern-Node haben. Ohne `parent_id` wird ein
        neuer Root-Node angelegt.
      tags: [Nodes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 255
                description:
                  type: string
                  nullable: true
                parent_id:
                  type: string
                  nullable: true
                  description: UUID des Eltern-Nodes
                position:
                  type: integer
                  minimum: 0
                  default: 0
      responses:
        "200":
          description: Node erstellt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Node'

  /nodes/{node}:
    get:
      summary: Einzelnen Node abrufen
      tags: [Nodes]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Node'

    patch:
      summary: Node aktualisieren
      description: Alle Felder sind optional (partial update).
      tags: [Nodes]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 255
                description:
                  type: string
                  nullable: true
                is_completed:
                  type: boolean
                position:
                  type: integer
                  minimum: 0
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Node'

    delete:
      summary: Node löschen
      description: Löscht den Node und alle Nachfahren (Kaskade).
      tags: [Nodes]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
                  id:
                    type: string

  /nodes/{node}/children:
    get:
      summary: Direkte Kinder eines Nodes auflisten
      tags: [Nodes]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Node'

    post:
      summary: Kind-Node erstellen
      description: Erstellt einen neuen Kind-Node direkt unter dem angegebenen Node.
      tags: [Nodes]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 255
                description:
                  type: string
                  nullable: true
                position:
                  type: integer
                  minimum: 0
                  default: 0
      responses:
        "200":
          description: Kind-Node erstellt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Node'

  /nodes/{node}/ancestors:
    get:
      summary: Vorfahren eines Nodes abrufen
      description: Gibt alle Eltern-Nodes bis zur Wurzel zurück (aufsteigend sortiert).
      tags: [Nodes]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Node'

  /nodes/{node}/subtree:
    get:
      summary: Teilbaum eines Nodes abrufen
      description: |
        Gibt den Node mit rekursiv eingebetteten Kindern zurück. Die Tiefe ist
        über `max_depth` konfigurierbar (Standard: 5, Maximum: 10).
      tags: [Nodes]
      parameters:
        - $ref: '#/components/parameters/nodeId'
        - name: max_depth
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 10
            default: 5
          description: Maximale Rekursionstiefe
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/NodeSubtree'

  /nodes/{node}/move:
    post:
      summary: Node verschieben
      description: |
        Verschiebt einen Node in einen anderen Eltern-Node oder an die Wurzelebene
        (`parent_id: null`). Der Ziel-Eltern-Node muss Schreibzugriff erlauben.
        Zirkuläre Verschiebungen (in den eigenen Teilbaum) werden abgelehnt (422).
      tags: [Nodes]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                parent_id:
                  type: string
                  nullable: true
                  description: UUID des neuen Eltern-Nodes; null = Root-Ebene
                position:
                  type: integer
                  minimum: 0
      responses:
        "200":
          description: Node nach dem Verschieben
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Node'
        "422":
          description: Zirkuläre Verschiebung nicht erlaubt

  # ── Kommentare ───────────────────────────────────────────────────────────────

  /nodes/{node}/comments:
    get:
      summary: Kommentare eines Nodes abrufen
      tags: [Comments]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Comment'

    post:
      summary: Kommentar erstellen
      tags: [Comments]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content:
                  type: string
                  maxLength: 5000
      responses:
        "200":
          description: Kommentar erstellt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Comment'

  /comments/{comment}:
    delete:
      summary: Kommentar löschen
      description: |
        Eigene Kommentare kann jeder Nutzer löschen. Fremde Kommentare
        können nur Node-Admins löschen.
      tags: [Comments]
      parameters:
        - name: comment
          in: path
          required: true
          schema:
            type: string
          description: UUID des Kommentars
      responses:
        "204":
          description: Kommentar gelöscht

  # ── Berechtigungen ───────────────────────────────────────────────────────────

  /nodes/{node}/permissions:
    get:
      summary: Berechtigungen eines Nodes auflisten
      description: Erfordert `nodes:read`-Ability und `admin`-Berechtigung auf dem Node.
      tags: [Permissions]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Permission'

    post:
      summary: Berechtigung vergeben
      description: |
        Erfordert `nodes:admin`-Ability und `admin`-Berechtigung auf dem Node.
        Man kann keine höhere Berechtigung vergeben als man selbst hat.
        Existiert bereits eine Berechtigung für den Nutzer, wird sie überschrieben.
      tags: [Permissions]
      parameters:
        - $ref: '#/components/parameters/nodeId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id, level]
              properties:
                user_id:
                  type: integer
                level:
                  type: string
                  enum: [read, write, admin]
      responses:
        "201":
          description: Berechtigung erstellt oder aktualisiert
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      user_id:
                        type: integer
                      level:
                        type: string

  /nodes/{node}/permissions/{user}:
    put:
      summary: Berechtigung aktualisieren
      tags: [Permissions]
      parameters:
        - $ref: '#/components/parameters/nodeId'
        - name: user
          in: path
          required: true
          schema:
            type: integer
          description: ID des Nutzers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [level]
              properties:
                level:
                  type: string
                  enum: [read, write, admin]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      user_id:
                        type: integer
                      level:
                        type: string

    delete:
      summary: Berechtigung entziehen
      tags: [Permissions]
      parameters:
        - $ref: '#/components/parameters/nodeId'
        - name: user
          in: path
          required: true
          schema:
            type: integer
          description: ID des Nutzers
      responses:
        "204":
          description: Berechtigung entfernt

  # ── Batch-Operationen ────────────────────────────────────────────────────────

  /batch/nodes:
    post:
      summary: Mehrere Nodes auf einmal erstellen
      description: |
        Erstellt bis zu 50 Nodes in einer atomaren Transaktion. Schlägt ein
        Node fehl, werden alle zurückgerollt.
      tags: [Batch]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nodes]
              properties:
                nodes:
                  type: array
                  minItems: 1
                  maxItems: 50
                  items:
                    type: object
                    required: [name]
                    properties:
                      name:
                        type: string
                        maxLength: 255
                      description:
                        type: string
                        nullable: true
                      parent_id:
                        type: string
                        nullable: true
                      position:
                        type: integer
                        minimum: 0
            example:
              nodes:
                - name: "Projekt A"
                  position: 0
                - name: "Projekt B"
                  position: 1
                - name: "Unteraufgabe"
                  parent_id: "uuid-von-projekt-a"
                  position: 0
      responses:
        "201":
          description: Alle Nodes erstellt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Node'

    patch:
      summary: Mehrere Nodes auf einmal aktualisieren
      description: |
        Aktualisiert bis zu 50 Nodes in einer atomaren Transaktion. Alle Felder
        außer `id` sind optional (partial update).
      tags: [Batch]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nodes]
              properties:
                nodes:
                  type: array
                  minItems: 1
                  maxItems: 50
                  items:
                    type: object
                    required: [id]
                    properties:
                      id:
                        type: string
                        description: UUID des zu aktualisierenden Nodes
                      name:
                        type: string
                        maxLength: 255
                      description:
                        type: string
                        nullable: true
                      is_completed:
                        type: boolean
                      position:
                        type: integer
                        minimum: 0
      responses:
        "200":
          description: Alle Nodes aktualisiert
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Node'

  /batch/nodes/tree:
    post:
      summary: Ganzen Teilbaum auf einmal erstellen
      description: |
        Erstellt einen Node mit beliebig tief verschachtelten Kindern in einer
        atomaren Transaktion. Limits: max. **50 Nodes** gesamt,
        max. **5 Ebenen** Tiefe.

        Die Struktur wird rekursiv im `children`-Feld angegeben.
      tags: [Batch]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 255
                description:
                  type: string
                  nullable: true
                parent_id:
                  type: string
                  nullable: true
                  description: UUID eines bestehenden Nodes als Wurzel des neuen Baums
                position:
                  type: integer
                  minimum: 0
                children:
                  type: array
                  description: Rekursiv verschachtelte Kind-Nodes
                  items:
                    $ref: '#/components/schemas/TreeNodeInput'
            example:
              name: "Sprint 1"
              children:
                - name: "Feature A"
                  children:
                    - name: "Aufgabe 1"
                    - name: "Aufgabe 2"
                - name: "Feature B"
      responses:
        "201":
          description: Baum erstellt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/NodeSubtree'
        "422":
          description: Limit überschritten (max. 50 Nodes oder max. 5 Ebenen)

  parameters:
    nodeId:
      name: node
      in: path
      required: true
      schema:
        type: string
      description: UUID des Nodes

  schemas:
    TreeNodeInput:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 255
        description:
          type: string
          nullable: true
        position:
          type: integer
          minimum: 0
        children:
          type: array
          items:
            $ref: '#/components/schemas/TreeNodeInput'

tags:
  - name: Auth
    description: Nutzerinformationen
  - name: Tokens
    description: API-Token verwalten
  - name: Nodes
    description: Nodes lesen und bearbeiten
  - name: Comments
    description: Kommentare an Nodes
  - name: Permissions
    description: Zugriffsberechtigungen für Nodes
  - name: Batch
    description: Mehrere Operationen in einem Request
