Commit Graph

530 Commits

Author SHA1 Message Date
Serreau Jovann
6438afadbf Add first-party analytics tracker with encrypted transmissions
Core system:
- AnalyticsUniqId entity (visitor identity with device/os/browser parsing)
- AnalyticsEvent entity (page views linked to visitor)
- POST /t endpoint with AES-256-GCM encrypted payloads
- HMAC-SHA256 visitor hash for anti-tampering
- Async processing via Messenger
- JS module: auto page_view tracking, setAuth for logged users
- Encryption key shared via data-k attribute on body
- setAuth only triggers when cookie consent is accepted
- Clean CSP: remove old tracker domains (Cloudflare, Umami)

100% first-party, no cookies, invisible to adblockers, RGPD-friendly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:07 +01:00
Serreau Jovann
3a85b6ef68 Remove all trackers (Umami, Cloudflare, insights-js) from cookie consent
Strip loadAnalytics, loadCloudflareTunnel and insights-js dependency.
Cookie consent banner kept for future use without any tracking scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:29:22 +01:00
Serreau Jovann
3bb676bfda Adjust container resource limits for 6 CPU / 30 GB RAM server
Resource allocation (limits / reservations):
- php x2:        1.5 CPU / 1G    |  0.5 CPU / 256M
- db-master:     2.0 CPU / 4G    |  0.5 CPU / 1G    (shared_buffers=1GB, effective_cache_size=3GB)
- db-slave:      1.5 CPU / 2G    |  0.25 CPU / 512M
- pgbouncer:     0.5 CPU / 128M  |  0.1 CPU / 32M
- messenger x2:  1.0 CPU / 512M  |  0.25 CPU / 128M
- redis:         1.0 CPU / 1G    |  0.25 CPU / 128M  (maxmemory 768mb)
- meilisearch:   1.0 CPU / 1G    |  0.25 CPU / 256M

Total max with replicas: ~12 CPU / ~13G RAM (overcommit OK, reservations fit)
Total reservations: ~3.6 CPU / ~4G RAM (guaranteed minimum)
Added PostgreSQL tuning: shared_buffers, effective_cache_size, work_mem.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:25:35 +01:00
Serreau Jovann
04c9df7638 Add CPU and memory limits to all production containers
Resource allocation (limits / reservations):
- php x2:        1.0 CPU / 512M  |  0.25 CPU / 128M
- db-master:     1.5 CPU / 2G    |  0.5 CPU / 512M
- db-slave:      1.0 CPU / 1G    |  0.25 CPU / 256M
- pgbouncer:     0.25 CPU / 64M  |  0.05 CPU / 16M
- messenger x2:  0.5 CPU / 384M  |  0.1 CPU / 64M
- redis:         0.5 CPU / 256M  |  0.1 CPU / 64M  (maxmemory 200mb)
- meilisearch:   0.5 CPU / 512M  |  0.1 CPU / 128M

Total max: ~8.5 CPU / ~6.5G RAM (with 2 php + 2 messenger replicas)
Redis maxmemory with allkeys-lru eviction policy added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:24:27 +01:00
Serreau Jovann
3197cc764d Force full-width layout on admin Infra page
Override admin-main max-width to 100% for the infra dashboard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:23:18 +01:00
Serreau Jovann
d2fb17bf50 Add app:infra:snapshot command, page reads from var/infra.json
The /admin/infra page was slow because Docker stats API blocks per container.
Now a cron (every 5min) generates var/infra.json via app:infra:snapshot,
and the page reads the static JSON file instantly.
Mount Docker socket in cron container for snapshot access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:19:26 +01:00
Serreau Jovann
7a370b1e02 Fix Docker socket access: add docker GID to PHP container group
The PHP container user needs the docker group to read the socket.
Uses DOCKER_GID env var in dev (defaults to 989) and dynamic GID
detection via Ansible stat in prod.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:53:28 +01:00
Serreau Jovann
8db44017d2 Redesign admin Infra page: full-screen 4-column layout with Docker stats
Complete rewrite of /admin/infra into 4 columns:
- Col 1 (Serveur): CPU, RAM, Disk, System, Services (Caddy, Docker, SSL cert)
- Col 2 (Containers): All Docker containers with CPU%, RAM, state via Docker API
- Col 3 (Redis): Global stats + per-DB (Messenger, Sessions, Cache)
- Col 4 (PostgreSQL): Instance stats + PgBouncer pools/stats

Extract all infra logic into InfraService. Mount Docker socket (read-only)
in PHP container for container stats. Check SSL cert expiry and Caddy status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:51:04 +01:00
Serreau Jovann
cb35efe3ff Fix PgBouncer dev auth: use plain auth_type for PostgreSQL 16 scram-sha-256
PostgreSQL 16 defaults to scram-sha-256, md5 hashes in userlist are
rejected. Using auth_type=plain in dev with plaintext password.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:44:29 +01:00
Serreau Jovann
ae338bb74a Fix PgBouncer stats: fallback to pgbouncer:6432 when DATABASE_URL points to postgres directly
If DATABASE_URL uses port 5432 (direct postgres), the PgBouncer stats
connection falls back to pgbouncer:6432. Fixes error when .env.local
overrides DATABASE_URL to bypass pgbouncer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:42:53 +01:00
Serreau Jovann
08433321d0 Add Server stats (CPU, RAM, Disk, System) to admin Infra page
First row shows host-level stats from /proc: CPU model, cores, load
average with charge %, RAM total/used/available with usage %, disk
total/used/free with usage %, hostname, OS and uptime. All color-coded
green <70%, yellow <90%, red >=90%.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:41:41 +01:00
Serreau Jovann
d7cecfe9ef Fix PgBouncer admin console: enable PDO emulated prepares
PgBouncer admin console does not support extended query protocol
(prepared statements). Setting PDO::ATTR_EMULATE_PREPARES = true
forces PDO to use simple query protocol for SHOW commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:40:21 +01:00
Serreau Jovann
7e53e3343b Add PHP container stats to admin Infra page
Shows per-container: hostname, PHP version, SAPI, uptime, CPU cores,
CPU usage % (sampled from cgroup), load averages (1/5/15m), RAM used/
total/free with usage %. Color-coded: green <70%, yellow <90%, red >=90%.
Reads from cgroup v2 (fallback v1) and /proc for container-level stats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:38:33 +01:00
Serreau Jovann
d2be7311d9 Add admin/stats users to PgBouncer config for SHOW commands
Without admin_users/stats_users, connecting to the pgbouncer virtual
database fails with "database pgbouncer does not exist".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:35:43 +01:00
Serreau Jovann
27db5e96cf Reorganize admin Infra page: group PostgreSQL+PgBouncer and Redis+DBs
PostgreSQL & PgBouncer on same row, Redis Global + 3 DB cards on same row.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:34:46 +01:00
Serreau Jovann
74c10a60f5 Add PgBouncer to dev and PgBouncer stats to admin Infra page
- Add pgbouncer service to docker-compose-dev.yml with dev config
- Route DATABASE_URL through pgbouncer:6432 in dev (matches prod)
- Add PgBouncer pools and stats tables to /admin/infra with color-coded
  avg query/xact times and client waiting indicators
- php, messenger, cron now depend on pgbouncer instead of database directly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:33:51 +01:00
Serreau Jovann
1a336edac5 Split admin Infra page into Redis global + per-database stats
Shows 3 Redis databases separately (Messenger, Sessions, Cache) with
key count and average TTL, alongside global Redis stats and PostgreSQL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:28:37 +01:00
Serreau Jovann
58301840a6 Add admin Infra page with Redis and PostgreSQL monitoring
Shows real-time stats with color-coded indicators:
- Redis: version, memory, hit rate, ops/sec, evicted keys
- PostgreSQL: version, db size, connections, cache hit ratio, dead tuples
Uses MESSENGER_TRANSPORT_DSN for Redis auth (works in dev and prod).
Accessible via /admin/infra with nav link.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:27:15 +01:00
Serreau Jovann
e4edc76f58 Add Redis cache for Meilisearch search results and admin dashboard stats
- Configure Redis DB 2 as Symfony cache adapter
- Cache Meilisearch search results for 5 minutes (invalidated on writes)
- Cache admin dashboard stats for 10 minutes
- Add invalidateSearchCache() called after each Meilisearch write
- Update tests to support cache mock injection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:24:35 +01:00
Serreau Jovann
a544496104 Add PWA scanner app for organizers at /scanner
Standalone installable PWA with:
- JWT login via /api/auth/login
- Event list from /api/live/events
- QR code camera scanning (html5-qrcode library)
- Scan results with accepted/refused state and ticket details
- Auto token refresh on expiry
- Offline caching via service worker
- Dark theme optimized for outdoor scanning
- Vibration feedback on scan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:04:56 +01:00
Serreau Jovann
d6ead88d3d Add logo upload to admin organizer edit page
Admin can now view the current logo and upload a new one via the
organizer edit form. Uses VichUploader with the existing organizer_logo
mapping. Adds test with fixture image.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:00:33 +01:00
Serreau Jovann
fd1162b7af Add Stripe sync cron (every 6h) to Ansible deploy playbook
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:58:14 +01:00
Serreau Jovann
b2f1110a43 Add app:stripe:sync to cron every 6 hours
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:57:33 +01:00
Serreau Jovann
c207fd31b1 Add app:stripe:sync command to sync Stripe status for all organizers
Fetches charges_enabled and payouts_enabled from Stripe API for each
organizer with a connected account and updates the local database.
Also adds retrieveAccountStatus() to StripeService for testability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:56:11 +01:00
Serreau Jovann
b43c6bcbab Add Stripe account status display on event edit page for ROLE_ROOT
Show charges/payouts acceptance status and Stripe connection state
when an admin views an organizer's event. Pass owner to template
and use it for Stripe checks instead of app.user.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:51:44 +01:00
Serreau Jovann
aa2add2696 fix error navbar 2026-03-26 09:42:20 +01:00
Serreau Jovann
0aed47c86c Fix meilisearch:check-consistency to create missing indexes with --fix
Previously, missing indexes were silently skipped. Now with --fix,
the command creates the index and indexes all documents from the database.
Without --fix, missing indexes are reported. Also checks all organizer
and event indexes regardless of whether they already exist in Meilisearch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:41:12 +01:00
Serreau Jovann
7e9f99a88e fix error navbar 2026-03-26 09:37:28 +01:00
Serreau Jovann
3a7f92c02d Remove Meilisearch healthcheck, use service_started instead
The getmeili/meilisearch image (Debian slim) has neither curl nor wget,
so healthcheck commands always fail. Use condition: service_started
and rely on Messenger retry mechanism for brief startup delays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:32:22 +01:00
Serreau Jovann
264a82a97c Fix Meilisearch healthcheck: use wget instead of curl
The getmeili/meilisearch image does not include curl, causing the
healthcheck to fail and blocking messenger startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:24:35 +01:00
Serreau Jovann
23b92f101c Add admin event actions (online/offline, edit, delete) and fix Meilisearch depends_on
- Add toggle online/offline and delete routes in AdminController
- Add action buttons (En ligne, Modifier, Supprimer) in admin events template
- Bypass requireEventOwnership and requireStripeReady for ROLE_ROOT so admin can edit any event
- Add Meilisearch healthcheck and depends_on in messenger service (prod + dev)
- Add tests for all new admin routes and ROLE_ROOT bypass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:04:27 +01:00
Serreau Jovann
531c7da051 Add debt system: track refunds/disputes, redirect payments until cleared
- Add nullable debt field to User entity with addDebt/reduceDebt helpers
- On refund webhook: add refunded amount to organizer debt
- On dispute webhook (charge.dispute.created): add disputed amount to debt
- OrderController: if organizer has debt > 0, payment goes to main Stripe
  account instead of connected account, debt reduced on payment success
- Display debt amount on organizer dashboard with warning message
- Add dispute notification email template
- Migration for debt column on user table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:15:01 +01:00
Serreau Jovann
972e4b63d2 aa 2026-03-24 15:36:08 +01:00
Serreau Jovann
0b5758e3a0 Fix stripeConnect to 3 returns, add codeCoverageIgnore on billing success and Stripe check
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:39:40 +01:00
Serreau Jovann
e4c701456b Add billing system: subscription, webhooks, and access control
- Add billing fields to User (isBilling, billingAmount, billingState,
  billingStripeSubscriptionId) and OrganizerInvitation (billingAmount)
- Registration: organizer gets billingState="poor" (pending review)
- Admin approval: sets isBilling=true, billingAmount from form, state="good"
- Invitation: billingAmount from invitation, if 0 then isBilling=false
- ROLE_ROOT accounts: billing free (amount=0, state="good")
- Block Stripe Connect creation and all organizer features if state is
  "poor" or "suspendu"
- Hide Stripe configuration section if billing not settled
- Add billing checkout via Stripe subscription with success route
- Webhooks: checkout.session.completed activates billing,
  invoice.payment_failed and customer.subscription.deleted suspend
  account and disable online events
- Show billing alert on /mon-compte with amount and subscribe button
- Display billing info in invitation email and landing page
- Add email templates for billing activated/failed/cancelled

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:30:21 +01:00
Serreau Jovann
b14a15f0a4 Fix undefined $email variable in createInvitation, use $order->getEmail()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:44:00 +01:00
Serreau Jovann
b6a3a629f7 Extract buildInvitationOrder to reduce createInvitation returns from 4 to 3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:35:19 +01:00
Serreau Jovann
ee2cc6d0d7 Fix $order undefined warning, add codeCoverageIgnore on requireStripeReady calls, explicit USER in cron Dockerfile
- Restructure createInvitation to ensure $order is always defined
- Mark all requireStripeReady guard blocks as codeCoverageIgnore
- Add explicit USER root in cron Dockerfile with justification comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:27:03 +01:00
Serreau Jovann
ca13660aea Remove unused $user variables, add cron container for dev
- Remove 11 unused $user assignments from requireEventOwnership calls
- Add cron container to docker-compose-dev.yml with scheduled tasks:
  expire-pending orders (5min), messenger monitor (hourly),
  meilisearch consistency (daily 3am)
- Cron logs show registered tasks at startup and timestamps per execution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:52:31 +01:00
Serreau Jovann
876cf60db6 Refactor: extract requireEventOwnership/requireCategory, reduce returns in createInvitation
- Extract requireEventOwnership() to deduplicate event owner checks (18 occurrences)
- Extract requireCategory() to deduplicate category validation (2 occurrences)
- Reduce createInvitation() returns from 4 to 3 by merging validation errors
- Mark requireStripeReady() as codeCoverageIgnore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:23:15 +01:00
Serreau Jovann
f9788adab3 Block organizer features when Stripe Connect account is not validated
- Hide organizer tabs (events, subaccounts, payouts) if Stripe not ready
- Redirect organizer tab content and all organizer routes to /mon-compte
- Add requireStripeReady() guard on all ROLE_ORGANIZER routes
- Force default tab to 'tickets' when Stripe is not validated
- Update test fixtures: approved organizers get Stripe enabled by default
- Add tests for blocked tabs and blocked event creation without Stripe

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:06:39 +01:00
Serreau Jovann
7d81fa3604 Update ansible vault
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:05:07 +01:00
Serreau Jovann
b075209746 Add tools-security.esy-web.dev to script-src CSP directive
Direct script loading requires the domain in script-src,
not just connect-src. Added to both base and prod config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:01:47 +01:00
Serreau Jovann
8223e0b954 Fix indentation in deploy.yml after LibreTranslate removal
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:49:30 +01:00
Serreau Jovann
6dfffac457 Remove LibreTranslate tasks from deploy, add SESSION_HANDLER_DSN to prod env
- Remove Start/Wait/Translate LibreTranslate tasks from deploy.yml
- Add SESSION_HANDLER_DSN with Redis in env.local.j2 for prod sessions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:46:47 +01:00
Serreau Jovann
2987dbfd28 Fix Redis session DSN, remove LibreTranslate from prod, track all translations
- Fix SESSION_HANDLER_DSN: use Redis db index (/1) instead of /sessions
  which caused "dbindex must be a number" error
- Remove LibreTranslate service and volume from docker-compose prod
- Remove gitignore rules for translation files (en, es, de, it)
  so all languages are tracked in git
- Apply PHP CS Fixer style fixes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:35:13 +01:00
Serreau Jovann
d44e75e3fd Fix SonarQube issues, store sessions in Redis, use direct analytics URLs
- ApiSandboxController: reduce scan() returns from 4 to 3 via ternary
- ApiDocController: add MIME_JSON constant, extract buildInsomniaRequest()
  and buildInsomniaBody() to reduce cognitive complexity
- Store sessions in Redis to fix SSO disconnect with 2 PHP replicas
  (round-robin load balancing caused session loss on filesystem storage)
- Configure session cookie: 24h lifetime, secure auto, samesite lax
- Replace Caddy analytics proxies (/stats/*, /assets/perf.js, /sperf)
  with direct URLs to tools-security.esy-web.dev and cloudflareinsights.com
- Update JS tests for new direct analytics URLs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:21:19 +01:00
Serreau Jovann
867eaadddf Fix SonarQube: inject appSecret via constructor, add constants, reduce scan returns
ApiLiveController:
- Inject appSecret via constructor (was 6x in method params)
- Add ERR_EVENT/ERR_BILLET/ERR_CATEGORY constants
- Extract processScan() to reduce scan() from 7→3 returns

ApiSandboxController:
- Inject appSecret via constructor (was 6x in method params)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 21:39:23 +01:00
Serreau Jovann
dad4a5fc50 Cover remaining branches in api-env-switcher: unknown env, missing DOM elements
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 21:00:17 +01:00
Serreau Jovann
4f71b0db60 Mark ApiAuthController constructor as codeCoverageIgnore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 20:59:22 +01:00