メインコンテンツへ移動

APIドキュメント

OpenAPI 3.1.0仕様に基づくREST APIドキュメント

API概要

PowerPoint翻訳ツールのAPIは、PPTXファイルのアップロード、テキスト抽出、翻訳、ダウンロードなどの機能を提供します。

認証

APIはSupabase JWTトークンを使用したBearer認証をサポートしています。リクエストヘッダーに Authorization: Bearer <token> を含めてください。

ベースURL

現在のベースURL: https://trans-ppt.work

主要エンドポイント

ファイルアップロード(Server Action)

PPTXファイルのアップロードはServer Action経由で行います。最大25MBまで対応しています。API Routeとしての直接呼び出しはできません。

extractTextFromPPTXAction (Server Action)

アップロードされたPPTXファイルからテキスト要素を抽出します。Server Action経由で呼び出します。

// Server Action (extractTextFromPPTXAction)
// クライアントコンポーネントから直接呼び出し可能
import { extractTextFromPPTXAction } from "@/app/actions/pptx";
const result = await extractTextFromPPTXAction(fileId, filePath);

POST /api/translate

指定されたテキストを目標言語に翻訳します。バッチ翻訳にも対応しています。

curl -X POST https://trans-ppt.work/api/translate/stream \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "texts": [{"id": "1", "text": "Hello"}],
    "targetLanguage": "ja"
  }'

DELETE /api/files/{id}

指定されたIDのファイルを削除します。

curl -X DELETE https://trans-ppt.work/api/files/{id} \
  -H "Authorization: Bearer <token>"

GET /api/health

システムの健全性を確認します。認証は不要です。

curl https://trans-ppt.work/api/health

OpenAPI仕様書

完全なOpenAPI仕様書は以下のリンクからダウンロードまたは表示できます。

openapi: 3.1.0
info:
  title: PPT Translation API
  version: 1.0.0
  description: |
    PowerPoint プレゼンテーション翻訳サービスのAPI仕様書
    
    このAPIは、PPTXファイルのアップロード、テキスト抽出、翻訳、ダウンロードなどの機能を提供します。
  contact:
    name: API Support
  license:
    name: MIT
    identifier: MIT

servers:
  - url: "{baseUrl}"
    description: |
      本番環境: NEXT_PUBLIC_APP_URL で決定される。
      デプロイ前に実際のURLへ差し替えること。
    variables:
      baseUrl:
        default: http://localhost:3000
        description: アプリケーションのベースURL(環境変数 NEXT_PUBLIC_APP_URL)
  - url: http://localhost:3000
    description: 開発環境

tags:
  - name: Files
    description: ファイル操作関連のエンドポイント
  - name: Translation
    description: 翻訳関連のエンドポイント
  - name: Preview
    description: ハイブリッドプレビュー関連のエンドポイント(PDF/サムネイル生成)
  - name: Auth
    description: 認証関連のエンドポイント
  - name: Stripe
    description: Stripe決済・サブスクリプション関連のエンドポイント
  - name: Health
    description: ヘルスチェック関連のエンドポイント
  - name: Admin
    description: 管理者専用エンドポイント(withAdminGetHandler / withAdminPostHandler 経由)

paths:
  # NOTE: ファイルアップロード・一覧取得はServer Action(src/app/actions/)で実装。
  # GET /api/files(一覧)は Issue #403 完了に伴い削除済み。

  /api/files/{id}:
    delete:
      summary: ファイルを削除
      description: 指定されたIDのファイルを削除します。
      operationId: deleteFile
      x-audience: public
      x-auth: cookie
      tags:
        - Files
      security:
        - CookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: ファイルID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: 削除成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/translate/jobs:
    post:
      summary: durable翻訳ジョブを作成
      operationId: createTranslationJob
      x-audience: public
      x-auth: cookie
      tags: [Translation]
      security: [{ CookieAuth: [] }]
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string, minLength: 1, maxLength: 255 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TranslationJobCreateRequest' }
      responses:
        '202':
          description: queued job
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TranslationJobResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /api/translate/jobs/{jobId}:
    get:
      summary: durable翻訳ジョブ状態を取得
      operationId: getTranslationJob
      x-audience: public
      x-auth: cookie
      tags: [Translation]
      security: [{ CookieAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/TranslationJobId'
      responses:
        '200':
          description: owner-scoped job status
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TranslationJobResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /api/translate/jobs/{jobId}/cancel:
    post:
      summary: durable翻訳ジョブをキャンセル
      operationId: cancelTranslationJob
      x-audience: public
      x-auth: cookie
      tags: [Translation]
      security: [{ CookieAuth: [] }]
      parameters: [{ $ref: '#/components/parameters/TranslationJobId' }]
      responses:
        '200':
          description: cancelled job
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TranslationJobResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/TooManyRequests' }

  /api/translate/jobs/{jobId}/retry:
    post:
      summary: durable翻訳ジョブを同じjobで再試行
      operationId: retryTranslationJob
      x-audience: public
      x-auth: cookie
      tags: [Translation]
      security: [{ CookieAuth: [] }]
      parameters: [{ $ref: '#/components/parameters/TranslationJobId' }]
      responses:
        '200':
          description: requeued job
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TranslationJobResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/TooManyRequests' }

  /api/translate/jobs/{jobId}/terminal-notice/claim:
    post:
      summary: terminal toast表示権を原子的にclaim
      operationId: claimTranslationJobTerminalNotice
      x-audience: public
      x-auth: cookie
      tags: [Translation]
      security: [{ CookieAuth: [] }]
      parameters: [{ $ref: '#/components/parameters/TranslationJobId' }]
      responses:
        '200':
          description: claim result
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TranslationJobTerminalNoticeClaim' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }

  /api/translate/single:
    post:
      summary: 単一テキストを翻訳
      description: |
        単一テキストを目標言語に翻訳します。
        優先度キュー対応、DBキャッシュ統合、AbortSignal対応。
      operationId: translateSingle
      x-audience: public
      x-auth: cookie
      tags:
        - Translation
      security:
        - CookieAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: 同一リクエストの再送を識別する冪等性キー
          schema:
            type: string
            minLength: 1
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SingleTranslationRequest'
      responses:
        '200':
          description: 翻訳成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'
        '400':
          description: 入力エラー
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'
        '401':
          description: 認証エラー
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'
        '403':
          description: 翻訳機能または権限が利用できない
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'
        '404':
          description: 対象ファイルが見つからない
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'
        '409':
          description: 同一の冪等性キーで翻訳処理中
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'
        '422':
          description: 冪等性キーのリクエスト不一致
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'
        '429':
          description: レート制限を超過
          headers:
            Retry-After:
              description: 再試行可能になるまでの秒数
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'
        '499':
          description: クライアントによる翻訳キャンセル(サーバー側観測用。クライアントabort後は通常このレスポンスを受信しない)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'
        '500':
          description: 翻訳エラー
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'
        '503':
          description: 翻訳機能停止中
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SingleTranslationResponse'

  /api/translate/stream:
    post:
      summary: ストリーミング翻訳(SSE)
      description: |
        Server-Sent Events(SSE)を使用してリアルタイムに翻訳結果をストリーミングします。
        同時接続数はユーザーあたり最大3本に制限されます。
      operationId: translateStream
      x-audience: public
      x-auth: cookie
      tags:
        - Translation
      security:
        - CookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TranslationRequest'
      responses:
        '200':
          description: SSEストリーミング翻訳レスポンス
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  Server-Sent Eventsストリーム。
                  各イベントはJSON形式で翻訳の進捗・結果を含む。
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/health:
    get:
      summary: ヘルスチェック
      description: システムの健全性を確認します。
      operationId: healthCheck
      x-audience: internal
      x-auth: none
      tags:
        - Health
      security: []
      responses:
        '200':
          description: システムは正常に動作しています
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: サービスが利用できません
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/stripe/webhook:
    post:
      summary: Stripe Webhook受信
      description: |
        Stripeからのイベント通知を受信し、処理します。
        署名検証を必須とし、べき等性を保証します。
      operationId: handleStripeWebhook
      x-audience: internal
      x-auth: signature
      tags:
        - Stripe
      security: []  # Webhookは署名ヘッダーで検証するためBearerトークン不要
      parameters:
        - in: header
          name: stripe-signature
          required: true
          schema:
            type: string
          description: Stripe署名ヘッダー
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Stripeイベントペイロード
      responses:
        '200':
          description: Webhook処理成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
                    example: true
        '400':
          description: 無効な署名または不正なペイロード
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # NOTE: Customer Portal Session作成はServer Action(src/app/actions/payment.ts)で実装。
  # API Routeとしては存在しない。

  # ============================================================================
  # Preview API エンドポイント(ハイブリッドプレビュー機能)
  # ============================================================================

  /api/preview/start:
    post:
      summary: プレビュージョブ開始
      description: |
        PPTXファイルのプレビュージョブを開始します。
        Python service が所有者確認済みの Storage bytes と実際の converter identity を使ってジョブを解決し、
        新規作成時は同じ Python request 内で PDF を生成します。

        同一 file、PPTX bytes の SHA-256、converter identity の active job は再利用されます。
        同一 identity の completed と completed_with_errors はどちらも再利用されます。
        completed_with_errors の再利用は既存の失敗ページと retry count を維持します。
        異なる identity の active job がある場合は 409 です。
        Python が実際の converter identity を確認できない場合は、job を作成せず 503 です。
      operationId: startPreviewJob
      x-audience: public
      x-auth: cookie
      tags:
        - Preview
      security:
        - CookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewStartRequest'
      responses:
        '200':
          description: ジョブ開始成功(または既存ジョブを返却)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewStartResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: 別 identity のプレビュージョブが既に実行中
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Python converter identity または依存サービスを利用できない
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/preview/status:
    get:
      summary: プレビュージョブステータス取得
      description: |
        プレビュージョブの状態とページ一覧を返します。
        ポーリングで進捗確認する用途で使用します。
      operationId: getPreviewStatus
      x-audience: public
      x-auth: cookie
      tags:
        - Preview
      security:
        - CookieAuth: []
      parameters:
        - name: jobId
          in: query
          required: true
          description: プレビュージョブID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: ステータス取得成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewStatusResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/preview/pdf-url:
    get:
      summary: PDF署名URL取得
      description: |
        PDFの一時的な閲覧URLを生成して返します。
        URLは1時間有効です。
      operationId: getPreviewPdfUrl
      x-audience: public
      x-auth: cookie
      tags:
        - Preview
      security:
        - CookieAuth: []
      parameters:
        - name: jobId
          in: query
          required: true
          description: プレビュージョブID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: URL取得成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewSignedUrlResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: ジョブが見つからない、またはPDFが未生成
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/preview/thumb-url:
    get:
      summary: サムネイル署名URL取得
      description: |
        指定ページのサムネイル一時閲覧URLを生成して返します。
        URLは1時間有効です。
      operationId: getPreviewThumbUrl
      x-audience: public
      x-auth: cookie
      tags:
        - Preview
      security:
        - CookieAuth: []
      parameters:
        - name: jobId
          in: query
          required: true
          description: プレビュージョブID
          schema:
            type: string
            format: uuid
        - name: page
          in: query
          required: true
          description: ページインデックス(0始まり)
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: URL取得成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewSignedUrlResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: ジョブが見つからない、またはサムネイルが未生成
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/preview/thumb-urls:
    get:
      summary: サムネイル署名URL一括取得
      description: |
        指定ジョブの done ページのサムネイル署名URLをバッチで返す。
        個別 /api/preview/thumb-url の N+1 問題を解消する。
        URL は1時間有効。
      operationId: getPreviewThumbUrls
      x-audience: public
      x-auth: cookie
      tags:
        - Preview
      security:
        - CookieAuth: []
      parameters:
        - name: jobId
          in: query
          required: true
          description: プレビュージョブID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: 一括URL取得成功(page_index → signed URL のマップ)
          content:
            application/json:
              schema:
                type: object
                required:
                  - urls
                properties:
                  urls:
                    type: object
                    additionalProperties:
                      type:
                        - string
                        - 'null'
                      description: 署名済み URL(thumb 未生成の場合は null)
                    description: page_index をキー、署名URLを値とするマップ
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/preview/thumbs-start:
    post:
      summary: サムネイル生成開始
      description: |
        Cloud Runにサムネイル生成を依頼します。
        リースロックにより多重起動は防止されます。
      operationId: startPreviewThumbs
      x-audience: public
      x-auth: cookie
      tags:
        - Preview
      security:
        - CookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewThumbsStartRequest'
      responses:
        '200':
          description: サムネイル生成開始成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewThumbsStartResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: サービス未設定(PYTHON_SERVICE_URLが未設定)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/preview/thumbs-retry:
    post:
      summary: 失敗ページ一括再生成
      description: |
        failed状態の全ページを再生成します。
      operationId: retryPreviewThumbs
      x-audience: public
      x-auth: cookie
      tags:
        - Preview
      security:
        - CookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewThumbsRetryRequest'
      responses:
        '200':
          description: 再生成開始成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewThumbsRetryResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: サービス未設定
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/preview/thumbs-retry-page:
    post:
      summary: 単一ページ再生成
      description: |
        指定ページのサムネイルを再生成します。
      operationId: retryPreviewThumbPage
      x-audience: public
      x-auth: cookie
      tags:
        - Preview
      security:
        - CookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewThumbsRetryPageRequest'
      responses:
        '200':
          description: 再生成成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewThumbsRetryPageResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: サービス未設定
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ============================================================================
  # Auth エンドポイント
  # ============================================================================

  /api/auth/callback:
    get:
      summary: OAuth/Magic Linkコールバック
      description: |
        Supabase Auth の PKCE フロー完了後にリダイレクトされるコールバック。
        認証コードをセッションに交換し、next パラメータへリダイレクトする。
      operationId: authCallback
      x-audience: public
      x-auth: none
      tags:
        - Auth
      security: []
      parameters:
        - name: code
          in: query
          description: Supabase Auth が発行する認証コード
          schema:
            type: string
        - name: next
          in: query
          description: 認証後のリダイレクト先パス(sanitizeNextPath で検証)
          schema:
            type: string
            default: /dashboard
      responses:
        '307':
          description: |
            認証成功時は next へリダイレクト。
            失敗時(コード無効・未提供)は /login?error=auth_callback_error へリダイレクト。
            Rate Limit超過時は /login?error=rate_limited へリダイレクト。

  /api/auth/csrf:
    get:
      summary: CSRFトークン取得
      description: |
        フォーム送信前にCSRFトークンを取得するエンドポイント。
        Rate Limit 適用。キャッシュ無効化ヘッダー付き。
      operationId: getCsrfToken
      x-audience: public
      x-auth: none
      tags:
        - Auth
      security: []
      responses:
        '200':
          description: トークン取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    required:
                      - data
                    properties:
                      data:
                        type: object
                        required:
                          - csrfToken
                        properties:
                          csrfToken:
                            type: string
                            description: CSRFトークン
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/auth/session:
    get:
      summary: セッション確認
      description: |
        現在のユーザーセッションを検証し、ユーザー情報を返す。
        Origin 検証 + Rate Limit 適用。
      operationId: getSession
      x-audience: public
      x-auth: cookie
      tags:
        - Auth
      security:
        - CookieAuth: []
      responses:
        '200':
          description: セッション有効
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    required:
                      - user
                      - session
                    properties:
                      user:
                        type: object
                        required:
                          - id
                        additionalProperties: false
                        properties:
                          id:
                            type: string
                            format: uuid
                          email:
                            type: string
                            format: email
                      session:
                        type: object
                        required:
                          - user
                        properties:
                          user:
                            type: object
                            required:
                              - id
                            additionalProperties: false
                            properties:
                              id:
                                type: string
                                format: uuid
                              email:
                                type: string
                                format: email
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/auth/logout:
    post:
      summary: ログアウト
      description: |
        Supabase Auth からサインアウトし、Clear-Site-Data ヘッダーで
        Cookie/Storage を一括クリアする。Origin 検証 + Rate Limit 適用。
      operationId: logout
      x-audience: public
      x-auth: cookie
      tags:
        - Auth
      security:
        - CookieAuth: []
      responses:
        '200':
          description: ログアウト成功
          headers:
            Clear-Site-Data:
              description: Cookie/Storage クリア指示
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                      redirectTo:
                        type: string
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/auth/is-admin:
    get:
      summary: 管理者判定
      description: |
        現在のユーザーが管理者かどうかを profiles テーブルの role で判定する。
        未認証の場合は isAdmin: false を返す(エラーではない)。
      operationId: isAdmin
      x-audience: public
      x-auth: cookie
      tags:
        - Auth
      security:
        - CookieAuth: []
      responses:
        '200':
          description: 判定結果
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    required:
                      - isAdmin
                    properties:
                      isAdmin:
                        type: boolean
                      reason:
                        type: string
                        enum: [profiles, none, unauthenticated, error]
                      hasProfile:
                        type: boolean
                      email:
                        type: string
                        description: マスク済みメールアドレス
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ============================================================================
  # Files 追加エンドポイント
  # ============================================================================

  /api/files/{id}/download:
    get:
      summary: ファイルダウンロード
      description: |
        元ファイルまたは翻訳済みファイルをダウンロードする。
        ファイル名を指定した短期署名URLを生成し、Storageへ一時リダイレクトする。
        アプリケーションサーバーはファイル本体をバッファリングしない。
        FILE_DOWNLOAD監査イベントはStorage取得完了ではなく署名URL発行時に記録する。
        Rate Limit 適用(ダウンロード専用プリセット)。
      operationId: downloadFile
      x-audience: public
      x-auth: cookie
      tags:
        - Files
      security:
        - CookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: ファイルID
          schema:
            type: string
            format: uuid
        - name: type
          in: query
          description: ダウンロード種別
          schema:
            type: string
            enum: [original, translated]
            default: original
      responses:
        '302':
          description: Supabase Storageの短期署名URLへリダイレクト
          headers:
            Location:
              required: true
              description: downloadファイル名を含む短期署名URL
              schema:
                type: string
                format: uri
            X-Request-Id:
              description: リクエスト追跡ID
              schema:
                type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ============================================================================
  # Usage エンドポイント
  # ============================================================================

  /api/usage/check:
    get:
      summary: 使用量チェック
      description: |
        ユーザーの月間使用量と残り翻訳可能ファイル数を返す。
        Origin 検証適用。
      operationId: checkUsage
      x-audience: public
      x-auth: cookie
      tags:
        - Files
      security:
        - CookieAuth: []
      responses:
        '200':
          description: 使用量情報
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageCheckResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ============================================================================
  # Error Report エンドポイント
  # ============================================================================

  /api/error-report:
    post:
      summary: クライアントエラーレポート
      description: |
        クライアント側で発生したエラーをサーバーに報告する。
        error-boundary-wrapper.tsx から自動送信される。
        CSRF + Origin 検証 + Rate Limit 適用。匿名ユーザーも受理可能。
      operationId: reportError
      x-audience: public
      x-auth: none
      tags:
        - Health
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ErrorReportRequest'
      responses:
        '200':
          description: レポート受信成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  requestId:
                    type: string
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ============================================================================
  # Internal Health エンドポイント
  # ============================================================================

  # ============================================================================
  # Admin API エンドポイント(withAdmin*Handler 経由・admin role 必須)
  # ============================================================================
  # NOTE: /api/admin/* は順次 OpenAPI に追記中(#1100 / #1105 follow-up)。
  # 本セクションは Phase A: GET 系の登録から開始。

  /api/admin/overview:
    get:
      summary: 管理者ダッシュボード概要
      description: |
        管理者向けダッシュボードに表示する集計データを返す。
        ユーザー集計・ファイル集計・直近のアクティビティログ・当月の利用統計を一括取得する。
        Service role client を使用するため、`withAdminGetHandler` で admin role を必須とする。
      operationId: getAdminOverview
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: 集計データ取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/AdminOverviewResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/analytics:
    get:
      summary: 翻訳・利用分析データ
      description: |
        翻訳パイプラインの分析データを返す。
        クエリパラメータで取得対象を切り替える: `summary` / `failure` / `performance` / `language` / `retry` / `timeseries` / `translation`。
        SSOT: `src/lib/analytics/queries.ts` および `src/lib/admin/translation-analytics.ts`。
        レスポンス shape は type ごとに異なるため、本仕様では汎用 object として表現する。
      operationId: getAdminAnalytics
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      parameters:
        - in: query
          name: type
          schema:
            type: string
            enum: [summary, failure, performance, language, retry, timeseries, translation]
            default: summary
        - in: query
          name: startDate
          schema:
            type: string
            format: date-time
          description: ISO 8601 形式(snake_case の `start_date` も受理)
        - in: query
          name: endDate
          schema:
            type: string
            format: date-time
          description: ISO 8601 形式(snake_case の `end_date` も受理)
        - in: query
          name: granularity
          schema:
            type: string
            enum: [hour, day, week]
          description: timeseries 取得時のみ有効
      responses:
        '200':
          description: 分析データ取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
                        description: type パラメータごとに異なる集計結果
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/health:
    get:
      summary: 管理者用ヘルスメトリクス
      description: |
        エラー率・応答時間・API 成功率などのシステムヘルスメトリクスを返す。
        SSOT: `src/lib/admin/health-monitoring.ts`。
        `/api/health` (public uptime monitor) や `/api/internal/health` (deep check) とは別系統。
      operationId: getAdminHealthMetrics
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      parameters:
        - in: query
          name: start_date
          schema:
            type: string
            format: date-time
        - in: query
          name: end_date
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: ヘルスメトリクス取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/operations:
    get:
      summary: オペレーションメトリクス
      description: |
        失敗率・p50/p95 レイテンシ・リトライ分布などのオペレーションメトリクスを返す。
        SSOT: `src/lib/analytics/operations-metrics.ts`。
      operationId: getAdminOperationsMetrics
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      parameters:
        - in: query
          name: days
          schema:
            type: integer
            default: 30
            minimum: 1
          description: 集計対象の過去日数
      responses:
        '200':
          description: オペレーションメトリクス取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/quality:
    get:
      summary: 翻訳品質メトリクス
      description: |
        ダウンロード率・再翻訳率・編集率などの品質指標を返す。
        SSOT: `src/lib/analytics/quality-metrics.ts`。
      operationId: getAdminQualityMetrics
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      parameters:
        - in: query
          name: days
          schema:
            type: integer
            minimum: 1
            maximum: 365
            default: 30
          description: 集計対象とする過去日数
      responses:
        '200':
          description: 品質メトリクス取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/QualityMetrics'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/revenue:
    get:
      summary: 収益メトリクス
      description: |
        MRR / ARR / 解約率 / 支払い失敗 / 返金などの収益指標を返す。
        Stripe データと内部 subscription state を集計。
      operationId: getAdminRevenueMetrics
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: 収益メトリクス取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/costs:
    get:
      summary: コスト監視メトリクス
      description: |
        Claude API 使用量・Supabase Storage 使用量を集計する。
        SSOT: `src/lib/admin/cost-monitoring.ts`。
      operationId: getAdminCostMetrics
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: コストメトリクス取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/abuse:
    get:
      summary: 不正利用検知
      description: |
        異常利用パターン(短時間大量アップロード等)を検出する。
        SSOT: `src/lib/admin/abuse-detection.ts`。
      operationId: getAdminAbuseDetection
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: 検知結果取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/security:
    get:
      summary: セキュリティメトリクス
      description: |
        Rate Limit 統計・認証エラー・異常検知・スタックファイル検知などを集計する。
        SSOT: `src/lib/analytics/anomaly-detection.ts`, `src/lib/analytics/stuck-detection.ts`。
      operationId: getAdminSecurityMetrics
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: セキュリティメトリクス取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/users:
    get:
      summary: ユーザー一覧(管理者用)
      description: |
        管理者向けユーザー一覧を返す。フィルタ・ソート・ページネーション可。
        SSOT: `src/lib/admin/users.ts`。
      operationId: getAdminUsers
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      parameters:
        - in: query
          name: search
          schema:
            type: string
          description: ユーザー名 / email 部分一致
        - in: query
          name: role
          schema:
            type: string
            enum: [user, admin]
        - in: query
          name: is_blocked
          schema:
            type: string
            enum: ["true", "false"]
          description: ブロック状態フィルタ(boolean を文字列で渡す)
        - in: query
          name: has_subscription
          schema:
            type: string
            enum: ["true", "false"]
        - in: query
          name: plan_name
          schema:
            type: string
            enum: [free, standard, pro]
        - in: query
          name: sort_by
          schema:
            type: string
            default: created_at
        - in: query
          name: sort_order
          schema:
            type: string
            enum: [asc, desc]
            default: desc
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - in: query
          name: offset
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: ユーザー一覧取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/users/{id}:
    get:
      summary: ユーザー詳細(管理者用)
      description: 指定 ID のユーザー詳細・関連 metadata を返す。
      operationId: getAdminUserDetail
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: ユーザー詳細取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/users/{id}/block:
    post:
      summary: ユーザーブロック切替
      description: |
        指定 ID のユーザーのブロック状態を変更する。
        body の `is_blocked` で true/false を切替。
      operationId: postAdminUserBlock
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - is_blocked
              properties:
                is_blocked:
                  type: boolean
      responses:
        '200':
          description: ブロック状態変更成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/users/{id}/subscription:
    post:
      summary: ユーザーサブスクリプション変更
      description: |
        指定 ID のユーザーのプランを変更する。Stripe との同期は内部で実施。
      operationId: postAdminUserSubscription
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - plan_name
              properties:
                plan_name:
                  type: string
                  enum: [free, standard, pro]
      responses:
        '200':
          description: プラン変更成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/recover-stuck:
    get:
      summary: スタックファイル一覧
      description: |
        翻訳が長時間進まないファイルを一覧で返す(運用復旧支援)。
        SSOT: `src/lib/analytics/stuck-detection.ts`。
      operationId: getAdminRecoverStuck
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: スタックファイル一覧取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      summary: スタックファイル復旧実行
      description: |
        thresholdMs を超えて停止しているファイルの翻訳を再キックする。
        body 省略時は default の閾値で実行する。
      operationId: postAdminRecoverStuck
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                thresholdMs:
                  type: integer
                  minimum: 1
                  description: スタック判定の閾値(ミリ秒)。省略時は default を使用
      responses:
        '200':
          description: 復旧処理実行成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/admin/metrics:
    get:
      summary: 監視メトリクス
      description: |
        直近1時間の翻訳実行イベント成功/失敗率とファイル数を返す。
        翻訳実行イベントが0件の場合、失敗率は未計測として null を返す。
        5 分間のキャッシュ付き。
      operationId: getAdminMonitoringMetrics
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: メトリクス取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [metrics, cached]
                        properties:
                          metrics:
                            type: object
                            required:
                              - cachedAt
                              - failedTranslationExecutionsLastHour
                              - totalFilesLastHour
                              - totalTranslationExecutionsLastHour
                              - translationFailureRate
                            properties:
                              cachedAt:
                                type: string
                                format: date-time
                              failedTranslationExecutionsLastHour:
                                type: integer
                                minimum: 0
                              totalFilesLastHour:
                                type: integer
                                minimum: 0
                              totalTranslationExecutionsLastHour:
                                type: integer
                                minimum: 0
                              translationFailureRate:
                                type: [string, "null"]
                                description: 翻訳実行が0件の場合は未計測のnull。
                          cached:
                            type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ============================================================================
  # Internal / 観測系(#1105 Phase B)
  # ============================================================================

  /api/health/db:
    get:
      summary: DB ヘルスチェック(運用者向け)
      description: |
        Postgres / Supabase の実接続チェックを返す。
        本番では `Authorization: Bearer ${HEALTH_CHECK_SECRET}` 必須(未提示時は admin のみ許可)。
        本番でトークンも admin もない場合は 403 (fail-closed)。
      operationId: getHealthDb
      x-audience: internal
      x-auth: bearer_token
      tags:
        - Health
      security:
        - HealthCheckSecretAuth: []
      responses:
        '200':
          description: DB 正常
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '403':
          description: 未認証または HEALTH_CHECK_SECRET 未設定(fail-closed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: DB 異常
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /api/health/auth:
    get:
      summary: 認証システムヘルスチェック(運用者向け)
      description: |
        認証コンポーネント(Supabase Auth, JWT 等)の設定確認エンドポイント。
        本番では `Authorization: Bearer ${HEALTH_CHECK_SECRET}` または admin 必須。
      operationId: getHealthAuth
      x-audience: internal
      x-auth: bearer_token
      tags:
        - Health
      security:
        - HealthCheckSecretAuth: []
      responses:
        '200':
          description: 認証システム正常
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '403':
          description: 未認証
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: 認証システム異常
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /api/security/stats:
    get:
      summary: セキュリティ統計(管理者用)
      description: |
        Rate Limit / IP block / 異常検知などのセキュリティ統計を返す。
        SSOT: `src/lib/security/security-metrics.ts`。
      operationId: getSecurityStats
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: 統計取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/security/token-stats:
    get:
      summary: トークン統計(管理者用)
      description: |
        JWT / refresh token の発行・失効・回転状況を集計する。
        SSOT: `src/lib/security/jti-tracker.ts`, `token-rotation.ts`。
      operationId: getSecurityTokenStats
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: トークン統計取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      summary: トークン統計クリア
      description: 管理者向け運用操作。トークン統計のクリア・リセット。
      operationId: postSecurityTokenStats
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: 操作成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/translate/metrics:
    get:
      summary: 翻訳メトリクス(管理者用)
      description: |
        詳細な翻訳性能メトリクス収集は停止中。全variantで collectionStatus=paused、
        measured=false とし、測定値は null を返す。0 を健全な実測値として扱わない。
        public ユーザーには公開しない管理者専用エンドポイント。
      operationId: getTranslateMetrics
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: 翻訳メトリクス取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        oneOf:
                          - type: object
                            required: [type, fileId, collectionStatus, measured, metrics]
                            properties:
                              type:
                                type: string
                                const: file
                              fileId:
                                type: string
                              collectionStatus:
                                type: string
                                const: paused
                              measured:
                                type: boolean
                                const: false
                              metrics:
                                type: "null"
                          - type: object
                            required: [type, period, collectionStatus, measured, metrics]
                            properties:
                              type:
                                type: string
                                const: realtime
                              period:
                                type: string
                                const: 1h
                              collectionStatus:
                                type: string
                                const: paused
                              measured:
                                type: boolean
                                const: false
                              metrics:
                                type: "null"
                          - type: object
                            required:
                              - type
                              - period
                              - collectionStatus
                              - measured
                              - totals
                              - averages
                              - dailyMetrics
                            properties:
                              type:
                                type: string
                                const: summary
                              period:
                                type: string
                                description: Requested day count followed by d.
                              collectionStatus:
                                type: string
                                const: paused
                              measured:
                                type: boolean
                                const: false
                              totals:
                                type: "null"
                              averages:
                                type: "null"
                              dailyMetrics:
                                type: "null"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/metrics/cache:
    get:
      summary: 翻訳キャッシュメトリクス(管理者用)
      description: |
        翻訳キャッシュ(RPC v2)のヒット率・命中件数・失効件数等を返す。
      operationId: getMetricsCache
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: キャッシュメトリクス取得成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      summary: 翻訳キャッシュ操作(管理者用)
      description: 管理者向けキャッシュ操作(パージ等)。
      operationId: postMetricsCache
      x-audience: admin
      x-auth: admin_session
      tags:
        - Admin
      security:
        - CookieAuth: []
      responses:
        '200':
          description: 操作成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/preview/recovery:
    get:
      summary: プレビュー復旧対象一覧(cron / 監視向け)
      description: |
        失敗 / スタックしたプレビュージョブのリストを返す。
        staging / production は Cloud Scheduler OIDC(`SCHEDULER_SA_EMAILS` allowlist)必須。
        local / test は OIDC または `x-cron-secret` で許可。
        詳細: `docs/preview-system.md`。
      operationId: getPreviewRecovery
      x-audience: internal
      x-auth: oidc
      tags:
        - Preview
      security: []
      responses:
        '200':
          description: 対象ジョブ一覧取得成功
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      summary: プレビュー復旧バッチ実行(cron)
      description: |
        失敗 / スタックジョブの再キックを実行する。Cloud Scheduler から定期実行される想定。
      operationId: postPreviewRecovery
      x-audience: internal
      x-auth: oidc
      tags:
        - Preview
      security: []
      responses:
        '200':
          description: 復旧処理実行成功
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/internal/health:
    get:
      summary: 内部ヘルスチェック(認証必須)
      description: |
        DB/Storage の実接続チェックを含む詳細なヘルスチェック。
        Service Role Key を使用するため、Bearer token(HEALTH_CHECK_SECRET)で保護。
        用途: 本番環境の深いヘルスモニタリング(Vercel cron / 内部監視)。
      operationId: internalHealthCheck
      x-audience: internal
      x-auth: bearer_token
      tags:
        - Health
      security:
        - HealthCheckSecretAuth: []
      responses:
        '200':
          description: 全コンポーネント正常
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InternalHealthResponse'
        '403':
          description: 未認証または HEALTH_CHECK_SECRET 未設定(fail-closed)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '503':
          description: 一部コンポーネント異常
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InternalHealthResponse'

components:
  securitySchemes:
    # NOTE (#1089 / #1094 follow-up): Cookie session を SSOT として CookieAuth に統一。
    # 旧 BearerAuth 記述は 18 endpoint 全てを CookieAuth に移行済み。
    # 新規エンドポイントは必ず CookieAuth を使用すること。
    CookieAuth:
      type: apiKey
      in: cookie
      name: sb-access-token
      description: |
        Supabase Auth の Cookie session(HttpOnly + Secure + SameSite=Lax)。
        実装上の cookie 名は project ref に依存する
        (例: `sb-<project-ref>-auth-token`) が、論理的に本スキームで表現する。

    HealthCheckSecretAuth:
      type: http
      scheme: bearer
      description: HEALTH_CHECK_SECRET 環境変数による内部ヘルスチェック認証(Supabase JWT とは別)

  parameters:
    TranslationJobId:
      name: jobId
      in: path
      required: true
      schema: { type: string, format: uuid }

  schemas:
    # ============================================================================
    # 基本型定義
    # ============================================================================

    TranslationJobItem:
      type: object
      additionalProperties: false
      required: [item_key, original_text]
      properties:
        item_key: { type: string, minLength: 1, maxLength: 255 }
        original_text: { type: string, minLength: 1, maxLength: 20000 }
        slide_number: { type: integer, minimum: 0 }
        element_index: { type: integer, minimum: 0 }

    TranslationJobOwnedManifest:
      description: Server-owned manifest derived from the file extracted_data. Create requests do not accept an items body; this schema documents the execution cap.
      type: array
      minItems: 1
      maxItems: 5000
      items:
        $ref: '#/components/schemas/TranslationJobItem'

    TranslationJobCreateRequest:
      type: object
      additionalProperties: false
      required: [fileId, targetLanguage]
      properties:
        fileId: { type: string, format: uuid }
        targetLanguage: { type: string, pattern: '^[a-z]{2}(-[A-Z]{2})?$' }
        sourceLanguage: { type: string, pattern: '^[a-z]{2}(-[A-Z]{2})?$' }
        options:
          type: object
          additionalProperties: false
          properties:
            customInstruction: { type: string, maxLength: 100000 }
            glossary:
              type: object
              additionalProperties: { type: string, maxLength: 500 }
            includeSpeakerNotes: { type: boolean }
            qualityTier: { type: string, enum: [standard, high] }
            sourceLang: { type: string, pattern: '^[a-z]{2}(-[A-Z]{2})?$' }
            style: { type: string, enum: [standard, business, technical, casual] }

    TranslationJobResponse:
      type: object
      additionalProperties: false
      required: [id, fileId, status, targetLanguage, totalItems, completedItems, failedItems, pagesTranslated, errorCode, revisionId, createdAt, updatedAt, completedAt, cancelledAt]
      properties:
        id: { type: string, format: uuid }
        fileId: { type: string, format: uuid }
        status: { type: string, enum: [queued, running, ready_to_finalize, finalizing, completed, completed_with_errors, failed, cancelled] }
        targetLanguage: { type: string }
        totalItems: { type: integer, minimum: 0 }
        completedItems: { type: integer, minimum: 0 }
        failedItems: { type: integer, minimum: 0 }
        pagesTranslated: { type: integer, minimum: 0 }
        errorCode: { type: [string, 'null'] }
        revisionId: { type: [string, 'null'], format: uuid }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        completedAt: { type: [string, 'null'], format: date-time }
        cancelledAt: { type: [string, 'null'], format: date-time }

    TranslationJobTerminalNoticeClaim:
      oneOf:
        - type: object
          additionalProperties: false
          required: [claimed, status]
          properties:
            claimed: { const: false }
            status: { type: 'null' }
        - type: object
          additionalProperties: false
          required: [claimed, status]
          properties:
            claimed: { const: true }
            status: { type: string, enum: [completed, completed_with_errors, failed, cancelled] }

    QualityMetrics:
      type: object
      additionalProperties: false
      required:
        - cohortBasis
        - downloadUrlIssuanceRate
        - editCohortBasis
        - retranslationRate
        - editFileRate
        - editElementRate
        - editObservationPeriod
        - editReasonCounts
        - totalTranslations
        - totalDownloadUrlsIssued
        - period
      properties:
        cohortBasis:
          type: string
          enum: [file_created_at]
          description: downloadUrlIssuanceRateとtotalTranslationsのcompletedファイルコホートを期間抽出する時刻列。現行はファイル作成日時
        downloadUrlIssuanceRate:
          type: number
          minimum: 0
          maximum: 100
          description: 期間内作成かつ現在completedのファイルのうち、同期間内に1回以上署名ダウンロードURLが発行されたファイルの割合。Storage取得完了率ではない
        editCohortBasis:
          type: string
          enum: [file_completed_at]
          description: 編集機会の開始を表すファイル翻訳完了日時を編集率コホートの基準とする
        retranslationRate:
          $ref: '#/components/schemas/RetranslationRateMeasurement'
          description: 翻訳operation種別の計測開始前後を明示する再翻訳率
        editFileRate:
          $ref: '#/components/schemas/EditRateMeasurement'
          description: 編集されたファイル数を翻訳完了ファイル数で割った割合
        editElementRate:
          $ref: '#/components/schemas/EditRateMeasurement'
          description: 編集された翻訳要素数を翻訳完了要素数で割った割合
        editObservationPeriod:
          $ref: '#/components/schemas/EditObservationPeriod'
        editReasonCounts:
          $ref: '#/components/schemas/EditReasonCounts'
        totalTranslations:
          type: integer
          minimum: 0
          description: 期間内に作成され、現在completedのファイル数
        totalDownloadUrlsIssued:
          type: integer
          minimum: 0
          description: 期間内の署名ダウンロードURL発行イベント総数(同一ファイルの重複を含む)。Storage取得完了数ではない
        period:
          type: object
          additionalProperties: false
          required: [start, end]
          properties:
            start:
              type: string
              format: date-time
            end:
              type: string
              format: date-time

    EditRateMeasurement:
      description: 編集履歴計測の開始前後を明示する測定値
      oneOf:
        - type: object
          additionalProperties: false
          required: [status, value, reason, numerator, denominator]
          properties:
            status:
              type: string
              enum: [available]
            value:
              type: number
              minimum: 0
              maximum: 100
            reason:
              type: "null"
            numerator:
              type: integer
              minimum: 0
            denominator:
              type: integer
              minimum: 1
        - type: object
          additionalProperties: false
          required: [status, value, reason]
          properties:
            status:
              type: string
              enum: [unavailable]
            value:
              type: "null"
            reason:
              type: string
              enum: [translation_edit_tracking_not_started, no_eligible_cohort]

    EditObservationPeriod:
      type: object
      additionalProperties: false
      required: [requestedStart, effectiveStart, end, trackingStartedAt, coverageStatus]
      properties:
        requestedStart:
          type: string
          format: date-time
        effectiveStart:
          type: [string, "null"]
          format: date-time
        end:
          type: string
          format: date-time
        trackingStartedAt:
          type: [string, "null"]
          format: date-time
        coverageStatus:
          oneOf:
            - type: string
              enum: [full, tracking_started_during_period, retention_truncated]
            - type: "null"

    EditReasonCounts:
      type: object
      additionalProperties: false
      required: [manual_edit, ai_refine_accept, overflow_shorten_accept, unclassified]
      properties:
        manual_edit:
          type: integer
          minimum: 0
        ai_refine_accept:
          type: integer
          minimum: 0
        overflow_shorten_accept:
          type: integer
          minimum: 0
        unclassified:
          type: integer
          minimum: 0

    RetranslationRateMeasurement:
      description: 翻訳operation種別の計測開始前後を明示する測定値
      oneOf:
        - type: object
          additionalProperties: false
          required: [status, value, reason]
          properties:
            status:
              type: string
              enum: [available]
            value:
              type: number
              minimum: 0
              maximum: 100
            reason:
              type: "null"
        - type: object
          additionalProperties: false
          required: [status, value, reason]
          properties:
            status:
              type: string
              enum: [unavailable]
            value:
              type: "null"
            reason:
              type: string
              enum: [translation_operation_tracking_not_started]

    TargetLanguage:
      type: string
      description: "翻訳対象言語コード(BCP 47)。SSOT: config/languages.yaml"
      enum:
        - ja
        - en
        - zh
        - zh-TW
        - ko
        - es
        - fr
        - de
        - it
        - pt
        - ru
        - ar
        - hi
        - th
        - vi

    # ============================================================================
    # 翻訳関連スキーマ
    # ============================================================================
    
    TextToTranslate:
      type: object
      description: 翻訳リクエスト用のテキスト
      required:
        - id
        - text
      properties:
        id:
          type: string
          description: テキスト要素のID
        text:
          type: string
          description: 翻訳するテキスト
        context:
          type: string
          description: テーブル等のコンテキスト情報

    TranslatedTextResult:
      type: object
      description: 翻訳結果
      required:
        - id
        - original
        - translated
      properties:
        id:
          type: string
          description: テキスト要素のID
        original:
          type: string
          description: 元のテキスト
        translated:
          type: string
          description: 翻訳されたテキスト
        confidence:
          type: number
          description: 翻訳の信頼度(0-1)
          minimum: 0
          maximum: 1

    SingleTranslationRequest:
      type: object
      description: 単一テキスト翻訳リクエスト(/api/translate/single 用)
      required:
        - fileId
        - text
        - textId
        - slideIndex
        - targetLanguage
      properties:
        fileId:
          type: string
          format: uuid
          description: 翻訳対象ファイルのID
        text:
          type: string
          description: 翻訳するテキスト
        textId:
          type: string
          description: テキスト要素のID
        slideIndex:
          type: integer
          minimum: 0
          description: スライドインデックス(0始まり)
        targetLanguage:
          $ref: '#/components/schemas/TargetLanguage'
        sourceLanguage:
          type: string
          description: "ソース言語コード(オプション)"
        priority:
          type: string
          enum: [INTERACTIVE, BACKGROUND, BATCH]
          description: 翻訳優先度

    SingleTranslationResponse:
      type: object
      description: 単一テキスト翻訳レスポンス
      required:
        - success
      properties:
        success:
          type: boolean
        translatedText:
          type: string
          description: 翻訳結果テキスト
        fromCache:
          type: boolean
          description: キャッシュから取得したかどうか
        errorCode:
          type: string
          description: エラーコード(エラー時)
        retryAfter:
          type: integer
          minimum: 0
          description: 再試行可能になるまでの秒数(エラー時)

    TranslationRequest:
      type: object
      description: ストリーミング翻訳リクエスト(/api/translate/stream 用)
      required:
        - texts
        - targetLanguage
      properties:
        texts:
          type: array
          description: 翻訳するテキストの配列
          items:
            $ref: '#/components/schemas/TextToTranslate'
        targetLanguage:
          $ref: '#/components/schemas/TargetLanguage'
        sourceLanguage:
          type: string
          description: "ソース言語コード(オプション、自動検出の場合は \"auto\")"

    # ============================================================================
    # API リクエスト/レスポンススキーマ
    # ============================================================================
    
    SuccessResponse:
      type: object
      description: 成功レスポンス
      required:
        - success
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          description: メッセージ

    HealthResponse:
      type: object
      description: ヘルスチェックレスポンス
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - ok
            - error
          description: システムの状態
        timestamp:
          type: string
          format: date-time
          description: チェック時刻
        version:
          type: string
          description: APIバージョン

    # ============================================================================
    # Preview API スキーマ
    # ============================================================================

    PreviewJobStatus:
      type: string
      description: プレビュージョブの状態
      enum:
        - queued
        - pdf_generating
        - pdf_ready
        - thumbs_running
        - completed
        - completed_with_errors
        - failed

    PreviewPageStatus:
      type: string
      description: プレビューページの状態
      enum:
        - pending
        - done
        - failed

    PreviewPage:
      type: object
      description: プレビューページ情報
      required:
        - page_index
        - status
      properties:
        page_index:
          type: integer
          description: ページインデックス(0始まり)
          minimum: 0
        status:
          $ref: '#/components/schemas/PreviewPageStatus'
        width:
          type: integer
          description: ページ幅(ピクセル)
        height:
          type: integer
          description: ページ高さ(ピクセル)
        thumb_path:
          type: string
          description: サムネイルのStorage内パス

    PreviewStartRequest:
      type: object
      description: プレビュージョブ開始リクエスト
      required:
        - fileId
      properties:
        fileId:
          type: string
          format: uuid
          description: 対象ファイルのID

    PreviewStartResponse:
      type: object
      description: プレビュージョブ開始レスポンス
      required:
        - jobId
      properties:
        jobId:
          type: string
          format: uuid
          description: プレビュージョブID
        existing:
          type: boolean
          description: 既存ジョブを返却した場合true

    PreviewStatusResponse:
      type: object
      description: プレビュージョブステータスレスポンス
      required:
        - status
      properties:
        status:
          $ref: '#/components/schemas/PreviewJobStatus'
        totalPages:
          type: integer
          description: 総ページ数
          minimum: 0
        thumbsDone:
          type: integer
          description: サムネイル生成完了数
          minimum: 0
        thumbsFailed:
          type: integer
          description: サムネイル生成失敗数
          minimum: 0
        pdfPath:
          type: string
          description: PDFのStorage内パス
        pages:
          type: array
          description: ページ情報の配列
          items:
            $ref: '#/components/schemas/PreviewPage'
        error:
          type: string
          description: エラーメッセージ(失敗時)

    PreviewSignedUrlResponse:
      type: object
      description: 署名付きURL取得レスポンス
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          description: 署名付きURL(1時間有効)

    PreviewThumbsStartRequest:
      type: object
      description: サムネイル生成開始リクエスト
      required:
        - jobId
      properties:
        jobId:
          type: string
          format: uuid
          description: プレビュージョブID

    PreviewThumbsStartResponse:
      type: object
      description: サムネイル生成開始レスポンス
      properties:
        success:
          type: boolean
          description: 処理成功フラグ
        message:
          type: string
          description: メッセージ

    PreviewThumbsRetryRequest:
      type: object
      description: 失敗ページ一括再生成リクエスト
      required:
        - jobId
      properties:
        jobId:
          type: string
          format: uuid
          description: プレビュージョブID

    PreviewThumbsRetryResponse:
      type: object
      description: 失敗ページ一括再生成レスポンス
      properties:
        retriedCount:
          type: integer
          description: 再生成を試みたページ数
          minimum: 0
        message:
          type: string
          description: メッセージ

    PreviewThumbsRetryPageRequest:
      type: object
      description: 単一ページ再生成リクエスト
      required:
        - jobId
        - pageIndex
      properties:
        jobId:
          type: string
          format: uuid
          description: プレビュージョブID
        pageIndex:
          type: integer
          description: ページインデックス(0始まり)
          minimum: 0

    PreviewThumbsRetryPageResponse:
      type: object
      description: 単一ページ再生成レスポンス
      properties:
        success:
          type: boolean
          description: 処理成功フラグ
        message:
          type: string
          description: メッセージ

    # ============================================================================
    # Usage / Error Report / Internal Health スキーマ
    # ============================================================================

    UsageCheckResponse:
      type: object
      description: 使用量チェックレスポンス
      required:
        - canTranslate
        - remainingFiles
      properties:
        canTranslate:
          type: boolean
          description: 翻訳可能かどうか
        remainingFiles:
          type: integer
          description: 残り翻訳可能ファイル数
          minimum: 0
        currentUsage:
          type: integer
          description: 今月の使用ファイル数
        monthlyLimit:
          type: integer
          description: 月間制限ファイル数
        plan:
          type: string
          description: 現在のプラン名

    ErrorReportRequest:
      type: object
      description: クライアントエラーレポートリクエスト
      properties:
        error:
          type: string
          description: エラーメッセージ
        message:
          type: string
          description: エラーメッセージ(error-boundary-wrapper 互換)
        errorInfo:
          type: object
          properties:
            componentStack:
              type: string
            stack:
              type: string
            type:
              type: string
            url:
              type: string
        stack:
          type: string
          description: スタックトレース(フラットペイロード互換)
        url:
          type: string
          description: エラー発生URL(フラットペイロード互換)
        userAgent:
          type: string
        timestamp:
          type: string

    InternalHealthResponse:
      type: object
      description: 内部ヘルスチェックレスポンス
      required:
        - status
        - timestamp
        - checks
        - test_mode_enabled
      properties:
        status:
          type: string
          enum: [healthy, unhealthy]
        timestamp:
          type: string
          format: date-time
        test_mode_enabled:
          type: boolean
          description: |
            production runtime では常に false。
            true が返った場合は env 設定不備(Vercel に E2E_* が残存等)または production 判定失敗を示す。
            #1128: post-deploy probe で本フィールドを監視し、true なら deploy job を fail させる。
        checks:
          type: object
          properties:
            database:
              $ref: '#/components/schemas/HealthCheckResult'
            storage:
              $ref: '#/components/schemas/HealthCheckResult'
            rpc:
              $ref: '#/components/schemas/HealthCheckResult'
            anthropic:
              $ref: '#/components/schemas/HealthCheckResult'
            stripe:
              $ref: '#/components/schemas/HealthCheckResult'

    HealthCheckResult:
      type: object
      description: 個別コンポーネントのヘルスチェック結果
      required:
        - status
      properties:
        status:
          type: string
          enum: [healthy, unhealthy, configured, disabled, missing, partial]
          description: |
            stripe の disabled は Stripe の 4 つの env が全部ない無料公開で、正常な状態。
            partial は一部だけ設定された不正な状態で、unhealthy として扱う。
        latency:
          type: integer
          description: レイテンシ(ミリ秒)

    # ============================================================================
    # エラーレスポンススキーマ
    # ============================================================================

    ErrorResponse:
      type: object
      description: エラーレスポンス
      required:
        - error
      properties:
        error:
          type: object
          required:
            - type
            - message
            - timestamp
          properties:
            type:
              type: string
              enum:
                - VALIDATION_ERROR
                - AUTHENTICATION_ERROR
                - AUTHORIZATION_ERROR
                - NOT_FOUND
                - CONFLICT
                - RATE_LIMIT_ERROR
                - EXTERNAL_SERVICE_ERROR
                - DATABASE_ERROR
                - FILE_PROCESSING_ERROR
                - INTERNAL_SERVER_ERROR
              description: エラータイプ
            message:
              type: string
              description: エラーメッセージ
            code:
              type: string
              description: エラーコード(オプション)
            details:
              type: object
              description: エラーの詳細情報(オプション)
              additionalProperties: true
            timestamp:
              type: string
              format: date-time
              description: エラー発生時刻
            requestId:
              type: string
              format: uuid
              description: リクエストID(デバッグ用)

    # ============================================================================
    # 共通レスポンスエンベロープ(src/lib/api/response-builder.ts と整合)
    # ============================================================================

    ApiSuccessEnvelope:
      type: object
      description: |
        `createApiSuccessResponse()` が返す共通成功レスポンスエンベロープ。
        個別エンドポイントは `allOf` で `data` プロパティを上書きする。
      required:
        - success
        - status
        - data
        - timestamp
        - requestId
      properties:
        success:
          type: boolean
          enum: [true]
        status:
          type: integer
          description: HTTP ステータスコード
        data:
          description: ペイロード本体(個別エンドポイントで型を上書き)
        timestamp:
          type: string
          format: date-time
        requestId:
          type: string
          format: uuid
        metadata:
          type: object
          additionalProperties: true
          description: 追加メタデータ(オプション)

    # ============================================================================
    # Admin API スキーマ (#1100 / #1105 follow-up)
    # ============================================================================
    # SSOT: src/types/admin.ts (AdminOverviewResponse 等)。
    # 型同期は CI 強制ではないため、admin.ts の変更時は本ファイルも更新すること。

    AdminOverviewStats:
      type: object
      description: 管理者ダッシュボードの集計値
      required:
        - totalUsers
        - adminUsers
        - totalFiles
        - completedFiles
        - totalStorageUsed
        - monthlyUploads
        - monthlyTranslations
        - successRate
      properties:
        totalUsers:
          type: integer
        adminUsers:
          type: integer
        totalFiles:
          type: integer
        completedFiles:
          type: integer
        totalStorageUsed:
          type: integer
          description: 累計ストレージ使用量(bytes)
        monthlyUploads:
          type: integer
          description: 当月のアップロード件数
        monthlyTranslations:
          type: integer
          description: 当月の翻訳件数
        successRate:
          type: number
          description: 翻訳成功率 (0-100)

    AdminUser:
      type: object
      description: 管理者ダッシュボード向けユーザー情報
      required:
        - id
        - username
        - role
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        username:
          type: string
        full_name:
          type: string
        role:
          type: string
          enum: [user, admin]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        fileCount:
          type: integer
        storageUsed:
          type: integer

    AdminActivity:
      type: object
      description: アクティビティログエントリ
      required:
        - id
        - user_id
        - action
        - created_at
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        action:
          type: string
        description:
          type: [string, "null"]
        metadata:
          description: 任意の構造化メタデータ
        ip_address:
          type: [string, "null"]
        user_agent:
          type: [string, "null"]
        created_at:
          type: string
          format: date-time
        user:
          type: [object, "null"]
          properties:
            username:
              type: string
            full_name:
              type: string

    AdminUsageStat:
      type: object
      description: 利用統計の日次集計
      required:
        - date
        - uploads
        - translations
        - activeUsers
      properties:
        date:
          type: string
        uploads:
          type: integer
        translations:
          type: integer
        activeUsers:
          type: integer

    AdminOverviewResponse:
      type: object
      description: 管理者ダッシュボード概要レスポンス
      required:
        - stats
        - users
        - activities
        - usage_stats
      properties:
        stats:
          $ref: '#/components/schemas/AdminOverviewStats'
        users:
          type: array
          items:
            $ref: '#/components/schemas/AdminUser'
        activities:
          type: array
          items:
            $ref: '#/components/schemas/AdminActivity'
        usage_stats:
          type: array
          items:
            $ref: '#/components/schemas/AdminUsageStat'

  responses:
    BadRequest:
      description: リクエストが不正です
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: VALIDATION_ERROR
              message: ファイル情報が不足しています
              timestamp: '2024-01-01T00:00:00Z'
              requestId: '123e4567-e89b-12d3-a456-426614174000'

    Unauthorized:
      description: 認証が必要です
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: AUTHENTICATION_ERROR
              message: 認証が必要です
              timestamp: '2024-01-01T00:00:00Z'
              requestId: '123e4567-e89b-12d3-a456-426614174000'

    Forbidden:
      description: アクセス権限がありません
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: AUTHORIZATION_ERROR
              message: このファイルを削除する権限がありません
              timestamp: '2024-01-01T00:00:00Z'
              requestId: '123e4567-e89b-12d3-a456-426614174000'

    Conflict:
      description: 現在の状態または冪等性キーと競合しています
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

    NotFound:
      description: リソースが見つかりません
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: NOT_FOUND
              message: ファイルが見つかりません
              timestamp: '2024-01-01T00:00:00Z'
              requestId: '123e4567-e89b-12d3-a456-426614174000'

    TooManyRequests:
      description: レート制限を超えました
      headers:
        Retry-After:
          description: 再試行可能なまでの秒数
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: RATE_LIMIT_ERROR
              message: レート制限を超えました
              timestamp: '2024-01-01T00:00:00Z'
              requestId: '123e4567-e89b-12d3-a456-426614174000'
              details:
                retryAfter: 60

    InternalServerError:
      description: サーバー内部エラー
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: INTERNAL_SERVER_ERROR
              message: サーバーエラー
              timestamp: '2024-01-01T00:00:00Z'
              requestId: '123e4567-e89b-12d3-a456-426614174000'

エラーレスポンス

APIは標準的なHTTPステータスコードとエラーレスポンス形式を使用します。

エラーレスポンス形式

{
  "error": {
    "type": "ERROR_TYPE",
    "message": "Error message",
    "code": "ERROR_CODE",
    "timestamp": "2024-01-01T00:00:00Z",
    "requestId": "uuid"
  }
}

主なエラータイプ

  • VALIDATION_ERROR: リクエストのバリデーションエラー(400)
  • AUTHENTICATION_ERROR: 認証エラー(401)
  • AUTHORIZATION_ERROR: 認可エラー(403)
  • NOT_FOUND: リソースが見つからない(404)
  • RATE_LIMIT_ERROR: レート制限超過(429)
  • INTERNAL_SERVER_ERROR: サーバー内部エラー(500)