Controle de Permissões de Acesso - SIGEEDU

Manual Técnico

Arquitetura, schema, controle de acesso e lógica de negócio deste painel, para quem for mantê-lo ou estendê-lo. Para o passo a passo de uso, veja o Manual do Usuário; para instalação/deploy, veja o README.md do projeto.

architecture 1. Arquitetura geral

Aplicação estática (HTML + JS vanilla + Tailwind via CDN), sem build step, seguindo o mesmo padrão dos demais projetos SIGEEDU em F:\CLAUDE (ex: remanejamento/). Toda a lógica de negócio e de acesso a dados vive no navegador, falando diretamente com o Supabase via supabase-js (chave pública anon); a segurança vem inteiramente de RLS + funções SECURITY DEFINER, nunca do front-end.

  • Sem framework: DOM manipulado diretamente, sem virtual DOM/reatividade.
  • Sem bundler: cada <script> é um arquivo solto, sem transpilação.
  • Login e conexão com o Supabase são compartilhados entre projetos: ../login.html e ../config.js ficam na raiz do servidor (um nível acima da pasta deste projeto), usados também por remanejamento/, main-relatorios/ etc.

folder_open 2. Estrutura de arquivos

index.html            - painel administrativo (exige login + permissão no módulo)
js/auth-guard.js      - protege index.html (SSO, permissão por módulo, expiração de sessão)
js/gestao.js          - lógica inteira da tela (5 abas + modais + upload de foto)
sql/001_migration.sql - schema-adjacent: tabelas já existiam; cria RPCs
                         iniciais e (histórico) políticas hoje removidas
sql/002_correcoes.sql - correção de timeout em auth.users + bucket "fotos"
sql/003_ajuste_rls.sql- remove a função/policies próprias do 001, migra
                         as RPCs para verifica_permissao_modulo
sql/004_relatorio_view.sql - view usada pelo botão "Relatórios" (ESTADO ATUAL)
README.md              - passo a passo de instalação/deploy
MANUAL_USUARIO.html    - guia de uso para administradores do painel
MANUAL_TECNICO.html    - este arquivo

Os quatro arquivos SQL são migrações sequenciais, não intercambiáveis - representam o histórico real do que foi executado no banco, nesta ordem: 001 → 002 → 003 → 004. Não edite 001/002/003 retroativamente; correções viram um novo arquivo (005_...sql e assim por diante).

table_chart 3. Modelo de dados

Tabelas que já existiam no banco (este projeto não as cria, só as usa/estende):

TabelaPapel neste projeto
public.main_permissao_usuariosTabela central: liga user_id + modulo + nivel_permissao (1-5) + locais (array de ids de instituicao, opcional)
public.main_modulosCatálogo de módulos do SIGEEDU (modulo text, PK lógica única; descricao; id_documento_tipo opcional)
public.documento_tipoCatálogo de tipos de documento (tipo, sigla, sigla_main)
public.permissao_usuariosRótulos dos níveis de permissão (id 1-5 tem significado fixo no código, ver seção 9.2 do modelo)
public.usuarios_cadastradosPerfil do usuário (PK = user_id, FK para auth.users); usada por muitos outros módulos do SIGEEDU (dezenas de colunas permissao_* legadas, ignoradas por este projeto)
auth.users / auth.identitiesSchema interno de autenticação do Supabase/GoTrue - nunca exposto via PostgREST, só via RPC SECURITY DEFINER
public.view_funiatView de RH (funcionários) - fonte de auto-preenchimento do cadastro
public.cargo, public.instituicaoCatálogos usados nos comboboxes de Função/Unidade e na composição da "Lotação"
storage.buckets / storage.objectsBucket fotos (criado por 002_correcoes.sql), pasta foto_funcionario/
public.view_permus_permissoesView criada por 004_relatorio_view.sql, usada só pelo botão "Relatórios" (SSO para main-relatorios) - uma linha por permissão concedida, já com nome/e-mail do usuário, descrição do módulo, rótulo do nível e nomes dos locais resolvidos

Não há migrations de CREATE TABLE neste projeto para as tabelas acima - elas foram fornecidas prontas. 003_ajuste_rls.sql adiciona uma constraint nova (única) em main_permissao_usuarios (user_id, modulo) para permitir upsert seguro no lote de concessão (seção 6).

vpn_key 4. Autenticação e SSO

Segue o mesmo padrão corrigido de kimape-modo-kiosk (referência atual para todos os módulos SIGEEDU - mais completo que o padrão antigo de remanejamento/, que não conferia permissão por módulo). Resumo:

  • js/auth-guard.js esconde a página, verifica sessão via supabase.auth.getSession(), e redireciona para ../login.html?pagina=permus.a se não houver sessão válida.
  • Aceita SSO por ?refresh_token=...&access_token=... na URL (aplica a sessão e limpa a URL imediatamente).
  • ../login.html tem uma entrada 'permus.a' (e também 'permus.relatorios') no mapa PAGINAS apontando para ./permus-permissoes-acesso-sigeedu/index.html, com o módulo derivado (permus) verificado contra main_permissao_usuarios no momento do login (mesmo mecanismo usado por todos os módulos).

4.1 Permissão por módulo verificada pelo próprio guard (não só no login)

warning

Correção de segurança importante: a sessão do Supabase é compartilhada entre todos os módulos do SIGEEDU (mesmo navegador/origem). A versão anterior deste auth-guard.js só conferia se havia uma sessão válida, não se o usuário tinha permissão para este módulo especificamente - então alguém logado em qualquer outro sistema (ex: kimape) conseguia abrir este painel diretamente pela URL, mesmo sem nenhuma permissão `permus` concedida. Corrigido: o guard agora consulta main_permissao_usuarios (modulo = 'permus') antes de liberar a tela, exatamente como login.html já fazia no momento do login.

conferirPermissao(user) devolve 'ok' (nível 1-4), 'negada' (sem registro, nível vazio ou 5) ou 'erro' (falha de conexão/consulta - por segurança, a página não é liberada nesse caso, só mostra "Tentar novamente"). Quando negada, uma tela de bloqueio em tela cheia (HTML/CSS inline, não depende de nada da página) aparece com dois botões: "Entrar com outro usuário" (logout local + volta ao login deste módulo) e "Sair". A permissão é reconferida a cada 5 minutos enquanto a página está aberta (RECONFERIR_PERMISSAO_A_CADA_MS), para o caso de alguém revogar o acesso com a pessoa já usando o painel.

A função guarda o nível encontrado em window.__nivelPermissaoPagina - gestao.js reaproveita esse valor (via exibirNivelUsuario()) para mostrar "Nível N - Rótulo" embaixo do e-mail no rodapé do menu lateral, sem precisar repetir a mesma consulta.

4.2 Expiração de sessão

O token de acesso dura pouco e o cliente Supabase o renova sozinho com o refresh token. Quando a renovação não é mais possível (sessão vencida/revogada no servidor), o guard limpa a sessão local e redireciona para ../login.html?pagina=permus.a&motivo=expirou (que mostra o aviso). Isso é verificado em três momentos:

  1. ao abrir a página (havia uma sessão guardada no localStorage, mas ela não pôde ser renovada);
  2. durante o uso: a cada minuto (VERIFICAR_SESSAO_A_CADA_MS), quando o Supabase emite SIGNED_OUT, ao voltar o foco da aba/janela, e ao voltar a conexão (evento online);
  3. falha de rede não é tratada como expiração (ehErroDeRede() filtra isso) - a pessoa continua na tela e tenta de novo no próximo ciclo.

security 5. Controle de acesso (RLS)

Regra do sistema (válida para todo o SIGEEDU, não só este projeto): o único primitivo de controle de acesso é public.verifica_permissao_modulo(p_modulo text, p_id_local bigint, p_operacao text) (função pré-existente, SECURITY DEFINER, já usada por todos os outros módulos). Nunca criar função de permissão própria por módulo.

-- assinatura (já existia antes deste projeto):
verifica_permissao_modulo(p_modulo text, p_id_local bigint, p_operacao text) returns boolean

Semântica por nivel_permissao em main_permissao_usuarios (fixa no código da função, independe do rótulo em permissao_usuarios):

nívelselectinsert/updatedeleteignora locais
1simnãonãonão
2simsimnãonão
3simsimsimnão
4 (Full)simsimsimsim
5nãonãonão- (bloqueia tudo)

Para níveis 1-3, a função exige p_id_local = ANY(locais) - ou seja, locais vazio/nulo bloqueia acesso a qualquer local para esses níveis, a menos que o chamador passe um p_id_local que o próprio módulo trate como "não aplicável" (ver convenção abaixo).

5.1 Convenção p_id_local = 0 para módulos sem conceito de local

Este painel (permus) não é escopado por unidade escolar - toda permissão dele é "tudo ou nada" por usuário. Seguindo a convenção já usada em outros módulos sem local (ex: gdrive_grupos), todas as chamadas a verifica_permissao_modulo neste projeto passam 0 como p_id_local:

verifica_permissao_modulo('permus', 0, 'select')
verifica_permissao_modulo('permus', 0, 'insert')
verifica_permissao_modulo('permus', 0, 'update')
verifica_permissao_modulo('permus', 0, 'delete')

Isso significa que, para um usuário ter qualquer acesso de escrita/leitura restrito (níveis 1-3) neste painel, a linha dele em main_permissao_usuarios (modulo = 'permus') precisa ter 0 dentro do array locais (ex: locais = ARRAY[0]) - ou ter nível 4 (Full, que ignora locais de qualquer forma). O manual de instalação (README.md) já orienta cadastrar o primeiro administrador com nível 4 para evitar essa pegadinha.

5.2 Quem cria as policies

As policies RLS não são criadas pelos arquivos de migração deste projeto. Convenção do sistema inteiro (F:\CLAUDE): o próprio usuário cria/edita cada CREATE POLICY manualmente no editor de políticas do painel do Supabase, sempre chamando verifica_permissao_modulo. Os arquivos SQL só podem:

  • alter table ... enable row level security (infraestrutura);
  • drop policy if exists ... (limpeza de policies antigas/erradas).

As sugestões de SQL para cada tabela (para colar manualmente no editor) estão na seção "Políticas RLS" do README.md.

Nota histórica: 001_migration.sql originalmente criava uma função própria (verifica_permissao_permus) e CREATE POLICY diretamente no arquivo - um desvio da convenção do sistema, corrigido por 003_ajuste_rls.sql (remove a função e as policies criadas por engano; a partir daí, tudo passa por verifica_permissao_modulo e as policies voltam a ser gerenciadas manualmente). Isso está documentado aqui para quem for ler o histórico dos arquivos 001/002 e estranhar a função verifica_permissao_permus mencionada neles - ela não existe mais no banco depois de rodar o 003.

5.3 usuarios_cadastrados: cuidado extra

Tabela compartilhada por muitos módulos do SIGEEDU (a lista de colunas permissao_* legadas nela é prova disso). 001_migration.sql propositalmente não ativa RLS nela (linha comentada) - habilitar RLS pela primeira vez numa tabela sem nenhuma policy previamente existente bloqueia todo acesso até que policies sejam criadas, o que pode quebrar telas de outros módulos que hoje dependem do acesso aberto padrão do Postgres/Supabase (RLS desativado = sem restrição). Antes de ativar, rode:

select relrowsecurity from pg_class where relname = 'usuarios_cadastrados';

Se já vier true, é seguro ativar policies aditivas a qualquer momento.

functions 6. Funções RPC

Todas SECURITY DEFINER, com REVOKE ALL FROM PUBLIC + GRANT EXECUTE TO authenticated, e conferindo verifica_permissao_modulo('permus', 0, operação) internamente antes de qualquer efeito - a segurança não depende de quem descobre o nome da função, só de quem tem a permissão concedida.

FunçãoParâmetrosOperaçãoO que faz
main_permus_listar_auth_users()-selectLê auth.users (nunca exposto via PostgREST) e devolve id, email, created_at, last_sign_in_at, email_confirmed_at, banned_until
main_permus_criar_auth_user(p_email, p_senha, p_confirmar)e-mail, senha, confirmar automaticamenteinsertInsere diretamente em auth.users + auth.identities (ver 6.1)
main_permus_redefinir_senha(p_user_id, p_nova_senha)uuid, nova senhaupdateUPDATE auth.users SET encrypted_password = crypt(...)
main_permus_banir_auth_user(p_user_id, p_banir)uuid, true/falseupdateUPDATE auth.users SET banned_until = ...
main_permus_excluir_auth_user(p_user_id)uuiddeleteApaga main_permissao_usuarios do usuário, depois auth.users (cascata para usuarios_cadastrados e auth.identities)
main_permus_conceder_lote(p_user_ids, p_modulo, p_nivel_permissao, p_locais)array de uuids, módulo, nível, locaisinsertUpsert manual (update; se não encontrar linha, insert) por usuário
main_permus_revogar_lote(p_ids)array de bigint (ids de main_permissao_usuarios)deleteDELETE ... WHERE id = ANY(p_ids)

6.1 Sobre as funções que tocam auth.users

Técnica usada em instâncias self-hosted do Supabase sem Edge Functions: como não há como usar a Admin API do GoTrue (que exige a service_role key, que nunca deve ir para o front-end) com segurança direto do navegador, as funções inserem/atualizam diretamente nas tabelas internas auth.users/auth.identities, dentro do próprio Postgres (mesma técnica documentada em comunidades Supabase self-hosted).

Riscos e cuidados:

  • O INSERT foi escrito para o layout padrão dessas tabelas numa versão comum de GoTrue; se a instância tiver colunas obrigatórias diferentes, o erro do Postgres aponta exatamente qual coluna falta - ajuste o INSERT e rode CREATE OR REPLACE FUNCTION de novo.
  • As funções de criação/senha/ban/exclusão têm set local statement_timeout = '30s' no início - o padrão da API (~8s) não é suficiente para um DELETE/UPDATE em auth.users num banco com muitas tabelas referenciando auth.users(id) sem índice nessas colunas (ver seção 12, "erro 500 / query_canceled").
  • main_permus_excluir_auth_user apaga primeiro main_permissao_usuarios do usuário (a FK para auth.users não tem ON DELETE CASCADE) antes de apagar auth.users - sem isso, a exclusão falharia por violação de FK sempre que o usuário tivesse alguma permissão concedida.

code 7. Frontend: padrões reutilizáveis

Todo em js/gestao.js, sem módulos ES/import - um único arquivo, organizado em blocos comentados. Padrões relevantes para quem for mexer:

7.1 Combobox pesquisável (initCombobox)

Substitui <select> simples nos campos que pedem busca (Usuário, Módulo, Conta vinculada, Função, Unidade de trabalho). Um input de texto + div de dropdown + (opcional) input hidden para o valor real:

initCombobox({
  inputId, hiddenId, dropdownId,   // ids dos elementos no HTML
  getItens,                         // () => array atual (lido ao vivo, sem cache)
  getValue, getLabel,               // como extrair valor/rótulo de cada item
  renderItem,                       // opcional: HTML customizado por item
  onSelect, onManualChange          // callbacks
})

Retorna { setValue(v), clear() } para controlar o campo ao abrir um modal de edição. Quando hiddenId é omitido (caso de "Função", que é texto livre), o próprio input de busca guarda o valor final.

7.2 Grade de "Locais" pesquisável

Substituiu o antigo <select multiple>. Um Set() em memória (permissaoLocaisSelecionados / loteLocaisSelecionados) guarda os ids marcados; renderLocaisGrid(gridId, selecionados, termoBusca) redesenha os cartões (destacando os marcados) a cada mudança - sem framework de estado, só um Set + re-render manual.

7.3 Modais e toasts

abrirModal(id)/fecharModal(id) (toggle de .hidden); qualquer botão com class="modal-fechar" data-modal="idDoModal" fecha o modal correspondente (delegação de evento única, no topo do arquivo). mostrarToast(texto, tipo) cria uma notificação flutuante temporária; mostrarMensagem(elemento, texto, tipo) preenche a faixa de erro/sucesso fixa dentro de um formulário.

7.4 Recarregamento de dados

Não há cache incremental: qualquer criação/edição/exclusão chama await carregarTudo() no final, que busca as 7 fontes de dados de novo (Promise.all) e re-renderiza as 6 telas (renderAll()). Simples e robusto para o volume de dados deste painel; não otimize prematuramente isso sem necessidade real.

7.5 Botão "Relatórios" (SSO para main-relatorios)

Mesmo padrão de kimape-modo-kiosk/funiat-gestao: abrirRelatorios() em gestao.js pega a sessão atual (supabase.auth.getSession()) e abre ../main-relatorios/index.html em outra aba, já autenticado, passando pagina=permus.relatorios + refresh_token + access_token na querystring.

O relatório em si não tem código próprio - main-relatorios/index.html é genérico: lê ?pagina=, procura a entrada correspondente no objeto RELATORIOS (dentro do próprio arquivo) e monta a tela de colunas/filtros/exportação a partir dali. A entrada 'permus.relatorios' aponta para a view public.view_permus_permissoes (criada por sql/004_relatorio_view.sql - ver seção 3). O próprio js/auth-guard.js de main-relatorios deriva o módulo a conferir a partir do texto antes do ponto em pagina ('permus.relatorios'.split('.')[0] = 'permus'), então a permissão exigida lá é automaticamente a mesma deste painel - não precisa de nenhuma permissão extra.

crop 8. Recorte automático de foto de perfil

Implementado inteiramente em Canvas 2D, sem nenhuma biblioteca externa, em duas camadas (a primeira que funcionar é usada; se nenhuma funcionar, cai para o recorte manual neutro sem quebrar a tela):

8.1 Camada 1: FaceDetector (Shape Detection API)

API experimental do navegador (hoje só alguns Chromium). Quando disponível, usa boundingBox para centralizar e, se o navegador também devolver landmarks com pontos de olho (type contendo "eye"), calcula o ângulo exato da reta entre os dois olhos e usa isso como correção fina (técnica clássica de alinhamento facial). Tudo dentro de try/catch - qualquer falha cai silenciosamente para a camada 2.

8.2 Camada 2: detecção por região de conteúdo (PCA + componente conectado)

Função detectarRegiaoDeConteudo(img), roda numa cópia reduzida da imagem (máx. 240px no maior lado, para performance):

  1. Estima o fundo pela média dos pixels da borda da imagem.
  2. Binariza por distância de cor ao fundo estimado (limiar 38) - gera uma máscara "primeiro plano" vs "fundo".
  3. Isola o maior componente conectado (busca em largura, 4-vizinhos) dessa máscara - separa a foto real de poeira/ruído de scanner espalhado pela página (cada speck vira um componente isolado e pequeno; a foto é um bloco sólido bem maior). Essencial para funcionar com uma foto pequena dentro de uma página digitalizada maior, não só uma foto que ocupa o quadro inteiro.
  4. PCA (autovalores/autovetores 2x2 em forma fechada) sobre os pixels desse componente, para achar o ângulo do eixo mais alongado - assume-se retrato (mais alto que largo), então esse eixo deveria ficar vertical.
  5. Normalização de ângulo em duas etapas (ponto crítico, ver "Armadilhas" abaixo): primeiro reduz o ângulo PCA para (-90°, 90°] (uma reta e sua oposta representam o mesmo eixo), só então arredonda para o múltiplo de 90° mais próximo, separando em rotacaoBase (0/90/180/270) + anguloFino (residual dentro de ±45°).
  6. Detecção de "de cabeça para baixo": compara a largura nos 30% mais extremos do topo vs. da base (no referencial já alinhado pelo passo 5) - ombros (mais largos) devem ficar embaixo; se o topo for nitidamente mais largo (>15%), soma 180° a rotacaoBase.
  7. Devolve rotacaoBase, anguloFino, centro e tamanho do bloco detectado (em pixels da imagem original) - usados para já deixar zoom/posição/ângulo ajustados automaticamente.

Armadilhas já resolvidas aqui (documentadas porque não são óbvias e foram encontradas por teste extensivo, não por leitura de código):

  • Ambiguidade de sinal do atan2: 0.5 * atan2(2*sxy, sxx-syy) pode devolver um ângulo ou seu suplementar por ruído numérico infinitesimal (sxy perto de zero), fazendo a correção "vazar" 180° de forma arbitrária caso você arredonde para o múltiplo de 90° mais próximo antes de normalizar para (-90°,90°]. Sempre normalize primeiro.
  • Divisão topo/base pelo ponto médio geométrico é frágil: um bloco assimétrico (cabeça estreita + ombros largos) desloca o centroide para o lado mais "pesado", então (minY+maxY)/2 não cai exatamente na fronteira real entre as duas partes. Usar os 30% mais extremos de cada ponta evita esse viés.
  • Offset deve ser aplicado no espaço LOCAL da imagem, não no espaço do canvas: se o deslocamento de centralização for aplicado via ctx.translate() antes de ctx.rotate(), girar depois desloca tudo (o pivô da rotação passa a ser o ponto deslocado, não o centro do canvas). A solução usada aqui é armazenar offsetImgX/offsetImgY em pixels da própria imagem (não do canvas) e aplicá-los dentro de ctx.drawImage(img, -w/2 + offsetImgX, -h/2 + offsetImgY), depois de translate(centroDoCanvas) → rotate → scale - assim o deslocamento gira e escala junto com a imagem automaticamente, e girar/dar zoom depois nunca desalinha o que já estava centralizado. O arraste do mouse (em pixels de tela) é convertido para esse mesmo espaço local desfazendo a rotação/escala atual antes de somar.

A suíte de validação usada durante o desenvolvimento (26 cenários sintéticos cobrindo ângulos de 0° a 350° combinados com orientação normal/invertida) não faz parte do repositório - foi rodada localmente via Node para validar a matemática antes de aplicar ao gestao.js; se for alterar esta lógica, recomenda-se recriar testes semelhantes antes de confiar em mudanças.

rule_folder 9. Regras de negócio específicas

9.1 Código automático do módulo

Ao escolher um "Tipo de documento vinculado" (tanto ao criar quanto ao editar um módulo), o código é lowercase(sigla_main + sigla) do tipo de documento (ex: sigla_main='PER', sigla='MUS' → permus). Sem tipo de documento, o código é digitado manualmente. main_modulos.descricao é NOT NULL no banco mas não tem mais campo próprio na tela - é preenchida automaticamente com o tipo do documento (ou o próprio código, se não houver tipo de documento).

9.2 Telefone, CPF, Portaria, Iniciais

  • Telefone: só dígitos; se não começar com 55 e tiver 10-11 dígitos (DDD+número sem código de país), o 55 é prefixado automaticamente (normalizarTelefone).
  • CPF: só dígitos, validado por dígito verificador real (cpfValido, algoritmo padrão de checksum), não só formato/tamanho.
  • Portaria: máscara progressiva 0000/0000 (mascararPortaria).
  • Iniciais: primeira letra de cada palavra do nome, ignorando DE, DA, DO, DAS, DOS, E (calcularIniciais).
  • Lotação: SIGLA_CARGO/SIGLA_UNIDADE, recalculada automaticamente sempre que Função ou Unidade de trabalho mudam (via combobox), editável manualmente depois.

9.3 Auto-preenchimento a partir de view_funiat.json_lotacoes

json_lotacoes é uma coluna que guarda uma lista JSON como texto (precisa de JSON.parse, não vem como objeto/array nativo do PostgREST - extrairLotacaoAtual trata os dois casos por segurança). Cada item tem cargo (id de public.cargo), local (id de public.instituicao), portaria e is_atual (boolean). A regra de escolha: filtra só is_atual = true; se houver mais de um, prefere o que tiver portaria preenchida; usa esse item para preencher Função/Unidade/Portaria/Lotação.

help 10. Suposições de schema não confirmadas

Estas colunas foram citadas pelo usuário mas não constavam nos CREATE TABLE/CREATE VIEW originalmente fornecidos - o código tenta usá-las e se degrada automaticamente (sem quebrar) se não existirem:

  • public.instituicao.sigla, public.cargo.sigla - usadas para montar a "Lotação" automática. fetchInstituicoes()/fetchCargos() tentam selecionar com essas colunas; se der erro, refazem a consulta sem elas (mapeando sigla: null) e seguem normalmente - a Lotação simplesmente não é sugerida automaticamente.
  • public.view_funiat.foto_perfil - usada para pré-carregar a foto ao vincular um funcionário. buscarFuncionarios() tenta uma vez com essa coluna na lista de select; se der erro, remove a coluna do conjunto usado (colunasFuncionarioBusca, variável de módulo) e nunca mais tenta na mesma sessão de página.

usuarios_cadastrados.user_iniciais, .tipo_user e .user_foto já existiam no CREATE TABLE original fornecido - usadas diretamente, sem fallback.

extension 11. Como estender

11.1 Adicionar um novo módulo ao sistema de permissões

Não precisa mexer neste painel - basta cadastrar o módulo (aba Módulos, ou insert into main_modulos) e conceder permissões normalmente pela aba Permissões. Se o novo módulo tiver conceito de local, suas próprias policies (no projeto dele) devem chamar verifica_permissao_modulo('codigo_do_modulo', p_id_local, operacao) com o p_id_local real (id de instituicao); se não tiver, siga a convenção p_id_local = 0 (seção 5.1) e documente isso no README daquele projeto.

11.2 Adicionar uma nova aba/tela

Siga o padrão das abas existentes em index.html (tab-btn + tab-content + header-content) e em gestao.js (uma seção render<Nome>() + wiring de eventos própria, chamada dentro de renderAll()). Não introduza um framework de front-end - o padrão deliberado deste projeto (e dos demais em F:\CLAUDE) é HTML+JS puro.

11.3 Nova função RPC que grava em auth.users

Copie o padrão de main_permus_redefinir_senha/_banir_auth_user: SECURITY DEFINER, set local statement_timeout generoso (30s), checagem verifica_permissao_modulo('permus', 0, operacao) como primeira linha do corpo, REVOKE ALL FROM PUBLIC + GRANT EXECUTE TO authenticated depois de criada. Nunca exponha a service_role key no front-end como alternativa.

build 12. Troubleshooting

Erro 500 / proxy-status: error=57014 ao chamar uma função RPC

57014 é query_canceled do Postgres - a função foi cancelada por exceder o statement_timeout padrão da role authenticated (~8s no Supabase), não um erro de lógica. Acontece em operações que tocam auth.users, porque o Postgres precisa checar todas as tabelas que referenciam auth.users(id) (há muitas no SIGEEDU: colunas criadopor/editadopor em vários módulos) - se alguma não tiver índice nessa coluna, a checagem de FK vira varredura completa da tabela. Solução já aplicada nas funções deste projeto: set local statement_timeout = '30s' só dentro da função afetada (não mexe no limite padrão da role, que continua protegendo o resto do sistema). Se o erro voltar a aparecer em outra função nova, aplique o mesmo padrão nela.

Painel carrega, mas nenhuma tabela mostra dados (tudo vazio) depois de rodar 003_ajuste_rls.sql

Esperado: o 003 remove as policies antigas e RLS continua habilitado nas tabelas - RLS habilitado sem nenhuma policy nega tudo. Crie as policies manualmente no painel do Supabase (seção "Políticas RLS" do README.md) antes de usar o painel de novo.

"Não é possível concluir: existem registros vinculados a este item"

Erro 23503 (foreign_key_violation) traduzido pela função traduzirErro() no front-end - tentativa de excluir/renomear algo ainda referenciado por outra linha (ex: excluir um módulo com permissões concedidas, ou renomear o código de um módulo em uso). Resolva a dependência primeiro (revogue as permissões, ou aceite que o código não pode mudar).

Conta criada pelo painel não consegue fazer login

O INSERT em auth.users/auth.identities foi escrito para um layout comum de GoTrue self-hosted; se a instância usada tiver colunas obrigatórias adicionais (versões diferentes do GoTrue variam), o erro do Postgres na criação apontará a coluna faltante - ajuste o INSERT em main_permus_criar_auth_user e rode CREATE OR REPLACE FUNCTION. Teste sempre com uma conta de teste antes de confiar no fluxo em produção.

Combobox pesquisável não mostra nenhum resultado, mesmo com dados carregados

Confira se getItens() está lendo a variável global correta (elas são recarregadas inteiras a cada carregarTudo(), não incrementalmente) e se getLabel(item) não está retornando undefined para algum item (quebra o .toLowerCase() do filtro de busca).

Tela "Sem permissão" aparece para um usuário que deveria ter acesso

Confira em main_permissao_usuarios se existe uma linha para esse user_id com modulo = 'permus' e nivel_permissao diferente de null/5. Lembre que a permissão é reconferida a cada 5 minutos com a página já aberta (seção 4.1) - uma alteração recente pode levar até esse tempo para refletir numa aba já aberta (atualizar a página força a reconferência na hora). Erros de rede/consulta também bloqueiam a tela por segurança ("Não foi possível verificar sua permissão") - veja o console do navegador para a causa exata.