openapi: 3.0.3
info:
  title: FamilyDash Agent API
  version: "1.0.0"
  description: |
    A small REST surface for agents and scripts to read and change a
    FamilyDash household — chores, kids, chore templates, verses, birthdays
    and today's meal plan.

    ## Authentication

    Every request needs an API token:

        Authorization: Bearer fd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

    Create one in the admin panel at https://dashmin.foxego.com under
    **Tokens**. The token is shown once and stored only as a SHA-256 hash, so
    it cannot be recovered later — mint a new one if you lose it. Tokens can
    be revoked at any time, which takes effect immediately.

    A token is bound to exactly one household. Every request is scoped to that
    household; there is no way to name another one.

    ## Conventions

    * Dates are `YYYY-MM-DD` in the household's own timezone.
    * `4xx` responses carry `{"error": "..."}`.
    * Fields not listed as writable are ignored in request bodies, so a
      request can never move a row between households or set its own id.

servers:
  - url: https://dash.foxego.com/functions/v1/api
    description: Production

security:
  - bearerAuth: []

tags:
  - name: chores
  - name: kids
  - name: household

paths:
  /:
    get:
      tags: [household]
      summary: Service index
      description: Confirms the token works and names the household it belongs to.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  service: { type: string, example: familydash }
                  household:
                    type: object
                    properties:
                      id: { type: string, format: uuid }
                      timezone: { type: string, example: America/Chicago }
                  resources:
                    type: array
                    items: { type: string }
                  spec: { type: string, format: uri }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /household:
    get:
      tags: [household]
      summary: The household this token belongs to
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Household" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /children:
    get:
      tags: [kids]
      summary: List the children, in display order
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Child" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [kids]
      summary: Add a child
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChildInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Child" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /children/{id}:
    parameters: [{ $ref: "#/components/parameters/Id" }]
    get:
      tags: [kids]
      summary: Fetch one child
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Child" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [kids]
      summary: Update a child
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChildInput" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Child" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [kids]
      summary: Delete a child
      description: Cascades to their chores and points history.
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }

  /chores:
    get:
      tags: [chores]
      summary: Chores for a day
      description: |
        Returns the chores that count for `date`: those falling due that day,
        plus weekly ones already showing and not yet due.

        Recurring chores for the day are generated first if they do not exist
        yet, so an agent sees the same list the dashboard does.
      parameters:
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
          description: Defaults to today in the household's timezone.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  date: { type: string, format: date }
                  chores:
                    type: array
                    items: { $ref: "#/components/schemas/Chore" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [chores]
      summary: Create a one-off chore
      description: |
        For a recurring chore, create a chore template instead — templates
        generate their chores automatically each day or week.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChoreInput" }
            examples:
              assigned:
                summary: Assigned to one child today
                value: { title: Tidy the porch, points: 2, child_id: 00000000-0000-0000-0000-000000000000, due_date: "2026-09-01" }
              open:
                summary: Up for grabs
                value: { title: Bring in the bins, points: 1, due_date: "2026-09-01" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Chore" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /chores/{id}:
    parameters: [{ $ref: "#/components/parameters/Id" }]
    get:
      tags: [chores]
      summary: Fetch one chore
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Chore" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [chores]
      summary: Update a chore
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChoreInput" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Chore" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [chores]
      summary: Delete a chore
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }

  /chores/{id}/complete:
    parameters: [{ $ref: "#/components/parameters/Id" }]
    post:
      tags: [chores]
      summary: Mark a chore done and award its points
      description: |
        Idempotent: completing an already-completed chore succeeds and awards
        nothing further.

        For a chore assigned to a child, `child_id` may be omitted. For an
        open chore it is required, since the points must go to someone.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                child_id:
                  type: string
                  format: uuid
                  description: Required for open chores; must match the assignee otherwise.
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400":
          description: An open chore was completed without naming a child, or the child is not its assignee.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404": { $ref: "#/components/responses/NotFound" }

  /chores/{id}/uncomplete:
    parameters: [{ $ref: "#/components/parameters/Id" }]
    post:
      tags: [chores]
      summary: Undo a completion and take the points back
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }

  /chore-templates:
    get:
      tags: [chores]
      summary: List recurring chore templates
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ChoreTemplate" }
    post:
      tags: [chores]
      summary: Create a recurring chore
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChoreTemplateInput" }
            examples:
              everyone:
                summary: Every child, every day
                value: { title: Clean Room, points: 1, kind: each, cadence: daily }
              rotating:
                summary: Taken in turn, a different child each week
                value: { title: Take out trash, points: 2, kind: rotating, cadence: daily }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChoreTemplate" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /chore-templates/{id}:
    parameters: [{ $ref: "#/components/parameters/Id" }]
    get:
      tags: [chores]
      summary: Fetch one template
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChoreTemplate" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [chores]
      summary: Update a template
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChoreTemplateInput" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChoreTemplate" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [chores]
      summary: Delete a template
      description: Cascades to the chores it generated.
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }

  /verses:
    get:
      tags: [household]
      summary: List verses
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Verse" }
    post:
      tags: [household]
      summary: Add a verse
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VerseInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Verse" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /verses/{id}:
    parameters: [{ $ref: "#/components/parameters/Id" }]
    patch:
      tags: [household]
      summary: Update a verse
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VerseInput" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Verse" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [household]
      summary: Delete a verse
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }

  /birthdays:
    get:
      tags: [household]
      summary: List birthdays
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Birthday" }
    post:
      tags: [household]
      summary: Add a birthday
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BirthdayInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Birthday" }
        "400":
          description: The date is in the future, or the name is blank.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /birthdays/{id}:
    parameters: [{ $ref: "#/components/parameters/Id" }]
    patch:
      tags: [household]
      summary: Update a birthday
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BirthdayInput" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Birthday" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [household]
      summary: Delete a birthday
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }

  /meals:
    get:
      tags: [household]
      summary: Today's meal plan
      description: |
        Read from the household's Tandoor instance, if one is configured.
        Returns an empty list when none is, and reports a reachable-but-broken
        Tandoor in `error` rather than failing the request.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  date: { type: string, format: date }
                  meals:
                    type: array
                    items: { $ref: "#/components/schemas/Meal" }
                  error:
                    type: string
                    nullable: true

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: An API token minted in the admin panel, of the form `fd_…`.

  parameters:
    Id:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }

  responses:
    Ok:
      description: Success
      content:
        application/json:
          schema:
            type: object
            properties:
              ok: { type: boolean, example: true }
    BadRequest:
      description: The request body was rejected.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: The API token is missing, malformed, unknown or revoked.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: No such record in this household.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string }

    Household:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        timezone: { type: string, example: America/Chicago }

    Child:
      type: object
      properties:
        id: { type: string, format: uuid }
        household_id: { type: string, format: uuid }
        name: { type: string }
        color: { type: string, example: "#EF4444" }
        sort_order: { type: integer }
        created_at: { type: string, format: date-time }

    ChildInput:
      type: object
      properties:
        name: { type: string, maxLength: 40 }
        color:
          type: string
          pattern: "^#[0-9A-Fa-f]{3,8}$"
          example: "#EF4444"
        sort_order: { type: integer, minimum: 0 }

    Chore:
      type: object
      properties:
        id: { type: string, format: uuid }
        household_id: { type: string, format: uuid }
        child_id:
          type: string
          format: uuid
          nullable: true
          description: Null means the chore is open — up for grabs.
        template_id:
          type: string
          format: uuid
          nullable: true
          description: Null for a one-off chore.
        title: { type: string }
        points: { type: integer }
        due_date: { type: string, format: date, nullable: true }
        show_from: { type: string, format: date, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }
        completed_by: { type: string, format: uuid, nullable: true }

    ChoreInput:
      type: object
      required: [title]
      properties:
        title: { type: string, maxLength: 120 }
        points: { type: integer, minimum: 0, default: 1 }
        child_id:
          type: string
          format: uuid
          nullable: true
          description: Omit to leave the chore open.
        due_date: { type: string, format: date }
        show_from:
          type: string
          format: date
          description: When it starts appearing. Defaults to the due date.

    ChoreTemplate:
      type: object
      properties:
        id: { type: string, format: uuid }
        household_id: { type: string, format: uuid }
        title: { type: string }
        points: { type: integer }
        kind: { $ref: "#/components/schemas/TemplateKind" }
        cadence: { $ref: "#/components/schemas/Cadence" }
        rotation_offset:
          type: integer
          description: Which child a rotation starts with. Only meaningful for `rotating`.
        active: { type: boolean }
        sort_order: { type: integer }

    ChoreTemplateInput:
      type: object
      required: [title, kind, cadence]
      properties:
        title: { type: string, maxLength: 120 }
        points: { type: integer, minimum: 0, default: 1 }
        kind: { $ref: "#/components/schemas/TemplateKind" }
        cadence: { $ref: "#/components/schemas/Cadence" }
        rotation_offset: { type: integer, minimum: 0 }
        active: { type: boolean, default: true }
        sort_order: { type: integer, minimum: 0 }

    TemplateKind:
      type: string
      enum: [rotating, each, open]
      description: |
        * `rotating` — one child per period, taken in turn.
        * `each` — every child gets their own copy.
        * `open` — unassigned, up for grabs.

    Cadence:
      type: string
      enum: [daily, weekly]

    Verse:
      type: object
      properties:
        id: { type: string, format: uuid }
        household_id: { type: string, format: uuid }
        reference: { type: string, example: "Psalm 23:1" }
        text: { type: string }
        sort_order: { type: integer }

    VerseInput:
      type: object
      required: [reference, text]
      properties:
        reference: { type: string }
        text: { type: string }
        sort_order: { type: integer, minimum: 0 }

    Birthday:
      type: object
      properties:
        id: { type: string, format: uuid }
        household_id: { type: string, format: uuid }
        name: { type: string }
        birth_date: { type: string, format: date }

    BirthdayInput:
      type: object
      required: [name, birth_date]
      properties:
        name: { type: string, maxLength: 60 }
        birth_date:
          type: string
          format: date
          description: Must not be in the future.

    Meal:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        mealType: { type: string, nullable: true, example: Supper }
        mealTypeOrder: { type: integer, nullable: true }
        servings: { type: number, nullable: true }
        note: { type: string, nullable: true }
