- Labels and card text: text-gray-400 -> text-gray-600 on #fbfbfb bg
- Empty state message: text-gray-400 -> text-gray-600 on white bg
- Add explicit width/height to navbar logo to prevent CLS
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- text-yellow-500 on white bg had ratio ~1.9 (need 4.5), now text-yellow-700
- text-indigo-600 links on white bg had ratio ~3.8, now text-indigo-800
with permanent underline for link visibility (WCAG 1.4.1)
- Cookie banner link also updated
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents stale Doctrine L2 cache and app cache from causing issues
after schema changes. Clears both filesystem cache and Redis pool.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New SECRET_ANALYTICS variable replaces kernel.secret for analytics
- Ansible generates a random 32-char secret at each deploy
- Endpoint token and encryption key change with every deployment
- Existing sessions will get new visitor_id after deploy (expected)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The endpoint path is now /t/<8-char hash of APP_SECRET> instead of
static /t. Token is injected via data-e attribute on body, read by JS.
Server validates token on every hit, returns 404 if invalid.
Changes with each APP_SECRET = impossible to hardcode in a blocker.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SUBSTRING does not work on timestamp in PostgreSQL. Use native SQL
with CAST(created_at AS DATE) for daily chart aggregation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bar chart: visitors per day
- Line chart: pageviews per day (with fill)
- Bounce rate KPI with color coding (green/yellow/red)
- Filter out self-referrers (ticket.e-cosplay.fr, esyweb.local)
- Uses Chart.js via cdn.jsdelivr.net (already in CSP)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bounce rate = visitors with only 1 pageview / total visitors.
Color-coded: green <40%, yellow <60%, red >=60%.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Send confirmation email when no data found for access or deletion request
- Add DPO contact (DPO-167945, E-Cosplay) to both access and deletion PDFs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
symfony/uid not installed, replace Uuid::v4() with random_int based
UUID v4 generation (RFC 4122 compliant).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Web Crypto API AES-GCM outputs: iv + ciphertext + tag (tag appended)
PHP openssl was using: iv + tag + ciphertext (tag in middle)
Now both use the same format: iv (12 bytes) + ciphertext + tag (16 bytes).
Decrypt tries JS format first, falls back to PHP format for compatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove dev environment check — tracking runs everywhere.
Data won't mix since each environment has its own database.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RGPD (/rgpd):
- Access form: search by IP, generate PDF with all visitor data, email it
- Deletion form: delete all visitor data by IP, generate attestation PDF
- Both forms pre-fill client IP, require email for response
- PDF templates with E-Cosplay branding, RGPD article references
Admin Analytics (/admin/analytics):
- KPIs: unique visitors, pageviews, pages/visitor
- Top pages and referrers tables
- Device type, browser, OS breakdowns
- Period filter: today, 7d, 30d, all
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cloudflare WAF requires cloudflareinsights.com and challenges.cloudflare.com
in script-src, connect-src, frame-src and external_redirects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>