openapi: 3.0.3
info:
  title: Coolock Village Civic Platform API
  version: 2.0.0
  description: |-
    Open311 GeoReport v2 compatible API for civic issue reporting in Coolock Village, Dublin 5.

    Follows the [Open311 specification](http://wiki.open311.org/GeoReport_v2/) and
    [Smart Dublin data standards](https://data.smartdublin.ie).

    ## Authentication
    - Public endpoints: No auth required (rate-limited)
    - Reporter endpoints: Magic Link / Passkey session (`coolock_session` cookie)
    - Admin endpoints: API key in `X-API-Key` header, or Magic Link / Passkey session
    - Admin panel supports all three methods: API key, Magic Link, and Passkey

    ## Rate Limits
    All POST endpoints enforce 5 requests per 15 minutes per IP.
    Rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`) included in all responses.

    ## CSRF Protection
    All POST endpoints validate `Origin`/`Referer` headers against `coolockvillage.ie`.

    ## Input Sanitization
    All text inputs are HTML-stripped and length-capped.

    Generated from the live Astro route tree at build time.

    Public discovery is deny-by-default. Auth, admin, and user-private routes are intentionally excluded.
  contact:
    name: Coolock Village Regeneration Project
    email: info@coolockvillage.ie
    url: https://coolockvillage.ie
servers:
  - url: https://coolockvillage.ie
    description: Production (Cloudflare Pages)
  - url: http://127.0.0.1:4321
    description: Local development (Astro dev server)
tags:
  - name: Open311
    description: Open311 GeoReport v2 endpoints
  - name: Reports
    description: Issue report management
  - name: Volunteers
    description: Volunteer signup (feature-flagged)
  - name: Ideas
    description: Idea submission (feature-flagged)
  - name: Newsletter
    description: Newsletter subscription (feature-flagged)
  - name: Comments
    description: Unified comment threads for any content type
  - name: Businesses
    description: Canonical civic business directory (read-only, public)
  - name: CivicDashboard
    description: Live civic data dashboard — planning, transport, environment (feature-flagged)
components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
    cookieAuth:
      type: apiKey
      in: cookie
      name: coolock_session
      description: Session cookie set after Magic Link or Passkey authentication
    oidc:
      type: openIdConnect
      openIdConnectUrl: https://auth.mygov.ie/.well-known/openid-configuration
      description: 'Future: mygov.ie OIDC (not currently active)'
  schemas:
    Service:
      type: object
      properties:
        service_code:
          type: string
        service_name:
          type: string
        description:
          type: string
        metadata:
          type: boolean
        type:
          type: string
          enum:
            - realtime
            - batch
            - blackbox
        keywords:
          type: string
        group:
          type: string
      strict: true
      additionalProperties: false
    ServiceRequest:
      type: object
      properties:
        service_request_id:
          type: string
        status:
          type: string
          enum:
            - open
            - closed
        service_code:
          type: string
        description:
          type: string
        requested_datetime:
          type: string
          format: date-time
        updated_datetime:
          type: string
          format: date-time
        lat:
          type: number
        long:
          type: number
        address:
          type: string
      strict: true
      additionalProperties: false
    CreateRequest:
      type: object
      required:
        - service_code
      properties:
        service_code:
          type: string
        lat:
          type: number
        long:
          type: number
        description:
          type: string
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
      strict: true
      additionalProperties: false
    CreateResponse:
      type: object
      properties:
        service_request_id:
          type: string
        token:
          type: string
      strict: true
      additionalProperties: false
    Discovery:
      type: object
      properties:
        changeset:
          type: string
        contact:
          type: string
        endpoints:
          type: array
          items:
            type: object
            properties:
              specification:
                type: string
              url:
                type: string
              type:
                type: string
              formats:
                type: array
                items:
                  type: string
            strict: true
            additionalProperties: false
      strict: true
      additionalProperties: false
    ReportCreate:
      type: object
      required:
        - title
        - description
        - category
        - location
      properties:
        title:
          type: string
          maxLength: 200
        description:
          type: string
          maxLength: 5000
        category:
          type: string
        location:
          type: object
          properties:
            latitude:
              type: number
            longitude:
              type: number
          strict: true
          additionalProperties: false
      strict: true
      additionalProperties: false
    Report:
      type: object
      required:
        - id
        - title
        - description
        - category
        - status
        - location
        - created_at
        - updated_at
      properties:
        id:
          type: string
        title:
          type: string
        description:
          type: string
        category:
          type: string
        subcategory:
          type: string
        status:
          type: string
          enum:
            - open
            - pending_moderation
            - acknowledged
            - in_progress
            - resolved
            - closed
        location:
          type: object
          properties:
            latitude:
              type: number
            longitude:
              type: number
          strict: true
          additionalProperties: false
        address_text:
          type: string
        reporter_name:
          type: string
        dispatch_status:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      strict: true
      additionalProperties: false
    VolunteerCreate:
      type: object
      required:
        - name
        - email
        - interests
      properties:
        name:
          type: string
          maxLength: 200
        email:
          type: string
          format: email
          maxLength: 320
        phone:
          type: string
          maxLength: 30
        interests:
          type: array
          items:
            type: string
            maxLength: 50
        availability:
          type: string
          enum:
            - weekdays
            - weekends
            - both
      strict: true
      additionalProperties: false
    SuccessResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          strict: true
          additionalProperties: false
      strict: true
      additionalProperties: false
    IdeaCreate:
      type: object
      required:
        - title
        - description
        - category
      properties:
        title:
          type: string
          maxLength: 200
        description:
          type: string
          maxLength: 5000
        category:
          type: string
        author_name:
          type: string
        author_email:
          type: string
          format: email
      strict: true
      additionalProperties: false
    IdeaPublic:
      type: object
      description: Public idea (PII stripped — no author_email)
      properties:
        id:
          type: string
        title:
          type: string
        description:
          type: string
        category:
          type: string
        author_name:
          type: string
        support_count:
          type: integer
        status:
          type: string
          enum:
            - approved
            - featured
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      strict: true
      additionalProperties: false
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                example: false
              error:
                type: string
            strict: true
            additionalProperties: false
    Forbidden:
      description: CSRF origin validation failed
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                example: false
              error:
                type: string
                example: Forbidden
            strict: true
            additionalProperties: false
    NotFound:
      description: Resource not found
    RateLimited:
      description: Rate limit exceeded (5 requests per 15 minutes per IP)
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until rate limit resets
        X-RateLimit-Limit:
          schema:
            type: integer
          description: Maximum requests per window
        X-RateLimit-Remaining:
          schema:
            type: integer
          description: Requests remaining in current window
        X-RateLimit-Reset:
          schema:
            type: integer
          description: Unix timestamp when window resets
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                example: false
              error:
                type: string
                example: Too many requests. Please try again later.
            strict: true
            additionalProperties: false
paths:
  /api/ap/comments:
    get:
      operationId: getapiApComments
      summary: Federated Comments API — Coolock Village Forge
      description: >-
        Returns federated comments (from remote Fediverse actors) that were

        received via the /ap/inbox endpoint and stored in D1.

        Only returns approved comments. Includes basic author metadata

        (name, avatar) from the federated_actors cache.

        Edge-cached for 5 minutes — comments are infrequent.


        Auto-generated from live route file: src/pages/api/ap/comments.ts.


        This operation was discovered from the route tree and should be enriched with a curated request/response schema
        if it is intended for external agent use.
      tags:
        - 'ActivityPub (Tenant: coolock-village-forge)'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                strict: true
                additionalProperties: false
                properties:
                  success:
                    type: boolean
                    example: true
                  route:
                    type: string
                    example: /api/ap/comments
                  method:
                    type: string
                    example: GET
                  message:
                    type: string
                    example: Operation exists in code. Schema details should be curated.
                required:
                  - success
                  - route
                  - method
        '400':
          description: Client error
          content:
            application/json:
              schema:
                type: object
                strict: true
                additionalProperties: false
                properties:
                  type:
                    type: string
                    format: uri
                  title:
                    type: string
                  status:
                    type: integer
                  detail:
                    type: string
                  instance:
                    type: string
                required:
                  - title
                  - status
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                strict: true
                additionalProperties: false
                properties:
                  type:
                    type: string
                    format: uri
                  title:
                    type: string
                  status:
                    type: integer
                  detail:
                    type: string
                  instance:
                    type: string
                required:
                  - title
                  - status
      x-generated-from-route: src/pages/api/ap/comments.ts
      x-generated-placeholder: true
      x-visibility: public
      x-ai-exposed: true
  /api/civic/planning:
    get:
      tags:
        - CivicDashboard
      summary: Proxy for Agile Applications planning search
      operationId: getAgilePlanning
      description: |
        Server-side proxy that attempts to bypass client-side bot protection locks
        on the Agile Applications portal when LGMA data is stale.
      responses:
        '200':
          description: Scraped and formatted planning applications
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  items:
                    type: array
                    items:
                      type: object
                      strict: true
                      additionalProperties: false
                  lastUpdated:
                    type: string
                  attribution:
                    type: string
                  error:
                    type: string
                strict: true
                additionalProperties: false
        '500':
          description: Failed to proxy Agile Applications
      x-generated-from-route: src/pages/api/civic/planning.ts
      x-generated-placeholder: false
      x-visibility: public
      x-ai-exposed: true
  /api/search.json:
    get:
      tags:
        - Search
      summary: Search the site content (AI/MCP)
      operationId: searchSite
      description: |
        Tenant-isolated search endpoint. Returns matching pages, businesses, news, and events.
        Exposed specifically for Agentic interoperability (MCP / LLM consumption).
        Provides identical search capability without the UI overhead of Pagefind.
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
          description: Search query string
      responses:
        '200':
          description: Search results JSON array
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        title:
                          type: string
                        url:
                          type: string
                        excerpt:
                          type: string
                      strict: true
                      additionalProperties: false
                strict: true
                additionalProperties: false
      x-generated-from-route: src/pages/api/search.json.ts
      x-generated-placeholder: false
      x-visibility: public
      x-ai-exposed: true
  /api/v1/businesses:
    get:
      tags:
        - Businesses
      summary: List all businesses in the canonical dataset
      operationId: listBusinesses
      description: |
        Returns the canonical business directory as a schema.org ItemList.
        Supports filtering by category, schemaType, and free-text search.
      parameters:
        - name: category
          in: query
          schema:
            type: string
          description: Filter by display category (e.g. "Food & Drink")
        - name: schemaType
          in: query
          schema:
            type: string
          description: Filter by schema.org type (e.g. "Restaurant")
        - name: q
          in: query
          schema:
            type: string
          description: Free-text search across name and description
      responses:
        '200':
          description: Business directory listing
          content:
            application/json:
              schema:
                type: object
                properties:
                  '@context':
                    type: string
                    example: https://schema.org
                  '@type':
                    type: string
                    example: ItemList
                  numberOfItems:
                    type: integer
                  itemListElement:
                    type: array
                    items:
                      type: object
                      strict: true
                      additionalProperties: false
                strict: true
                additionalProperties: false
      x-generated-from-route: src/pages/api/v1/businesses/index.ts
      x-generated-placeholder: false
      x-visibility: public
      x-ai-exposed: true
  /api/v1/businesses/{id}:
    get:
      tags:
        - Businesses
      summary: Get a single business by canonical ID
      operationId: getBusinessById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The business canonicalId (e.g. "spar-malahide-road")
      responses:
        '200':
          description: Full schema.org JSON-LD for a single business
          content:
            application/json:
              schema:
                type: object
                strict: true
                additionalProperties: false
        '404':
          description: Business not found
      x-generated-from-route: src/pages/api/v1/businesses/[id].ts
      x-generated-placeholder: false
      x-visibility: public
      x-ai-exposed: true
  /api/v1/diff:
    get:
      tags:
        - Businesses
      summary: Get dataset changes since a given date
      operationId: getDatasetDiff
      description: |
        Returns business records that changed since the specified date.
        Designed for map correction ingestion and downstream sync.
      parameters:
        - name: since
          in: query
          required: true
          schema:
            type: string
            format: date
          description: ISO 8601 date (e.g. "2026-02-01")
      responses:
        '200':
          description: Diff result
          content:
            application/json:
              schema:
                type: object
                properties:
                  since:
                    type: string
                  hasChanges:
                    type: boolean
                  summary:
                    type: object
                    properties:
                      added:
                        type: integer
                      modified:
                        type: integer
                      removed:
                        type: integer
                    strict: true
                    additionalProperties: false
                  changes:
                    type: array
                    items:
                      type: object
                      strict: true
                      additionalProperties: false
                strict: true
                additionalProperties: false
        '400':
          description: Missing or invalid since parameter
      x-generated-from-route: src/pages/api/v1/diff.ts
      x-generated-placeholder: false
      x-visibility: public
      x-ai-exposed: true
