Files
fitme3070.pt/CLAUDE.md
T
2026-10-09 19:41:01 +01:00

8.3 KiB

fitme3070 — architecture

Backoffice + public site for a gym (ginásio) chain, plain PHP 8.4, no framework. This file exists because the pattern below was inherited from a CodeIgniter 4 port and had never been written down as a decision (audit 2026-09-24, item 6.1) — it describes what the codebase actually does, kept deliberately, not a design proposal for a rewrite.

Request lifecycle

Every page is its own controller-and-view combined. A page in views/admin/*.php or site/*.php does, in order: bootstrap (config/db.php + config/functions.php), an auth guard, read $_POST/$_GET and call model functions, then fall through into its own HTML. There is no separate template layer and no front controller — 71+ PHP files are directly reachable by URL (docroot is the repo root; .htaccess denies partials, shared/platform-tools/*.php, and *.md, but doesn't allowlist).

This is a known, accepted gap, not an oversight — see "What's not done" below. Introducing a real front controller/router for the app's own pages would touch most of views/, .htaccess's friendly-URL block, and every hardcoded base_url('views/...') call; it's a genuine rewrite, tracked separately, not bundled into this doc.

Data layer: config/functions_*.php as models

SQL lives in config/functions_*.php, one file per domain (functions_team.php, functions_galerias.php, functions_gyms.php, functions_memberships.php, …) plus functions_db.php for cross-cutting users queries. Functions are named db_<noun>_<verb>() (db_gyms_create, db_team_update, db_gallery_list). Views never write SQL directly — if you're adding a query, it goes in the matching functions_*.php, not inline in the page. (Audit 6.3, closed 2026-09-25 — grep -rl '\$pdo->\(query\|prepare\|exec\)' views/ should always return nothing.)

config/functions.php is the loader: every page requires config/db.php (opens $pdo, starts the session, connects to platform-tools) then config/functions.php (requires every functions_*.php in order). Read that file's own header comment before adding a new one — order matters for a few (auth.php before anything that calls require_login(), etc.).

Auth

Login/session/roles run on shared/platform-tools/functions_auth.php ('auth' => 'shared' in config/db.php, migrated 2026-09-25 — audit 1.3). config/auth.php keeps the app's own guard names (require_login(), require_admin(), require_staff(), require_sysadmin(), is_admin(), etc.) as a thin layer over the shared primitives, so none of the ~30 call sites in views/ had to change. Role ladder: sysadmin(100) > admin(80) > staff(40) > client(10, portal-only). "Manter sessão iniciada" is the shared auth_remember_* module.

The login-attempt throttle (config/functions_throttle.php) stays the app's own, deliberately not the shared one — see the comment in config/db.php for why (avoids double-counting the same form with two independent throttles). A throttled login feeds platform_ip_record_offence(), so repeat offenders get fleet-wide IP-banned.

Soft delete / trash

Deletes are soft (deleted_at/deleted_by) via trash_delete() in config/functions_trash.php. Gestão → Lixeira lists everything deleted across entity types and only allows permanent purge after 30 days (enforced in the DELETE's WHERE, not just hidden in the UI). Adding a new deletable entity means adding it to trash_types() in that file.

CSRF

csrf_check() (config/functions_csrf.php) refuses any non-POST request outright — state changes must be POST forms carrying csrf_field()'s token. There is no PRG (Post-Redirect-Get) convention beyond that; several admin pages still re-render the form directly on POST rather than redirecting (audit flags this as inconsistent, not yet unified — tracked separately, low priority).

Audit log

Models call audit_row() before a write and audit_change() after (diffs before/after automatically); ad-hoc events elsewhere use audit('entity.verb', [...]). Both come from shared/platform-tools via config/functions_audit.php. Read via Sistema → Auditoria (platform.php?tool=audit).

Multi-gym

Since 2026-09-25, one install serves several physical gyms: the gyms table, one URL folder per gym (/3070/, /<postal-code>/), content rows carry gym_id, and the backoffice has an "a editar" gym switcher (admin_gym_id() / admin_gym() in config/functions_gyms.php). / and /home 301 to the default gym. An unpublished gym 404s publicly but previews for staff.

platform-tools

shared/platform-tools is a git submodule (pinned branch = main in .gitmodules), not vendored — see PLATFORM_TOOLS.md for why it went vendored→submodule→ back, and don't repeat that mistake without reading it first. config/db.php wires it via platform_connect(): shared auth (above), fleet IP-ban guard (runs on every request), the sysadmin router (platform.php / api/platform.php — Backups, Actualizações, Auditoria, IP bans, Geo, Origem das visitas, all one front controller, just for those pages, not the app's own).

Deploy

Push to master → Gitea webhook → api/platform.php?endpoint=auto_update → the VPS pulls, syncs the submodule, applies pending database/migration_*.sql files, done in seconds. No staging branch; the VPS checkout is what's live. deploy.sh (on the VPS, not in this repo) is the manual fallback. See PLATFORM_TOOLS.md.

Dependencies

composer.json/composer.lock are real (composer install works, composer audit/outdated are meaningful) — added 2026-09-25, matching the fotografia platform's setup. vendor/PHPMailer/ is the one hand-vendored exception, gitignore- allowlisted: shared/platform-tools/functions_mail.php hard-codes that exact path fleet-wide, so Composer's own vendor/phpmailer/phpmailer/ exists in parallel but isn't what actually gets require_once'd. See ASSETS.md for frontend asset versions. Both the public site and the backoffice load Bootstrap 5.3.3 (unified 2026-09-25 — the public site ran 4.3.1 before that); jQuery is one shared copy, not duplicated.

What's not done (tracked, not silently missing)

The full audit and its live status live in a separate report, not in this repo (CLAUDE_GITEA/audits/fitme3070-auditoria-2026-09-24.html on the maintainer's machine). As of 2026-09-25: no real front controller (6.2, above), no central listing registry, no DataTables on backoffice listings, no srcset (deliberate — uploads are already close to their CSS display size, see the audit item), views/admin/galerias/ still has separate create/edit (deliberately — they're not actually a duplicated pair, see the audit item). None of these are urgent; none should be "fixed" as a side effect of an unrelated change — each is its own piece of work.

Dev test accounts (DEV ONLY)

In the development database there must always exist two logins for testing: sysadmin@mail.pt (role sysadmin) and admin@mail.pt (role admin), password 123. Ensure/repair them with php scripts/dev_accounts.php (CLI only, idempotent upsert with password_hash, active flags set). The script fails closed: it refuses unless .env has APP_ENV=dev AND DB_HOST is 192.168.1.3:3181 or localhost. It must never be run, deployed or adapted for production, and these accounts must never exist in a production database.

Brand feed (2026-10-09)

  • api/brand.php is a one-line stub to shared/platform-tools/api/brand.php (public JSON: name, logos, colours; README "Brand feed"). Declared in platform_connect(['brand' => ...]) in config/db.php; colours are the :root tokens (--fitme-accent #c2d500, --fitme-dark #0b0b0d, --fitme-accent-dark #a6b600). logo_light = light-coloured logo (logo_crossfit_white.svg), logo_dark = logo_crossfit.svg.
  • site_logo_rel() (config/functions.php) resolves logos for both the feed and load_email_template(): setting file in uploads/public/logo/ (convention), then legacy assets/img/logo/, then the static logo. views/sysadmin/system_settings.php now uploads logos to uploads/public/logo/ (dir created on demand, 0755; uploads/.htaccess blocks script execution); old files in assets/img/logo/ still resolve as fallback and in the settings preview. Brand block in config/db.php already prefers the setting file via site_logo_rel(), else the static SVGs.