Commit Graph

557 Commits

Author SHA1 Message Date
Serreau Jovann
61946f724e Fix scanner service worker CDN URL and clean up MeilisearchConsistencyCommand
- Replace unpkg.com with cdn.jsdelivr.net in sw.js cache list
- Fix sw.js scope to /scanner/
- Remove unused $indexes parameter from checkAllIndexes()
- Extract duplicated " [%s] Index missing" literal to INDEX_MISSING_MSG constant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:14:33 +01:00
Serreau Jovann
15616167d0 Add attestation system with digital signature, public verification, and detailed ticket listing
- Create Attestation entity with reference, signature hash (HMAC-SHA256), event, user, payload
- Add migration Version20260326180000 for attestation table
- Save each attestation in DB with unique signature for tamper-proof verification
- Add public route /attestation/ventes/r/{reference} for QR code verification (short URL)
- Keep fallback /attestation/ventes/{hash} route for base64-signed verification
- Public page shows "Attestation conforme" with signature proof, no detailed data
- QR code on PDF now uses short reference URL instead of full base64 hash (scannable)
- Increase QR code resolution to 300px for better readability
- Display verification URL on PDF next to QR code

Attestation PDF improvements:
- Rename "ATTESTATION DE VENTES" to "ATTESTATION"
- Add two modes: "Attestation detaillee" (with ticket list) and "Attestation simple" (certification only)
- Simple mode: certifies figures are valid, only paid billets/votes confirmed by Stripe count
- Detailed mode: adds full ticket listing with reference, order number, billet name, buyer name
- No amounts displayed in either mode
- Gold color scheme (#fabf04) for headers, borders, table headers, summary box
- Larger text in QR verification box for readability

Scanner: ROLE_ROOT buyer tickets always validate at scan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:13:40 +01:00
Serreau Jovann
822bf8915f Scanner: SSO login, 2 scan modes (camera/security key), sound feedback, order details, force validation, staff/exposant badges
- Add SSO login button to scanner PWA with Keycloak redirect flow via session state
- Add manual scan mode via security key (16 chars) alongside QR camera scan
- Add audio feedback: good (accepted), warning (already scanned), refused sounds
- Add unique scan counter per reference (no double counting same ticket)
- Add order details display in scan results (order number, email, total, items)
- Add force validation button for refused tickets (organizer/ROLE_ROOT only), sends email notification
- Add already_scanned warning only for same-day scans, exit_definitive only same day
- Staff and exposant tickets always validate regardless of state

API: ROLE_ROOT access to all events, categories, billets, and scan endpoints

- ROLE_ROOT bypasses ownership checks on all /api/live/* endpoints
- ROLE_ROOT can login via API (email/password and SSO)
- Scan API accepts securityKey parameter in addition to reference
- Scan response includes billetType, buyerEmail, and full order details with items

Event management: tickets tab, staff/exposant accreditations, attestation PDF

- Add Tickets tab listing all sold tickets with search, download PDF, resend email, cancel actions
- Add Staff/Exposant accreditation form in Invitations tab, generates dedicated non-buyable billet
- Add Attestation tab to generate sales certificate PDF with category/billet selection
- PDF billet template shows STAFF/EXPOSANT badge with distinct colors (black/purple)
- Exclude invitations from all financial stats (event stats, admin dashboard, organizer finances)
- Fix sold counts to exclude invitations in categories recap
- Use actual Stripe fee parameters instead of hardcoded values in commission calculations
- Add commission detail breakdown (E-Ticket + Stripe) in categories and stats tabs

Admin: download tickets for orders

- Add download button on admin orders page (single PDF or ZIP for multiple tickets)

Scanner PWA fixes: CSP (unpkg -> jsdelivr), service worker scope (/scanner/)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:50:42 +01:00
Serreau Jovann
6ce56c11e0 Clear sessionStorage on 403 and retry with fresh visitor
When SECRET_ANALYTICS changes (deploy), old uid/hash become invalid.
On 403, clear session and auto-retry with a new visitor creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:41:34 +01:00
Serreau Jovann
832387876e Fix robots.txt: replace /track/ with /t/ for analytics endpoint
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:01:30 +01:00
Serreau Jovann
19b973ebc0 Allow indexing of legal pages in robots.txt
Mentions legales, CGU, CGV, hebergement, cookies, RGPD, conformite
are now allowed for crawling instead of disallowed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:00:06 +01:00
Serreau Jovann
6062a35d97 Fix WCAG contrast on organizers page: text-gray-400 -> text-gray-600
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:57:43 +01:00
Serreau Jovann
220ea937dc Fix WCAG contrast on events page and add width/height to logo
- 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>
2026-03-26 12:56:54 +01:00
Serreau Jovann
d1fed64d72 Fix WCAG contrast issues: yellow-500->700, indigo-600->800 on links
- 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>
2026-03-26 12:55:56 +01:00
Serreau Jovann
3b4b51e3f2 Clear Symfony + Redis cache before migrations in deploy
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>
2026-03-26 12:50:35 +01:00
Serreau Jovann
83583e0d3d Enable Doctrine L2 cache and add Redis cache pools
Doctrine Second Level Cache (NONSTRICT_READ_WRITE) on:
- Event, User, Category, Billet, BilletDesign
- Default region: 1h TTL, short_lived region: 5min TTL

Redis cache pools added:
- app.cache.events (30min) — for event listings
- app.cache.homepage (5min) — for homepage data
- doctrine.result_cache_pool — DQL result cache via Redis
- doctrine.system_cache_pool — metadata/query cache

All pools backed by Redis DB 2. Reduces DB queries significantly
for read-heavy pages (event listings, user profiles, categories).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:50:13 +01:00
Serreau Jovann
a0cb564c00 Restore libretranslate in docker-compose dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:35:37 +01:00
Serreau Jovann
da2ee96979 Remove libretranslate from docker-compose dev
No longer needed, frees up RAM and CPU.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:35:14 +01:00
Serreau Jovann
300e7f0be9 Add scheduled auto-deploy at 3h, 13h, 19h, 23h daily
Also rotates SECRET_ANALYTICS at each deploy for dynamic endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:27:35 +01:00
Serreau Jovann
98b0b41064 Use SECRET_ANALYTICS env var, regenerated at each deployment
- 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>
2026-03-26 12:27:05 +01:00
Serreau Jovann
a139f86b90 Make analytics endpoint dynamic: /t/{token} derived from APP_SECRET
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>
2026-03-26 12:22:59 +01:00
Serreau Jovann
0952bc6e17 Fix chart height to 150px on analytics dashboard
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:15:53 +01:00
Serreau Jovann
d4c897fd89 Fix PostgreSQL SUBSTRING on timestamp: use CAST AS DATE with native SQL
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>
2026-03-26 12:15:06 +01:00
Serreau Jovann
375357ddde Add charts and bounce rate to admin Analytics, filter self-referrers
- 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>
2026-03-26 12:14:33 +01:00
Serreau Jovann
efe967389d Add bounce rate to admin Analytics dashboard
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>
2026-03-26 12:13:08 +01:00
Serreau Jovann
3945fbb0ef Send email when no RGPD data found, add DPO contact to PDFs
- 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>
2026-03-26 12:11:25 +01:00
Serreau Jovann
e5110017d9 Fix RGPD email templates: use email/base.html.twig not emails/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:09:48 +01:00
Serreau Jovann
c9a040804e Fix Uuid class not found: use native PHP UUID v4 generation
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>
2026-03-26 12:07:42 +01:00
Serreau Jovann
ce1b153cc1 Fix AES-GCM format mismatch between JS Web Crypto and PHP openssl
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>
2026-03-26 12:07:08 +01:00
Serreau Jovann
d57669b5b9 Enable analytics tracking in dev and prod
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>
2026-03-26 12:06:12 +01:00
Serreau Jovann
2ae28089d5 Add RGPD data access/deletion forms and admin Analytics dashboard
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>
2026-03-26 11:59:34 +01:00
Serreau Jovann
809a1055ec Restore Cloudflare CSP rules needed for WAF/Turnstile
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>
2026-03-26 11:52:56 +01:00
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