docs: CLAUDE.md — decisão de arquitetura escrita (fecha 6.1)

Documenta o padrão atual (páginas = controller+vista, config/functions_*.php
como models, sem front controller próprio) como decisão mantida, não
herança silenciosa do CodeIgniter — mais auth partilhado, trash,
CSRF, audit, multi-ginásio, platform-tools, deploy e Composer. Já
bloqueado por HTTP (.htaccess já negava CLAUDE.md por nome).

Inclui uma secção "What's not done" honesta: sem front controller
real, sem DataTables, Bootstrap 4+5 em simultâneo — para não passar a
imagem de que isto já está resolvido.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Sonnet 5 committed 2026-09-25 17:44:27 +01:00
1 parent 43e0fc7be6
commit 8300f2c421
1 file changed
+119
+119
View File
@@ -0,0 +1,119 @@
# 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
(two Bootstrap majors, duplicated jQuery — known debt, not yet unified).
## 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 unified
create/edit view pairs, no DataTables on backoffice listings, public site still on
Bootstrap 4 while the backoffice runs 5, no `srcset`/responsive images. None of these
are urgent; none should be "fixed" as a side effect of an unrelated change — each is
its own piece of work.