openapi: 3.0.3
info:
  title: NASEBANAL Recorder API
  description: |-
    Time-series data recorder API built with Cloudflare Workers and D1.

    ## Authentication

    Create a Personal Access Token (PAT) in **Profile Settings > API Tokens** (requires Pro plan or above), then include it in the `Authorization` header.

    Tokens use the format `nbpat_` followed by 40 random characters.

    **Usage:**
    ```
    Authorization: Bearer nbpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ```

    **curl example:**
    ```bash
    curl -H "Authorization: Bearer nbpat_xxxxx..." https://api.example.com/api/v1/records
    ```

    **Plan limits:**
    - Free: API tokens not available
    - Pro: up to 5 tokens
    - Family: up to 20 tokens
    - Enterprise: unlimited tokens

    ## SNS Shared Graph Embed

    Publicly shared graphs can be embedded in external sites (blogs, portfolios, etc.) via iframe without authentication.

    **Endpoint:** `GET https://growth.nasebanal.com/{lang}/recorder/shared/{id}/embed` (served by NASEBANAL Growth, not by this API)

    | Parameter | Type | Required | Description |
    |-----------|------|----------|-------------|
    | `lang` | path | Yes | Language: `ja` or `en` |
    | `id` | path | Yes | Shared graph ID |

    **Features:** Line chart with auto-scale, goal lines, statistics. No navigation header or footer — designed for iframe embedding.

    **Auth:** Not required (public access).

    **iframe example:**
    ```html
    <iframe src=\"https://growth.nasebanal.com/ja/recorder/shared/{id}/embed\" width=\"100%\" height=\"500\" frameborder=\"0\"></iframe>
    ```

    **Note:** The embed displays a snapshot of the data at the time the SNS share was created. To update the data, recreate the SNS share.
  version: 1.7.0
  license:
    name: MIT
servers:
  - url: http://localhost:8787/api/v1
    description: Local dev (wrangler dev)
  - url: https://api.recorder.nasebanal.com/api/v1
    description: Production
security:
  - BearerAuth: []
paths:
  /health:
    get:
      summary: Health check
      description: Check if the API is running
      operationId: getHealth
      tags:
        - Health
      security: []
      responses:
        "200":
          description: API is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      status:
                        type: string
                        example: ok
                      timestamp:
                        type: string
                        format: date-time
  /me:
    get:
      summary: Get current user profile
      description: Retrieve the authenticated user's profile data
      operationId: getCurrentUser
      tags:
        - User
      responses:
        "200":
          description: Current user profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/User"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /records:
    get:
      summary: Get all records
      description: Retrieve all records with optional filtering
      operationId: getRecords
      tags:
        - Records
      parameters:
        - name: name
          in: query
          description: "Filter by record name. A name is unique only per owner: a tag shared with you can have the same name as one of your own, and this then matches both. Use `tag_id` to pick exactly one tag."
          schema:
            type: string
        - name: tag_id
          in: query
          description: Filter by tag id; repeat the parameter for several tags. An id names exactly one tag, so unlike `name` it cannot mix in a same-named tag of another owner. When `tag_id` is given, `name` is ignored. An id of a tag you cannot read (neither yours nor shared with you) matches nothing.
          style: form
          explode: true
          schema:
            type: array
            items:
              type: integer
              minimum: 1
        - name: start_date
          in: query
          description: Filter by start date (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: end_date
          in: query
          description: Filter by end date (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          description: "Maximum number of records to return for unbounded queries (max: 1000). When start_date or end_date is supplied, all records in the range are returned — the date filter itself bounds the payload."
          schema:
            type: integer
            default: 100
            maximum: 1000
        - name: offset
          in: query
          description: Number of records to skip
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: List of records
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordListResponse"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      summary: Create a new record
      description: Create a new time-series record
      operationId: createRecord
      tags:
        - Records
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRecordInput"
      responses:
        "201":
          description: Record created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /records/{id}:
    get:
      summary: Get a single record
      description: Retrieve a record by ID
      operationId: getRecord
      tags:
        - Records
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: Record details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    put:
      summary: Update a record
      description: Update an existing record by ID
      operationId: updateRecord
      tags:
        - Records
      parameters:
        - $ref: "#/components/parameters/RecordId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateRecordInput"
      responses:
        "200":
          description: Record updated successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      summary: Delete a record
      description: Delete a record by ID
      operationId: deleteRecord
      tags:
        - Records
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: Record deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                        enum:
                          - true
                        example: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /records/{id}/visibility:
    patch:
      summary: Toggle record visibility
      description: Show or hide a record
      operationId: toggleRecordVisibility
      tags:
        - Records
      parameters:
        - $ref: "#/components/parameters/RecordId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                hide:
                  type: boolean
              required:
                - hide
      responses:
        "200":
          description: Visibility updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                      is_hidden:
                        type: integer
                        description: 0 = visible, 1 = hidden
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /records/bulk-delete:
    post:
      summary: Bulk delete records
      description: Delete multiple records at once by providing an array of IDs
      operationId: bulkDeleteRecords
      tags:
        - Records
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                  description: Array of record IDs to delete
                  example:
                    - 1
                    - 2
                    - 3
              required:
                - ids
      responses:
        "200":
          description: Records deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      deleted:
                        type: integer
                        description: Number of records deleted
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /tags:
    get:
      summary: Get all tags
      description: Retrieve all tags for the authenticated user
      operationId: getTags
      tags:
        - Tags
      responses:
        "200":
          description: List of tags
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Tag"
                      total:
                        type: integer
        "500":
          $ref: "#/components/responses/InternalError"
  /tags/{id}:
    put:
      summary: Update a tag
      description: Update an existing tag by ID
      operationId: updateTag
      tags:
        - Tags
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                unit:
                  type: string
                  nullable: true
                  description: Unit of measurement. For a `time`-typed tag, must be `hour`, `minute`, or `second`, controlling whether the value formats as H:MM:SS, MM:SS, or decimal seconds.
                chart_type:
                  type: string
                  enum:
                    - line
                    - smooth
                    - step
                data_type:
                  type: string
                  description: "The scale every stored value is written against (a `time` tag's 90 means 90 minutes). Changing it does NOT convert existing values, so it is accepted only when no stored value would change meaning: widening (`integer` → `decimal`) is always allowed, narrowing only while every stored value is representable in the new type, and moving into or out of `time` never is once the tag holds data. Otherwise the request is refused with 409 `TAG_HAS_DATA`, whose `error.details.reason` carries the same code as `Tag.data_type_options[].reason`. Read `data_type_options` off `GET /tags` rather than re-deriving this. Sending the tag's current value is not a change and is always accepted."
                  enum:
                    - binary
                    - decimal
                    - integer
                    - time
      responses:
        "200":
          description: Tag updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/Tag"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: "Refused: the requested `data_type` would change the meaning of values already stored. `error.details.reason` is one of `SCALE_CHANGE`, `NON_INTEGER_VALUES`, `NON_BINARY_VALUES`."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                success: false
                error:
                  code: TAG_HAS_DATA
                  message: "Cannot change data_type: this tag already has records or goals written against its current scale."
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      summary: Delete a tag
      description: Delete a tag and all associated records by ID
      operationId: deleteTag
      tags:
        - Tags
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Tag deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                        enum:
                          - true
                        example: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /tags/{id}/visibility:
    patch:
      summary: Toggle tag visibility
      description: Show or hide a tag
      operationId: toggleTagVisibility
      tags:
        - Tags
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                hide:
                  type: boolean
              required:
                - hide
      responses:
        "200":
          description: Visibility updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/Tag"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /tags/bulk-delete:
    post:
      summary: Bulk delete tags
      description: Delete multiple tags and their associated records at once
      operationId: bulkDeleteTags
      tags:
        - Tags
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                  description: Array of tag IDs to delete
                  example:
                    - 1
                    - 2
                    - 3
              required:
                - ids
      responses:
        "200":
          description: Tags deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      deleted:
                        type: integer
                        description: Number of tags deleted
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /shares:
    get:
      summary: Get my shares
      description: Retrieve shares created by the authenticated user
      operationId: getMyShares
      tags:
        - Shares
      responses:
        "200":
          description: List of shares
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Share"
                      total:
                        type: integer
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      summary: Create a share (sends approval email)
      description: Create a tag share addressed to another user by email. The share is created in status `pending` and does NOT grant data access until the recipient clicks the approval link in the invitation email delivered via the Account API (Resend). Invitations expire after 7 days; after expiry the recipient must be re-invited. The raw invitation token is never stored — only a SHA-256 hash — so even database access does not expose reusable accept links.
      operationId: createShare
      tags:
        - Shares
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                shared_with_email:
                  type: string
                  format: email
                tag_id:
                  type: integer
                  nullable: true
                permission:
                  type: string
                  enum:
                    - read
                    - write
              required:
                - shared_with_email
                - permission
      responses:
        "201":
          description: Share created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/Share"
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /shares/with-me:
    get:
      summary: Get shares shared with me
      description: Retrieve shares that other users have shared with the authenticated user. Returns only `accepted` shares by default. Pass `?status=pending` to list invitations awaiting approval, `?status=rejected` for declined invitations, or `?status=all` for every row.
      operationId: getSharedWithMe
      tags:
        - Shares
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - accepted
              - pending
              - rejected
              - all
            default: accepted
          description: Filter by approval status.
      responses:
        "200":
          description: List of shares shared with the user
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Share"
                      total:
                        type: integer
        "500":
          $ref: "#/components/responses/InternalError"
  /shares/{id}:
    put:
      summary: Update a share
      description: Update an existing share
      operationId: updateShare
      tags:
        - Shares
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tag_id:
                  type: integer
                permission:
                  type: string
                  enum:
                    - read
                    - write
              required:
                - tag_id
                - permission
      responses:
        "200":
          description: Share updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/Share"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      summary: Delete a share
      description: Delete a share by ID
      operationId: deleteShare
      tags:
        - Shares
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Share deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                        enum:
                          - true
                        example: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /shares/{id}/visibility:
    patch:
      summary: Toggle share visibility
      description: Show or hide a share from the dashboard
      operationId: toggleShareVisibility
      tags:
        - Shares
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                is_hidden:
                  type: boolean
              required:
                - is_hidden
      responses:
        "200":
          description: Visibility updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/Share"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /shares/{id}/pause:
    patch:
      summary: Toggle share paused status
      description: Pause or unpause a share (owner-side)
      operationId: toggleSharePaused
      tags:
        - Shares
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                is_paused:
                  type: boolean
              required:
                - is_paused
      responses:
        "200":
          description: Paused status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/Share"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /shares/{id}/transfer:
    post:
      summary: Transfer tag ownership
      description: Transfer tag ownership to the shared-with user
      operationId: transferShareOwnership
      tags:
        - Shares
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Ownership transferred successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                        enum:
                          - true
                        example: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /shares/bulk-delete:
    post:
      summary: Bulk delete shares
      description: Delete multiple shares at once
      operationId: bulkDeleteShares
      tags:
        - Shares
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                  description: Array of share IDs to delete
                  example:
                    - 1
                    - 2
                    - 3
              required:
                - ids
      responses:
        "200":
          description: Shares deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      deleted:
                        type: integer
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /shares/accept:
    post:
      summary: Accept a pending share invitation
      description: Called from the /share/accept frontend page after the recipient clicks the link in their invitation email. Requires the authenticated user to be the invited recipient (`shared_with_user_id`). On success the share transitions from `pending` to `accepted` and data access is enabled.
      operationId: acceptShare
      tags:
        - Shares
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  description: Opaque invitation token delivered via email. Hashed server-side before lookup.
              required:
                - token
      responses:
        "200":
          description: Share accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                      status:
                        type: string
                        enum:
                          - accepted
                      already:
                        type: boolean
                        description: True if the share was already accepted.
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
  /shares/reject:
    post:
      summary: Decline a pending share invitation via emailed token
      description: Called from the /share/reject frontend page after the recipient clicks the decline link in their invitation email. Same token and auth model as /shares/accept (requires the authenticated user to be the invited recipient); on success the share transitions from `pending` to `rejected`. Cannot be used on already-accepted shares (delete instead, via DELETE /shares/{id}).
      operationId: rejectShareByToken
      tags:
        - Shares
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  description: Opaque invitation token delivered via email. Hashed server-side before lookup.
              required:
                - token
      responses:
        "200":
          description: Share rejected
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                      status:
                        type: string
                        enum:
                          - rejected
                      already:
                        type: boolean
                        description: True if the share was already rejected.
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
  /shares/{id}/reject:
    post:
      summary: Reject a pending share invitation
      description: Called from the recipient's SharingManager UI. Marks the share as `rejected`; cannot be used on already-accepted shares (delete instead).
      operationId: rejectShare
      tags:
        - Shares
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Share rejected
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                      status:
                        type: string
                        enum:
                          - rejected
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
  /shared-graphs:
    get:
      summary: Get my shared graphs
      description: Retrieve shared graphs created by the authenticated user
      operationId: getMySharedGraphs
      tags:
        - Shared Graphs
      responses:
        "200":
          description: List of shared graphs
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SharedGraph"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      summary: Create a shared graph
      description: Create a publicly shareable graph snapshot for SNS sharing
      operationId: createSharedGraph
      tags:
        - Shared Graphs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSharedGraphInput"
      responses:
        "201":
          description: Shared graph created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/SharedGraph"
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /shared-graphs/{id}:
    put:
      summary: Update a shared graph
      description: Update title or description of a shared graph
      operationId: updateSharedGraph
      tags:
        - Shared Graphs
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                description:
                  type: string
      responses:
        "200":
          description: Shared graph updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/SharedGraph"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      summary: Delete a shared graph
      description: Delete a shared graph by ID
      operationId: deleteSharedGraph
      tags:
        - Shared Graphs
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Shared graph deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                        enum:
                          - true
                        example: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /shared-graphs/{id}/toggle:
    patch:
      summary: Toggle shared graph active status
      description: Activate or deactivate a shared graph
      operationId: toggleSharedGraphActive
      tags:
        - Shared Graphs
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Status toggled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      is_active:
                        type: integer
                        description: 1 for active, 0 for inactive
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /shared-graphs/bulk-delete:
    post:
      summary: Bulk delete shared graphs
      description: Delete multiple shared graphs at once
      operationId: bulkDeleteSharedGraphs
      tags:
        - Shared Graphs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ids:
                  type: array
                  items:
                    type: string
                  description: Array of shared graph IDs to delete
                  example:
                    - abc123
                    - def456
              required:
                - ids
      responses:
        "200":
          description: Shared graphs deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      deleted:
                        type: integer
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /public/shared-graphs/{id}:
    get:
      summary: Get public shared graph
      description: Retrieve a shared graph's public data (no authentication required)
      operationId: getPublicSharedGraph
      tags:
        - Shared Graphs
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Shared graph public data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/SharedGraphPublic"
        "404":
          $ref: "#/components/responses/NotFound"
  /public/shared-graphs/{id}/og-image:
    get:
      summary: Generate OG image for shared graph
      description: Generate an Open Graph image for a shared graph (no authentication required)
      operationId: getSharedGraphOgImage
      tags:
        - Shared Graphs
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OG image
          content:
            image/png:
              schema:
                type: string
                format: binary
        "404":
          $ref: "#/components/responses/NotFound"
  /records/export:
    get:
      summary: Export records as CSV
      description: Export records to CSV format with optional filtering
      operationId: exportRecords
      tags:
        - Records
      parameters:
        - name: name
          in: query
          description: Filter by record name (supports wildcards with *)
          schema:
            type: string
        - name: start_date
          in: query
          description: Filter by start date (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: end_date
          in: query
          description: Filter by end date (ISO 8601)
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: CSV file containing records
          content:
            text/csv:
              schema:
                type: string
                format: binary
                example: |-
                  Tag,Value,Unit,Recorded At
                  "temperature",25.5,"°C","2024-01-01T10:00:00Z"
        "500":
          $ref: "#/components/responses/InternalError"
  /records/import:
    post:
      summary: Import records from CSV
      description: Import records from CSV file. Records with matching tag and recorded_at will be updated, others will be created.
      operationId: importRecords
      tags:
        - Records
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: "CSV file to import (columns: Tag, Value, Unit, Recorded At)"
              required:
                - file
      responses:
        "200":
          description: Import completed successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImportResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /records/stats:
    get:
      summary: Get record statistics
      description: Get aggregated statistics for the records of one tag, picked by `name` or, unambiguously, by `tag_id`. Send one of the two.
      operationId: getRecordStats
      tags:
        - Statistics
      parameters:
        - name: name
          in: query
          description: Record name to get statistics for. A name is unique only per owner, so a tag shared with you can share it with one of your own; prefer `tag_id`.
          schema:
            type: string
        - name: tag_id
          in: query
          description: Id of the tag to get statistics for. When given, `name` is ignored. An id of a tag you cannot read matches nothing.
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Statistics for the specified record name
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatsResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /media/upload:
    post:
      summary: Upload media file
      description: Upload a media file (image, video, or audio). Maximum file size is 100MB.
      operationId: uploadMedia
      tags:
        - Media
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: Media file to upload (max 100MB)
              required:
                - file
      responses:
        "200":
          description: File uploaded successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      url:
                        type: string
                        description: URL of the uploaded file
                      key:
                        type: string
                        description: Unique key for the uploaded file
                      mediaType:
                        type: string
                        enum:
                          - image
                          - video
                          - audio
                        description: Type of media
                      filename:
                        type: string
                        description: Original filename
                      size:
                        type: integer
                        description: File size in bytes
                      contentType:
                        type: string
                        description: MIME content type
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /media/upload-url:
    post:
      summary: Get upload URL info
      description: Get upload URL information for a media file
      operationId: getMediaUploadUrl
      tags:
        - Media
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                filename:
                  type: string
                  description: Original filename
                contentType:
                  type: string
                  description: MIME content type
                size:
                  type: integer
                  description: File size in bytes
              required:
                - filename
                - contentType
                - size
      responses:
        "200":
          description: Upload URL info
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      key:
                        type: string
                        description: Unique key for the file
                      mediaType:
                        type: string
                        description: Type of media
                      uploadMethod:
                        type: string
                        example: direct
                        description: Upload method
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /media/signed-url/{key}:
    get:
      summary: Get signed URL for media access
      description: Get a signed URL to access a media file
      operationId: getMediaSignedUrl
      tags:
        - Media
      parameters:
        - name: key
          in: path
          required: true
          description: Media file key
          schema:
            type: string
      responses:
        "200":
          description: Signed URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      signedUrl:
                        type: string
                        description: Signed URL for accessing the media file
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /media/delete/{key}:
    delete:
      summary: Delete media file
      description: Delete a media file by key
      operationId: deleteMedia
      tags:
        - Media
      parameters:
        - name: key
          in: path
          required: true
          description: Media file key
          schema:
            type: string
      responses:
        "200":
          description: Media file deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                        enum:
                          - true
                        example: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /goals:
    get:
      summary: List all goals
      description: Retrieve all goals for the authenticated user
      operationId: getGoals
      tags:
        - Goals
      responses:
        "200":
          description: List of goals
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Goal"
                      total:
                        type: integer
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      summary: Create a new goal
      description: Create a new goal for the authenticated user
      operationId: createGoal
      tags:
        - Goals
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateGoalInput"
      responses:
        "201":
          description: Goal created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/Goal"
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /goals/{id}:
    put:
      summary: Update a goal
      description: Update an existing goal by ID
      operationId: updateGoal
      tags:
        - Goals
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateGoalInput"
      responses:
        "200":
          description: Goal updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/Goal"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      summary: Delete a goal
      description: Delete a goal by ID
      operationId: deleteGoal
      tags:
        - Goals
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Goal deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                        enum:
                          - true
                        example: true
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /goals/bulk-delete:
    post:
      summary: Bulk delete goals
      description: Delete multiple goals at once by providing an array of IDs
      operationId: bulkDeleteGoals
      tags:
        - Goals
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                  description: Array of goal IDs to delete
                  example:
                    - 1
                    - 2
                    - 3
              required:
                - ids
      responses:
        "200":
          description: Goals deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      deleted:
                        type: integer
                        description: Number of goals deleted
        "400":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
  /goals/progress:
    get:
      summary: Get goal progress
      description: Retrieve progress for all visible goals of the authenticated user. Computes current values from latest records, including virtual tags (e.g. BMI). Used by NASEBANAL Target for cross-app goal tracking.
      operationId: getGoalProgress
      tags:
        - Goals
      responses:
        "200":
          description: Goal progress list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/GoalProgress"
        "500":
          $ref: "#/components/responses/InternalError"
  /goals/{id}/visibility:
    patch:
      summary: Toggle goal visibility
      description: Show or hide a goal
      operationId: toggleGoalVisibility
      tags:
        - Goals
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                hide:
                  type: boolean
              required:
                - hide
      responses:
        "200":
          description: Visibility updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    example: true
                  data:
                    $ref: "#/components/schemas/Goal"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: Personal Access Token (nbpat_...)
  parameters:
    RecordId:
      name: id
      in: path
      required: true
      description: Record ID
      schema:
        type: integer
        minimum: 1
  schemas:
    GoalProgress:
      type: object
      properties:
        goal_id:
          type: integer
          description: Goal ID
        tag_name:
          type: string
          description: Tag name associated with the goal
        tag_unit:
          type: string
          nullable: true
          description: Unit of the tag (e.g. kg, km)
        tag_data_type:
          type: string
          description: Data type of the tag (number, time, rank)
        virtual_tag_key:
          type: string
          nullable: true
          description: Virtual tag key if applicable (e.g. BodyWeight/BMI~)
        goal_value:
          type: number
          nullable: true
          description: Goal value for the goal
        direction:
          type: string
          enum:
            - above
            - below
          description: Direction of the goal (above = higher is better, below = lower is better)
        current:
          type: number
          nullable: true
          description: Current value computed from latest record
        met:
          type: boolean
          description: Whether the goal is currently met
        pct:
          type: integer
          description: Progress percentage (0-100)
    Record:
      type: object
      properties:
        id:
          type: integer
          description: Unique record ID
        name:
          type: string
          description: Record name/identifier
        value:
          type: number
          description: Numeric value
        unit:
          type: string
          nullable: true
          description: Unit of measurement. For a `time`-typed tag, `hour`, `minute`, or `second`, controlling whether the value formats as H:MM:SS, MM:SS, or decimal seconds.
        recorded_at:
          type: string
          format: date-time
          description: Timestamp when the value was recorded
        created_at:
          type: string
          format: date-time
          description: Timestamp when the record was created
        updated_at:
          type: string
          format: date-time
          description: Timestamp when the record was last updated
        owner_email:
          type: string
          description: Email of the tag's owner. Set on the caller's own rows too (it then holds the caller's own address), so compare it with the caller's to tell shared-in data apart.
        owner_name:
          type: string
          description: Display name the owner set in their profile. Absent when they set none. Clients name the owner by this, falling back to a masked email.
      required:
        - id
        - name
        - value
        - recorded_at
        - created_at
        - updated_at
    CreateRecordInput:
      type: object
      properties:
        name:
          type: string
          description: Record name/identifier
          example: temperature
        value:
          type: number
          description: Numeric value
          example: 25.5
        unit:
          type: string
          description: Unit of measurement. For a `time`-typed tag, must be `hour`, `minute`, or `second`, controlling whether the value formats as H:MM:SS, MM:SS, or decimal seconds.
          example: °C
        recorded_at:
          type: string
          format: date-time
          description: Timestamp when the value was recorded (defaults to current time)
      required:
        - name
        - value
    UpdateRecordInput:
      type: object
      properties:
        name:
          type: string
          description: Record name/identifier
        value:
          type: number
          description: Numeric value
        unit:
          type: string
          description: Unit of measurement. For a `time`-typed tag, must be `hour`, `minute`, or `second`, controlling whether the value formats as H:MM:SS, MM:SS, or decimal seconds.
        recorded_at:
          type: string
          format: date-time
          description: Timestamp when the value was recorded
    RecordStats:
      type: object
      properties:
        tag_id:
          type: integer
          description: Tag ID
        tag_name:
          type: string
          description: Tag name
        count:
          type: integer
          description: Total number of records
        min:
          type: number
          description: Minimum value
        min_recorded_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp when the minimum value was recorded
        max:
          type: number
          description: Maximum value
        max_recorded_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp when the maximum value was recorded
        average:
          type: number
          description: Average value
        latest:
          description: Latest record
          oneOf:
            - $ref: "#/components/schemas/Record"
            - type: object
              nullable: true
      required:
        - tag_id
        - tag_name
        - count
        - min
        - min_recorded_at
        - max
        - max_recorded_at
        - average
        - latest
    RecordResponse:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - true
          example: true
        data:
          $ref: "#/components/schemas/Record"
    RecordListResponse:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - true
          example: true
        data:
          type: object
          properties:
            data:
              type: array
              items:
                $ref: "#/components/schemas/Record"
            total:
              type: integer
              description: Total number of records matching the filter
            limit:
              type: integer
              description: Maximum number of records returned
            offset:
              type: integer
              description: Number of records skipped
    StatsResponse:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - true
          example: true
        data:
          $ref: "#/components/schemas/RecordStats"
    ImportResponse:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - true
          example: true
        data:
          type: object
          properties:
            imported:
              type: integer
              description: Total number of records processed
              example: 10
            created:
              type: integer
              description: Number of new records created
              example: 7
            updated:
              type: integer
              description: Number of existing records updated
              example: 3
          required:
            - imported
            - created
            - updated
    SeriesKind:
      type: string
      enum:
        - raw
        - diff
        - diff_pct
        - sum
        - bmi
        - annual_income
      description: "Stable code of a derived series — not the display suffix (`/Diff` etc.). `diff` difference from the previous record, `diff_pct` percent change from the previous record, `sum` running total, `bmi` BMI (body-weight category), `annual_income` annual income (currency category). The raw tag is `raw`: it is the default on the referencing side and never appears in a `derived_series` list."
    DerivedSeries:
      type: object
      description: "A series derived from a tag. No row of its own: it is derived from the tag's settings (`derived_config`, `category`). Identified by the source tag's `id` plus `kind` — no name in it, so a rename cannot break a reference."
      properties:
        kind:
          $ref: "#/components/schemas/SeriesKind"
        label:
          type: string
          description: Display name (e.g. `Body Weight/BMI`). Not an identifier.
        unit:
          type: string
          nullable: true
        data_type:
          type: string
          enum:
            - decimal
            - integer
            - time
        chart_type:
          type: string
          enum:
            - line
            - smooth
            - step
      required:
        - kind
        - label
        - data_type
        - chart_type
    Tag:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        name:
          type: string
        unit:
          type: string
          nullable: true
          description: Unit of measurement. For a `time`-typed tag, must be `hour`, `minute`, or `second`, controlling whether the value formats as H:MM:SS, MM:SS, or decimal seconds.
        data_type:
          type: string
          enum:
            - binary
            - decimal
            - integer
            - time
        chart_type:
          type: string
          enum:
            - line
            - smooth
            - step
        is_hidden:
          type: integer
          description: 0 = visible, 1 = hidden
        derived_series:
          type: array
          description: Derived series this tag can produce. Excludes the raw series (`raw`).
          items:
            $ref: "#/components/schemas/DerivedSeries"
        data_type_options:
          type: array
          description: "Which `data_type` this tag may be changed to, and why not for the rest. A change is safe exactly when no stored value changes meaning, which depends on the direction and on what is already stored — so this is server-computed rather than re-derived per client, and it carries a reason per option because the reason differs per option. A tag shared in from another user lists only its current type, not selectable: the recipient does not own the master data."
          items:
            type: object
            properties:
              value:
                type: string
                enum:
                  - binary
                  - decimal
                  - integer
                  - time
              selectable:
                type: boolean
              reason:
                type: string
                description: Why this type is unavailable; absent when selectable. `SCALE_CHANGE` — into or out of `time`, whose stored minutes would be re-read rather than converted (90 becomes 1:30). `NON_INTEGER_VALUES` — a stored value is not a whole number, so it would display rounded while the stored value stays fractional. `NON_BINARY_VALUES` — a stored value is neither 0 nor 1.
                enum:
                  - SCALE_CHANGE
                  - NON_INTEGER_VALUES
                  - NON_BINARY_VALUES
            required:
              - value
              - selectable
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        owner_email:
          type: string
          description: Email of the tag's owner. Set on the caller's own tags too (it then holds the caller's own address), so compare it with the caller's to tell shared-in tags apart.
        owner_name:
          type: string
          description: Display name the owner set in their profile. Absent when they set none. Clients name the owner by this, falling back to a masked email.
      required:
        - id
        - user_id
        - name
        - data_type
        - chart_type
        - created_at
        - updated_at
    Share:
      type: object
      properties:
        id:
          type: integer
        owner_user_id:
          type: integer
        shared_with_user_id:
          type: integer
        tag_id:
          type: integer
          nullable: true
        permission:
          type: string
          enum:
            - read
            - write
        is_hidden:
          type: integer
        status:
          type: string
          enum:
            - pending
            - accepted
            - rejected
          description: Approval status. New shares are created in 'pending' and require the recipient to click the accept link delivered via email. Only 'accepted' shares grant data access.
        expires_at:
          type: string
          format: date-time
          nullable: true
          description: Expiration timestamp for a pending invitation. After this time the invitation can no longer be accepted.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        owner_email:
          type: string
          description: Email of the sharing owner. Set on shares addressed to the caller.
        owner_name:
          type: string
          description: Display name the sharing owner set in their profile. Absent when they set none.
        shared_with_email:
          type: string
          description: Email of the recipient. Set on shares the caller owns.
        shared_with_name:
          type: string
          description: Display name the recipient set in their profile. Set on shares the caller owns, and only once the share is `accepted`, so an invitation does not reveal the profile of someone who has not accepted it.
      required:
        - id
        - owner_user_id
        - shared_with_user_id
        - permission
        - status
        - created_at
        - updated_at
    SharedGraph:
      type: object
      properties:
        id:
          type: string
        tag_id:
          type: integer
          description: Primary tag ID (first tag). For legacy compatibility.
        tag_ids:
          type: array
          items:
            type: integer
          description: All tag IDs included in this shared graph.
        tag_names:
          type: array
          items:
            type: string
          description: All tag names included in this shared graph.
        title:
          type: string
        description:
          type: string
          nullable: true
        is_active:
          type: integer
          description: 1 = active, 0 = inactive
        view_count:
          type: integer
        created_at:
          type: string
          format: date-time
      required:
        - id
        - tag_id
        - tag_ids
        - tag_names
        - title
        - is_active
        - view_count
        - created_at
    SharedGraphPublic:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        chart_data:
          type: string
        created_at:
          type: string
          format: date-time
      required:
        - id
        - chart_data
        - created_at
    CreateSharedGraphInput:
      type: object
      properties:
        tag_ids:
          type: array
          items:
            type: integer
          description: Tag IDs to include in the graph. Multiple tags can be specified.
        tag_id:
          type: integer
          description: Single tag ID (legacy, deprecated). Use tag_ids instead.
        title:
          type: string
          description: Title of the shared graph.
        description:
          type: string
          nullable: true
        start_date:
          type: string
          format: date
          description: Start date filter (YYYY-MM-DD). Defaults to 30 days ago if omitted.
        end_date:
          type: string
          format: date
          description: End date filter (YYYY-MM-DD).
        include_comments:
          type: boolean
          default: false
          description: Whether to include record comments in the snapshot.
        include_media:
          type: boolean
          default: false
          description: Whether to include media links (signed URLs) in the snapshot.
        auto_scale:
          type: boolean
          default: false
          description: Y-axis scale mode. true = auto scale to data range, false = start from 0.
        show_goals:
          type: boolean
          default: true
          description: Whether to show goal lines on the graph.
        badges:
          type: array
          items:
            type: string
          description: Earned badge IDs to include in the shared graph.
      required:
        - tag_ids
        - title
    User:
      type: object
      properties:
        id:
          type: integer
          description: User ID
        email:
          type: string
          format: email
          description: User email address
        name:
          type: string
          nullable: true
          description: Display name
        language:
          type: string
          nullable: true
          description: Preferred language
        plan:
          type: string
          description: Plan for this service, derived from the account's subscriptions. `free` when the account holds no active subscription for Recorder.
        subscription_status:
          type: string
          nullable: true
          description: Subscription status for that subscription, if any
      required:
        - id
        - email
    Goal:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        tag_id:
          type: integer
        goal_type:
          type: string
          enum:
            - goal_value
        goal_value:
          type: number
          nullable: true
        direction:
          type: string
          enum:
            - above
            - below
            - null
          nullable: true
        start_date:
          type: string
          nullable: true
        end_date:
          type: string
          nullable: true
        comment:
          type: string
          nullable: true
        virtual_tag_key:
          type: string
          nullable: true
        tag_kind:
          allOf:
            - $ref: "#/components/schemas/SeriesKind"
          description: Which series of the `tag_id` tag the goal targets. Absent or `raw` means the raw tag. Successor to `virtual_tag_key`, with no name in it.
        is_hidden:
          type: integer
          description: 0 = visible, 1 = hidden
        tag_name:
          type: string
        tag_unit:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - user_id
        - tag_id
        - goal_type
        - is_hidden
        - tag_name
        - created_at
        - updated_at
    CreateGoalInput:
      type: object
      properties:
        tag_id:
          type: integer
          description: Tag ID to associate with the goal
        goal_type:
          type: string
          enum:
            - goal_value
          description: Type of goal
        goal_value:
          type: number
          description: Goal value (for goal_value type)
        direction:
          type: string
          enum:
            - above
            - below
          description: Direction for target value comparison
        start_date:
          type: string
          description: Goal start date
        end_date:
          type: string
          description: Goal end date
        comment:
          type: string
          description: Optional comment
        virtual_tag_key:
          type: string
          description: Virtual tag key
        tag_kind:
          allOf:
            - $ref: "#/components/schemas/SeriesKind"
          description: Which series of the `tag_id` tag the goal targets. Absent or `raw` means the raw tag. Successor to `virtual_tag_key`, with no name in it.
      required:
        - tag_id
        - goal_type
    UpdateGoalInput:
      type: object
      properties:
        tag_id:
          type: integer
        goal_type:
          type: string
          enum:
            - goal_value
        goal_value:
          type: number
        direction:
          type: string
          enum:
            - above
            - below
        start_date:
          type: string
        end_date:
          type: string
        comment:
          type: string
        virtual_tag_key:
          type: string
        tag_kind:
          allOf:
            - $ref: "#/components/schemas/SeriesKind"
          description: Which series of the `tag_id` tag the goal targets. Absent or `raw` means the raw tag. Successor to `virtual_tag_key`, with no name in it.
    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - false
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              description: Error code
            message:
              type: string
              description: Error message
            details:
              type: object
              description: Additional error details
              additionalProperties: true
  responses:
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: VALIDATION_ERROR
              message: Invalid input data
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: NOT_FOUND
              message: Record not found
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: AUTH_ERROR
              message: No authentication token provided
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: INTERNAL_ERROR
              message: An unexpected error occurred
tags:
  - name: Health
    description: Health check endpoints
  - name: User
    description: User profile management
  - name: Records
    description: Time-series record management
  - name: Statistics
    description: Statistical analysis of records
  - name: Tags
    description: Tag management for organizing records
  - name: Shares
    description: Data sharing between users
  - name: Shared Graphs
    description: Public graph sharing for SNS
  - name: Media
    description: Media file upload and management
  - name: Goals
    description: Goal tracking and management
