openapi: 3.0.3
info:
  title: NASEBANAL - Target API
  description: "Target API for NASEBANAL. Provides hierarchical target management: users define goals and organize them in a tree structure. Parent-child links are persisted via `targets.parent_target_id` (authoritative since migration 0017); a legacy cell-based storage layer is kept in sync best-effort for backward compatibility. Targets can be shared with other users (read/write, optionally including the subtree and linked Recorder data), and ownership of a personal target subtree can be transferred to a recipient via an email-approved migration flow. Supports user authentication via Auth0 JWT and Personal Access Tokens (PAT)."
  version: 1.5.0
  contact:
    name: NASEBANAL
    url: https://www.nasebanal.com
servers:
  - url: http://localhost:8790
    description: Local dev (wrangler dev)
  - url: https://api.target.nasebanal.com
    description: Production
security:
  - BearerAuth: []
paths:
  /api/v1/health:
    get:
      operationId: getHealth
      tags:
        - Health
      summary: Health check
      description: Returns the health status of the API.
      security: []
      responses:
        "200":
          description: API is healthy
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      status:
                        type: string
                        example: ok
                      timestamp:
                        type: string
                        format: date-time
  /api/v1/me:
    get:
      operationId: getCurrentUser
      tags:
        - User
      summary: Get current user profile
      description: Returns the authenticated user's profile information (resolved from the Account API).
      responses:
        "200":
          description: User profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/targets:
    get:
      operationId: listTargets
      tags:
        - Targets
      summary: List targets
      description: Returns all targets belonging to the authenticated user in the active namespace (personal, or the active organization), ordered by last updated. By default each target includes its storage cells so clients do not need to issue a follow-up GET /targets/{id} per row. Pass `fields=summary` to omit `cells` and reduce payload size for callers (e.g. a tree/chart view) that only need `progress` — fetch full detail on demand via GET /targets/{id}.
      parameters:
        - name: fields
          in: query
          required: false
          schema:
            type: string
            enum:
              - full
              - summary
            default: full
          description: "`full` (default) returns every target's complete `cells` array — today's exact behavior, unchanged for callers that omit this param. `summary` omits `cells`; `progress` is present in both modes."
      responses:
        "200":
          description: List of targets with cells (or, with fields=summary, without cells)
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    required:
                      - data
                      - total
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/TargetWithCells"
                      total:
                        type: integer
              examples:
                full:
                  summary: Default (fields omitted or fields=full)
                  value:
                    success: true
                    data:
                      data:
                        - id: tgt_a1b2c3d4
                          user_id: 42
                          created_by_user_id: 42
                          organization_id: null
                          parent_target_id: null
                          title: Learn Japanese
                          description: null
                          memo: null
                          recorder_tag: null
                          recorder_goal_id: null
                          progress: 40
                          own_progress: 40
                          created_at: 2026-05-14T12:00:00Z
                          updated_at: 2026-05-14T12:00:00Z
                          owner_email: user@example.com
                          owner_name: Sample User
                          created_by_email: user@example.com
                          created_by_name: Sample User
                          created_by_deleted: false
                          cells:
                            - id: 1
                              target_id: tgt_a1b2c3d4
                              position: 40
                              text: Learn Japanese
                              progress: 40
                              created_by_user_id: 42
                              created_at: 2026-05-14T12:00:00Z
                              updated_at: 2026-05-14T12:00:00Z
                      total: 1
                summary:
                  summary: fields=summary
                  value:
                    success: true
                    data:
                      data:
                        - id: tgt_a1b2c3d4
                          user_id: 42
                          created_by_user_id: 42
                          organization_id: null
                          parent_target_id: null
                          title: Learn Japanese
                          description: null
                          memo: null
                          recorder_tag: null
                          recorder_goal_id: null
                          progress: 40
                          own_progress: 40
                          created_at: 2026-05-14T12:00:00Z
                          updated_at: 2026-05-14T12:00:00Z
                          owner_email: user@example.com
                          owner_name: Sample User
                          created_by_email: user@example.com
                          created_by_name: Sample User
                          created_by_deleted: false
                      total: 1
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createTarget
      tags:
        - Targets
      summary: Create target
      description: Creates a new target. The storage cells are initialized with the target title pre-populated at position 40 (the slot reserved for the target's own label). Optionally nests the new target under an existing parent.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTargetInput"
      responses:
        "201":
          description: Target created with its storage cells
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TargetWithCellsResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/targets/{id}:
    get:
      operationId: getTarget
      tags:
        - Targets
      summary: Get target
      description: Returns a single target with its storage cells. Accessible by the owner, by a user the target is shared with, or by an admin of the organization that owns the target.
      parameters:
        - $ref: "#/components/parameters/TargetId"
      responses:
        "200":
          description: Target with cells
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TargetWithCellsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateTarget
      tags:
        - Targets
      summary: Update target
      description: Updates a target's title, description, memo, and/or its Recorder tag binding.
      parameters:
        - $ref: "#/components/parameters/TargetId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTargetInput"
      responses:
        "200":
          description: Target updated
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: "#/components/schemas/Target"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteTarget
      tags:
        - Targets
      summary: Delete target
      description: Deletes a target and all its cells.
      parameters:
        - $ref: "#/components/parameters/TargetId"
      responses:
        "200":
          description: Target deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessFlagResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/targets/{id}/cells/{position}:
    put:
      operationId: updateTargetCell
      tags:
        - Target Cells
      summary: Update target cell
      description: Updates the text and/or progress of a specific storage cell on a target.
      parameters:
        - $ref: "#/components/parameters/TargetId"
        - name: position
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
            maximum: 80
          description: Cell position in the storage layout (0-80; position 40 is reserved for the target title)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTargetCellInput"
      responses:
        "200":
          description: Cell updated
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: "#/components/schemas/TargetCell"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/targets/{id}/parent:
    put:
      operationId: setTargetParent
      tags:
        - Targets
      summary: Set target parent
      description: Authoritative tree-move endpoint (migration 0017+). Sets or clears `parent_target_id` on the target. Pass `null` to detach (move to root). The legacy sub-goal cells are updated best-effort to keep older clients in sync. Parent and child must share the same organization namespace and the same owner; cycles are rejected.
      parameters:
        - $ref: "#/components/parameters/TargetId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetTargetParentInput"
      responses:
        "200":
          description: Parent updated; returns the updated target
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: "#/components/schemas/Target"
        "400":
          description: Invalid move (self-parent, namespace mismatch, or cycle)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/shared-targets:
    get:
      operationId: listSharedTargets
      tags:
        - Sharing
      summary: List targets shared with me
      description: Returns targets that other users have shared with the authenticated user and accepted (not paused, not hidden), scoped to the active namespace via each share's `shared_with_org_id`. When `include_descendants` was set on the share, the whole subtree is expanded. Each row carries the sharing owner and the effective permission.
      responses:
        "200":
          description: Shared targets
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    required:
                      - data
                      - total
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/SharedTarget"
                      total:
                        type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/shares:
    get:
      operationId: listMyShares
      tags:
        - Sharing
      summary: List shares I created
      description: Returns shares the authenticated user owns (i.e. shares of their own targets), scoped to the active namespace. Each row is enriched with the recipient email, the owner's plan, and the owner-side target chain.
      responses:
        "200":
          description: Shares owned by the caller
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShareListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createShare
      tags:
        - Sharing
      summary: Create a share invitation
      description: "Shares a target the caller owns with another user by email. Sends an invitation email containing an accept link; the share is `pending` until accepted. Optionally includes the subtree (`include_descendants`) and the linked Recorder tag data (`include_data`, which cascades matching tag shares to the Recorder API). `share_context: organization` routes the share into the caller's active organization view and requires the recipient to be a member of that organization."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateShareInput"
      responses:
        "201":
          description: Share created (pending acceptance)
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: "#/components/schemas/Share"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Sharing not permitted (plan limit, or recipient not in organization)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Target not found, or recipient email not registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/shares/with-me:
    get:
      operationId: listSharesWithMe
      tags:
        - Sharing
      summary: List shares addressed to me
      description: Returns shares where the authenticated user is the recipient, scoped to the active namespace. Filter by status with the `status` query parameter. Each row is enriched with the owner email and the owner-side target chain.
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - accepted
              - pending
              - rejected
              - all
            default: accepted
          description: Which share statuses to return (default `accepted`).
      responses:
        "200":
          description: Shares addressed to the caller
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShareListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/shares/recipient-context-options:
    get:
      operationId: getRecipientContextOptions
      tags:
        - Sharing
      summary: Resolve sharing-context options for a recipient
      description: Returns which sharing contexts the caller may offer for a given recipient email. Always includes `personal`; additionally includes `organization` when the caller is acting under an organization context and the recipient is a member of that same organization.
      parameters:
        - name: email
          in: query
          required: true
          schema:
            type: string
            format: email
          description: Recipient email to evaluate.
      responses:
        "200":
          description: Available sharing contexts
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    required:
                      - options
                    properties:
                      options:
                        type: array
                        items:
                          type: string
                          enum:
                            - personal
                            - organization
                        example:
                          - personal
                          - organization
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/shares/accept:
    post:
      operationId: acceptShareByToken
      tags:
        - Sharing
      summary: Accept a share invitation by token
      description: Accepts a pending share invitation using the token from the invitation email.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TokenInput"
      responses:
        "200":
          description: Share accepted (or already accepted)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShareStatusResponse"
        "400":
          description: Invitation expired or already declined
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/shares/migrate/accept:
    post:
      operationId: acceptMigrationByToken
      tags:
        - Sharing
      summary: Accept an ownership transfer by token
      description: Accepts a pending ownership-transfer (data migration) using the token from the migration approval email. On success the target (and, if the share included descendants, its subtree) is transferred to the caller, and a reverse read-only share is created so the original owner keeps access.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TokenInput"
      responses:
        "200":
          description: Ownership transferred
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MigrationStatusResponse"
        "400":
          description: Migration invitation no longer active or expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/shares/{id}:
    delete:
      operationId: deleteShare
      tags:
        - Sharing
      summary: Delete a share
      description: Removes a share the caller owns. If the share was created with `include_data`, the matching Recorder tag shares are revoked unless `cascade_tag_shares=false` is passed. A share with a pending ownership transfer cannot be deleted until the migration is cancelled.
      parameters:
        - $ref: "#/components/parameters/ShareId"
        - name: cascade_tag_shares
          in: query
          required: false
          schema:
            type: string
            enum:
              - "true"
              - "false"
            default: "true"
          description: Set to `false` to keep the linked Recorder tag shares when deleting.
      responses:
        "200":
          description: Share deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessFlagResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/shares/{id}/accept:
    post:
      operationId: acceptShareById
      tags:
        - Sharing
      summary: Accept a share invitation (in-app)
      description: In-app counterpart to POST /shares/accept — accepts a pending share addressed to the caller by id.
      parameters:
        - $ref: "#/components/parameters/ShareId"
      responses:
        "200":
          description: Share accepted (or already accepted)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShareStatusResponse"
        "400":
          description: Invitation expired or already declined
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/shares/{id}/reject:
    post:
      operationId: rejectShare
      tags:
        - Sharing
      summary: Reject a share invitation
      description: Declines a pending share addressed to the caller. An already-accepted share cannot be rejected (delete it instead).
      parameters:
        - $ref: "#/components/parameters/ShareId"
      responses:
        "200":
          description: Share rejected (or already rejected)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShareStatusResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/shares/{id}/pause:
    patch:
      operationId: toggleSharePaused
      tags:
        - Sharing
      summary: Pause or resume a share
      description: Owner-only. Pauses or resumes a share without deleting it; a paused share is hidden from the recipient's shared-targets list.
      parameters:
        - $ref: "#/components/parameters/ShareId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - is_paused
              properties:
                is_paused:
                  type: boolean
      responses:
        "200":
          description: Updated share row
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: "#/components/schemas/Share"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/shares/{id}/visibility:
    patch:
      operationId: toggleShareVisibility
      tags:
        - Sharing
      summary: Hide or unhide a share
      description: Recipient-only. Hides or unhides a share from the caller's own shared-targets list.
      parameters:
        - $ref: "#/components/parameters/ShareId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - is_hidden
              properties:
                is_hidden:
                  type: boolean
      responses:
        "200":
          description: Updated share row
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: "#/components/schemas/Share"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/shares/{id}/transfer:
    post:
      operationId: transferOwnership
      tags:
        - Sharing
      summary: Start an ownership transfer
      description: Owner-only. Begins transferring ownership of the shared target (subtree if the share included descendants) to the recipient by emailing them an approval link. The share must already be accepted. Organization-owned targets cannot be transferred. The transfer only completes when the recipient accepts.
      parameters:
        - $ref: "#/components/parameters/ShareId"
      responses:
        "200":
          description: Migration invitation issued
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    required:
                      - id
                      - migration_status
                    properties:
                      id:
                        type: integer
                      migration_status:
                        type: string
                        enum:
                          - pending
                      expires_at:
                        type: string
                        format: date-time
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/shares/{id}/migrate/accept:
    post:
      operationId: acceptMigrationById
      tags:
        - Sharing
      summary: Accept an ownership transfer (in-app)
      description: In-app counterpart to POST /shares/migrate/accept — accepts a pending ownership transfer addressed to the caller by id.
      parameters:
        - $ref: "#/components/parameters/ShareId"
      responses:
        "200":
          description: Ownership transferred
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MigrationStatusResponse"
        "400":
          description: Migration invitation no longer active or expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/shares/{id}/migrate/reject:
    post:
      operationId: rejectMigration
      tags:
        - Sharing
      summary: Reject an ownership transfer
      description: Recipient-only. Declines a pending ownership transfer; the underlying share keeps its existing permission.
      parameters:
        - $ref: "#/components/parameters/ShareId"
      responses:
        "200":
          description: Migration rejected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MigrationStatusResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/shares/{id}/migrate/cancel:
    post:
      operationId: cancelMigration
      tags:
        - Sharing
      summary: Cancel an ownership transfer
      description: Owner-only. Cancels an outstanding ownership-transfer invitation before the recipient acts.
      parameters:
        - $ref: "#/components/parameters/ShareId"
      responses:
        "200":
          description: Migration cancelled
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    required:
                      - id
                      - migration_status
                    properties:
                      id:
                        type: integer
                      migration_status:
                        type: string
                        nullable: true
                        enum:
                          - null
                        description: Always null after a cancel — the in-flight migration is cleared.
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT or PAT
      description: Use either an Auth0 JWT token or a Personal Access Token (PAT) starting with `nbpat_`.
  parameters:
    TargetId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Target ID
    ShareId:
      name: id
      in: path
      required: true
      schema:
        type: integer
      description: Share ID
  schemas:
    User:
      type: object
      required:
        - id
        - auth0_sub
        - email
        - name
        - language
        - plan
      properties:
        id:
          type: integer
        auth0_sub:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
        language:
          type: string
          enum:
            - ja
            - en
        plan:
          type: string
          enum:
            - free
            - pro
            - family
            - enterprise
        subscription_status:
          type: string
          enum:
            - active
            - canceled
            - past_due
            - trialing
            - null
          nullable: true
        subscription_expires_at:
          type: string
          format: date-time
          nullable: true
    UserResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          $ref: "#/components/schemas/User"
    Target:
      type: object
      properties:
        id:
          type: string
          description: Unique target ID
        user_id:
          type: integer
          description: Owner user id
        created_by_user_id:
          type: integer
          nullable: true
          description: User who created the target. Distinct from ownership; preserved across ownership transfer.
        organization_id:
          type: integer
          nullable: true
          description: Organization namespace. NULL = personal data. Non-null = created under an Enterprise org context; the org admin can read it and the member-creator owns it day-to-day but cannot transfer or delete it.
        parent_target_id:
          type: string
          nullable: true
          description: Direct parent reference (migration 0017+). Authoritative source of tree structure. NULL = root target.
        title:
          type: string
          description: Goal title
          maxLength: 200
        description:
          type: string
          nullable: true
        memo:
          type: string
          nullable: true
        recorder_tag:
          type: string
          nullable: true
          description: Name of the linked Recorder tag, or a derived series key such as `Body Weight/BMI~` (a name, not a tag row, so it has no id). Kept alongside `recorder_tag_id` for derived keys and for clients that predate it.
        recorder_tag_id:
          type: integer
          nullable: true
          description: Id of the linked Recorder tag, for a real tag (never for a derived series key). Unlike the name it stays valid when the tag is renamed or its ownership is transferred, and it tells apart same-named tags of different owners. NULL for a target linked only by name; clients prefer the id when it is set and fall back to `recorder_tag`.
        recorder_goal_id:
          type: integer
          nullable: true
          description: Recorder Goal id pinned at the moment the tag was bound. Progress for this target is computed against this specific goal so that later Goal period changes in Recorder do not silently re-route which goal is being tracked.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        owner_email:
          type: string
          format: email
          description: Resolved owner email (present in response payloads).
        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.
        created_by_email:
          type: string
          format: email
          description: Resolved creator email (present in response payloads). Absent when `created_by_deleted` is true — there is no creator left to name, and the owner must NOT be substituted.
        created_by_name:
          type: string
          description: Display name the creator set in their profile. Absent when they set none, or when `created_by_deleted` is true.
        created_by_deleted:
          type: boolean
          description: "True when the author deleted their NASEBANAL account. The row itself survives because it belongs to this target's owner; only the authorship is gone. Clients render a \"deleted user\" label (JA: 退会済ユーザー) rather than falling back to `owner_email`, which would show the owner as the author of an edit they never made."
    TargetCell:
      type: object
      properties:
        id:
          type: integer
        target_id:
          type: string
        position:
          type: integer
          minimum: 0
          maximum: 80
          description: Cell position in the storage layout (0-80; position 40 is reserved for the target title)
        text:
          type: string
          maxLength: 500
          description: Cell content
        progress:
          type: integer
          minimum: 0
          maximum: 100
          description: Progress percentage
        created_by_user_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TargetWithCells:
      allOf:
        - $ref: "#/components/schemas/Target"
        - type: object
          required:
            - progress
            - own_progress
          properties:
            cells:
              type: array
              items:
                $ref: "#/components/schemas/TargetCell"
              description: All storage cells ordered by position. Present when the list was requested without `fields`, or with `fields=full` (the default); omitted when requested with `fields=summary` — see `progress` and `own_progress` instead.
            progress:
              type: integer
              minimum: 0
              maximum: 100
              description: "A target's own roll-up progress, computed server-side: the average of its children's (recursively rolled-up) progress when it has sub-goals, or its own storage cell (position 40, `own_progress`) when it's a leaf. Always present, regardless of `fields`."
            own_progress:
              type: integer
              minimum: 0
              maximum: 100
              description: "The target's own storage cell (position 40) value, unrolled — what the \"clear\" toggle sets directly to 0 or 100, independent of any sub-goals. Distinct from `progress`: a target with sub-goals can have `progress = 100` (every child rolled up to 100) while `own_progress` stays whatever it was last explicitly set to, since a parent's own cell is never averaged into its own `progress`. Always present, regardless of `fields` — use this, not `progress`, to tell whether a target itself has been marked cleared."
    TargetWithCellsResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          $ref: "#/components/schemas/TargetWithCells"
    SharedTarget:
      allOf:
        - $ref: "#/components/schemas/TargetWithCells"
        - type: object
          properties:
            shared_by_user_id:
              type: integer
              description: User id of the sharing owner
            shared_by_email:
              type: string
              format: email
            shared_by_name:
              type: string
              description: Display name the sharing owner set in their profile. Absent when they set none.
            share_permission:
              type: string
              enum:
                - read
                - write
            share_root_target_id:
              type: string
              description: The root target id of the share that grants access to this row
            share_show_embed:
              type: integer
              enum:
                - 0
                - 1
              description: Whether the share permits embedding the linked Recorder graph
    Share:
      type: object
      properties:
        id:
          type: integer
        owner_user_id:
          type: integer
        shared_with_user_id:
          type: integer
        target_id:
          type: string
        permission:
          type: string
          enum:
            - read
            - write
        status:
          type: string
          enum:
            - pending
            - accepted
            - rejected
        expires_at:
          type: string
          format: date-time
          nullable: true
        migration_status:
          type: string
          enum:
            - pending
            - null
          nullable: true
          description: Ownership-transfer state. NULL = no migration in flight.
        migration_expires_at:
          type: string
          format: date-time
          nullable: true
        is_hidden:
          type: integer
          enum:
            - 0
            - 1
        is_paused:
          type: integer
          enum:
            - 0
            - 1
        show_embed:
          type: integer
          enum:
            - 0
            - 1
        include_descendants:
          type: integer
          enum:
            - 0
            - 1
        include_data:
          type: integer
          enum:
            - 0
            - 1
        parent_share_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        target_title:
          type: string
          description: Enriched — title of the shared target.
        target_chain:
          type: string
          description: Enriched — owner-side "Parent > Child" breadcrumb for the target.
        owner_email:
          type: string
          format: email
          description: Enriched — present when listing 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
          format: email
          description: Enriched — present when listing 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.
        owner_plan:
          type: string
          description: Enriched — the owner's plan, present when listing shares the caller owns.
    ShareListResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          type: object
          required:
            - data
            - total
            - limit
            - offset
          properties:
            data:
              type: array
              items:
                $ref: "#/components/schemas/Share"
            total:
              type: integer
            limit:
              type: integer
            offset:
              type: integer
    ShareStatusResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          type: object
          required:
            - id
            - status
          properties:
            id:
              type: integer
            status:
              type: string
              enum:
                - pending
                - accepted
                - rejected
            already:
              type: boolean
              description: True when the share was already in the requested state.
    MigrationStatusResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          type: object
          required:
            - id
            - migration_status
          properties:
            id:
              type: integer
            migration_status:
              type: string
              enum:
                - accepted
                - rejected
    SuccessFlagResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          type: object
          properties:
            success:
              type: boolean
              enum:
                - true
    CreateTargetInput:
      type: object
      required:
        - title
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
          example: Become a professional athlete
        description:
          type: string
        memo:
          type: string
        parent_id:
          type: string
          description: Optional parent target id to nest the new target under at creation time.
    UpdateTargetInput:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          nullable: true
        memo:
          type: string
          nullable: true
        recorder_tag:
          type: string
          nullable: true
          description: Tag to link with Recorder app, by name. Send the name together with `recorder_tag_id` for a real tag; for a derived series key (`Body Weight/BMI~`) this is the only field.
        recorder_tag_id:
          type: integer
          nullable: true
          description: Id of the Recorder tag to link, for a real tag. Send it together with `recorder_tag`; send `null` for both to unlink.
        recorder_goal_id:
          type: integer
          nullable: true
          description: Pin a specific Recorder Goal to this target. Typically resolved by the client at bind time from the active goal for the selected tag.
    UpdateTargetCellInput:
      type: object
      properties:
        text:
          type: string
          maxLength: 500
          description: Cell content
        progress:
          type: integer
          minimum: 0
          maximum: 100
          description: Progress percentage
    SetTargetParentInput:
      type: object
      properties:
        parent_target_id:
          type: string
          nullable: true
          description: New parent target id, or null to detach to root.
    CreateShareInput:
      type: object
      required:
        - shared_with_email
        - target_id
      properties:
        shared_with_email:
          type: string
          format: email
          description: Email of the user to share with (must be a registered user).
        target_id:
          type: string
        permission:
          type: string
          enum:
            - read
            - write
          default: read
        include_descendants:
          type: boolean
          default: false
          description: Share the whole subtree rooted at the target, not just the target itself.
        include_data:
          type: boolean
          default: false
          description: Also cascade-share the linked Recorder tag data so the recipient can view embedded graphs.
        share_context:
          type: string
          enum:
            - personal
            - organization
          default: personal
          description: "`organization` routes the share into the caller's active organization view; requires the recipient to be a member of that organization."
        extra_tag_names:
          type: array
          items:
            type: string
          description: Additional real Recorder tag names to include when cascading data shares.
    TokenInput:
      type: object
      required:
        - token
      properties:
        token:
          type: string
          description: The invitation token from the email link.
    Error:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: object
              additionalProperties: true
  responses:
    Unauthorized:
      description: Authentication required or invalid token
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            error:
              code: AUTH_ERROR
              message: No authentication token provided
    Forbidden:
      description: The caller is not allowed to perform this action
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            error:
              code: FORBIDDEN
              message: Cannot transfer ownership of organization-owned target. Contact the organization admin.
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            error:
              code: NOT_FOUND
              message: Target not found
    Conflict:
      description: The request conflicts with the current state of the resource
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            error:
              code: CONFLICT
              message: "Share already exists (status: pending)"
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            error:
              code: VALIDATION_ERROR
              message: Title is required
tags:
  - name: Health
    description: Health check endpoints
  - name: User
    description: User profile endpoints
  - name: Targets
    description: Target management
  - name: Target Cells
    description: Individual storage cell management on a target
  - name: Sharing
    description: Sharing targets with other users and transferring ownership
