openapi: "3.1.0"
info:
  title: LOCC — Laniway Observability and Control Center
  version: "0.1.0"
  description: |
    Observação e comando da plataforma Laniway. Os nós reportam **checks** (estado discreto
    com mensagem — gera alerta) e **gauges** (números — viram séries RRD de tamanho fixo).
    O LOCC avalia a cada minuto (staleness, transições), alerta pelo TEP e expõe dois sinais
    distintos: `/health` (o serviço está pronto — balanceador e deploy) e `/watchdog`
    (a observação está viva — monitor externo).

    **Autenticação:** Bearer token LIAM (app_token de identidade `server`), exceto `/health` e `/watchdog`.
servers:
  - url: https://api.locc.laniway.com.br
    description: Produção
components:
  securitySchemes:
    liamBearerAuth:
      type: http
      scheme: bearer
      description: Bearer token LIAM
  schemas:
    Check:
      type: object
      required: [key, status]
      properties:
        key:     { type: string, description: "ex.: replication, vpn.handshake, backup.seed" }
        status:  { type: string, enum: [ok, warn, crit] }
        message: { type: string }
        value:   { type: number, nullable: true }
paths:
  /health:
    get:
      summary: Prontidão do serviço (para o balanceador e o deploy)
      description: |
        O LOCC está de pé e pode trabalhar: processo no ar, banco alcançável e esquema
        aplicado (há RRA ativo em `rra`). **Não** diz nada sobre os nós observados — para
        isso é o `/watchdog`.

        `503` (código `5001004`) = banco inacessível ou base não migrada (ou apagada).
      responses:
        "200": { description: "ok — status, rras, nodes, uptime_seconds" }
        "503": { description: banco inacessível ou esquema não aplicado }
  /watchdog:
    get:
      summary: A observação está viva? (para o monitor externo)
      description: |
        O "monitor dos monitores": `200` se a última avaliação tem menos de
        `LOCC_WATCHDOG_MAX_AGE_SECONDS` (default 180 s); `503` (código `5001003`) se
        envelheceu — ninguém está avaliando os nós e o monitor externo deve disparar.

        Sem nenhuma avaliação registrada (base nova), responde `200` com
        `status: warming_up` durante `LOCC_WATCHDOG_GRACE_SECONDS` (default 300 s) desde o
        boot do processo, e `503` depois disso.

        `503` aqui **não** tira o LOCC do ar — o balanceador olha o `/health`.
      responses:
        "200": { description: "ok ou warming_up — inclui idade da última avaliação, nós, stale e alertas abertos" }
        "503": { description: avaliador parado, ou sem avaliação depois da carência }
  /report/v1.0:
    post:
      summary: Nó envia report (checks + gauges)
      description: |
        Permissão `locc.report.send`; identidade `server` cujo e-mail é `<função>@<projeto>.internal`
        e `<projeto>` tem de ser o `project` do payload (`5001002` se não casar). Primeiro report
        registra o nó. `ts` desviado > 1 h do relógio do LOCC é substituído. Aceita `dry_run`.

        **Duas formas de mandar número**, para a mesma série:
        - `gauges` — o valor de agora (`{"cpu.load1": 0.8}`), carimbado com o `ts` do report;
        - `samples` — lote carimbado (`{"net.bytes_in": [[ts, v], ...]}`), para quem amostra
          mais fino do que reporta: é o que permite RRA de 5 s com report de 60 s. Até 2000
          pares por report; par com `ts` fora de ±1 h do relógio do LOCC é descartado
          (`samples_dropped` na resposta).

        **`gauge_sets`** escolhe o conjunto de RRAs (resolução × retenção) da série:
        `{"net.bytes_in": "network"}`. Vale só na **criação** — em série que já existe é
        ignorado, porque trocar o conjunto invalidaria os pontos gravados. Conjunto
        inexistente é erro (`107`), não silêncio. Conjuntos semeados: `default` (uso geral),
        `network` (6 h a 5 s), `slow` (capacidade) e `daily` (alinhado a 00:00 BRT).
      security: [ { liamBearerAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project, server_name]
              properties:
                project:     { type: string, example: EP-MSH }
                server_name: { type: string, example: br1ds.laniway.net }
                instance:    { type: string, description: "vários contêineres do projeto no mesmo host" }
                ts:          { type: integer, description: "epoch do nó" }
                checks:      { type: array, items: { $ref: "#/components/schemas/Check" } }
                gauges:      { type: object, additionalProperties: { type: number }, example: { "sessions.imap": 41, "disk.maildir.pct": 63.2 } }
                samples:     { type: object, description: "chave -> lista de pares [ts, valor]", additionalProperties: { type: array, items: { type: array, items: { type: number } } }, example: { "net.bytes_in": [[1787156820, 118203], [1787156825, 96110]] } }
                gauge_sets:  { type: object, description: "chave -> conjunto de RRAs; só vale na criação da série", additionalProperties: { type: string }, example: { "net.bytes_in": "network", "disk.maildir.pct": "slow" } }
      responses:
        "200": { description: "accepted, status do nó, transições de checks, series/samples gravados, samples_dropped" }
        "403": { description: "5001001 (não é AppToken) · 5001002 (projeto ≠ identidade)" }
  /evaluate/v1.0:
    post:
      summary: Tick do avaliador (interno)
      description: Permissão `locc.evaluate.run`. Marca stale, abre/fecha alertas, notifica (TEP), grava `evaluation_run`.
      security: [ { liamBearerAuth: [] } ]
      responses:
        "200": { description: contadores do tick }
  /node/list/v1.0:
    post:
      summary: Lista nós (status, último report, alertas abertos)
      security: [ { liamBearerAuth: [] } ]
      requestBody: { content: { application/json: { schema: { type: object, properties: { project: { type: string } } } } } }
      responses: { "200": { description: nós } }
  /node/get/v1.0:
    post:
      summary: Um nó — checks, séries, último payload
      security: [ { liamBearerAuth: [] } ]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [node_id], properties: { node_id: { type: string } } } } } }
      responses: { "200": { description: nó }, "404": { description: não encontrado } }
  /series/fetch/v1.0:
    post:
      summary: Buckets de um gauge numa janela (o gráfico)
      description: |
        Escolhe, **dentro do conjunto da série**, o RRA de maior resolução que cobre a janela
        (ou o `rra_id` pedido). Default: últimas 24 h. A resposta traz `rra_set` e o `rra`
        usado (com `offset_seconds`, o alinhamento do bucket).
      security: [ { liamBearerAuth: [] } ]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [node_id, gauge], properties: { node_id: { type: string }, gauge: { type: string }, since_ts: { type: integer }, until_ts: { type: integer }, rra_id: { type: integer } } } } } }
      responses: { "200": { description: "rra, since/until, points[{ts, avg, min, max, count}]" } }
  /node/set_active/v1.0:
    post:
      summary: Liga/desliga a observação de um nó (drenado/desligado de propósito não gera stale)
      security: [ { liamBearerAuth: [] } ]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [node_id, active], properties: { node_id: { type: string }, active: { type: boolean } } } } } }
      responses: { "200": { description: ok } }
