mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
fix(extensoes): a falha de banco registra a causa, e os textos públicos deixam de afirmar o que não vale
Uma verificação adversarial de prontidão para merge (cinco lentes e um
crítico, somente leitura) achou defeitos em texto público e um silêncio
em código deste PR.
Código. A tela inicial do CRM, que é núcleo, lê as extensões a cada
render e engolia a falha num `catch {}` sem log. Pior: a `dbFailure`
converte qualquer erro desconhecido do banco em `upstream_unavailable` e
descartava o código e a mensagem originais. No caminho da Vercel, onde o
código pode chegar antes da migration, todo usuário veria o aviso de
extensões e nenhum log diria que a tabela não existe. Agora a
`dbFailure` registra `db_code` e o detalhe onde a causa ainda existe, e
a página registra que o hub degradou. Teste: o erro desconhecido fica no
log com a causa; o código de domínio que a migration levanta de
propósito (P0001) não vira ruído.
Textos públicos:
- a D4 da ADR-0002 afirmava que a conexão de dono do banco não chega aos
contêineres. Chega: o compose entrega o .env inteiro ao app e ao
worker, e o gate citado só proíbe o código de usá-la. O argumento de
segurança passa a se apoiar no que é verdade — a provisionadora não dá
ao app DDL que ele possa escolher;
- a D5 e a doutrina diziam "as proteções que o baseline aplica a toda
tabela"; viraram a regra normativa ("que toda tabela de organização
precisa ter"), sem afirmar o comportamento do baseline;
- o CLAUDE.md anunciava cinco não-negociáveis e listava seis; o número
saiu;
- seis citações de commit apontavam para uma branch de trabalho que
nunca foi publicada; passam aos equivalentes publicados, cujas árvores
só diferem pela pasta de documentos internos;
- a pesquisa dizia em que branch local havia cópia de documentos
internos; agora aponta a doutrina e a ADR, que são públicas.
O aviso de release acrescenta o efeito para quem opera: enquanto houver
guia em preparação, a atualização pela tela espera, e a tela diz como
retomar ou cancelar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,6 @@ titulo: Guias opcionais podem ser instalados e ativados por organização
|
||||
|
||||
A área Extensões permite ao responsável pela instalação admitir um catálogo revisado e baixar guias declarativos sem reconstruir o aplicativo. Cada organização escolhe quais guias ativar e como apresentá-los no CRM; desativar preserva a configuração. Os pedidos ficam registrados, com retomada e cancelamento de preparações interrompidas. O conteúdo instalado continua disponível quando o catálogo está fora do ar.
|
||||
|
||||
O responsável pela instalação também atualiza um guia para outra versão do catálogo, desfaz a última troca mesmo com o catálogo fora do ar e remove um guia da instalação. Remover desliga o guia em todas as organizações, guarda a configuração de cada uma e registra na auditoria de cada organização por que ele saiu; ao reinstalar, cada organização decide se ativa de novo.
|
||||
O responsável pela instalação também atualiza um guia para outra versão do catálogo, desfaz a última troca mesmo com o catálogo fora do ar e remove um guia da instalação. Enquanto houver um guia sendo preparado, a atualização do sistema pela tela espera; a própria tela diz como retomar ou cancelar essa preparação em Extensões. Remover desliga o guia em todas as organizações, guarda a configuração de cada uma e registra na auditoria de cada organização por que ele saiu; ao reinstalar, cada organização decide se ativa de novo.
|
||||
|
||||
Este primeiro perfil aceita apenas conteúdo e ações conhecidas do sistema. Código externo e o catálogo público com avaliações ainda não são oferecidos. O sistema continua funcionando com zero extensões, e a atualização normal aplica as tabelas necessárias, sem variável obrigatória nem edição manual de arquivo. A única variável nova, `EXTENSIONS_LOCAL_CATALOG_ORIGIN`, é de laboratório e fica vazia por padrão.
|
||||
|
||||
@@ -286,7 +286,7 @@ contrato que existe hoje em
|
||||
[`docs/specs/extensoes-declarativas-v1.md`](docs/specs/extensoes-declarativas-v1.md).
|
||||
A pergunta que decide o destino de uma mudança não é "isto serve a muita gente?",
|
||||
e sim **"se nenhuma organização ativar isto, a operação comum continua inteira?"**.
|
||||
O não-negociável, em cinco linhas:
|
||||
O não-negociável:
|
||||
|
||||
1. **O núcleo continua útil com zero extensões.** Identidade, autorização,
|
||||
isolamento, auditoria, contratos e cadeia de envio são núcleo; jornada de nicho,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { NavHub } from "@/components/shell/NavHub";
|
||||
import { requireAuth, resolveActiveOrg } from "@/lib/auth/server";
|
||||
import { loadCrmExtensions } from "@/lib/extensions/service";
|
||||
import { logger } from "@/lib/logger";
|
||||
import type { ExtensionGuideView } from "@/lib/extensions/view";
|
||||
import { traduzir } from "@/lib/i18n/dicionario";
|
||||
|
||||
@@ -34,9 +35,15 @@ export default async function CrmHubPage() {
|
||||
if (activeOrg) {
|
||||
try {
|
||||
extensionGuides = await loadCrmExtensions(activeOrg.orgId);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// O hub continua útil sem extensões, mas a falha precisa ser distinguível
|
||||
// de uma lista legitimamente vazia. A gestão oferece a reconciliação.
|
||||
// de uma lista legitimamente vazia. A gestão oferece a reconciliação — e o
|
||||
// log diz que o hub degradou, para a falha não existir só na tela.
|
||||
const code = (error as { code?: unknown } | null)?.code;
|
||||
logger.warn("[crm] hub aberto sem as orientações das extensões", {
|
||||
organization_id: activeOrg.orgId,
|
||||
error_code: typeof code === "string" ? code : null,
|
||||
});
|
||||
extensionsUnavailable = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
- **Status:** aceito em 2026-09-17 pelo dono do produto
|
||||
- **Data:** 2026-09-17
|
||||
- **Contexto medido em:** `84788aa64` (`main`) e `e24a2b94e` (branch de extensões)
|
||||
- **Contexto medido em:** `84788aa64` (`main`) e na branch do PR #1016
|
||||
- **Lei que muda quando aceita:** [`docs/doctrine/extensoes.md`](../doctrine/extensoes.md), não-negociável 9 e a linha "Schema próprio de extensão" da tabela do que ainda não existe
|
||||
|
||||
---
|
||||
@@ -100,8 +100,11 @@ porque:
|
||||
0167).
|
||||
- **É idempotente.** Chamá-la de novo não muda nada além do que a primeira chamada fez.
|
||||
- **A execução é só de `service_role`**: `revoke execute … from public, anon, authenticated`.
|
||||
- **A separação de DDL continua:** DDL arbitrária segue exigindo a conexão de dono, que não vai para
|
||||
os contêineres (`tests/unit/env-ddl-fora-do-app.test.ts`).
|
||||
- **Não é a separação de DDL que sustenta o argumento.** A conexão de dono do banco
|
||||
(`SUPABASE_DB_ADMIN_URL`) **chega aos contêineres** quando declarada — o `docker-compose.prod.yml`
|
||||
entrega o `.env` inteiro ao app e ao worker —, e o que existe hoje é um gate que proíbe o código do
|
||||
app de usá-la (`tests/unit/env-ddl-fora-do-app.test.ts`). O argumento desta decisão é o anterior:
|
||||
a provisionadora não dá ao app nenhuma DDL que ele possa escolher.
|
||||
- **Um invariante novo reprova** função provisionadora com parâmetro, com `execute` concedido a
|
||||
qualquer papel além de `service_role`, ou com corpo que referencie tabela de fora do módulo.
|
||||
|
||||
@@ -109,10 +112,10 @@ O caminho manual de self-host concede `execute` em todas as funções de `public
|
||||
(`docs/deploy-selfhost/README.md:96`). Quando esta ADR for aceita, esse passo passa a revogar
|
||||
explicitamente as funções provisionadoras, e o invariante confere o resultado depois do grant.
|
||||
|
||||
### D5 — A função termina aplicando as proteções que o baseline aplica a toda tabela
|
||||
### D5 — A função termina aplicando as proteções que toda tabela de organização precisa ter
|
||||
|
||||
Tabela criada fora do baseline não recebe as proteções que ele aplica em laço. Por isso a função
|
||||
provisionadora termina, **na mesma transação**, chamando as mesmas rotinas de proteção do baseline:
|
||||
Tabela criada fora do baseline não recebe sozinha as proteções que ele aplica ao catálogo. Por isso a
|
||||
função provisionadora termina, **na mesma transação**, chamando as rotinas de proteção:
|
||||
RLS ligada, `revoke all … from anon`, isolamento por organização e as policies restritivas que
|
||||
valem para sessão de suporte. Essas rotinas saem do laço do baseline para funções sem parâmetro,
|
||||
chamadas pelo baseline e pela provisionadora. A prova de que a tabela recém-provisionada está
|
||||
|
||||
@@ -10,8 +10,8 @@ Esta é a **lei**. As decisões e o que foi recusado vivem nos documentos de dec
|
||||
|
||||
| Se você quer… | Vá para |
|
||||
|---|---|
|
||||
| o critério núcleo × extensão aprovado e o desenho do programa inteiro | `Decisão Implementações/PROG-017 — Extensões — arquitetura e contratos.md` |
|
||||
| as políticas de publicação, incidentes e métricas (Rafael aprovou A nas três) | `Decisão Implementações/DEC-004 — Extensões — publicação, incidentes e métricas.md` |
|
||||
| o critério núcleo × extensão aprovado e o desenho do programa inteiro | `Decisão Implementações/PROG-017 — Extensões — arquitetura e contratos.md` (documento interno de decisão, fora deste repositório; o que vale para PR está nesta doutrina) |
|
||||
| as políticas de publicação, incidentes e métricas (Rafael aprovou A nas três) | `Decisão Implementações/DEC-004 — Extensões — publicação, incidentes e métricas.md` (documento interno; as três políticas estão no não-negociável 13) |
|
||||
| o contrato que existe hoje (pacote, catálogo, RPCs, portas HTTP, versões) | [`../specs/extensoes-declarativas-v1.md`](../specs/extensoes-declarativas-v1.md) |
|
||||
| classificar o destino de um PR de contribuidor | [`../../triagem/TRIAGEM.md`](../../triagem/TRIAGEM.md), seção 2-bis |
|
||||
| o mapa das peças e das arestas | [`../architecture/extensoes-declarativas.architecture.json`](../architecture/extensoes-declarativas.architecture.json) |
|
||||
@@ -100,8 +100,8 @@ preserve o trabalho do contribuidor e registre a dependência.
|
||||
aceita em 17/09/2026): um banco só e o schema `public`; as tabelas nascem por uma função
|
||||
provisionadora fixa do módulo — sem parâmetro, executável só por `service_role`, entregue pela
|
||||
tripla de sempre — quando o módulo é **instalado na instância**, nunca na ativação por
|
||||
organização. A função aplica na mesma transação as proteções que o baseline aplica a toda
|
||||
tabela; reaplicar nas atualizações é explícito e falha alto; anonimização, export e varreduras
|
||||
organização. A função aplica na mesma transação as proteções que toda tabela de
|
||||
organização precisa ter; reaplicar nas atualizações é explícito e falha alto; anonimização, export e varreduras
|
||||
alcançam as tabelas do módulo. Pacote de terceiro continua sem trazer SQL.
|
||||
|
||||
10. **Publicar espera o sistema; tirar não espera.** Preparar, concluir e desfazer recusam enquanto
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Marco 2 — bases reais para instalação declarativa
|
||||
|
||||
**Referência:** `44dcdafa8`, branch `feat/extensoes-declarativas-20260914`, já integrada a `origin/main` @ `60079eb5`. Investigação de 14/set/2026, somente leitura de código; nenhum serviço iniciado, teste/migration executado ou credencial acessada. **CONFIRMADO** indica código lido; **INFERIDO** indica consequência sem ensaio; **NECESSÁRIO** traduz o aceite aprovado em trabalho ainda não implementado.
|
||||
**Referência:** `b3a056b85`, na branch do PR #1016, já integrada a `origin/main` @ `60079eb5`. Investigação de 14/set/2026, somente leitura de código; nenhum serviço iniciado, teste/migration executado ou credencial acessada. **CONFIRMADO** indica código lido; **INFERIDO** indica consequência sem ensaio; **NECESSÁRIO** traduz o aceite aprovado em trabalho ainda não implementado.
|
||||
|
||||
**Conclusão:** podemos reutilizar preparação de conteúdo, versões imutáveis, publicação por ponteiro, Storage privado, autorização canônica e pedido durável ao host. Não existe nessas bases um catálogo de extensões separado nem um instalador completo com confiança de origem. Ativar implementação já compilada não demonstra baixar e instalar pacote novo sem rebuild.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Marco 2 — o que será impactado
|
||||
|
||||
Referência informada: branch `feat/extensoes-declarativas-20260914`, HEAD `44dcdafa`, incorporando `origin/main` `60079eb5`. Investigação por leitura de código em 14/set/2026; não executou SQL, serviços, testes ou jornadas. `CONFIRMADO` abaixo significa observado no código, não comportamento medido nesta rodada.
|
||||
Referência informada: a branch do PR #1016, commit `b3a056b85`, incorporando `origin/main` `60079eb5`. Investigação por leitura de código em 14/set/2026; não executou SQL, serviços, testes ou jornadas. `CONFIRMADO` abaixo significa observado no código, não comportamento medido nesta rodada.
|
||||
|
||||
## Fronteira aprovada
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Integração declarativa — riscos e mínimo viável
|
||||
|
||||
14/set/2026 · leitura em `feat/extensoes-declarativas-20260914`, HEAD `44dcdafa8`.
|
||||
14/set/2026 · leitura na branch do PR #1016, commit `b3a056b85`.
|
||||
|
||||
Escopo: marco 2; pesquisa documental, sem implementação, serviços, testes ou SQL executados.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Os relatórios foram produzidos por agentes distintos e reconciliados na síntes
|
||||
|
||||
## Escopo e preservação do trabalho existente
|
||||
|
||||
A pesquisa usa a branch `docs/plataforma-extensoes-20260914`, numa worktree própria. A pasta principal estava em outra branch, com alterações de outras sessões. Esses arquivos não foram incorporados nem revertidos. Os documentos solicitados ficam em `Decisão Implementações`; a branch da pesquisa preserva uma cópia versionada e as evidências técnicas.
|
||||
A pesquisa foi feita numa worktree própria, sem incorporar nem reverter alterações de outras sessões. As decisões que ela alimentou são documentos internos, fora deste repositório; o que vale para PR está na [doutrina de extensões](../../doctrine/extensoes.md) e na [ADR-0002](../../adr/0002-tabelas-de-modulo-num-banco-so.md).
|
||||
|
||||
O diagrama da proposta descreve responsabilidades futuras. Ele não foi publicado como mapa de componentes já operacionais, nem autoriza declarar a arquitetura implantada. Escolhas técnicas que dependem de medição permanecem explícitas no plano de provas.
|
||||
|
||||
@@ -42,7 +42,7 @@ O [runbook da bancada](../../../experiments/extensoes/README.md) permite repetir
|
||||
|
||||
## Primeira integração ao CRM
|
||||
|
||||
A branch `feat/extensoes-declarativas-20260914` preserva a bancada e incorpora `origin/main` em `60079eb5`. O PROG-021 *(documento interno de decisão)* acompanha implementação e provas do marco 2. O [contrato v1](../../specs/extensoes-declarativas-v1.md) define o pacote declarativo, admissão e jornada.
|
||||
A bancada foi preservada na branch deste PR, que incorpora `origin/main`. O PROG-021 *(documento interno de decisão)* acompanha implementação e provas do marco 2. O [contrato v1](../../specs/extensoes-declarativas-v1.md) define o pacote declarativo, admissão e jornada.
|
||||
|
||||
| Investigação | Foco desta integração |
|
||||
|---|---|
|
||||
|
||||
@@ -2152,15 +2152,15 @@ Testes: `tests/e2e/agenda-google-meet.spec.ts`, `tests/invariants/agenda-meet.te
|
||||
|
||||
Specs: `tests/e2e/extensoes-declarativas.spec.ts` e `tests/e2e/extensoes-recuperacao.spec.ts`.
|
||||
Estado: **as duas passaram inteiras em 16/09/2026**, sobre o build `mpuyz81eEv5s9QLf96iqr` gerado do
|
||||
commit `6a5d46710` — a branch já integrada com a `main` —, a principal em 39,1 s e a de recuperação em
|
||||
commit `2bce4b7ec` — a branch já integrada com a `main` —, a principal em 39,1 s e a de recuperação em
|
||||
16,6 s, rodando sozinhas depois de duas tentativas mortas por ambiente (a fixture recebeu
|
||||
`Processing this request timed out` com a máquina em load 53; depois o servidor de teste foi morto
|
||||
com 0,06 GB livres). A primeira vez que passaram inteiras foi em 15/09, sobre o build
|
||||
`ALAqeLI0VQJi4bpWWFbUL` do commit `b4b186219`. Foram sete
|
||||
`ALAqeLI0VQJi4bpWWFbUL` do commit `5a19ce8fa`. Foram sete
|
||||
rodadas até lá: quatro defeitos da própria prova (espera por URL que a aba já tinha, seletor
|
||||
`data-slot` que o Card do repositório não tem, clique no cabeçalho rolado para fora da vista, prazo
|
||||
de 5 s em asserções que dependem de duas idas ao servidor) e um defeito de produto que só ela achou
|
||||
(a aba original não recarregava depois que outra aba reconciliava o recibo). Entre `b4b186219` e o
|
||||
(a aba original não recarregava depois que outra aba reconciliava o recibo). Entre `5a19ce8fa` e o
|
||||
HEAD, `git diff --stat b4b186219..HEAD -- app lib components` só mostra arquivos de voz vindos da
|
||||
`main`, um comentário e as frases de voz no dicionário — nada do caminho das extensões. A fixture
|
||||
recusa credenciais fora das portas locais dedicadas, cria usuários e organizações exclusivos,
|
||||
@@ -2201,9 +2201,9 @@ religar o catálogo no meio da jornada. Evidência: `evidence/extensoes/versao/`
|
||||
catálogo daquela rodada).
|
||||
|
||||
Estado: **passou inteira em 16/09/2026**, em 28,3 s, sobre o build `mpuyz81eEv5s9QLf96iqr` (do
|
||||
commit `6a5d46710`, a branch já integrada com a `main`), na mesma rodada das duas specs do J25. As
|
||||
commit `2bce4b7ec`, a branch já integrada com a `main`), na mesma rodada das duas specs do J25. As
|
||||
capturas abaixo são dessa rodada. Antes dela: seis rodadas até a primeira vez inteira (sobre o build
|
||||
`FL9GZvWqPoj8aE9XSY2E_`, commit `540a76082`), cujos defeitos da própria prova estão listados abaixo,
|
||||
`FL9GZvWqPoj8aE9XSY2E_`, commit `213dee0d4`), cujos defeitos da própria prova estão listados abaixo,
|
||||
e duas repetições mortas por ambiente — uma na fixture com a máquina em load 53, outra com o servidor
|
||||
de teste morto por falta de memória.
|
||||
|
||||
|
||||
@@ -409,6 +409,32 @@ describe("listExtensions: removidas e conferência da plataforma", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("falha do banco sem código conhecido", () => {
|
||||
it("fica registrada com a causa do banco, e o erro de domínio esperado não vira ruído no log", async () => {
|
||||
let resposta: Result = {
|
||||
data: null,
|
||||
error: { code: "42P01", message: 'relation "public.extension_installations" does not exist' },
|
||||
};
|
||||
mocks.admin = fakeClient({}, { fn_extensions_remove_installation: () => resposta });
|
||||
|
||||
await expect(
|
||||
removeExtension(ACTOR, randomUUID(), randomUUID(), { expected_installation_revision: 1 }),
|
||||
).rejects.toMatchObject({ code: "upstream_unavailable", status: 503 });
|
||||
expect(mocks.warn).toHaveBeenCalledWith("[extensions] falha do banco sem código conhecido", {
|
||||
db_code: "42P01",
|
||||
detail: 'relation "public.extension_installations" does not exist',
|
||||
});
|
||||
|
||||
// Controle: um código que a migration levanta de propósito é resposta, não falha a investigar.
|
||||
mocks.warn.mockReset();
|
||||
resposta = { data: null, error: { code: "P0001", message: "extension_removed" } };
|
||||
await expect(
|
||||
removeExtension(ACTOR, randomUUID(), randomUUID(), { expected_installation_revision: 1 }),
|
||||
).rejects.toMatchObject({ code: "extension_removed", status: 410 });
|
||||
expect(mocks.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("auditoria só quando a chamada fez a transição", () => {
|
||||
it("remover grava a linha da instância e uma por organização desligada; a repetição não grava", async () => {
|
||||
const installation = randomUUID();
|
||||
|
||||
@@ -117,6 +117,13 @@ function dbFailure(error: { code?: string; message?: string } | null): void {
|
||||
const known = error.code === "P0001" && error.message ? SQL_ERRORS[error.message] : undefined;
|
||||
if (known && error.message)
|
||||
throw new ExtensionServiceError(error.message, known.message, known.status);
|
||||
// O código e a mensagem do banco só existem aqui: o erro que sobe é genérico de propósito, porque
|
||||
// a tela não mostra detalhe do banco. Sem este registro, "a tabela não existe" — um deploy cujo
|
||||
// código chegou antes da migration — virava um aviso na tela de todo mundo e nada no log.
|
||||
logger.warn("[extensions] falha do banco sem código conhecido", {
|
||||
db_code: error.code ?? null,
|
||||
detail: (error.message ?? "").slice(0, 200),
|
||||
});
|
||||
throw new ExtensionServiceError(
|
||||
"upstream_unavailable",
|
||||
"Não foi possível confirmar o resultado. Consulte o histórico antes de repetir o pedido.",
|
||||
|
||||
Reference in New Issue
Block a user