Parcourir la source

adding swagger docs

Josef Straßl il y a 1 mois
Parent
commit
14b52d18b6
4 fichiers modifiés avec 771 ajouts et 1 suppressions
  1. 7 1
      README.md
  2. 58 0
      docs/API.md
  3. 52 0
      public/docs/index.html
  4. 654 0
      public/openapi.yaml

+ 7 - 1
README.md

@@ -77,6 +77,8 @@ Danach ist die Anwendung erreichbar unter:
 - Dashboard: `http://localhost:8000/`
 - Dashboard: `http://localhost:8000/`
 - Adminpanel: `http://localhost:8000/admin/`
 - Adminpanel: `http://localhost:8000/admin/`
 - Status-API: `http://localhost:8000/api/v1/status.php`
 - Status-API: `http://localhost:8000/api/v1/status.php`
+- Swagger UI: `http://localhost:8000/docs/`
+- OpenAPI-Spec: `http://localhost:8000/openapi.yaml`
 
 
 ## Default-Zugangsdaten
 ## Default-Zugangsdaten
 
 
@@ -161,7 +163,11 @@ Damit repraesentiert `distance_per_unit` die Aenderung des Messwerts pro Flasche
 
 
 ## API-Referenz
 ## API-Referenz
 
 
-Die kurze Uebersicht steht hier. Eine ausfuehrlichere Referenz liegt in [docs/API.md](docs/API.md).
+Die API ist jetzt auf drei Ebenen dokumentiert:
+
+- Interaktive Swagger UI unter `http://localhost:8000/docs/`
+- Maschinenlesbare OpenAPI-Spec unter `http://localhost:8000/openapi.yaml`
+- Erlaeuternde Referenz in [docs/API.md](docs/API.md)
 
 
 ### `POST /api/v1/readings.php`
 ### `POST /api/v1/readings.php`
 
 

+ 58 - 0
docs/API.md

@@ -2,6 +2,20 @@
 
 
 Diese Datei beschreibt die HTTP-Schnittstellen der Anwendung.
 Diese Datei beschreibt die HTTP-Schnittstellen der Anwendung.
 
 
+## Swagger / OpenAPI
+
+Zusatzlich zur textuellen Referenz gibt es jetzt zwei API-Dokumentationsziele in der laufenden Anwendung:
+
+- Swagger UI: `/docs/`
+- OpenAPI-Spec: `/openapi.yaml`
+
+Beim lokalen Start mit `php -S localhost:8000 -t public` sind die URLs:
+
+- `http://localhost:8000/docs/`
+- `http://localhost:8000/openapi.yaml`
+
+Die OpenAPI-Datei ist handgeschrieben und dient als maschinenlesbare Quelle fuer die Swagger UI.
+
 ## Basis
 ## Basis
 
 
 - API-Stil: REST-nah
 - API-Stil: REST-nah
@@ -52,7 +66,9 @@ Nimmt genau einen Sensorwert fuer genau ein Fach entgegen.
 ### Verhalten
 ### Verhalten
 
 
 - Der Endpunkt akzeptiert nur `POST`.
 - Der Endpunkt akzeptiert nur `POST`.
+- Der Endpunkt beantwortet `OPTIONS` fuer Preflight-Requests mit `204 No Content`.
 - Bei fehlendem oder falschem Bearer-Token antwortet die API mit `401`.
 - Bei fehlendem oder falschem Bearer-Token antwortet die API mit `401`.
+- Bei ungueltigem JSON antwortet die API mit `400`.
 - Bei ungueltigen Feldern antwortet die API mit `422`.
 - Bei ungueltigen Feldern antwortet die API mit `422`.
 - Wenn `machine_id` oder `sensor_id` nicht bekannt sind, antwortet die API mit `404`.
 - Wenn `machine_id` oder `sensor_id` nicht bekannt sind, antwortet die API mit `404`.
 - Bei erfolgreicher Verarbeitung wird `state.json` aktualisiert.
 - Bei erfolgreicher Verarbeitung wird `state.json` aktualisiert.
@@ -120,6 +136,28 @@ Status: `405 Method Not Allowed`
 }
 }
 ```
 ```
 
 
+#### Ungueltiger JSON-Body
+
+Status: `400 Bad Request`
+
+```json
+{
+  "ok": false,
+  "error": "Ungueltiger JSON-Body."
+}
+```
+
+#### Interner Fehler
+
+Status: `500 Internal Server Error`
+
+```json
+{
+  "ok": false,
+  "error": "Interner Fehler."
+}
+```
+
 ### Beispiel mit `curl`
 ### Beispiel mit `curl`
 
 
 ```bash
 ```bash
@@ -192,6 +230,26 @@ Beispielstruktur:
 - `webhook_ids`
 - `webhook_ids`
 - `email_ids`
 - `email_ids`
 
 
+### Wichtige Felder in `alerts[]`
+
+- `id`
+- `created_at`
+- `payload.event`
+- `payload.machine_id`
+- `payload.machine_name`
+- `payload.sensor_id`
+- `payload.slot_label`
+- `payload.product_name`
+- `payload.distance_mm`
+- `payload.units_estimated`
+- `payload.max_units`
+- `payload.fill_percent`
+- `payload.state`
+- `payload.previous_state`
+- `payload.measured_at`
+- `deliveries.webhooks[]`
+- `deliveries.emails[]`
+
 ### Falsche Methode
 ### Falsche Methode
 
 
 Status: `405 Method Not Allowed`
 Status: `405 Method Not Allowed`

+ 52 - 0
public/docs/index.html

@@ -0,0 +1,52 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Getraenkeautomat Monitor API Docs</title>
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
+  <style>
+    html {
+      box-sizing: border-box;
+      overflow-y: scroll;
+    }
+
+    *,
+    *::before,
+    *::after {
+      box-sizing: inherit;
+    }
+
+    body {
+      margin: 0;
+      background: #f4f6f8;
+    }
+
+    .topbar {
+      display: none;
+    }
+  </style>
+</head>
+<body>
+  <div id="swagger-ui"></div>
+  <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
+  <script>
+    window.addEventListener('load', function () {
+      SwaggerUIBundle({
+        url: '../openapi.yaml',
+        dom_id: '#swagger-ui',
+        deepLinking: true,
+        docExpansion: 'list',
+        displayRequestDuration: true,
+        persistAuthorization: true,
+        presets: [
+          SwaggerUIBundle.presets.apis,
+          SwaggerUIStandalonePreset
+        ],
+        layout: 'StandaloneLayout'
+      });
+    });
+  </script>
+</body>
+</html>

+ 654 - 0
public/openapi.yaml

@@ -0,0 +1,654 @@
+openapi: 3.1.0
+info:
+  title: Getraenkeautomat Monitor API
+  version: 1.0.0
+  summary: HTTP API for ingesting sensor readings and retrieving vending machine status.
+  description: |
+    Handwritten OpenAPI specification for the Getraenkeautomat Monitor application.
+
+    The API currently exposes two endpoints:
+    - `POST /api/v1/readings.php` for ingesting one sensor measurement
+    - `GET /api/v1/status.php` for retrieving the aggregated application state
+
+    The readings endpoint uses Bearer token authentication. The status endpoint is
+    intentionally public so the dashboard can poll it without a login.
+servers:
+  - url: /
+    description: Same-origin deployment
+tags:
+  - name: Readings
+    description: Receive one sensor reading and update the persisted slot state.
+  - name: Status
+    description: Retrieve the current aggregated machine, slot, and alert status.
+paths:
+  /api/v1/readings.php:
+    post:
+      tags:
+        - Readings
+      summary: Submit one reading
+      description: |
+        Accepts exactly one sensor measurement for one configured machine slot.
+
+        Processing behavior:
+        - Requires a Bearer token in the `Authorization` header
+        - Accepts JSON request bodies
+        - Updates `data/state.json` on success
+        - Triggers alerts only when the slot state changes from `ok` to `critical`
+          or from `critical` to `ok`
+      operationId: submitReading
+      security:
+        - bearerAuth: []
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/ReadingRequest'
+            examples:
+              lobbySlot:
+                summary: Reading for the A1 slot in the lobby machine
+                value:
+                  machine_id: automat-lobby
+                  sensor_id: fach-a1
+                  distance_mm: 184
+                  measured_at: '2026-04-15T19:20:00Z'
+      responses:
+        '200':
+          description: Reading processed successfully.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ReadingSuccessResponse'
+              examples:
+                ok:
+                  value:
+                    ok: true
+                    machine_id: automat-lobby
+                    sensor_id: fach-a1
+                    slot_label: A1
+                    units_estimated: 4
+                    fill_percent: 63
+                    state: ok
+        '400':
+          description: Invalid JSON body or malformed request payload.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                invalidJson:
+                  value:
+                    ok: false
+                    error: Ungueltiger JSON-Body.
+        '401':
+          description: Missing or invalid Bearer token.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                unauthorized:
+                  value:
+                    ok: false
+                    error: Nicht autorisiert.
+        '404':
+          description: The referenced machine or sensor is not configured.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                unknownMachineOrSensor:
+                  value:
+                    ok: false
+                    error: Unbekannter Automat oder Sensor.
+        '405':
+          description: Only POST is supported for this endpoint.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                wrongMethod:
+                  value:
+                    ok: false
+                    error: Nur POST ist erlaubt.
+        '422':
+          description: Semantic validation failed.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                missingIdentifiers:
+                  value:
+                    ok: false
+                    error: machine_id und sensor_id sind erforderlich.
+                nonNumericDistance:
+                  value:
+                    ok: false
+                    error: distance_mm muss numerisch sein.
+                invalidTimestamp:
+                  value:
+                    ok: false
+                    error: measured_at ist kein gueltiger ISO-Zeitstempel.
+        '500':
+          description: Unexpected internal server error.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                internalError:
+                  value:
+                    ok: false
+                    error: Interner Fehler.
+    options:
+      tags:
+        - Readings
+      summary: CORS preflight for readings
+      description: |
+        Preflight handler for browser-based clients. The endpoint responds with
+        `204 No Content` and emits `Access-Control-Allow-Methods` and
+        `Access-Control-Allow-Headers`.
+      operationId: readingsPreflight
+      responses:
+        '204':
+          description: Preflight accepted without a response body.
+          headers:
+            Access-Control-Allow-Methods:
+              description: Allowed methods for this endpoint.
+              schema:
+                type: string
+                example: POST, OPTIONS
+            Access-Control-Allow-Headers:
+              description: Allowed request headers for this endpoint.
+              schema:
+                type: string
+                example: Authorization, Content-Type
+  /api/v1/status.php:
+    get:
+      tags:
+        - Status
+      summary: Retrieve the current application status
+      description: |
+        Returns the aggregated state for the dashboard and admin panel.
+
+        The response contains:
+        - app metadata
+        - machine and slot status
+        - a summary section
+        - the most recent alert log entries
+      operationId: getStatus
+      responses:
+        '200':
+          description: Aggregated status generated successfully.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/StatusResponse'
+              examples:
+                dashboard:
+                  value:
+                    ok: true
+                    generated_at: '2026-04-15T20:10:00+00:00'
+                    app:
+                      name: Getraenkeautomat Monitor
+                      dashboard_refresh_seconds: 15
+                    summary:
+                      machine_count: 2
+                      slot_count: 3
+                      critical_count: 1
+                    machines:
+                      - id: automat-lobby
+                        name: Lobby Automat
+                        location: Erdgeschoss
+                        slots:
+                          - machine_id: automat-lobby
+                            machine_name: Lobby Automat
+                            sensor_id: fach-a1
+                            slot_label: A1
+                            product_name: Cola 0,5l
+                            fill_percent: 63
+                            units_estimated: 4
+                            max_units: 7
+                            distance_mm: 184
+                            state: ok
+                            measured_at: '2026-04-15T19:20:00+00:00'
+                            updated_at: '2026-04-15T19:20:02+00:00'
+                            alert_below_units: 2
+                            webhook_ids:
+                              - lager-webhook
+                            email_ids:
+                              - lager-team
+                    alerts:
+                      - id: alert_680004979d8512.07480974
+                        created_at: '2026-04-15T19:20:02+00:00'
+                        payload:
+                          event: critical
+                          machine_id: automat-lobby
+                          machine_name: Lobby Automat
+                          sensor_id: fach-a1
+                          slot_label: A1
+                          product_name: Cola 0,5l
+                          distance_mm: 320
+                          units_estimated: 1
+                          max_units: 7
+                          fill_percent: 14
+                          state: critical
+                          previous_state: ok
+                          measured_at: '2026-04-15T19:19:59+00:00'
+                        deliveries:
+                          webhooks:
+                            - id: lager-webhook
+                              success: false
+                              message: Webhook nicht gefunden oder deaktiviert.
+                          emails:
+                            - id: lager-team
+                              success: true
+                              message: Email versendet.
+        '405':
+          description: Only GET is supported for this endpoint.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                wrongMethod:
+                  value:
+                    ok: false
+                    error: Nur GET ist erlaubt.
+components:
+  securitySchemes:
+    bearerAuth:
+      type: http
+      scheme: bearer
+      bearerFormat: opaque token
+      description: Bearer token stored in config.json under api.bearer_token.
+  schemas:
+    ReadingRequest:
+      type: object
+      additionalProperties: false
+      required:
+        - machine_id
+        - sensor_id
+        - distance_mm
+      properties:
+        machine_id:
+          type: string
+          description: Configured machine identifier from config.json.
+          minLength: 1
+          example: automat-lobby
+        sensor_id:
+          type: string
+          description: Slot or sensor identifier within the machine.
+          minLength: 1
+          example: fach-a1
+        distance_mm:
+          type: number
+          description: Measured distance in millimeters.
+          example: 184
+        measured_at:
+          type:
+            - string
+            - 'null'
+          description: |
+            Optional measurement timestamp. When omitted or empty, the server uses
+            the current time. Values are parsed with PHP `strtotime()` and returned
+            as an ISO-8601 timestamp.
+          format: date-time
+          example: '2026-04-15T19:20:00Z'
+    ReadingSuccessResponse:
+      type: object
+      additionalProperties: false
+      required:
+        - ok
+        - machine_id
+        - sensor_id
+        - slot_label
+        - units_estimated
+        - fill_percent
+        - state
+      properties:
+        ok:
+          type: boolean
+          const: true
+        machine_id:
+          type: string
+          example: automat-lobby
+        sensor_id:
+          type: string
+          example: fach-a1
+        slot_label:
+          type: string
+          example: A1
+        units_estimated:
+          type: integer
+          example: 4
+        fill_percent:
+          type: integer
+          example: 63
+        state:
+          $ref: '#/components/schemas/SlotState'
+    ErrorResponse:
+      type: object
+      additionalProperties: false
+      required:
+        - ok
+        - error
+      properties:
+        ok:
+          type: boolean
+          const: false
+        error:
+          type: string
+          example: Nicht autorisiert.
+    StatusResponse:
+      type: object
+      additionalProperties: false
+      required:
+        - ok
+        - generated_at
+        - app
+        - summary
+        - machines
+        - alerts
+      properties:
+        ok:
+          type: boolean
+          const: true
+        generated_at:
+          type: string
+          format: date-time
+          description: Timestamp when the response was generated.
+        app:
+          $ref: '#/components/schemas/AppStatus'
+        summary:
+          $ref: '#/components/schemas/StatusSummary'
+        machines:
+          type: array
+          items:
+            $ref: '#/components/schemas/MachineStatus'
+        alerts:
+          type: array
+          items:
+            $ref: '#/components/schemas/AlertEvent'
+    AppStatus:
+      type: object
+      additionalProperties: false
+      required:
+        - name
+        - dashboard_refresh_seconds
+      properties:
+        name:
+          type: string
+          example: Getraenkeautomat Monitor
+        dashboard_refresh_seconds:
+          type: integer
+          minimum: 1
+          example: 15
+    StatusSummary:
+      type: object
+      additionalProperties: false
+      required:
+        - machine_count
+        - slot_count
+        - critical_count
+      properties:
+        machine_count:
+          type: integer
+          minimum: 0
+          example: 2
+        slot_count:
+          type: integer
+          minimum: 0
+          example: 3
+        critical_count:
+          type: integer
+          minimum: 0
+          example: 1
+    MachineStatus:
+      type: object
+      additionalProperties: false
+      required:
+        - id
+        - name
+        - location
+        - slots
+      properties:
+        id:
+          type: string
+          example: automat-lobby
+        name:
+          type: string
+          example: Lobby Automat
+        location:
+          type: string
+          example: Erdgeschoss
+        slots:
+          type: array
+          items:
+            $ref: '#/components/schemas/SlotStatus'
+    SlotStatus:
+      type: object
+      additionalProperties: false
+      required:
+        - machine_id
+        - machine_name
+        - sensor_id
+        - slot_label
+        - product_name
+        - fill_percent
+        - units_estimated
+        - max_units
+        - distance_mm
+        - state
+        - measured_at
+        - updated_at
+        - alert_below_units
+        - webhook_ids
+        - email_ids
+      properties:
+        machine_id:
+          type: string
+          example: automat-lobby
+        machine_name:
+          type: string
+          example: Lobby Automat
+        sensor_id:
+          type: string
+          example: fach-a1
+        slot_label:
+          type: string
+          example: A1
+        product_name:
+          type: string
+          example: Cola 0,5l
+        fill_percent:
+          type:
+            - integer
+            - 'null'
+          minimum: 0
+          maximum: 100
+          description: Null until a first reading has been received.
+          example: 63
+        units_estimated:
+          type:
+            - integer
+            - 'null'
+          minimum: 0
+          description: Null until a first reading has been received.
+          example: 4
+        max_units:
+          type: integer
+          minimum: 0
+          example: 7
+        distance_mm:
+          type:
+            - number
+            - 'null'
+          description: Last measured distance in millimeters.
+          example: 184
+        state:
+          $ref: '#/components/schemas/SlotState'
+        measured_at:
+          type:
+            - string
+            - 'null'
+          format: date-time
+          example: '2026-04-15T19:20:00+00:00'
+        updated_at:
+          type:
+            - string
+            - 'null'
+          format: date-time
+          example: '2026-04-15T19:20:02+00:00'
+        alert_below_units:
+          type: integer
+          minimum: 0
+          example: 2
+        webhook_ids:
+          type: array
+          items:
+            type: string
+          example:
+            - lager-webhook
+        email_ids:
+          type: array
+          items:
+            type: string
+          example:
+            - lager-team
+    SlotState:
+      type: string
+      enum:
+        - ok
+        - critical
+        - unknown
+      example: ok
+    AlertEvent:
+      type: object
+      additionalProperties: false
+      required:
+        - id
+        - created_at
+        - payload
+        - deliveries
+      properties:
+        id:
+          type: string
+          description: Unique alert log entry ID generated with PHP uniqid().
+          example: alert_680004979d8512.07480974
+        created_at:
+          type: string
+          format: date-time
+          example: '2026-04-15T19:20:02+00:00'
+        payload:
+          $ref: '#/components/schemas/AlertPayload'
+        deliveries:
+          $ref: '#/components/schemas/AlertDeliveries'
+    AlertPayload:
+      type: object
+      additionalProperties: false
+      required:
+        - event
+        - machine_id
+        - machine_name
+        - sensor_id
+        - slot_label
+        - product_name
+        - distance_mm
+        - units_estimated
+        - max_units
+        - fill_percent
+        - state
+        - previous_state
+        - measured_at
+      properties:
+        event:
+          type: string
+          enum:
+            - critical
+            - recovered
+          example: critical
+        machine_id:
+          type: string
+          example: automat-lobby
+        machine_name:
+          type: string
+          example: Lobby Automat
+        sensor_id:
+          type: string
+          example: fach-a1
+        slot_label:
+          type: string
+          example: A1
+        product_name:
+          type: string
+          example: Cola 0,5l
+        distance_mm:
+          type:
+            - number
+            - 'null'
+          example: 320
+        units_estimated:
+          type:
+            - integer
+            - 'null'
+          example: 1
+        max_units:
+          type:
+            - integer
+            - 'null'
+          example: 7
+        fill_percent:
+          type:
+            - integer
+            - 'null'
+          example: 14
+        state:
+          $ref: '#/components/schemas/SlotState'
+        previous_state:
+          type:
+            - string
+            - 'null'
+          description: Previous slot state before the transition.
+          example: ok
+        measured_at:
+          type:
+            - string
+            - 'null'
+          format: date-time
+          example: '2026-04-15T19:19:59+00:00'
+    AlertDeliveries:
+      type: object
+      additionalProperties: false
+      required:
+        - webhooks
+        - emails
+      properties:
+        webhooks:
+          type: array
+          items:
+            $ref: '#/components/schemas/DeliveryResult'
+        emails:
+          type: array
+          items:
+            $ref: '#/components/schemas/DeliveryResult'
+    DeliveryResult:
+      type: object
+      additionalProperties: false
+      required:
+        - id
+        - success
+        - message
+      properties:
+        id:
+          type: string
+          example: lager-team
+        success:
+          type: boolean
+          example: true
+        message:
+          type: string
+          example: Email versendet.