- Política de privacidade reescrita: tabela de dados/finalidades/fundamento
(inclui os pedidos de aula, inscrição e eventos com IP), prazos concretos,
subcontratantes, Google Maps e reviews, cookies. Termos e condições novos
(/home/termos), no rodapé e no sitemap. Campos [ENTRE PARÊNTESES RETOS]
e revisão por jurista pendentes antes de publicar no domínio real.
- Google Maps só carrega ao carregar em «Mostrar mapa» (map_click_to_load);
as fotos dos autores das reviews são guardadas pelo servidor em
uploads/reviews/ e servidas localmente (review_avatar_url).
- Retenção que a política declara, aplicada pelo cron (04:00): leads com
mais de 12 meses → lixeira; blocked_submissions e outbox de segurança
apagados aos 90 dias (trash_retention_sweep, auditado).
- Backoffice e login com tema claro/escuro/automático: data-bs-theme
aplicado no <head> antes do CSS (sem flash), escolha em localStorage,
segue o sistema em tempo real, color-scheme e theme-color para os dois.
Cores próprias passaram a variáveis do Bootstrap. lang="pt-PT".
Testado numa cópia da BD de produção: 72/72 verificações.
Co-Authored-By: Claude Opus 5.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>
- shared/platform-tools: ea3616a → 12f26fd (27 commits). Traz o módulo de
audit (f761d99, o pin da frota), «manter sessão iniciada», o registo de
segurança da frota (o gancho que o login já tinha passa a funcionar) e a
correção do IP do cliente encontrada nesta auditoria (último X-Forwarded-For
/ X-Real-IP), promovida ao partilhado. client_ip() da app delega nele.
- config/functions_audit.php: audit() sobre platform_audit(); tabela
audit_log criada no primeiro uso, segredos ocultados, nunca parte a ação.
- Registado: login, falha de login, bloqueio por excesso de tentativas,
logout, pedidos de recuperação, mudança/definição de palavra-passe,
registo e criação de utilizadores, convites, perfil (antes/depois), todas
as definições (db_update_setting, com valor anterior), cada passo da
lixeira (apagar, restaurar, eliminar de vez) e ações nas mensagens.
- Sistema → Auditoria (views/sysadmin/audit.php), só sysadmin.
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>
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>
- 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>
Outbound email was relayed to the hub's /notify endpoint, so this install had
no SMTP configuration of its own and no way to see what had been sent. Bring
delivery back in-house, following the fotografia.andmore.pt stack rather than
inventing a second shape for it.
- Submodule bumped 603ff84 -> ea3616a, the pin fotografia already uses. That
is what provides functions_mail.php (shared transport), functions_settings.php
(platform_smtp_config + encrypted settings) and compat_secrets.php. Verified
no function-name collisions with this repo's own config/functions_settings.php:
platform-tools namespaces everything platform_*.
- PHPMailer 7.1.1 vendored at vendor/PHPMailer/src/. platform_mail_available()
looks for exactly this hand-vendored layout before falling back to composer's
autoloader, so the host needs no shell and no composer install. LGPL-2.1
license text included.
- config/functions_queue.php replaced: was a thin facade over the hub relay,
now fotografia's real queue. Rows are claimed atomically, retried with
exponential backoff up to max_attempts, and park in 'failed' with the real
SMTP rejection in last_error.
- SMTP resolution matches the fleet: .env wins for the SMTP_* constants, while
the actual send goes through platform_smtp_config($pdo), which is DB-first.
That ordering is deliberate -- the database is the half an operator can
change from the backoffice without shell access. Empty SMTP_HOST falls back
to PHP mail().
- Sistema -> Comunicações (views/sysadmin/comms.php): credentials plus a live
SMTP connection test and a verdict on what would happen to a message sent
right now. Secret-valued settings are write-only in the form and encrypted
at rest (AES-256-GCM, key from .env, never in the DB dump). Which keys count
as secret is platform_settings_is_secret()'s decision, not a list kept here.
- Sistema -> Mensagens (views/sysadmin/messages.php): the queue monitor.
Filters, per-message detail with last_error, and retry/cancel/archive.
Retry flushes inline so the operator sees the outcome immediately.
- cron.php drains the queue every 5 minutes, serialised with a lock file, and
runs retention once a day. Over HTTP it answers 404 until CRON_TOKEN is set
to something other than replace_me. Deliberately not a copy of fotografia's
cron: the session-reminder, download-digest and photo-archive steps have no
modules here and would be dead code.
- config/functions_notify_client.php deleted, and ANDMORE_ID_URL with it --
the hub relay was its only caller.
Verified without a database: PHP lint across the tree; PHPMailer builds a full
MIME message with RFC 2047 UTF-8 subject encoding from the three vendored files;
platform_mail_available() finds them; platform_mail_make() returns a correctly
configured mailer; the diagnosis states report Operacional / Não configurado.
NOT verified: the settings encryption round-trip -- this machine's PHP has no
openssl extension, so platform_secret_key() returns null and values pass through
in clear. The code degrades safely (stores the value rather than losing it) and
the Comunicações page warns when the key is unavailable.
Deployment: apply database/migration_20260913_message_queue.sql, set SMTP in
Comunicações or .env, set APP_SECRET_KEY, and schedule cron.php.
Co-Authored-By: Claude Opus 5 <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>