## API dos produtos

A API do IDeia Design guarda os dados de cada produto: os textos e os cards do rodapé, as cores, os logos e o
`globals.css`. Os sistemas leem dela sem login. Quem escreve é o painel administrativo, e só o Sysadmin entra nele.

- **Endereço:** `https://design-api.ideiabusiness.com.br/api/v1`
- **Contrato:** https://design-api.ideiabusiness.com.br/api-docs (o Swagger, para ler e testar no navegador) e
  https://design-api.ideiabusiness.com.br/api-docs-json (o OpenAPI, para gerar os tipos).
- **Painel:** https://design-admin.ideiabusiness.com.br

## Leitura dos sistemas (sem login)

| Método e caminho | O que devolve |
|---|---|
| `GET /footer?product=<slug>` | O rodapé (2.21): os textos da empresa e os cards dos produtos, em ordem alfabética, sem o card do sistema que pede. Sem `product`, vêm todos |
| `GET /products` | Os produtos que não estão arquivados, em ordem alfabética, com os textos, as cores e os logos. `metadata.total` diz quantos são |
| `GET /products/<slug>` | Um produto |
| `GET /products/<slug>/globals.css` | O `globals.css` do produto (shadcn/ui + Tailwind 4), o mesmo do `get_tokens` do MCP. Vem o arquivo, não JSON |
| `GET /health` | Se a API e o banco respondem: `status` `ok` ou `degraded` |
| `GET /guide/products` | Os produtos no formato do guia, com a cor do logo e as medidas de cada PNG: o que o build do site e o MCP leem. Os sistemas usam `/products` |

- `<slug>` é o do produto no guia (`ideia-chat`, `ideia-devtasks`…): a lista sai de `GET /products` ou do
  `list_products` do MCP. Só letras minúsculas sem acento, números e hífen.
- **Sem login, de qualquer origem:** CORS aberto, só `GET`. Não mande cookie nem `Authorization`.
- **Cache:** `Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400`. A borda da Vercel guarda a
  resposta por 5 minutos; passado esse tempo, ainda entrega a anterior por até um dia enquanto busca a nova.
- **Leia em SSG, consultando a API 1 ou 2 vezes por dia** (D38 e D47): a página sai pronta do build e o sistema só
  volta à API quando a revalidação vence, a cada 12 ou 24 horas (no Next, `fetch` com `next: { revalidate: 43200 }`
  ou `86400`). Em tela com login, que se monta a cada pedido, o `fetch` vem do cache do Next, não da API. Nunca SSR
  sem cache (uma chamada por página), nunca do navegador, e nada de `cache: "no-store"`, `revalidate: 0` ou
  `fetchCache = "force-no-store"` nesse caminho. Uma mudança no painel chega a todos os sistemas em até um dia.
- **Com a API fora,** o sistema segue com a última resposta boa: o `fetch` do Next só guarda resposta 200. Sem nenhuma
  guardada, o que depende dela não aparece (o rodapé, por exemplo) e a página segue. A API nunca derruba uma tela.
- **Produto arquivado** (D37): sai de `/products` e do rodapé, e `/products/<slug>` responde 404. As URLs dos logos
  dele continuam no ar.
- **Logos:** use a URL que a API devolve. Ela traz o `?v=` da versão atual, e um logo trocado chega com uma URL nova,
  sem esperar cache. Em `logos` só estão as variantes que o produto tem (1.1). Na tela, `<img>` ou
  `<Image unoptimized>`: o arquivo vai direto para o navegador, sem passar pelo otimizador de imagens.
- **Cores:** ficam compiladas no `globals.css` de cada sistema, e a API não muda a cor de um sistema que já está no ar.
  Depois de uma troca de cor no painel, o sistema baixa o `globals.css` de novo.
- **O guia também lê daqui.** O build do site lê os produtos da API, e o painel pede um build novo a cada escrita: uma
  cor, um texto ou um logo trocado no painel chega ao guia, à /live e ao MCP em cerca de um minuto.

### O rodapé: `GET /footer?product=ideia-chat`

| Campo | O que é |
|---|---|
| `company.brandName` | O nome da marca: "Um produto da …" e o nome da seta, "Produtos da …" |
| `company.legalName`, `company.cnpj` | A razão social e o CNPJ, com a máscara, na linha do rodapé |
| `company.email` | O e-mail de contato, na linha do rodapé |
| `company.title`, `company.tagline` | O título e a frase da parte estendida |
| `company.aboutUrl` | Para onde o "Quem está por trás" leva |
| `products[].slug`, `products[].name` | O produto do card |
| `products[].description` | O texto do card: o do rodapé no cadastro do produto; vazio, o do login |
| `products[].url` | O endereço do produto; `null`, o card sai sem link |
| `products[].icon` | O ícone claro (o logo claro, se o produto não tem ícone); `null`, sem imagem |

```json
{
  "success": true,
  "data": {
    "company": {
      "brandName": "Ideia Business",
      "legalName": "Ideia Business Soluções Tecnológicas LTDA",
      "cnpj": "30.082.914/0001-00",
      "email": "contato@ideiabusiness.com.br",
      "title": "Os negócios por trás deste produto",
      "tagline": "Mais de 20 anos empreendendo, mais de 500 mil empresas atendidas e uma rede com mais de 5 mil contadores.",
      "aboutUrl": "https://ideiabusiness.com.br"
    },
    "products": [
      {
        "slug": "ideia-devtasks",
        "name": "IDeia DevTasks",
        "description": "Crie tickets com ajuda de IA e acompanhe o time no kanban.",
        "url": null,
        "icon": "https://cdn.ideiabusiness.com.br/logos/ideia-devtasks/ideia-devtasks-icone-clara.png?v=7832ddae0d57"
      }
    ]
  },
  "metadata": { "timestamp": "2026-10-29T22:43:16.710Z", "version": "v1" }
}
```

### Um produto: `GET /products/ideia-chat`

| Campo | O que é |
|---|---|
| `slug` | Fixo: não muda depois de criado. É também o nome dos arquivos dos logos |
| `name` | O nome do produto |
| `family` | `ideia` ou `propria` (1.1) |
| `pitch`, `description` | O título e o texto do painel do login (2.20) |
| `footerDescription` | O texto do card do rodapé (2.21); vazio, vale o `description` |
| `siteUrl` | O endereço do produto, para onde o card do rodapé leva; `null` quando não tem |
| `showInFooter` | Se o card do produto aparece no rodapé dos outros sistemas |
| `colors.primary` | A primária de cada tema e o texto sobre ela: `light`, `lightForeground`, `dark` e `darkForeground` (1.2) |
| `colors.brand` | A primária clara, fixa nos dois temas: só o painel do login (2.20) |
| `colors.link` | O texto de link de cada tema, `light` e `dark` (1.2) |
| `logos` | A URL de cada PNG que existe: `clara`, `escura`, `icone-clara` e `icone-escura` (1.1) |

```json
{
  "success": true,
  "data": {
    "slug": "ideia-chat",
    "name": "IDeia Chat",
    "family": "propria",
    "pitch": "Atendimento multicanal pelo WhatsApp",
    "description": "Filas, SLA e dashboards para o seu time atender melhor.",
    "footerDescription": "Atendimento multicanal pelo WhatsApp, com filas, SLA e dashboards.",
    "siteUrl": null,
    "showInFooter": true,
    "colors": {
      "primary": { "light": "#265e9f", "lightForeground": "#ffffff", "dark": "#2e71c0", "darkForeground": "#ffffff" },
      "brand": "#265e9f",
      "link": { "light": "#265e9f", "dark": "#5c90ce" }
    },
    "logos": {
      "clara": "https://cdn.ideiabusiness.com.br/logos/ideia-chat/ideia-chat-clara.png?v=985d0c40f5c8",
      "escura": "https://cdn.ideiabusiness.com.br/logos/ideia-chat/ideia-chat-escura.png?v=1096dde5c415",
      "icone-clara": "https://cdn.ideiabusiness.com.br/logos/ideia-chat/ideia-chat-icone-clara.png?v=19c9a6d7496c",
      "icone-escura": "https://cdn.ideiabusiness.com.br/logos/ideia-chat/ideia-chat-icone-escura.png?v=251c730a4a82"
    }
  },
  "metadata": { "timestamp": "2026-10-29T22:47:50.483Z", "version": "v1" }
}
```

## O envelope e os erros

Toda resposta em JSON vem no mesmo envelope: `success`, `data` (ou `error`, quando falha) e `metadata`, com
`timestamp`, `version` e, nas listas, `total`. Só o `globals.css` vem como arquivo.

```json
{
  "success": false,
  "error": {
    "code": "PRODUCT_NOT_FOUND",
    "message": "Produto não encontrado: nao-existe"
  },
  "metadata": {
    "timestamp": "2026-10-29T22:47:50.675Z",
    "version": "v1"
  }
}
```

| Status | `error.code` | Quando |
|---|---|---|
| 404 | `PRODUCT_NOT_FOUND` | O slug não existe ou o produto está arquivado |
| 404 | `ROUTE_NOT_FOUND` | O caminho não existe |
| 422 | `VALIDATION_ERROR` | Parâmetro inválido; `error.details` traz cada campo, com `path` e `message` |
| 500 | `INTERNAL_ERROR`, `FOOTER_NOT_CONFIGURED` | Falha da API: siga com a última resposta boa |

As rotas do painel respondem também 401 (sem sessão), 403 (sem o papel Sysadmin), 409 `SLUG_TAKEN` e 422
`CONTRAST_FAILED` ou `LOGO_INVALID`. A lista completa está no contrato.

## O painel (com login)

Só o painel administrativo escreve, e só o Sysadmin entra nele (D34). Nele ficam **Cadastros › Produtos** (cadastrar,
editar os textos, as cores e o rodapé de cada produto, arquivar) e **Configurações › Rodapé** (os textos da empresa).
Os sistemas de produto nunca chamam estas rotas.

| Método e caminho | O que faz |
|---|---|
| `POST /auth/login` | Entra com e-mail e senha e recebe o token (JWT) |
| `POST /auth/github` | Entra pelo GitHub: só quem é da organização Ideia-Business e está cadastrado como Sysadmin |
| `GET /auth/me`, `POST /auth/logout` | Quem está na sessão; sair |
| `GET /admin/products` | A lista, com busca (`search`), família (`family`), situação (`status`: `ativos`, `arquivados` ou `todos`), rodapé (`footer`) e em revisão (`review`) |
| `POST /admin/products` | Cadastra um produto. O que faltar das cores sai da receita do guia (1.2) |
| `GET /admin/products/<slug>`, `PATCH /admin/products/<slug>` | Lê o produto, com as cores e os logos; edita só os campos enviados |
| `POST /admin/products/<slug>/archive`, `POST /admin/products/<slug>/restore` | Arquiva (D37) e tira do arquivo |
| `POST /admin/products/<slug>/logos/<variante>/uploads` | Assina o envio de um logo: o navegador sobe o PNG direto no armazenamento, num link que vale 5 minutos |
| `POST /admin/products/<slug>/logos/<variante>` | Confere o arquivo enviado e publica como versão nova: a URL fixa troca e o cache do CDN é limpo |
| `POST /admin/products/<slug>/logos/<variante>/versions/<n>/restore` | Volta o logo para uma versão guardada |
| `POST /admin/colors/derive` | Sugere a primária clara, a escura e o texto sobre elas a partir de uma cor, com o contraste do resultado |
| `GET /admin/footer`, `PATCH /admin/footer` | Os textos da empresa no rodapé (D40) |

- **Sessão:** o token vai no cabeçalho `Authorization: Bearer <token>`. No painel, ele fica num cookie httpOnly, e
  quem o manda à API é o servidor do painel (BFF): o navegador nunca o vê. A API lê o usuário no banco a cada pedido,
  e quem é desativado perde o acesso na hora.
- **Contraste:** toda cor salva passa pelas contas do guia (1.2); a que reprova é recusada com 422 `CONTRAST_FAILED`
  e os motivos (D36).
- **Auditoria:** toda escrita grava quem fez, o quê, o antes e o depois.
- **Logos:** PNG com fundo transparente (pelo menos um canto), até 2 MB e 4096 px de lado; o ícone, quadrado, com
  128 px ou mais; o logo completo, com 64 px de altura ou mais (1.1). O que não serve volta com 422 `LOGO_INVALID` e
  os motivos. Cada envio guarda uma versão, e o mesmo arquivo de uma versão antiga volta para ela.

## No código (Next.js, no servidor)

O `lib/ideia-design.ts` abaixo lê o rodapé e o próprio produto em SSG, voltando à API a cada 12 horas. Os tipos
saem do contrato, com o `openapi-typescript` (o comando está no começo do arquivo). O componente do rodapé que recebe
esses dados está no 2.21.

```ts
// lib/ideia-design.ts — só servidor, em SSG: a API do guia é lida 1 ou 2 vezes por dia (D38, D47)
// Os tipos saem do contrato, uma vez e a cada mudança na API:
//   pnpm dlx openapi-typescript https://design-api.ideiabusiness.com.br/api-docs-json -o lib/ideia-design-api.d.ts
import type { components } from "./ideia-design-api";

type Rodape = components["schemas"]["PublicFooter"];
type Produto = components["schemas"]["PublicProduct"];

const API = "https://design-api.ideiabusiness.com.br/api/v1";
const SLUG = "ideia-chat";   // o slug deste sistema no guia (GET /products ou list_products do MCP)

async function ler<T>(caminho: string): Promise<T | null> {
  try {
    // 12 horas (86400: 24); nunca no-store nem revalidate: 0, que chamam a API a cada página
    const res = await fetch(`${API}${caminho}`, { next: { revalidate: 43200 } });
    if (!res.ok) return null;                                  // só a resposta 200 entra no cache: a última boa continua valendo
    return ((await res.json()) as { data?: T }).data ?? null;
  } catch {
    return null;                                               // API fora e nada guardado: a tela segue sem o que dependia dela
  }
}

// o rodapé (2.21): os textos da empresa e os cards, sem o deste sistema
export const getRodape = () => ler<Rodape>(`/footer?product=${SLUG}`);
// este produto: o título e o texto do login (2.20), as cores e as URLs dos logos, com ?v=
export const getProduto = () => ler<Produto>(`/products/${SLUG}`);

// no layout (servidor):
//   const [rodape, produto] = await Promise.all([getRodape(), getProduto()]);
//   … {rodape && <Footer rodape={rodape} />}
```

---
Exemplo vivo: https://design.ideiabusiness.com.br#api · Relacionados: components/rodape, style/marca-e-logo, style/cores, stacks/shadcn-tailwind/tokens · Fonte: guide/api.md
