Mesmo padrão de horario/: extrai dia/abertura/fecho para _form.php.
create.php formatava open/close sem substr() enquanto edit.php cortava
para "HH:MM" com substr(..., 0, 5) por vir de uma coluna TIME da BD;
unificado na versão de edit.php (sem efeito em create.php).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extrai os campos comuns (modalidade, dia, início, fim, sala) para
_form.php. create.php formatava hora_inicio/hora_fim sem substr()
enquanto edit.php aplicava substr(..., 0, 5) para cortar os segundos
que vêm da coluna TIME da BD — unificado na versão de edit.php (mais
defensiva; sem efeito em create.php, cujos valores já vêm em "HH:MM").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extrai os campos comuns (nome, cor, ícone/upload, ordem, descrição, e a
biblioteca de ícones disponíveis) para _form.php, seguindo o padrão de
events/ e memberships/. A diferença genuína entre criar e editar — o
bloco de ícone mostra pré-visualização e checkbox "remover imagem
atual" só quando já existe um registo — fica como um if ($item ===
null) explícito dentro do parcial, tal como o hero_image em events/.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
create.php e edit.php tinham os mesmos campos de formulário duplicados
lado a lado. Extrai o markup comum (nome, nascimento, área, estado,
foto, título, aptidões, formações, descrição) para _form.php, seguindo
o padrão já usado em events/ e memberships/. Cada view fica só com a
lógica que lhe é própria (criar vs. carregar registo existente,
upload/validação de foto, título da página, redirect).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Os últimos 3 pontos de SQL direto em vistas, que tinham entrado depois
da auditoria original (trabalho de multi-ginásio): gyms/create.php e
gyms/edit.php partilhavam a mesma query da lista de galerias — agora
db_galleries_options() em functions_galerias.php (e simplificada: o
_gyms_has_col() defensivo já não é preciso, galleries.deleted_at
existe desde a migração de 25/09). reviews/index.php gravava
google_place_id à mão — agora db_gym_set_place_id(), separado do
db_gyms_update() geral para não auditar/validar o resto do registo
por uma só coluna.
Zero vistas com SQL direto agora (grep confirma).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auditoria 6.3 apontava dashboard.php (2×), user/index.php e
user/profile.php como as únicas vistas com SQL direto, quebrando a
convenção da frota (config/functions_*.php como camada de models). As
quatro passam a chamar db_users_count_clients(), db_users_count_team(),
db_users_list() e db_user_update_profile() — mesmo SQL, mesmos
parâmetros, só movido para onde o resto do código já vai buscar dados.
Nota: views/admin/gyms/{create,edit}.php e views/admin/reviews/index.php
também têm SQL direto, mas não estavam no âmbito dos "3 vistas" da
auditoria (entraram depois, no trabalho de multi-ginásio) — por tratar
numa próxima passagem.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Pedido de aula experimental passa a ficar em trial_leads (por ginásio)
ANTES do email: sem SMTP o visitante já não vê «erro» e o pedido não se
perde. Gestão → Aulas experimentais: estados, notas, CSV (auditado),
apagar para a lixeira; aviso «sem email» quando o aviso não saiu.
- Aula experimental, inscrição e eventos usam o anti-spam do platform-tools
(honeypot, carimbo assinado com tempo mínimo, prova de JS, limite por IP,
pontuação de conteúdo) e o bloqueio de IP, em vez de honeypots locais.
Recusas ficam em blocked_submissions; resposta igual à de sucesso.
Falha aberto se o módulo rebentar.
- Checkbox de consentimento da aula experimental passa a ser guardada.
- Audit das escritas de conteúdo: criar/editar equipa, modalidades,
eventos, mensalidades, pedidos de inscrição, galerias, horário (incl.
arrastar no calendário), horas, contactos e ginásios, com antes/depois
campo a campo (audit_row/audit_change). Exportação CSV de inscrições
auditada.
- .table-responsive em todas as tabelas do backoffice.
- Migração: database/migration_20260926_trial_leads.sql (só cria tabelas).
Testado numa cópia da BD de produção: 63/63 verificações HTTP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Estrutura multi-ginásio: as mesmas secções do site, conteúdo próprio por
ginásio, e cada um numa pasta da URL com o código postal da zona.
- migration_20260926_gyms: tabela gyms (pasta, nome, localidade, publicado,
principal, secções visíveis, título/imagem do header, SEO, galeria, Place ID,
página CrossFit) com o FitMe 3070 como ginásio 1 em /3070/; gym_id em team,
modalidades (o horário segue a modalidade), work_hours, memberships,
membership_leads e website_offer (tudo o existente → ginásio 1); events com
gym_id NULL = todos; fitme3070_info passa a uma linha por ginásio (id = gym).
- Site: /<pasta>/, /<pasta>/crossfit, /<pasta>/agendar|inscricao. Ginásio por
publicar → 404 para o público, pré-visualização (com faixa) para a equipa.
/ e /home → 301 para o ginásio principal (a manter a query); com mais de um
ginásio publicado, a raiz passa a uma página para escolher. Secções
desligadas não aparecem nem no menu; menu, canonical, JSON-LD, sitemap,
formulários, e-mails e reviews (Place ID e cache) são do ginásio.
- Backoffice: Gestão → Ginásios (criar/editar/apagar, secções, e «começar
com o conteúdo de» outro ginásio — cópias independentes) e o seletor
«A editar» no topo; equipa, modalidades, horários, horário de
funcionamento, mensalidades, pedidos, contactos e reviews trabalham sobre o
ginásio escolhido, e um registo de outro ginásio não abre.
- Modalidades passam para logo a seguir à galeria das instalações.
- Galeria sem cantos arredondados (raio da marca).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- assets/js/password-toggle.js, carregado nos dois layouts: liga os botões
existentes e cria um para cada campo de password que não o tenha (eram 3
em 12). aria-pressed, aria-label que muda, fora da ordem de tabulação, e
volta a ocultar ao submeter.
- Removidas 23 bibliotecas/folhas sem nenhuma referência (193 ficheiros),
incluindo o jQuery 3.2.1 (CVE-2020-11022/11023) que se servia por URL, o
Splide (a galeria passou a masonry própria em d70f13a), owl, mixitup,
masonry, magnific-popup, slicknav, jquery.lazy, fullpage, TweenMax,
circle-progress, progressbar, o template polo e cópias antigas do
Bootstrap/FontAwesome em site/assets.
- .table-responsive nas listagens de eventos e galerias.
- .gitignore: sem os 5 padrões herdados do fotografia; *.sql/*.dump
ignorados (exceto migrações e o esquema); .trash/ das imagens; nota sobre
vendor/ versionado de propósito. database/andmore_fitme3070_dev.sql passa a
database/schema.sql (só estrutura, 0 INSERT).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Correções da auditoria de 24/09 (audits/fitme3070-auditoria-2026-09-24.html).
Lixeira
- Tudo o que é importante passa a soft delete (deleted_at + deleted_by):
equipa, modalidades, eventos, mensalidades, galerias, imagens, horário,
horas de funcionamento, inscrições em eventos e pedidos de inscrição.
- config/functions_trash.php: um registo por tipo; apagar, restaurar, listar
e eliminar leem todos dele. Eliminar de vez só com 30 dias na lixeira —
a condição está no WHERE do DELETE, um POST forjado não apaga nada.
- Gestão → Lixeira (views/admin/trash/index.php): lista única com quem e
quando, restauro a qualquer momento.
- Imagens de galeria: o site lista a pasta, por isso o ficheiro passa para
.trash/ (negado pelo .htaccess) e volta no restauro.
- As listagens e o site público filtram deleted_at (horário também filtra
modalidades apagadas). Migração: database/migration_20260925_trash.sql
(só acrescenta colunas; tem de ser aplicada antes deste código).
CSRF
- csrf_check() recusa qualquer pedido que não seja POST (405). Antes saía
sem verificar nada em GET, e dez ações de apagar/restaurar eram GET.
- Os dez handlers passaram a POST com token; links viraram formulários.
Autenticação e sessão
- client_ip(): X-Real-IP / último X-Forwarded-For, só vindo de proxy privado
com TRUSTED_PROXY. O throttle contava o IP do proxy (limite global).
- Throttle também por conta (hash do email), além do IP.
- Sem oráculo de tempo: login verifica contra um hash de referência quando
a conta não existe; a recuperação deixa o email na fila.
- Cookie de sessão Secure/HttpOnly/SameSite=Lax e use_strict_mode.
- Definições de sistema só para sysadmin; autocomplete=username.
Servidor e uploads
- .htaccess: nosniff, Referrer-Policy, X-Frame-Options, Permissions-Policy,
CSP (frame-ancestors, base-uri, object-src, form-action); nega parciais
_*.php, PHP soltos do platform-tools e *.md; 404 própria.
- Uploads validados pelo conteúdo (finfo/getimagesize); SVG só sem scripts,
handlers on*=, foreignObject ou hrefs externos.
Testado numa cópia da BD de produção (fitme_audit_test) com um contentor
temporário: 34/34 verificações HTTP ponta-a-ponta.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Places API (New) não tem parâmetro de ordenação e devolve no máximo 5
reviews, por isso ordem e filtro aplicam-se a essas 5: relevância (ordem da
Google), mais recentes (publishTime) ou mais completas (texto mais longo), e
opcionalmente só quem tem foto de perfil. Mudar estas opções já não apaga a
cache — só uma nova chave ou Place ID obriga a um novo pedido pago.
Cartão: foto de 84px (ou inicial), nome, data e estrelas grandes em cima;
texto mais pequeno por baixo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Contactos: nova página no backoffice (fitme3070_info) e 964 201 137 como
telefone principal; o telefone secundário só aparece quando existe.
- Equipa: título, aptidões/funções e formações; o cartão mostra foto, nome e
título e abre um cartão horizontal de detalhe (hover no desktop, toque no
telemóvel) sem mexer na grelha.
- Mensalidades: cartões editáveis com um destaque, modal de inscrição que
guarda o pedido (RGPD + estado + campos reservados para o CRM) e avisa o
ginásio por e-mail; lista de pedidos com estados, notas e CSV.
- Reviews Google via Places API (New), filtradas por classificação mínima e
em cache configurável; página de configuração com pesquisa do Place ID.
- Assets com ?v=<mtime>: site/assets é servido com cache imutável de 1 ano.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The save AJAX endpoint swallowed DB exceptions behind a generic
"Erro ao guardar" with no logging, making failures undiagnosable.
Now log the real exception (same sink as the global handler) and
echo the detail back in dev.
Also validate `local` against the allowed sala set instead of only
its length, so an out-of-enum value returns a clean 422 instead of
a 500 (horario.local is a strict ENUM).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plain-PHP fleet platform with id.andmore.pt SSO. Wires shared/platform-tools
as a submodule (platform_update, backups, git/migration tools) like the other
platforms.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>