{
  "openapi": "3.1.0",
  "info": {
    "title": "Flow Reels Studio API",
    "version": "0.2.0",
    "description": "API própria sobre a sessão autenticada do Chrome. Não é a API oficial ou completa do Google Flow. Geração usa ajustes escolhidos localmente no navegador."
  },
  "security": [
    {
      "appBearer": []
    },
    {
      "panelSession": []
    }
  ],
  "components": {
    "securitySchemes": {
      "appBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "APP_TOKEN"
      },
      "workerBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "WORKER_TOKEN separado"
      },
      "panelSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-flow-session",
        "description": "Sessão Google autorizada do painel. POST exige Origin igual a STUDIO_URL."
      }
    },
    "schemas": {
      "LoginInput": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "const": "login"
          }
        },
        "description": "Abre o seletor de contas no Chrome do worker; cancela tarefas na fila e pausa a produção automática. Não confirma login."
      },
      "ReelInput": {
        "type": "object",
        "required": [
          "type",
          "accountId",
          "projectUrl",
          "maxCredits",
          "script",
          "articles"
        ],
        "properties": {
          "type": {
            "const": "reel"
          },
          "accountId": {
            "type": "string"
          },
          "projectUrl": {
            "type": "string",
            "format": "uri"
          },
          "maxCredits": {
            "const": 45
          },
          "script": {
            "type": "object"
          },
          "articles": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        },
        "description": "Criado internamente pela agenda. Não aceito em POST /api/jobs."
      },
      "InspectInput": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "const": "inspect"
          }
        }
      },
      "GenerateInput": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "prompt",
          "expectedContext",
          "confirmCredits"
        ],
        "properties": {
          "type": {
            "const": "generate"
          },
          "prompt": {
            "type": "string",
            "minLength": 1,
            "maxLength": 8000
          },
          "expectedContext": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$"
          },
          "confirmCredits": {
            "const": true
          }
        }
      },
      "Context": {
        "type": "object",
        "required": [
          "projectUrl",
          "settings",
          "expectedContext",
          "observedAt",
          "notice"
        ],
        "properties": {
          "accountId": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$",
            "description": "Identificador derivado da conta observada; nenhuma credencial Google."
          },
          "projectUrl": {
            "type": "string",
            "format": "uri"
          },
          "settings": {
            "type": "string"
          },
          "expectedContext": {
            "type": "string"
          },
          "observedAt": {
            "type": "string",
            "format": "date-time"
          },
          "notice": {
            "type": "string"
          }
        }
      },
      "Job": {
        "type": "object",
        "required": [
          "id",
          "input",
          "status",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "input": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/LoginInput"
              },
              {
                "$ref": "#/components/schemas/ReelInput"
              },
              {
                "$ref": "#/components/schemas/InspectInput"
              },
              {
                "$ref": "#/components/schemas/GenerateInput"
              }
            ]
          },
          "status": {
            "enum": [
              "queued",
              "running",
              "needs_review",
              "reviewed",
              "submitted",
              "completed",
              "cancelled"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "finishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "reviewedAt": {
            "type": "string",
            "format": "date-time"
          },
          "result": {
            "type": "object",
            "description": "inspect: Context. generate: dispatched=true e resumo settings; não confirma renderização."
          },
          "error": {
            "type": "string"
          }
        }
      }
    }
  },
  "paths": {
    "/api/news/production": {
      "post": {
        "summary": "Vincular conta/projeto para produção diária ou pausar novas produções",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "required": [
                      "enabled",
                      "inspectionJobId",
                      "maxCredits"
                    ],
                    "properties": {
                      "enabled": {
                        "const": true
                      },
                      "inspectionJobId": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "maxCredits": {
                        "const": 45
                      },
                      "includeCurrent": {
                        "type": "boolean",
                        "default": true,
                        "description": "False vincula apenas campanhas criadas após a ativação."
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "enabled"
                    ],
                    "properties": {
                      "enabled": {
                        "const": false
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Configuração persistida; inspeção precisa ter menos de 10 minutos"
          },
          "400": {
            "description": "Inspeção ou limite inválido"
          }
        }
      }
    },
    "/api/worker/jobs/{id}/heartbeat": {
      "post": {
        "summary": "Renovar claim ativo; não revive claim expirado",
        "security": [
          {
            "workerBearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "claimToken"
                ],
                "properties": {
                  "claimToken": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Claim renovado"
          },
          "400": {
            "description": "Claim inválido ou expirado"
          }
        }
      }
    },
    "/api/worker/jobs/{id}/video": {
      "post": {
        "summary": "Enviar MP4 antes de finalizar produção; até 150 MiB",
        "security": [
          {
            "workerBearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "X-Claim-Token",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "video/mp4": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "MP4 persistido"
          },
          "400": {
            "description": "Claim, tipo ou tamanho inválido"
          }
        }
      }
    },
    "/api/jobs/{id}/video": {
      "get": {
        "summary": "Baixar MP4 de produção concluída",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "MP4",
            "content": {
              "video/mp4": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Vídeo ainda indisponível"
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "security": [],
        "summary": "Estado HTTP do servidor; não verifica Chrome",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/jobs": {
      "get": {
        "summary": "Histórico, do mais recente para o mais antigo",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Job"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Pedido inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Token ausente ou inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Enfileirar inspeção ou geração",
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,100}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/LoginInput"
                  },
                  {
                    "$ref": "#/components/schemas/InspectInput"
                  },
                  {
                    "$ref": "#/components/schemas/GenerateInput"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Tarefa aceita; worker executará em série",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "400": {
            "description": "Pedido inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Token ausente ou inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "413": {
            "description": "Corpo acima de 20KB"
          }
        }
      }
    },
    "/api/jobs/{id}/review": {
      "post": {
        "summary": "Liberar fila após conferir resultado incerto no Flow",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirmReviewed"
                ],
                "properties": {
                  "confirmReviewed": {
                    "const": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Pedido inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Token ausente ou inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/worker/claim": {
      "post": {
        "security": [
          {
            "workerBearer": []
          }
        ],
        "summary": "Retirar próxima tarefa; no máximo uma em execução",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Job"
                        },
                        {
                          "type": "object",
                          "required": [
                            "claimToken"
                          ],
                          "properties": {
                            "claimToken": {
                              "type": "string",
                              "format": "uuid"
                            }
                          }
                        }
                      ]
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Pedido inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Token ausente ou inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/worker/jobs/{id}/finish": {
      "post": {
        "security": [
          {
            "workerBearer": []
          }
        ],
        "summary": "Reportar execução; erro bloqueia a fila para revisão",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "claimToken"
                ],
                "properties": {
                  "claimToken": {
                    "type": "string"
                  },
                  "result": {
                    "type": "object"
                  },
                  "error": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Pedido inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Token ausente ou inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/news": {
      "get": {
        "summary": "Agenda, fontes, artigos e campanhas com roteiro e evidências",
        "responses": {
          "200": {
            "description": "Estado persistido da automação"
          },
          "401": {
            "description": "Sessão ou credencial administrativa inválida"
          }
        }
      }
    },
    "/api/news/run": {
      "post": {
        "summary": "Iniciar coleta assíncrona; respeita concorrência e cota diária, mesmo em modo manual",
        "responses": {
          "202": {
            "description": "Coleta aceita; acompanhar GET /api/news"
          },
          "401": {
            "description": "Sessão ou credencial administrativa inválida"
          }
        }
      }
    },
    "/api/news/config": {
      "post": {
        "summary": "Atualizar agenda, com cota máxima de uma campanha por dia em America/Sao_Paulo",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "enabled",
                  "intervalHours",
                  "maxVideosPerDay"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "intervalHours": {
                    "type": "integer",
                    "enum": [
                      6,
                      12,
                      24
                    ]
                  },
                  "maxVideosPerDay": {
                    "type": "integer",
                    "const": 1
                  },
                  "calendar": {
                    "description": "Agenda persistente em Brasília. Omitir preserva a agenda existente; null volta ao intervalo legado. Produção e publicação têm tolerância de 15 minutos.",
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "version",
                          "timeZone",
                          "days",
                          "collectAt",
                          "produceAt",
                          "instagramAt",
                          "linkedinAt",
                          "graceMinutes"
                        ],
                        "properties": {
                          "version": {
                            "const": 1
                          },
                          "timeZone": {
                            "const": "America/Sao_Paulo"
                          },
                          "days": {
                            "type": "array",
                            "uniqueItems": true,
                            "minItems": 1,
                            "items": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 5
                            }
                          },
                          "collectAt": {
                            "type": "string",
                            "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
                          },
                          "produceAt": {
                            "type": "string",
                            "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
                          },
                          "instagramAt": {
                            "type": "string",
                            "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
                          },
                          "linkedinAt": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "string",
                              "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
                            }
                          },
                          "graceMinutes": {
                            "const": 15
                          }
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Estado persistido da automação"
          },
          "400": {
            "description": "Configuração inválida"
          },
          "401": {
            "description": "Sessão ou credencial administrativa inválida"
          }
        }
      }
    },
    "/auth/status": {
      "get": {
        "security": [],
        "summary": "Estado do login do painel, sem dados pessoais",
        "responses": {
          "200": {
            "description": "configured e authenticated"
          }
        }
      }
    },
    "/auth/google": {
      "get": {
        "security": [],
        "summary": "Iniciar login Google do painel",
        "responses": {
          "303": {
            "description": "Redireciona ao Google com state, nonce e PKCE"
          },
          "429": {
            "description": "Limite de transações de login pendentes"
          }
        }
      }
    },
    "/auth/google/callback": {
      "get": {
        "security": [],
        "summary": "Retorno OAuth vinculado ao navegador",
        "responses": {
          "303": {
            "description": "Cria sessão e retorna ao painel"
          },
          "400": {
            "description": "Retorno inválido ou expirado"
          },
          "403": {
            "description": "Identidade inválida ou conta não autorizada"
          }
        }
      }
    },
    "/auth/logout": {
      "post": {
        "security": [
          {
            "panelSession": []
          }
        ],
        "summary": "Encerrar sessão do painel",
        "responses": {
          "200": {
            "description": "Sessão encerrada"
          },
          "403": {
            "description": "Sessão ou Origin inválida"
          }
        }
      }
    },
    "/api/jobs/{id}/retry": {
      "post": {
        "summary": "Repetir explicitamente um Reel revisado, sem ativar a produção automática",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "inspectionJobId",
                  "maxCredits"
                ],
                "properties": {
                  "inspectionJobId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "maxCredits": {
                    "type": "integer",
                    "const": 45
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Tentativa enfileirada; chamadas repetidas para a mesma tentativa anterior retornam a mesma job"
          },
          "400": {
            "description": "Tentativa anterior sem revisão, inspeção inválida/expirada ou conta/projeto diferente"
          },
          "401": {
            "description": "Autenticação do painel obrigatória"
          }
        }
      }
    },
    "/api/instagram": {
      "get": {
        "summary": "Estado da publicação automática Instagram; sem credenciais ou URLs temporárias",
        "security": [
          {
            "appBearer": []
          },
          {
            "panelSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "Configuração e publicações: enabled, account, status, error opcional e publications com campaignId, jobId, status, mediaId, permalink, error e updatedAt"
          },
          "401": {
            "description": "Requer administrador ou sessão Google autorizada"
          }
        }
      }
    },
    "/instagram/media/{jobId}": {
      "parameters": [
        {
          "name": "jobId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "grant",
          "in": "query",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Autorização opaca por arquivo, expira em 24 horas; não é APP_TOKEN."
        }
      ],
      "get": {
        "security": [],
        "summary": "MP4 com autorização temporária; suporta Range único",
        "responses": {
          "200": {
            "description": "MP4 completo"
          },
          "206": {
            "description": "Intervalo MP4"
          },
          "404": {
            "description": "Mídia ou autorização indisponível"
          },
          "416": {
            "description": "Intervalo inválido"
          }
        }
      },
      "head": {
        "security": [],
        "summary": "Cabeçalhos do MP4 sem corpo",
        "responses": {
          "200": {
            "description": "Metadados"
          },
          "206": {
            "description": "Intervalo"
          },
          "404": {
            "description": "Indisponível"
          },
          "416": {
            "description": "Intervalo inválido"
          }
        }
      }
    }
  }
}
