openapi: 3.1.0
info:
  title: Abfallkalender St. Gallen – API
  version: "1.0"
  summary: Abfallsammeltermine der 75 Gemeinden des Kantons St. Gallen als JSON und iCalendar.
  description: |
    Offene, kostenlose API. Daten werden **1× pro Monat** aus den Abfallkalendern der Gemeinden, ZAB und
    KVR Rheintal ausgelesen. Vor dem Schreiben prüft ein Plausibilitäts-Gate jeden Datensatz (Gemeinde × Abfallart):
    feste Regeln und danach KI-Prüfung (Cloudflare Workers AI). Abgelehnte Daten werden nicht übernommen –
    es bleibt der letzte gute Stand.

    **Gemeinde-Parameter:** BFS-Nummer (empfohlen) oder Name. Bei Namen spielen Gross-/Kleinschreibung,
    Akzente/Umlaute (`ü` → `u`), Leer- und Satzzeichen keine Rolle (`Rüthi` = `ruthi`, `St. Margrethen` = `stmargrethen`).
    Exakte Übereinstimmung gewinnt, sonst der erste Name, der mit der Eingabe beginnt.

    **Caching:** `Cache-Control: public, max-age=21600` für alle 200-Antworten. **CORS:** `Access-Control-Allow-Origin: *`.

    **Fair Use:** Free-Plan mit 100'000 Requests/Tag für alle Nutzer zusammen – bitte Antworten zwischenspeichern.
    Ohne Gewähr; verbindlich ist der Kalender der Gemeinde (`quelle`).
  license: { name: Daten ohne Gewähr – Quellen siehe Feld «quelle», identifier: CC0-1.0 }
servers:
  - url: /
    description: Gleicher Host wie diese Dokumentation
security: []  # öffentlich, keine Authentifizierung
tags:
  - name: Gemeinden
    description: Liste der unterstützten Gemeinden
  - name: Termine
    description: Sammeltermine als JSON
  - name: Kalender-Abo
    description: Sammeltermine als iCalendar zum Abonnieren
  - name: Meta
    description: Index und Metadaten
paths:
  /api:
    get:
      tags: [Meta]
      operationId: index
      summary: API-Index
      description: Übersicht der Endpunkte (absolute URLs aus dem Request-Origin bzw. `BASE_URL`), Link zur Doku und Spezifikation.
      responses:
        "200":
          description: Index
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Index" }
        "405": { $ref: "#/components/responses/Methode" }
  /api/gemeinden:
    get:
      tags: [Gemeinden]
      operationId: gemeinden
      summary: Liste aller Gemeinden
      responses:
        "200":
          description: Alle 75 Gemeinden mit Stand, Qualität und Anzahl Termine je Abfallart.
          headers: { Cache-Control: { $ref: "#/components/headers/CacheControl" }, Access-Control-Allow-Origin: { $ref: "#/components/headers/Cors" } }
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/GemeindeIndex" } }
              example:
                - gemeinde: Buchs
                  bfs: 3271
                  methode: "pdf: Parser Buchs"
                  abgerufen: "2026-10-13T20:35:42.911Z"
                  qualitaet: gut
                  vollstaendig: true
                  zonen: 0
                  anzahl: { Kehricht: 52, Papier: 12, Karton: 12, Grüngut: 46, Metall: 4 }
        "405": { $ref: "#/components/responses/Methode" }
  /api/abfallkalender:
    get:
      tags: [Termine]
      operationId: abfallkalender
      summary: Termine einer Gemeinde (JSON)
      parameters:
        - $ref: "#/components/parameters/Gemeinde"
        - name: art
          in: query
          required: false
          description: Nur eine Abfallart (Schreibweise/Umlaut egal, z.B. `grungut`).
          schema: { $ref: "#/components/schemas/Art" }
        - name: ab
          in: query
          required: false
          description: Nur Termine ab diesem Datum.
          schema: { type: string, format: date, example: "2026-10-01" }
        - $ref: "#/components/parameters/Zone"
      responses:
        "200":
          description: Termine
          headers: { Cache-Control: { $ref: "#/components/headers/CacheControl" }, Access-Control-Allow-Origin: { $ref: "#/components/headers/Cors" } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Ergebnis" }
              example:
                gemeinde: Buchs
                bfs: 3271
                quelle: https://buchs-sg.ch/wp-content/uploads/2025/12/BP220667_Stadt_Buchs_Abfallkalender_2026_WEB.pdf
                methode: "pdf: Parser Buchs"
                abgerufen: "2026-10-13T20:35:42.911Z"
                termine: { Metall: ["2026-03-05", "2026-06-04", "2026-09-03", "2026-12-03"] }
                hinweise: ["Kehricht: wöchentlich Freitag", "Grüngut: wöchentlich Montag ab 2026-02-23 bis 2026-12-07", "Keine Zonen: ganzes Stadtgebiet"]
                qualitaet: { stufe: gut, vollstaendig: true, fehler: [], warnungen: ["Kehricht: Termin an Feiertag 2026-01-02 (Verschiebung prüfen)"] }
                pruefung: { Kehricht: unchecked, Papier: unchecked, Karton: unchecked, Grüngut: unchecked, Metall: unchecked }
        "400": { $ref: "#/components/responses/GemeindeFehlt" }
        "404": { $ref: "#/components/responses/NichtGefunden" }
        "503": { $ref: "#/components/responses/KeineDaten" }
  /api/ics:
    get:
      tags: [Kalender-Abo]
      operationId: ics
      summary: Termine als abonnierbarer Kalender (iCalendar, RFC 5545)
      description: |
        Ganztägige VEVENTs mit stabilen UIDs (`<BFS>-<art>-<datum>@abfallkalender-ch`), `X-WR-CALNAME`,
        `REFRESH-INTERVAL`/`X-PUBLISHED-TTL` = P1D. Abonnieren: Apple → gleiche URL mit `webcal://`;
        Google → `https://calendar.google.com/calendar/render?cid=<URL-kodierte webcal-URL>`;
        Outlook → «Kalender hinzufügen → Aus dem Internet abonnieren».
      parameters:
        - $ref: "#/components/parameters/Gemeinde"
        - name: arten
          in: query
          required: false
          description: Kommagetrennte Abfallarten; Standard alle fünf.
          schema: { type: string, example: "Kehricht,Papier" }
        - $ref: "#/components/parameters/Zone"
      responses:
        "200":
          description: iCalendar-Datei
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            Access-Control-Allow-Origin: { $ref: "#/components/headers/Cors" }
            Content-Disposition: { schema: { type: string }, example: 'inline; filename="abfall-3271.ics"' }
          content:
            text/calendar:
              schema: { type: string }
              example: "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Abfall SG//DE\r\nCALSCALE:GREGORIAN\r\nMETHOD:PUBLISH\r\nX-WR-CALNAME:Abfall Buchs\r\nNAME:Abfall Buchs\r\nX-WR-CALDESC:Abfuhrtermine\\, monatlich aktualisiert\\, ohne Gewähr.\r\nX-WR-TIMEZONE:Europe/Zurich\r\nREFRESH-INTERVAL;VALUE=DURATION:P1D\r\nX-PUBLISHED-TTL:P1D\r\nBEGIN:VEVENT\r\nUID:3271-kehricht-2026-01-02@abfallkalender-ch\r\nDTSTAMP:20261013T203542Z\r\nDTSTART;VALUE=DATE:20260102\r\nDTEND;VALUE=DATE:20260103\r\nSUMMARY:Kehricht (Buchs)\r\nCATEGORIES:Kehricht\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
        "400":
          description: Gemeinde fehlt oder unbekannte Abfallart
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Fehler" }
              examples:
                gemeindeFehlt: { value: { error: "Parameter ?gemeinde=<Name oder BFS-Nr> fehlt" } }
                artUnbekannt: { value: { error: "Unbekannte Abfallart(en): foo", erlaubt: [Kehricht, Papier, Karton, Grüngut, Metall] } }
        "404": { $ref: "#/components/responses/NichtGefunden" }
        "503": { $ref: "#/components/responses/KeineDaten" }
  /ics/{bfs}.ics:
    get:
      tags: [Kalender-Abo]
      operationId: icsPfad
      summary: Kalender-Abo als Pfad-URL, alle 5 Arten (empfohlen für Google Kalender)
      description: |
        Gleicher Inhalt wie `/api/ics`, aber **ohne Query-String** – Google Kalender verwirft Abo-URLs mit `?…`
        teils kommentarlos. Google-Link: `https://calendar.google.com/calendar/r?cid=<encodeURIComponent("webcal://<host>/ics/3271.ics")>`
        (Google akzeptiert im `cid` zuverlässig nur die `webcal://`-Form). Kalendername: «Abfall <Gemeinde>» (max. ~20 Zeichen).
      parameters:
        - $ref: "#/components/parameters/Bfs"
      responses:
        "200": { $ref: "#/components/responses/Ics" }
        "404": { $ref: "#/components/responses/NichtGefunden" }
        "503": { $ref: "#/components/responses/KeineDaten" }
  /ics/{bfs}/{arten}.ics:
    get:
      tags: [Kalender-Abo]
      operationId: icsPfadArten
      summary: Kalender-Abo als Pfad-URL, Auswahl von Abfallarten
      parameters:
        - $ref: "#/components/parameters/Bfs"
        - name: arten
          in: path
          required: true
          description: "Kleingeschrieben, mit Bindestrich getrennt, alphabetisch sortiert; Slugs: kehricht, papier, karton, gruengut, metall."
          schema: { type: string, pattern: "^[a-z]+(-[a-z]+)*$" }
          example: kehricht-papier
      responses:
        "200": { $ref: "#/components/responses/Ics" }
        "400":
          description: Unbekannter Slug
          content: { application/json: { schema: { $ref: "#/components/schemas/Fehler" }, example: { error: "Unbekannte Abfallart(en) im Pfad: foo", erlaubt: [kehricht, papier, karton, gruengut, metall] } } }
        "404": { $ref: "#/components/responses/NichtGefunden" }
        "503": { $ref: "#/components/responses/KeineDaten" }
  /ics/{bfs}/zone/{zone}.ics:
    get:
      tags: [Kalender-Abo]
      operationId: icsPfadZone
      summary: Kalender-Abo einer Zone (Abfuhrgebiet/Tour/Ortsteil) als Pfad-URL
      description: |
        Wie `/ics/{bfs}.ics`, aber mit den Terminen einer Zone aus `zonen` (z.B. Stadt St. Gallen: `/ics/3203/zone/gebieta.ics`).
        Optional mit Artenauswahl: `/ics/{bfs}/zone/{zone}/{arten}.ics`, z.B. `/ics/3203/zone/gebietlost/kehricht-papier.ics`.
        Arten ohne Zonendaten → Gemeindewerte.
      parameters:
        - $ref: "#/components/parameters/Bfs"
        - name: zone
          in: path
          required: true
          description: "Zonen-Slug: Schlüssel aus `zonen` kleingeschrieben, ohne Umlaute/Leer-/Sonderzeichen (z.B. «Gebiet L Ost» → `gebietlost`)."
          schema: { type: string, pattern: "^[a-z0-9]+$" }
          example: gebieta
      responses:
        "200": { $ref: "#/components/responses/Ics" }
        "404":
          description: Gemeinde oder Zone unbekannt
          content: { application/json: { schema: { $ref: "#/components/schemas/Fehler" }, example: { error: "Zone 'x' unbekannt", zonen: ["Gebiet A", "Gebiet B"] } } }
        "503": { $ref: "#/components/responses/KeineDaten" }
components:
  parameters:
    Bfs:
      name: bfs
      in: path
      required: true
      description: BFS-Gemeindenummer (4-stellig)
      schema: { type: string, pattern: "^[0-9]{4}$" }
      example: "3271"
    Gemeinde:
      name: gemeinde
      in: query
      required: true
      description: BFS-Nummer oder Name (URL-kodiert; Umlaute/Grossschreibung egal).
      schema: { type: string }
      examples:
        bfs: { value: "3271", summary: BFS-Nummer }
        name: { value: "Bütschwil-Ganterschwil", summary: Name }
    Zone:
      name: zone
      in: query
      required: false
      description: "Zone (Schlüssel aus `zonen`, Gross-/Kleinschreibung, Umlaute, Leer- und Sonderzeichen egal, z.B. `Gebiet L Ost` = `gebietlost`). Arten ohne Zonendaten → Gemeindewerte. Unbekannte Zone → 404 mit Liste `zonen`."
      schema: { type: string }
      example: gebieta
  headers:
    CacheControl: { schema: { type: string }, example: "public, max-age=21600" }
    Cors: { schema: { type: string }, example: "*" }
  responses:
    Ics:
      description: iCalendar (RFC 5545), ganztägige VEVENTs, stabile UIDs
      headers:
        Cache-Control: { $ref: "#/components/headers/CacheControl" }
        Access-Control-Allow-Origin: { $ref: "#/components/headers/Cors" }
      content:
        text/calendar:
          schema: { type: string }
          example: "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Abfall SG//DE\r\nX-WR-CALNAME:Abfall Buchs\r\nNAME:Abfall Buchs\r\n…\r\nBEGIN:VEVENT\r\nUID:3271-papier-2026-01-13@abfallkalender-ch\r\nDTSTART;VALUE=DATE:20260113\r\nDTEND;VALUE=DATE:20260114\r\nSUMMARY:Papier (Buchs)\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
    Methode:
      description: Nur GET, HEAD und OPTIONS erlaubt
      content: { application/json: { schema: { $ref: "#/components/schemas/Fehler" }, example: { error: nur GET } } }
    GemeindeFehlt:
      description: Parameter fehlt
      content: { application/json: { schema: { $ref: "#/components/schemas/Fehler" }, example: { error: "Parameter ?gemeinde=<Name oder BFS-Nr> fehlt" } } }
    NichtGefunden:
      description: Gemeinde unbekannt
      content: { application/json: { schema: { $ref: "#/components/schemas/Fehler" }, example: { error: "Gemeinde 'xyz' nicht im Kanton SG gefunden", hilfe: /api/gemeinden } } }
    KeineDaten:
      description: Für diese Gemeinde liegen noch keine Daten vor
      content: { application/json: { schema: { $ref: "#/components/schemas/Fehler" }, example: { gemeinde: Buchs, bfs: 3271, error: noch keine Daten } } }
  schemas:
    Art:
      type: string
      enum: [Kehricht, Papier, Karton, Grüngut, Metall]
    Datumsliste:
      type: array
      items: { type: string, format: date }
    Termine:
      type: object
      description: Abfallart → sortierte ISO-Daten (ganztägig, Schweizer Lokaldatum).
      properties:
        Kehricht: { $ref: "#/components/schemas/Datumsliste" }
        Papier: { $ref: "#/components/schemas/Datumsliste" }
        Karton: { $ref: "#/components/schemas/Datumsliste" }
        Grüngut: { $ref: "#/components/schemas/Datumsliste" }
        Metall: { $ref: "#/components/schemas/Datumsliste" }
    Qualitaet:
      type: object
      required: [stufe, vollstaendig, fehler, warnungen]
      properties:
        stufe: { type: string, enum: [gut, teilweise, schlecht] }
        vollstaendig: { type: boolean, description: Alle 5 Arten haben Termine. }
        fehler: { type: array, items: { type: string } }
        warnungen: { type: array, items: { type: string } }
    Ergebnis:
      type: object
      required: [gemeinde, bfs, quelle, methode, abgerufen, termine, hinweise]
      properties:
        gemeinde: { type: string }
        bfs: { type: integer }
        quelle: { type: string, description: "Quell-URL(s), mehrere getrennt durch « | »" }
        methode: { type: string, description: "z.B. «pdf: Parser Buchs», «pdf: Parser ZAB (Monatsraster)», «i-web data-entities»" }
        abgerufen: { type: string, format: date-time }
        termine: { $ref: "#/components/schemas/Termine" }
        zonen:
          type: object
          description: Tour/Weiler/Abfuhrkreis → Termine je Art; `termine` ist dann die Vereinigung aller Zonen.
          additionalProperties: { $ref: "#/components/schemas/Termine" }
        nurKommende: { type: boolean, description: Quelle zeigt nur die nächsten Termine. }
        hinweise: { type: array, items: { type: string } }
        qualitaet: { $ref: "#/components/schemas/Qualitaet" }
        pruefung:
          type: object
          description: "Gate je Art: ok | unchecked | alt (letzter guter Stand) | abgelehnt"
          additionalProperties: { type: string, enum: [ok, unchecked, alt, abgelehnt] }
    GemeindeIndex:
      type: object
      required: [gemeinde, bfs, methode, abgerufen]
      properties:
        gemeinde: { type: string }
        bfs: { type: integer }
        methode: { type: string }
        abgerufen: { type: string, format: date-time }
        qualitaet: { type: string, enum: [gut, teilweise, schlecht] }
        vollstaendig: { type: boolean }
        zonen: { type: integer }
        anzahl: { type: object, additionalProperties: { type: integer } }
    Index:
      type: object
      properties:
        name: { type: string }
        doku: { type: string, description: URL der Redoc-Dokumentation (/docs) }
        openapi: { type: string, description: URL dieser Spezifikation }
        quelle: { type: string }
        endpunkte: { type: object, additionalProperties: { type: string } }
        arten: { type: array, items: { $ref: "#/components/schemas/Art" } }
    Fehler:
      type: object
      required: [error]
      properties:
        error: { type: string }
        hilfe: { type: string }
        erlaubt: { type: array, items: { type: string }, description: "Erlaubte Abfallarten (bei /api/*) bzw. Slugs (bei /ics/*)" }
        gemeinde: { type: string }
        bfs: { type: integer }
