Novidades, melhorias e correções da plataforma Donara.
Segurança
docker-compose.yml definia PMA_USER/PMA_PASSWORD no serviço phpmyadmin — isso ativa o modo auth_type=config do phpMyAdmin, que autentica qualquer visitante automaticamente, sem pedir credencial nenhuma. pma.donara.com.br estava publicamente acessível dando acesso total a todos os bancos de tenants para qualquer um que soubesse a URL. Variáveis removidas do compose — o phpMyAdmin volta ao login por cookie, exigindo usuário/senha do MySQL a cada acesso (docker-compose.yml)root (hardcoded no docker-compose.yml, nem vinha do .env apesar de existir uma variável MYSQL_ROOT_PASS não utilizada). Gerada uma senha forte, aplicada ao usuário root já em execução via ALTER USER, e o compose corrigido para referenciar ${MYSQL_ROOT_PASS} do .env (antes inerte) — passa a valer também em qualquer reprovisionamento futuro do volume (docker-compose.yml, .env do servidor).env: landing/saas/secrets.php tinha usuário/senha do SMTP Locaweb hardcoded diretamente no arquivo; passaram a vir de variáveis de ambiente (mesmo padrão já usado para as chaves do Asaas), com fallback apenas para os valores não sensíveis (host/porta/remetente). Nenhuma senha foi alterada nesta mudança — só a forma de armazenar (landing/saas/secrets.php, landing/saas/secrets.php.example, .env.example).env local: havia um Personal Access Token comentado dentro do .env (nunca commitado, mas ainda um segredo em texto puro em disco). Removido; recomendado revogá-lo em github.com/settings/tokens caso ainda esteja ativoASAAS_BASE_URL explícito no .env: antes dependia do fallback implícito do código (sandbox.asaas.com) quando a variável não estava definida no servidor. Ambiente sandbox confirmado como proposital (ainda não migramos para produção no Asaas) — agora está declarado explicitamente em vez de implícitovouchers_app/DB_PASS) rotacionada: senha anterior era fraca (Vouchers@1). Gerada uma senha forte, aplicada via ALTER USER e propagada ao .env do servidor; container da aplicação recriado para usar a credencial novachpasswd, escolhida pelo próprio responsável pela infraestrutura para permanecer memorizávelAdicionado
includes/cookie_consent.inc.php, css/custom_cookie_consent.css, js/customJQuery_cookie_consent.js) incluído nas 5 páginas públicas (landing, cadastro/assinar, simulador, política de privacidade e termos de uso): barra fixa com "Aceitar todos" / "Rejeitar não essenciais" / "Personalizar", modal com categorias Essenciais (sempre ativa) e Analytics/Marketing (hoje sem nenhum script real atrelado — infraestrutura pronta para quando for necessário). Preferência gravada em cookie de primeira parte (donara_cookie_consent, 180 dias) e exposta via window.DonaraConsent para scripts futuros de analytics consultarem antes de carregar. Política de Privacidade ganhou seção "7. Cookies" documentando os cookies essenciais já usados (sessão, CSRF, OAuth Google) e um link para reabrir as preferências; Termos de Uso ganhou seção equivalente com renumeração das seções seguintes (includes/cookie_consent.inc.php, css/custom_cookie_consent.css, js/customJQuery_cookie_consent.js novos; landing/index.php, landing/assinar/index.php, landing/simulador/index.php, privacidade/index.php, termos/index.php)/assinar) e do painel saas-admin endurecidos: só includes/session.inc.php (app principal) chamava session_set_cookie_params() com Secure/HttpOnly/SameSite=Lax antes de session_start() — as sessões de /assinar (cadastro, OAuth Google) e do saas-admin dependiam dos defaults do PHP. Mesma proteção replicada em todos os pontos de entrada de sessão dessas áreas (landing/assinar/index.php, action.php, oauth_google.php, oauth_callback.php, oauth_login.php, oauth_login_callback.php, oauth_link_confirm.php, ativar.php, landing/saas-admin/auth.php)scripts/backup_db.sh roda 1x/dia (3h) e faz dump de todos os bancos MySQL (donara_master + 1 por tenant, descobertos dinamicamente — tenant novo entra sem precisar editar o script), compacta os uploads de template por tenant (voucher/custom/) e os logs por tenant (logs/files/) — nenhum dos dois está versionado em git. Cópia local na VPS com 90 dias de retenção + réplica no Google Drive via rclone. Novo painel /saas-admin/backups.php: saúde da última execução (reaproveita saas_cron_runs, mesmo padrão do monitor de faturamento), status por tenant (nova tabela saas_backup_tenant_status), botão Pausar/Retomar o agendamento e botão Executar agora (nova tabela saas_backup_control) — como o container da aplicação não tem acesso ao Docker do host, "Executar agora" funciona por flag no banco lida por um watcher (scripts/backup_watch.sh, a cada 2min), não por execução direta (scripts/backup_db.sh, scripts/backup_watch.sh, landing/saas-admin/backups.php, landing/saas-admin/action.php, novas tabelas em landing/saas/patch_backup_control.sql)PAYMENT_OVERDUE suspende um tenant, a assinatura na Asaas passa a receber endDate na hora (mesmo mecanismo já usado no cancelamento), e qualquer cobrança futura já criada com antecedência (~9 dias) é excluída — a mensalidade não gera mais ciclos novos enquanto a conta estiver suspensa. Cobranças já vencidas/pendentes não são afetadas, continuam sendo cobradas normalmente. Ao confirmar o último pagamento pendente (PAYMENT_RECEIVED, reativação automática), o endDate é removido e a assinatura volta a gerar ciclos normalmente (landing/saas-webhook/index.php)cancelar_assinatura()) agora aceitam status suspended; nesse caso o encerramento é imediato (hoje), não no fim do ciclo, já que uma conta suspensa não tem acesso a preservar. Cancelar não apaga cobranças já emitidas — a Asaas continua tentando cobrar o que já venceu (config/assinatura.php)tenants.tier (o default do cadastro), sem opção de trocar — quem quisesse Pro ou Premium acabava assinando o Starter sem perceber. Agora exibe os 3 planos (Starter/Pro/Premium) lado a lado, nenhum pré-selecionado, com o resumo (mensalidade, taxa variável, primeira cobrança) atualizado dinamicamente conforme a escolha; o plano escolhido é gravado em tenants.tier junto com a criação da assinatura na Asaas (config/ativar_assinatura.php, includes/plans.inc.php)tenants.pending_downgrade_tier + pending_downgrade_effective_at, calculado via getTenantBillingState()) e só é efetivado pelo cron diário (cron_finalize_invoices.php) quando a data chega — a tela de Assinatura mostra um aviso explicando que o plano atual continua válido até lá. Upgrade continua cobrando o pró-rata antes de liberar, como já era (config/assinatura.php, landing/saas/cron_finalize_invoices.php, nova migração landing/saas/patch_pending_downgrade.sql)/saas-admin/ tinha o menu de navegação (<nav>) copiado e colado em 7 arquivos, sem nenhum include compartilhado, e a página de detalhe do tenant havia virado uma rolagem de 11 cards. Novo include landing/saas-admin/_sidebar.php (mesma lógica/dinâmica da sidebar do app principal em includes/navbar.php — colapso, overlay mobile — mas em tom preto/quase-preto para diferenciar visualmente) substitui o nav duplicado nas 8 páginas do painel. Na home (index.php), os cards de status (Ativos, Trial, Pendentes, Suspensos, Cancelados, Erro) agora funcionam como filtro da tabela de tenants, combinando com a busca por texto. Em tenant.php, os 11 cards viraram 8 seções isoladas (Dados Cadastrais, Linha do Tempo, Login Google, Gestão da Conta, Cobrança, Atividade, API, Zona de Perigo) navegáveis por um menu lateral secundário, preservando o link direto #danger já usado pela ação "Excluir tenant" da listagem. Em plans.php, os 3 planos passaram de formulário empilhado para cards lado a lado no mesmo estilo visual dos cards de planos da landing page, com nome/preço/badge/features editáveis in-place (mesmos campos de formulário, sem mudança no backend action.php). Em blacklist.php, o card "Adicionar à blacklist" foi movido para o topo da página (landing/saas-admin/_sidebar.php novo, css/custom_saas_admin.css novo, landing/saas-admin/icons.php, landing/saas-admin/index.php, landing/saas-admin/tenant.php, landing/saas-admin/plans.php, landing/saas-admin/blacklist.php, landing/saas-admin/signup_failures.php, landing/saas-admin/backups.php, landing/saas-admin/presets_editor.php, landing/saas-admin/default_rule_models.php)4.00 + % sobre vouchers de valor fixo); o texto final exibido na landing/assinatura é montado automaticamente concatenando os dois na hora de salvar. O preço mensal também deixou de exigir dois campos separados (parte inteira + centavos) — agora é um único campo decimal (ex: 29.90), convertido internamente para as mesmas colunas já usadas por todas as telas que exibem o preço formatado (landing/saas-admin/plans.php, landing/saas-admin/action.php, nova migração landing/saas/patch_plan_taxa_suffix.sql adicionando saas_plans.taxa_fix_suffix/taxa_pct_suffix)config/index.php, ajuda/index.php, primeiro_acesso/resetar_wizard.php)voucher/index.php), com os mesmos textos de amostra exibidos no editor — mesma ideia do botão equivalente no saas-admin, mas reaproveitando o renderizador real (novo modo de prévia opcional, ativado só com sessão de admin) em vez de duplicar toda a lógica de posicionamento (voucher/index.php, config/index.php, js/customJQuery_config.js)Corrigido
config/index.php)?tab=). Agora abre direto em Template Horizontal para administradores; nível 0 continua caindo em Aparência, única aba que enxerga (config/index.php)config/upload_logo.php chamava csrf_validate() sem nunca incluir o arquivo que a define — erro fatal silencioso, mascarado por um error_reporting(0) que suprimia até o registro no log de erros. Corrigido criando includes/csrf.inc.php (helper isolado, sem a dependência de Xajax que includes/auth.inc.php tem no nível principal do arquivo) e trocando error_reporting(0) por ini_set('display_errors', '0'), que mantém erros reais visíveis no log do servidor sem expô-los ao usuário. A mesma falta de proteção CSRF foi corrigida em config/upload_template.php (nunca validava o token, e o JS nunca enviava um) (includes/csrf.inc.php novo, includes/auth.inc.php, config/upload_logo.php, config/upload_template.php, js/customJQuery_config.js)config/index.php)config/index.php, js/customJQuery_config.js)config/index.php salva as cores personalizadas em vouchers_users.user_config (por usuário — mesmo local que o Dashboard já lia corretamente), mas gerencia/index.php (listagem, cards e o modal de estatísticas) e cadastro/index.php (cores do formulário de criação) liam de vouchers_config.colors, uma chave gravada uma única vez no provisionamento do tenant e nunca mais atualizada. Corrigidos os 4 pontos de leitura (2 em gerencia/index.php, 1 em cadastro/index.php, mais o endpoint de atualização de cards após ações) para ler do mesmo lugar que o Dashboard, eliminando a divergência entre telas (gerencia/index.php, cadastro/index.php)maxBarThickness: 24) e reduzido o incremento de altura por usuário no cálculo dinâmico do canvas, deixando as barras finas e consistentes independente da quantidade de usuários (js/customJQuery_dashboard.js)$_SERVER['REMOTE_ADDR'] sempre valia o IP do container do Traefik na rede Docker (ex.: 172.24.0.10) — o mesmo em qualquer tenant, sem nenhuma utilidade para auditoria. Nova função client_ip() (includes/client_ip.inc.php) lê X-Forwarded-For e usa o último IP da lista (o que o próprio Traefik anexa ao repassar a requisição — não o primeiro, que um cliente malicioso poderia forjar), com fallback para REMOTE_ADDR se o header não existir (ex.: acesso direto sem proxy). Substituídas todas as 40 ocorrências de $_SERVER['REMOTE_ADDR'] em 18 arquivos (logs de login, criação/edição de voucher, exportações, alterações de config, validação pública, cadastro SaaS, API externa, etc.). Testado em produção via bubba.donara.com.br, confirmando IP público real capturado corretamente (includes/client_ip.inc.php novo; includes/connect.inc.php, landing/saas/db.php incluem o helper; demais arquivos apenas trocam a chamada)config/assinatura.php)<p> do texto usava flex: 1 1 420px (herdado da regra desktop, onde o flex é em linha e 420px vira largura); no mobile, o media query trocava o flex container para coluna sem resetar esse valor — em flex-direction: column, o mesmo 420px passa a valer como altura mínima do parágrafo, esticando um espaço vazio enorme entre o texto e os botões. Resetado para flex: 0 1 auto no mobile, com botões empilhados em largura total e paddings reduzidos (css/custom_cookie_consent.css). O <link> desse CSS nas 5 páginas públicas não tinha cache-busting (?v=), então navegadores mobile continuavam servindo a versão antiga do cache mesmo após o deploy — agora usa filemtime() como as demais folhas de estilo do projeto (landing/index.php, landing/simulador/index.php, landing/assinar/index.php, privacidade/index.php, termos/index.php)config/assinatura (sem .php), rota que não existe nesse projeto (sem rewrite configurado) — voltar do formulário de ativação dava 404. Corrigidos para config/assinatura.php, incluindo os dois header('Location: ...') de redirecionamento (conta já ativa / conta interna) (config/ativar_assinatura.php)trocar_plano() contava voucher_usado = 0 sem filtrar por validade, inflando a contagem usada contra max_vouchers do plano de destino — um tenant com vouchers vencidos (mas ainda voucher_usado = 0) via um número maior que o real "Ativos" do Dashboard. Corrigido para usar o mesmo critério do KPI do Dashboard (voucher_usado = 0 AND data_validade >= hoje) (config/assinatura.php)/assinar) — texto desatualizado sobre plano padrão do trial: o card lateral dizia "Sem escolha, a conta é mantida no Starter", que não é mais verdade desde que a escolha do plano passou a acontecer em "Ativar plano pago" — removido, junto com os preços fixos (R$29,90/R$79,90/R$149,90) citados no mesmo card, já que os valores podem mudar e já são exibidos nas telas de planos (landing/assinar/index.php)cancelar_assinatura() também passou a rejeitar chamadas nesse status, por segurança (config/assinatura.php)landing/index.php)salvar_preset, salvar_layout, config/upload_template.php) só checavam nível de usuário — qualquer tenant Starter conseguia customizar livremente, apesar da tela de planos anunciar isso como exclusivo do Pro+. Segue o mesmo padrão já usado para a API (saas_plans.api_enabled): nova coluna saas_plans.template_customization_enabled (1 para Pro/Premium, 0 para Starter) checada nas 3 ações que de fato alteram um template, mais o Trial liberado (mesma regra "tudo liberado exceto API"). Na tela, o Starter continua podendo trocar entre os 3 modelos prontos, mas o botão "Novo modelo" fica bloqueado e o card de edição de posição/upload some, com aviso "disponível a partir do plano Pro" (landing/saas/db.php, config/index.php, config/upload_template.php, css/custom_config.css, js/customJQuery_config.js, nova migração landing/saas/patch_template_customization.sql)aplicar_preset() não checava elegibilidade — um tenant que criou modelos no Pro e caiu pro Starter conseguia continuar trocando livremente entre eles na galeria, mesmo sem poder editá-los. Agora modelos não-protegidos (os de fábrica continuam sempre liberados) ficam bloqueados na galeria com cadeado + aviso quando o tenant não é elegível; o template já ativo no momento do downgrade não é revertido automaticamente, continua valendo até o tenant trocar (config/index.php)TRIAL_EXPIRED é calculado só por status='trial' + trial_ends_at vencido, sem checar se a assinatura já foi criada na Asaas — e config/ativar_assinatura.php nunca muda tenants.status ao criar a assinatura (só vira active depois, via webhook PAYMENT_RECEIVED, quando o primeiro pagamento é confirmado). Nessa janela (entre ativar e a confirmação do pagamento), a tela de Assinatura repetia o banner "Período de trial encerrado / Ativar plano agora" mesmo já tendo assinatura criada — clicar no botão só devolvia "assinatura já está ativa", sem indicar o que fazer. Agora, quando já existe asaas_subscription_id, o banner troca para "Assinatura criada — aguardando confirmação do pagamento" com o link direto da fatura pendente (config/assinatura.php)Alterado
invoice_url salvo, o ícone vira um link (target="_blank") que abre a cobrança na Asaas para o cliente pagar. getTenantBillingState() passou a trazer invoice_url no histórico de cobranças, que já era gravado em saas_invoices mas não estava sendo selecionado (landing/saas/billing_calc.php, faturamentos/index.php)overdue, ou pending já vencida); cor do aviso "Conta suspensa — regularize o pagamento para reativar" trocada de text-warning para text-warning-emphasis (baixo contraste no tema claro); borda vermelha do card "Cancelar assinatura" removida, volta a ser uma borda normal como os demais cards (config/assinatura.php)Segurança
voucher/Voucher_modelo7_horizontal.png e _vertical.png (usados como fallback de template em TODO tenant sem template próprio configurado) eram, por engano, a arte de voucher real da Clínica Flórez — nome, foto, WhatsApp, Instagram e endereço físico reais. Qualquer tenant sem template_h/template_v configurado exibia esses dados nos próprios vouchers. Confirmado ativo em produção: os tenants trial gpclinica e paineisdefestasliberdade estavam servindo essa arte no momento da correção. Substituídos os 4 arquivos (H/V + variantes de impressão) por PNGs em branco nas dimensões corretas; coordenadas padrão ($APP_DEFAULT_LAYOUT_H/_V) ajustadas junto para combinar com fundo branco (texto preto em vez de branco). Backup dos arquivos originais guardado em /root/leaked_template_backup_20260824/ na VPS (voucher/Voucher_modelo7_*.png, includes/app_defaults.inc.php)Adicionado
voucher/index.php) usado em produção, com textos de amostra — permite validar o resultado final de cada modelo sem precisar criar um tenant de teste (landing/saas-admin/preset_voucher_preview.php novo, landing/saas-admin/presets_editor.php, landing/saas-admin/presets_editor.js)/saas-admin/default_rule_models.php) para editar o(s) modelo(s) de texto de regras que todo tenant novo já recebe pronto na tela de Cadastro de Vouchers — equivalente a "Presets de Templates", mas para o texto das pré-definições de regras. Só afeta tenants criados a partir de agora. Provisioner::provision() agora grava esse padrão (customizado ou o hardcoded do sistema, se nunca configurado) em vouchers_config.rule_models na criação do tenant — antes essa chave simplesmente não era escrita, e o tenant dependia do fallback embutido no código (landing/saas-admin/default_rule_models.php, .js novos; landing/saas/db.php, landing/saas/provisioner.php, landing/saas-admin/action.php; novo item de menu em todas as páginas do saas-admin; nova tabela saas_default_rule_models)cron_finalize.log) não tinha timestamp e sempre dizia "OK", mesmo com falhas individuais (capturadas só via error_log, nunca no próprio log). Agora cron_finalize_invoices.php grava cada execução em saas_cron_runs (início, fim, status, tenants processados, contagem e detalhe de erros), e a tela de Tenants do saas-admin mostra um banner com o status da última execução — verde quando OK e recente, vermelho quando terminou com erro, travou sem terminar, ou está há mais de ~30h sem rodar (landing/saas/cron_finalize_invoices.php, landing/saas-admin/index.php, nova tabela saas_cron_runs)tenants.tier de fato quando o pagamento é confirmado pelo webhook (PAYMENT_RECEIVED) — até lá o cliente permanece no plano atual, com um aviso na tela de Assinatura e o link de pagamento. Downgrade continua imediato e sem cobrança, como já era. Testado ponta a ponta em produção (sandbox Asaas): cálculo do pró-rata, criação da cobrança, confirmação via webhook e troca de tier — artefatos de teste limpos depois (landing/saas/billing_calc.php, config/assinatura.php, landing/saas-webhook/index.php, novas colunas tenants.pending_upgrade_tier e saas_invoices.type)Alterado
gerencia/index.php, css/custom_consulta.css)donara_master.tenants (empresa/pessoa, CPF/CNPJ, responsável, domínio, e-mail e celular da conta com badge de verificado/pendente, data de cadastro), somente leitura por enquanto. O card "Controlador" (nome da empresa/CNPJ editáveis, salvos em vouchers_config.dpo) foi removido — nome e CNPJ agora têm o cadastro em tenants como única fonte, eliminando a duplicação entre as duas origens (config/index.php, js/customJQuery_config.js)tenants.nome (donara_master) diretamente em vez de vouchers_config.dpo.clinica, acompanhando a mudança acima (index.php)$APP_DEFAULT_DPO reduzido a dpo_nome/dpo_email — clinica/cnpj não são mais gerenciados por essa tela (includes/app_defaults.inc.php)landing/index.php)config/index.php, js/customJQuery_config.js, css/custom_config.css)definir_padrao_modelos(), rule_models_user_default)config/index.php, css/custom_config.css)Removido
dpo_nome/dpo_email nunca alimentaram a Política de Privacidade de fato — essa página (privacidade/index.php) é estática desde que virou genérica para o modelo SaaS, e nunca leu esses campos dinamicamente. Removidos o card, salvar_dpo(), cfg_salvar_dpo() e $APP_DEFAULT_DPO (config/index.php, js/customJQuery_config.js, includes/app_defaults.inc.php, CLAUDE.md)Corrigido
config/index.php, css/custom_config.css)imageSmoothingQuality: 'low' por padrão ao redimensionar uma imagem num canvas — ao compor uma foto grande para caber no template, o resultado saía visivelmente mais borrado do que a resolução de origem permitiria. Ativado imageSmoothingQuality: 'high' explicitamente na composição, nos dois editores (saas-admin e Config do tenant). Presets/templates já salvos continuam com a versão borrada — é preciso reabrir o editor e clicar em Salvar de novo para gerar a versão nítida (landing/saas-admin/presets_editor.js, js/customJQuery_config.js)custom_landing.css declara font-family: 'Lato', sans-serif (usada no logotipo da navbar e em todo o texto da página), mas nunca havia um @font-face/link do Google Fonts trazendo a fonte de fato — o navegador sempre caía no fallback sans-serif genérico do sistema. Adicionado o link do Google Fonts (pesos 300/400/700/900) em landing/index.php e landing/simulador/index.phphandleDelete() apagava o tenant e seu banco, mas nunca limpava a linha correspondente em saas_google_links (sem FK/cascade nessa tabela). Se a mesma conta Google fosse usada para recriar um tenant com o mesmo slug depois, o INSERT batia na constraint UNIQUE de google_sub e travava com erro 500 não tratado — tanto na criação da conta quanto no botão "Entrar em conta existente", deixando o usuário sem saída em nenhuma das duas pontas. Agora a exclusão de tenant também remove o vínculo Google associado (landing/saas-admin/action.php)INSERT INTO saas_google_links e blocos de consulta ao banco no funil de OAuth que não tinham try/catch (PHP 8.2 lança mysqli_sql_exception por padrão em qualquer erro de banco, inclusive falha de conexão). Criado linkGoogleAccount() (landing/saas/db.php) centralizando a vinculação: se o google_sub já estiver vinculado ao mesmo tenant/usuário (corrida/retry), trata como sucesso; se apontar para outro tenant (conflito real, como o de uma linha órfã), loga em saas_signup_failures — visível no relatório "Falhas de Cadastro" do saas-admin — em vez de estourar um erro 500 cru. Aplicado nos 3 pontos que faziam esse INSERT diretamente (landing/assinar/action.php, oauth_link_confirm.php, oauth_login_callback.php) e envolvidos em try/catch com log os blocos de busca de tenant/usuário em oauth_callback.php, oauth_login_callback.php e action.php que também não tinham proteção_previewTexts) só existia no JS do canvas, nunca chegava no endpoint de renderização GD, que sempre usava os textos hardcoded de $APP_PREVIEW_TEXTS. Agora pe_visualizar_gd() envia os textos atuais do preview (editados ou não) como parâmetros, e preset_voucher_preview.php os usa na renderização (landing/saas-admin/presets_editor.js, landing/saas-admin/preset_voucher_preview.php).accordion, não no .accordion-item que tem o border-radius) — movida para o elemento certo; ícone de arrastar estava grande demais — reduzido; botão "Remover" agora pede confirmação antes de excluir (config/index.php, js/customJQuery_config.js, css/custom_config.css)$APP_DEFAULT_BLANK_LAYOUT_H/_V (includes/app_defaults.inc.php, config/index.php, js/customJQuery_config.js)js/customJQuery_config.js)overflow:hidden no item para acompanhar o arredondado da borda em qualquer estado (config/index.php, js/customJQuery_config.js, css/custom_config.css).cfg-preset-thumb (css/custom_config.css)<button class="accordion-button"> (com padding/line-height próprios) como irmão de <span>s simples (handle, botão remover) num align-items:stretch. Reescrito como um único elemento flex (.modelo-accordion-header) contendo todos os ícones — handle, ícone, título, preview do nome, seta e botão de remover —, alinhados com align-items:center, sem mais o componente .accordion-button do Bootstrap (config/index.php, js/customJQuery_config.js, css/custom_config.css)config/index.php, css/custom_config.css)body centralizava o card verticalmente com align-items: center — quando o conteúdo (título + 3 modelos com miniaturas H/V empilhadas) ficava mais alto que a tela, o transbordo ficava distribuído igualmente pra cima e pra baixo, e a posição de rolagem 0 já caía no meio do card, tornando o topo (justamente o título e as instruções) inalcançável por scroll. Trocado por margin: auto no card em vez de align-items: center no body — centraliza normalmente quando cabe na tela, e "recua" pra 0 (sem cortar nada) quando o conteúdo é mais alto que a viewport (primeiro_acesso/index.php)UX
.card-header do sistema (ex.: cards da Gerência), com variante clara para tema dark, em vez do estilo padrão do componente BootstrapAdicionado
saas-admin): tentativas de cadastro ou login via Google que falham antes de existir um tenant (sessão expirada, código ausente, falha na troca de token com o Google, perfil incompleto, login sem vínculo) agora ficam registradas em saas_signup_failures e visíveis na nova tela "Falhas de Cadastro"; e-mails com mais de uma falha são sinalizados como possível cliente perdido (landing/assinar/oauth_callback.php, oauth_login_callback.php, action.php, landing/saas/db.php, novo landing/saas-admin/signup_failures.php)/assinar/, com pré-preenchimento via link ?ref=CODIGO; gravado em tenants.affiliate_code e editável posteriormente pelo saas-admin (landing/assinar/index.php, action.php, landing/saas-admin/tenant.php, action.php)Mailer::sendPasswordReset, novo). Nova página pública recuperar_senha/ valida o token e exige a mesma política de senha do wizard de primeiro acesso (mínimo 8 caracteres, maiúscula, minúscula, número, especial), com checklist ao vivo. Colunas reset_token/reset_token_exp em vouchers_users são criadas sob demanda (auto-migração lazy, mesmo padrão já usado para tema/user_config — sem exigir alteração manual em cada banco de tenant) (index.php, includes/auth.inc.php, js/customJQuery_login.js, landing/saas/mailer.php, novo recuperar_senha/index.php)primeiro_acesso/index.php)landing/saas-admin/tenant.php)Alterado
saas-admin): clicar em qualquer parte da linha de um tenant agora abre seus detalhes, sem precisar abrir o menu "Ações" (landing/saas-admin/index.php)primeiro_acesso/index.php)primeiro_acesso/index.php)bi-google sobre fundo azul escuro, a cavaleiro no card) pelo "G" oficial com gradiente (vermelho→amarelo→verde→azul) do redesign 2025 da Google; o ícone agora substitui o ícone de pessoa no início da primeira linha do card, em vez de flutuar sobre o cabeçalho. Continua clicável para desvincular (usuarios/index.php, novo images/google-g-gradient.svg).vc-row-icon), centralizados dentro dela — antes cada ícone tinha padding próprio e tamanhos diferentes, então o texto de cada linha começava numa posição horizontal ligeiramente diferente (usuarios/index.php, css/custom_usuarios.css)index.php)index.php)landing/assinar/ativar.php, "Defina sua senha" após confirmar e-mail) e o modal "Alterar senha" da navbar (usuário já logado trocando a própria senha) agora exigem os mesmos requisitos e mostram o mesmo checklist ao vivo já usados no wizard de primeiro acesso e na recuperação de senha: mínimo 8 caracteres, maiúscula, minúscula, número e caractere especial. O modal da navbar também valida isso no servidor (senha_atende_politica(), novo em includes/auth.inc.php) — antes não exigia nada além de confirmação e senha atual correta (landing/assinar/ativar.php, includes/navbar.php, includes/auth.inc.php)Corrigido
<img src>) carregava mas não pintava nada em alguns navegadores; trocado por SVG inline (embutido direto no HTML, sem request separado), com id de gradiente único por usuário para não colidir quando há vários cards Google na mesma página (usuarios/index.php).vc-row-icon) usava inline-flex + vertical-align: middle, que empurrava todos os ícones (Google, e-mail, celular, nível) visivelmente abaixo do texto. Voltou para inline-block + text-align: center (só centraliza horizontalmente, sem sobrescrever o alinhamento vertical padrão que já funcionava) (css/custom_usuarios.css)upload_max_filesize=2M). Elevado para 20M/25M (docker/php.ini) — exigiu rebuild da imagem e recriação do container, já aplicado em produçãolanding/saas-admin/presets_editor.js, js/customJQuery_config.js)line-height), que sempre deixa um espaço "invisível" acima/abaixo da letra não presente na renderização real (GD) — mais perceptível em fontes pequenas, dava a impressão de que o texto "flutuava" ou ficava alinhado à base do quadro em vez do topo. Trocado por desenho em <canvas>, medindo a tinta real da fonte (TextMetrics.actualBoundingBox*) e replicando exatamente a fórmula do GD (imagettfbbox/imagettftext) — inclusive o algoritmo de quebra de linha da Descrição. Vale para valor, validade, código, descrição e status, nos dois editores (saas-admin e Config do tenant). A edição de texto por duplo clique foi reescrita (era contenteditable no texto; agora abre um campo temporário sobre o canvas) (js/customJQuery_config.js, landing/saas-admin/presets_editor.js, css/custom_config.css)js/customJQuery_config.js, landing/saas-admin/presets_editor.js)Verificado
saas_preset_layouts está vazia — os 3 modelos usam a posição padrão do sistema até alguém reposicionar no editor)Alterado
config/ativar_assinatura.php)cancels_at agora usa o fim do ciclo de cobrança já pago (via getTenantBillingState()), não mais o fim do mês calendário (config/assinatura.php)config/assinatura.php, novo método AsaasClient::deletePayment())tenants.asaas_subscription_started_at grava o momento exato da ativação; usada para zerar o variável de qualquer período anterior a esse momento, cobrindo ativação no prazo, antecipada ou tardia (landing/saas-webhook/index.php, landing/saas/billing_calc.php)Adicionado
landing/saas/cron_finalize_invoices.php, novo método AsaasClient::createPayment())cadastro/index.php)Alterado
saas_plans.api_enabled desativado para o tier pro)403 PLAN_NOT_ELIGIBLE; só conta com status=active (ou em carência paga pós-cancelamento) e tier Premium tem acesso. Contas internas (is_internal) continuam isentas dessa checagem (landing/api/bootstrap.php, config/index.php, landing/saas-admin/tenant.php)Adicionado
/changelog): renderiza CHANGELOG.md automaticamente (parser próprio, sem dependência nova), no mesmo padrão visual de /api-docs. Toda entrada futura deste arquivo aparece publicamente sem nenhum passo extra. Linkado no rodapé da landing e na navegação de /api-docs (changelog/index.php)saas-admin (landing/saas/apikeys.php), que continua disponível como via de suporte para intervenção manual (config/index.php, js/customJQuery_config.js)GET /api/v1/vouchers): espelha os filtros de consulta_avancada (gerencia/index.php) — comprador, CPF/celular (dígitos normalizados), descrição, paciente, criador, código, observação, intervalos de oid/valor/datas, tipo, modalidade e status (múltiplos valores por vírgula, combinados com AND entre parâmetros e OR dentro do mesmo parâmetro) — mas com paginação real (page/per_page, até 100 por página) em vez do LIMIT 10 fixo ou lazy-load de 20 em 20 da versão web, e tudo via prepared statements. Nunca retorna provisórios, mesma regra dos demais endpoints. Exige escopo read (landing/api/vouchers.php, landing/api/index.php)oid no Objeto Voucher: adicionado a todos os endpoints que retornam vouchers (consulta, busca, uso, cancelamento) como identificador numérico estável, alternativo ao codigo — mudança aditiva, não quebra clientes existentes/api-docs): página autocontida (sem login, no mesmo padrão de privacidade/ e termos/) com referência completa dos 5 endpoints, autenticação, escopos, rate limit, formato de resposta/erro, objeto Voucher e um índice de todos os error.code possíveis. Inclui busca em tempo real e sumário lateral com scroll-spy (mesmo padrão de ajuda/index.php), exemplos em curl com botão de copiar. Linkada no rodapé da landing pública, na Central de Ajuda e no painel de Chaves de API do saas-admin (api-docs/index.php, landing/index.php, ajuda/index.php, landing/saas-admin/tenant.php)POST /api/v1/vouchers): espelha cadastro/action.php. Só tipo comercial/campanha (provisório fora do escopo v1 — não tem cod_validacao e não é encontrável pelos demais endpoints da API). Aplica a mesma checagem de cota de vouchers por plano (trial/pago, is_internal sem limite). Gera cod_validacao único por voucher com o mesmo algoritmo de cadastro/action.php. Suporta quantidade (1–100) para criação em lote, retornando um array de códigos gerados. Exige escopo create. Erros: 422 VALIDATION_ERROR/TIPO_NOT_ALLOWED, 403 QUOTA_EXCEEDED (com tmax/tcur em error.details). Audita cada voucher criado em vouchers_log com tipo = "API - Novo Voucher" (landing/api/vouchers.php, landing/api/index.php) — com este endpoint fecha-se o catálogo mínimo planejado para a API externa: consultar, usar, criar e cancelar vouchersPOST /api/v1/vouchers/{codigo}/cancelar): espelha cancela_voucher (gerencia/index.php), incluindo o novo bloqueio de "já usado". Ordem de validação: existência, expiração (regra anti-abuso já existente — evita o loophole de estender-depois-cancelar), já usado, já cancelado. Exige escopo cancel. Não tem regra de "ownership por usuário" (nível 0 só cancela o que criou) — a API key já é escopada por tenant. Audita em vouchers_log com tipo = "API - Cancelamento de Voucher" (landing/api/vouchers.php, landing/api/index.php)POST /api/v1/vouchers/{codigo}/usar): espelha a validação estrita de marcar_usado (valida/index.php) — checa existência, já usado/cancelado, expiração e o vínculo de CPF (fraude) antes de marcar como usado, nessa ordem. Exige escopo use. Body: { nome_paciente, cpf_paciente }. Retorna o voucher atualizado no mesmo formato do endpoint de consulta. Audita em vouchers_log com tipo = "API - Uso Registrado" (landing/api/vouchers.php, landing/api/index.php, landing/api/bootstrap.php)GET /api/v1/vouchers/{codigo}): primeiro endpoint funcional de voucher da API, espelhando a lógica de leitura de valida/index.php. Retorna status (disponivel/usado/cancelado/expirado), tipo, valor, modalidade de desconto, validade, descrição, regras, data de uso e nome do paciente — sem dados do comprador (PII), mesmo comportamento da tela pública de consulta. Provisórios e códigos inexistentes retornam igualmente 404 VOUCHER_NOT_FOUND, para não vazar a existência de vouchers provisórios. Exige escopo read da API key. Consultas (encontradas ou não) são auditadas em vouchers_log com tipo = "API - Consulta Voucher" (landing/api/vouchers.php, landing/api/index.php)/api/v1/...): primeira fase de uma API REST para sistemas externos consultarem, usarem, criarem e cancelarem vouchers, sem depender de sessão/CSRF web. Autenticação por API key (Authorization: Bearer dnr_live_...), com o tenant resolvido a partir da própria chave (não do subdomínio), constant-time hash compare, escopos (read/use/create/cancel) e rate limit por minuto. Feature exclusiva de tenants em plano Pro/Premium (contas internas ignoram a checagem). Nesta fase só existe o health-check GET /api/v1/ping; os endpoints de voucher chegam nas próximas fases (landing/api/bootstrap.php, landing/api/index.php, landing/saas/apikeys.php, .htaccess)saas_api_keys e api_rate_counters em donara_master, mais a coluna saas_plans.api_enabled (landing/saas/patch_api_keys.sql)saas-admin/tenant.php: gerar (com label e escopos), listar e revogar chaves por tenant, com a chave em texto plano exibida uma única vez no momento da criação (landing/saas-admin/tenant.php, landing/saas-admin/action.php)Corrigido
cancela_voucher (gerencia/index.php) só bloqueava cancelamento de voucher expirado — não verificava se já estava usado ou já cancelado antes do UPDATE, dependendo só da UI esconder o botão nesses casos. Agora valida os dois casos explicitamente no backend, mesma regra usada no endpoint da APIusa_voucher (gerencia/index.php) não revalidava voucher_usado (já usado/cancelado) nem data_validade (expirado) antes do UPDATE — dependia só da UI esconder o botão nesses casos. Uma chamada repetida/forjada contra um voucher já usado, cancelado ou expirado re-gravava data_uso/nome_paciente/cpf_paciente silenciosamente. Agora valida explicitamente no backend, espelhando a mesma regra já usada em marcar_usado (valida/index.php) e no endpoint da APItenants.plan_tier (o nome real é tenants.tier); como o prepare() falhava e o erro era engolido por um catch (Throwable) silencioso, nenhum tenant tinha limite de vouchers ou usuários aplicado, independentemente do plano contratado. Corrigido em todas as 5 ocorrências (cadastro/action.php, usuarios/index.php)Documentação
/api-docs, Central de Ajuda, saas-admin e aba Integrações atualizados para refletir a restrição a Premium + conta ativa/api-docs e a Central de Ajuda atualizadas: a seção "Como obter acesso" não instrui mais contatar o suporte — orienta gerar a chave em Configurações → Integrações/api-docs atualizada com a referência completa do endpoint de busca e o campo oidCorrigido
facingMode: 'environment' é apenas uma constraint "ideal" — nem todo navegador/dispositivo a honra, podendo cair na câmera padrão (frontal). Trocado para listar as câmeras via Html5Qrcode.getCameras() antes de iniciar e selecionar por ID a que tem label contendo "back"/"traseira"/"rear"; só recorre ao facingMode como fallback se nenhuma câmera com esse label for encontrada (valida/index.php).cad-tipo-grid): abaixo de 991.98px força 2 colunas fixas e centraliza o card órfão da última linha (css/custom_cadastro.css)justify-content-center adicionado à row (dashboard/index.php)?v=filemtime(...) ao custom.css; os CSS específicos de cada página (custom_cadastro.css, custom_dashboard.css, custom_config.css, custom_fonts.css, custom_consulta.css, custom_logs.css, custom_usuarios.css) continuavam sem versionamento e ficavam presos em cache do navegador mesmo após deploy — corrigido em todas as páginas que os referenciam.app-sidebar usava height: 100vh, calculado sobre a maior altura possível da viewport; com a barra de endereço visível, o rodapé da sidebar (toggle de tema, usuário logado) ficava fora da área visível. Adicionado height: 100dvh como override progressivo (css/custom.css)css/custom.css: link do CSS agora inclui ?v=<?php echo filemtime(...) ?> em todas as páginas, forçando o navegador a buscar a versão atualizada a cada deploy sem depender do usuário limpar o cache manualmentehome/, faturamentos/ e config/ativar_assinatura.php: essas páginas incluíam a navbar (e o link "Editar cadastro") mas não tinham o setup completo do Xajax ($xajax = new xajax(), include de auth.inc.php, processRequest()/printJavascript()) nem o script js/customJQuery_geral.js, causando loadPerfilData is not defined e xajax_load_perfil_data is not defined no console ao abrir o modalAdicionado
facingMode: 'environment') não seja a correta, sem afetar o comportamento de abertura automática (valida/index.php)Alterado
Html5QrcodeScanner (UI própria com botão "Request Camera Permission" → seleção de câmera → "Start Scanning") por Html5Qrcode de baixo nível, iniciado com facingMode: 'environment' — abre a câmera traseira (principal) imediatamente ao abrir o modal, restando apenas o prompt nativo de permissão do navegador (inevitável, exigido pelo próprio browser). Fallback para a primeira câmera disponível via Html5Qrcode.getCameras() caso o dispositivo não identifique uma câmera traseira (valida/index.php)Adicionado
includes/app_defaults.inc.php expandido como fonte única de verdade para todos os defaults do sistema:$APP_DEFAULT_LAYOUT_H / $APP_DEFAULT_LAYOUT_V — posições, tamanhos e cores padrão de cada elemento do voucher (movidos de config/index.php); voucher/index.php agora faz um único merge ao carregar o layout do banco, eliminando ~50 fallbacks individuaisdefine('APP_DEFAULT_THEME', 'light') — tema padrão centralizado; referenciado em navbar.php (3 ocorrências) e comentado no SQL de auth.inc.php$APP_PREVIEW_TEXTS — textos de amostra do editor de templates; injetados via PHP como _appPreviewTexts na config page e consumidos por CFG_ELEMENTS.sample no JS (com fallback inline)$APP_VOUCHER_STATUS_TEXTS / $APP_VOUCHER_EMPTY_CAMPANHA — textos de status e placeholder de campanha vazia do renderizador GD$APP_DEFAULT_DPO — campos DPO vazios (movidos de config/index.php)$APP_DEFAULT_RULE_MODELS — modelo de regra pré-configurado exibido no primeiro acesso ao sistema; após o tenant salvar modelos pela primeira vez (mesmo que apague tudo), o default nunca mais é aplicado (distinção via presença da chave rule_models no banco)CORES, kpiConfig e finConfig alinhados com $APP_DEFAULT_COLORS (eram valores Bootstrap divergentes)$col_xl_map / $col_xxl_map) conforme total de cards visíveis, eliminando orphans desalinhados.col-xxl-5th (20%); min-height: 230px nos cards para manter proporção visual adequadajustify-content-center na row — orphans sempre centralizados$num_cards na condição correspondenteCorrigido
if($user_nivel >= 1))font-size: 0.82rem e padding alinhados com os demais links da sidebarincludes/app_defaults.inc.php: modelo de regra default com quebras de linha no textarea (\n em string de aspas duplas); tema padrão ajustado para lightAdicionado
user_config de vouchers_users (auto-migration no carregamento da página de config); dashboard lê as cores pessoais do usuário logado com fallback para os defaults do sistemaincludes/app_defaults.inc.php — defaults de cores centralizados em arquivo compartilhado ($APP_DEFAULT_COLORS); remove arrays duplicados de config/index.php e dashboard/index.phpAlterado
var(--bs-body-color) em ambos os temas (consistência claro/escuro); seletor de cor "Total" removido da aba AparênciaCorrigido
if(false) morto removido; migration da coluna user_config reposicionada para o carregamento da config page, eliminando dependência do toggle_temaorigHtml agora restaurado antes da chamada XajaxrenderDashboard() completo no MutationObserver por _updateChartsTheme(), que apenas atualiza as propriedades sensíveis ao tema (border, ticks, grid, legendas) via .update('none') sem recriar os canvasAdicionado
visible ausente = visível)voucher/custom/{slug}/logo.png e aplicado via GD com alpha blendingconfig/upload_logo.php; caminho armazenado na chave logo_src de vouchers_configvisible e proporção original da imagemv2.3.x em texto pequeno e semi-transparente; some suavemente quando a sidebar está colapsada$app_version em includes/about.php — única fonte de verdade usada pelo badge da sidebar e pelo modal "Sobre" (que também foi atualizado de 2026.04.22 para a versão corrente)Corrigido
display:flex; align-items:center ao .cfg-ic-align-colAdicionado
data_compra) e data de uso (data_uso): colunas migradas de DATE para DATETIME nos bancos de todos os tenants — permite auditoria com precisão de minutolanding/saas/patch_datetime_campos.sql para aplicar em novos tenants futurosconfig/ → abas Template H e V): em telas com ponteiro impreciso (pointer: coarse) ou janelas estreitas (<992 px), o drag-and-drop dos handles e os campos de coordenadas ficam desabilitados — o preview do layout permanece visível; um banner amarelo explica o motivopointer: coarse (ponteiro primário do sistema) em vez de navigator.maxTouchPoints, evitando falso positivo em notebooks com tela touch + mouseAlterado
DD/MM/YYYY HH:MMdatetime-local, exibindo e aceitando data + horadata_compra e data_uso exportadas com horário (DD/MM/YYYY HH:MM)DATE() nas comparações — evita exclusão silenciosa de registros do último dia do intervalo com colunas DATETIME_fpDataValidade.setDate()_fpDateOnlyConfig (dateFormat: 'Y-m-d'); formato enviado ao backend é idêntico ao anterior — nenhuma alteração no PHP de busca ou de savecfg-editor-wrap); z-index: 40 garante visibilidade sobre os drag handles; ícone fixo no canto inferior direito, label "Alterar imagem" expande para a esquerda no hoverprivacidade/index.php: reescrita como página pública standalone — removidos session.inc.php, redirect.inc.php, navbar.php, Xajax e query de DPO; texto da seção 1 tornado genérico ("empresa contratante") sem dependência de dados do banco; estilo alinhado ao termos/index.php (CDN Bootstrap, sem sidebar)termos/index.php: data de última atualização atualizada para Maio de 2026 (já era página pública — sem session/redirect)$_footer_name / dpo.clinica) movido do rodapé para o topo do formulário como elemento principal (#login_company_name): Lato 300, 2.2rem, gradient branco→lavandalink-secondary) com tooltip "Voucher expirado — cancelamento não permitido"; botão "Usar Voucher" permanece ativo para expirados (operador decide caso a caso)cancela_voucher Xajax): verificação de data_validade antes do UPDATE — retorna alerta e aborta se hoje > data_validade, impedindo cancelamento mesmo via requisição diretaeditar_voucher) não atualiza voucher_usado — gerenciado exclusivamente pelas ações dedicadas (usar/cancelar)voucher_usado = 1: a seção é envolta em <div id="section_paciente"> e mostrada/ocultada dinamicamente por load_voucher_data com base no status do voucher; ao ocultar, os campos ficam desabilitados para que não sejam submetidos via Xajaxeditar_voucher): nome_paciente e data_uso só são gravados no banco se voucher_usado = 1 na consulta atual — proteção dupla independente da UItype="date" (picker nativo do SO) para flatpickr (dateFormat: Y-m-d, disableMobile: true, minDate: today) — visual e comportamento agora consistentes com a gerência de vouchers independentemente do SO ou locale do browsercustomJQuery_cadastro.js: bloco de setAttribute('min', ...) removido — a restrição de data mínima é gerenciada diretamente pela opção minDate: 'today' do flatpickrCorrigido
0000-00-00 00:00:00 adicionado na query de utilizações mensais para retrocompatibilidade com dados legados pré-migraçãojustify-content: center, gap: 0 e remoção de margin/padding horizontal no modo colapsado — !important necessário para vencer utilities Bootstrap (px-3, gap-2) e inline styles do botão de temamax-width corrigido para calc(var(--sidebar-w-collapsed) - 16px) e depois para var(--sidebar-w-collapsed) com margin: 0, garantindo que o elemento ocupe exatamente os 60px sem overflow::after era compartilhado entre o tooltip CSS e o caret do Bootstrap Dropdown; corrigido com :not(.dropdown-toggle) na regra do tooltiptime_24hr: true, disableMobile: true) — elimina exibição AM/PM causada pelo locale do SO (Windows en-US) que o datetime-local nativo não consegue sobrescrevertype="text" com exibição em DD/MM/AAAA HH:MM (campo sempre desabilitado — apenas exibição)try-catch (Throwable) ao redor do UPDATE — PHP 8.2 mudou o padrão de mysqli_report para MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, fazendo com que erros MySQL lançassem mysqli_sql_exception não capturada; agora retorna alert() com a mensagem de erro em vez de HTTP 500valor_compra não era atualizado corretamente quando o campo estava desabilitado: Xajax getFormValues inclui inputs disabled (ao contrário do HTML form padrão), enviando string vazia para editar_valor; MySQL strict rejeita '' em coluna numérica — corrigido com guard !== '' na condição, igual ao já existente em editar_data_validade!important em padding e margin do .app-sidebar .nav-link para prevenir sobrescrita por qualquer CSS externo, e line-height: 1.5 explícito no link e line-height: 1 no ícone para altura de item determinística independente do browser, fonte ou CSS de terceirosvoucher_usado=2 mas não exclui expirados)editar_voucher): SELECT verifica data_validade atual no banco antes de processar a edição; se hoje > data_validade, retorna alerta e aborta sem alterar o bancoload_voucher_data): quando o voucher está expirado, o campo "Validade" é mantido desabilitado com tooltip "Voucher expirado — validade não pode ser alterada"; flatpickr tem clickOpens e allowInput desativados para bloquear também a interação com o calendário; ao abrir um voucher não expirado em seguida, as opções do flatpickr são restauradasminDate: 'today' adicionado via Object.assign sobre _fpDateOnlyConfig — dias anteriores a hoje ficam cinza e não são selecionáveis no calendárioeditar_voucher): guard data_validade < date("Y-m-d") antes do UPDATE — retorna alerta e aborta se a data enviada for anterior a hoje, impedindo expiração manual de vouchers válidos mesmo via requisição diretaCorrigido
#0a0a0a) em todos os estados: expandida, colapsada e barra mobile — seletor html[data-bs-theme="dark"] com especificidade superior à do Bootstrap garante a cor em qualquer situação#111827 inalteradooverflow-x: hidden da sidebar (elemento position: fixed) clipava o dropdown mesmo com data-bs-strategy='fixed'; corrigido via listeners show.bs.dropdown / hidden.bs.dropdown que liberam e restauram o overflow temporariamentenavbar.php introduzido junto com o fix acima: aspas duplas dentro de querySelector(...) dentro de echo "..." encerravam a string prematuramente, derrubando todas as páginasAdicionado
git pull).gitignore protegendo credenciais, logs e templates por clientesecrets.php.example como template documentado de configuraçãoactive e cancelled em carência agora são verificados contra saas_plans.max_vouchers ao criar vouchers comerciais/campanha — anteriormente apenas o trial era verificadosaas_plans.max_users; apenas ativo = 1 conta contra o limite (desativar um usuário libera uma vaga)trial_max_users no SaaS Admin: novo campo configurável na seção Trial (ao lado de Duração e Máx. vouchers); campo vazio = ilimitadotrial_max_vouchers e trial_max_users agora aceitam vazio = ilimitado (campo removido de saas_settings quando vazio, retornando NULL nas queries de verificação)autocomplete="off" nos formulários Trial e Motivos de Cancelamento do SaaS AdminCorrigido
#ID — provisóriovoucher_tipo diretamente no banco (em vez de inferir pela existência de cod_validacao); opção "Provisório" desabilitada no select ao abrir o modal para esses vouchersdata_compra, data_uso) eram enviadas como string vazia ao MySQL, que rejeita '' em colunas DATE com STRICT_TRANS_TABLES — adicionado guard !== '' antes de incluir qualquer data no UPDATEdata_compra (data de emissão) tornada imutável após definição: campo desabilitado no modal em três camadas — HTML sempre disabled, servidor nunca envia enableObj, e editar_voucher() ignora completamente o campo enviado pelo formulário; data de emissão só é definida na criação direta ou na conversão de tipo Provisório → Comercial/Campanhais_internal = 1) isentas de qualquer verificação de limite de vouchers e usuários — consistente com o comportamento já existente em config/assinatura.phpTypeError: Cannot read properties of null (reading 'addEventListener') na página de Usuários: listener do modalToggleUsuario executava antes do HTML do modal existir no DOMdonara_master (PHP 8 + MYSQLI_REPORT_ALL) não eram capturadas e derrubavam a função Xajax — adicionado try-catch (Throwable) em todos os três pontos de verificação de limiteAdicionado
Adicionado
Alterado
Adicionado
Corrigido
Adicionado
Adicionado
Melhorado
Corrigido
Adicionado
Corrigido
Melhorado