img_optimize() decodes .jxl with djxl or Imagick, resizes with GD, re-encodes
with cjxl and writes foto.jxl.webp beside it; skips cleanly without a decoder.
Gallery, hero, navbar logos and JSON-LD serve the JXL with the fallback, and
trash/removal move the fallback with its .jxl.
- Sidebar do backoffice: Backups/Auditoria/IP bans/Geo/Origem das visitas/
Atualizações deixam de ser um link cada (enumerados à mão) e passam a um
só link "Sistema" para o índice partilhado do platform-tools
(platform.php?tool=index), como a fotografia/design/dance já fazem — uma
ferramenta nova no platform-tools passa a aparecer sozinha, sem link novo
a escrever aqui. Definições, Mensagens e Comunicações continuam com link
próprio (são páginas do fitme, não do router).
- Horário de Aulas (home e CrossFit, mesmo componente partilhado): a grelha
fica fechada por omissão atrás de um botão "Ver Horário das aulas"
(Bootstrap collapse); clicar em "Horário" na navbar (ou abrir a página já
com #horario no URL) abre-a de imediato em vez de só rolar até uma secção
fechada.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
O logo tem style="width: 150px" sem height desde sempre; a auditoria
4.7 acrescentou width="756" height="252" (lidos do viewBox do SVG)
sem adicionar height:auto ao inline style. Sem height:auto, o browser
não escala a altura proporcionalmente ao width CSS — ficou fixa nos
252px do atributo, esticando a barra toda. Confirmado ao vivo:
getComputedStyle(img).height passou de "252px" para o valor
proporcional depois desta correção.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auditoria: "Site público em Bootstrap 4.3.1 (EOL); backoffice em
5.3.3" — o backoffice já usa a 5.3.3 vendorizada (assets/vendor/bootstrap5/),
o site público passa a usar a mesma cópia em vez da 4.3.1.
Levantamento antes de mexer: a folha de estilo própria (style.css)
quase não usa classes utilitárias cruas do Bootstrap (grep confirma
zero ml-/mr-/pl-/pr-/float-left/float-right/custom-select/
custom-control/badge-/sr-only/font-weight-/text-left/text-right —
tudo classes próprias como .site-btn). O que precisava de mudar:
- CSS/JS: assets/vendor/bootstrap/ -> assets/vendor/bootstrap5/
(bundle, inclui Popper).
- Os 7 atributos data-toggle/data-target/data-dismiss (BS4) ->
data-bs-toggle/data-bs-target/data-bs-dismiss (BS5): toggle da nav
mobile, os dois modais (aula experimental, inscrição), o botão de
fechar do alerta. O seletor em main.js que lê data-target também
mudou para não parar de encontrar os botões.
- .close -> .btn-close (BS5 renomeou o componente; markup sem o
× interno, que o BS5 desenha via CSS).
- .form-row -> .row (BS4 tinha form-row com gutters menores; BS5 não
tem, style.css não tinha overrides para form-row, confirmado).
- O apanhado maior: dois <script> inline chamavam
jQuery('#trialModal').modal('show') ao estilo de plugin jQuery do
BS4 (para reabrir o modal depois de um erro de submissão) — o BS5
não regista plugins jQuery nenhuns, isto ficava morto em silêncio.
Trocado por new bootstrap.Modal(...).show(), a API JS nativa do
BS5. grep confirma zero outras chamadas .modal()/.collapse()/etc.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
125 imagens na home, 0 com srcset e 122 sem width/height — o browser
só sabia o tamanho depois de descarregar, o que causa layout shift.
Uploads são redimensionados a um limite de lado maior (functions_images.php),
não a uma dimensão fixa (fotos de equipa vão de 389x512 a 512x512, ícones
de oferta de 128x128 a 256x256), por isso as dimensões não dão para
hardcode: site_img_dims_attr() em functions_site.php lê o ficheiro real
com getimagesize() por pedido, tal como o gallery.php já fazia, e
devolve o par width="" height="" ou "" se o ficheiro não existir. SVG
não passa por getimagesize() (sem decoder embutido) — svg_img_dims_attr()
lê os atributos width/height (ícones fitme-*.svg, 48x48 fixo) ou cai
para o viewBox (logos do navbar, só têm viewBox).
Aplicado a: ícones da oferta, fotos de equipa (card + painel de detalhe),
ícone de modalidade em modalidade_icon_html() e no horário (index.php e
includes/horario.php), logo do navbar.
srcset ficou de fora: as imagens já vêm coartadas para pouco mais do que
o maior tamanho a que aparecem (fotos de equipa a 512px cabidas numa caixa
de no máx. 200px CSS com object-fit:cover; ícones a 128-256px mostrados a
44px) — é a folga normal para ecrãs retina, não desperdício a resolver
com múltiplas resoluções. E o layout é sempre CSS-driven (width/height:
100% + object-fit), por isso um srcset não mudaria o que é descarregado
por variação de viewport, só pouparia bytes nalguns casos — gerar 2-3
tamanhos por upload via GD seria trabalho a mais para o ganho real neste
site. Fica só o fix de dimensões, que é o que resolve o CLS que a
auditoria mediu.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fecha os itens 3.2.2, 3.3.1, 4.6, 5.2, 5.6, 5.7 e 7.4 do relatório de
auditoria de 2026-09-24: escapa saídas de site_img_url()/canonical,
valida identificadores de coluna em db_info_save(), troca 100vh por
100dvh, remove a última cor solta, encurta a transição para ~150ms,
acrescenta decoding=async/fetchpriority=high nas imagens da home, e
documenta as versões vendorizadas em ASSETS.md.
Co-Authored-By: Claude Sonnet 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>
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>