feat: Stripe integration complete - products created, DB migration, payment_failed handler, credit buttons wired
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m5s

- Create Stripe products/prices (Starter/Pro/Business monthly+yearly)
- Fix CRITICAL bug: add subscription_ends_at + cancel_at_period_end columns to users table
- Alembic migration: f6a7b8c9d0e1_add_subscription_ends_at_cancel_at_period_end
- Fix: implement handle_payment_failed() to set subscription_status=PAST_DUE
- Fix: harmonize .env.production Stripe variable names to match pricing_config.py
- Fix: add missing FRONTEND_URL and STRIPE_PUBLISHABLE_KEY to .env.production
- Add all Stripe Price IDs (test mode) to .env.production
- Wire credit purchase buttons to /api/v1/auth/create-credits-checkout
- Dashboard sync post-checkout was already implemented (no change needed)

Stripe test keys: configured in .env.production
Webhook: must be configured on server via stripe CLI or Stripe Dashboard
Webhook URL: https://wordly.art/api/v1/auth/webhook/stripe
This commit is contained in:
2026-05-31 21:40:31 +02:00
parent 3a9de12f26
commit 374c605027
9 changed files with 550 additions and 12 deletions

View File

@@ -10,7 +10,9 @@
"WebSearch", "WebSearch",
"Bash(DEEPSEEK_API_KEY=sk-176db424442e40728c04c04a4170c224 python -u -c ' *)", "Bash(DEEPSEEK_API_KEY=sk-176db424442e40728c04c04a4170c224 python -u -c ' *)",
"Bash(python -u -c ' *)", "Bash(python -u -c ' *)",
"Bash(DEEPSEEK_API_KEY=sk-176db424442e40728c04c04a4170c224 PYTHONUNBUFFERED=1 python -u scripts/enrich_glossary_templates.py --api deepseek --model deepseek-chat --workers 5)" "Bash(DEEPSEEK_API_KEY=sk-176db424442e40728c04c04a4170c224 PYTHONUNBUFFERED=1 python -u scripts/enrich_glossary_templates.py --api deepseek --model deepseek-chat --workers 5)",
"Bash(ssh -o BatchMode=yes root@192.168.1.151 \"cd /opt/wordly && docker compose ps backend --format '{{.Status}}'\")",
"Bash(ssh -o BatchMode=yes root@192.168.1.151 'cd /opt/wordly && docker compose exec -T backend python -c \" *)"
] ]
} }
} }

View File

@@ -26,7 +26,11 @@
"Bash(docker exec *)", "Bash(docker exec *)",
"Bash(git commit -m ' *)", "Bash(git commit -m ' *)",
"Bash(python -c ' *)", "Bash(python -c ' *)",
"Bash(timeout 15 python -u -c ' *)" "Bash(timeout 15 python -u -c ' *)",
"Bash(python -c \"import openpyxl; print\\('openpyxl OK'\\)\")",
"Bash(python -c \"import docx; print\\('python-docx OK'\\)\")",
"Bash(npx tsc *)",
"Bash(.venv/Scripts/python.exe _bmad/scripts/resolve_customization.py --skill .claude/skills/bmad-quick-dev --key workflow)"
] ]
} }
} }

View File

@@ -15,6 +15,7 @@ LOG_LEVEL=INFO
# ---- Domaine ---- # ---- Domaine ----
DOMAIN=wordly.art DOMAIN=wordly.art
NEXT_PUBLIC_API_URL=https://wordly.art NEXT_PUBLIC_API_URL=https://wordly.art
FRONTEND_URL=https://wordly.art
BACKEND_PORT=8000 BACKEND_PORT=8000
FRONTEND_PORT=3000 FRONTEND_PORT=3000
@@ -91,8 +92,15 @@ GRAFANA_USER=admin
GRAFANA_PASSWORD=WordlyGrafana2026! GRAFANA_PASSWORD=WordlyGrafana2026!
# ---- Stripe ---- # ---- Stripe ----
STRIPE_SECRET_KEY= # Clés TEST — remplacer par sk_live_... / pk_live_... en production réelle
STRIPE_PUBLISHABLE_KEY=pk_test_51SkSHkCKXUJE51jnCr2QV4vCE18GH1XF59eHrHOV46EORuZTPVXFXxrbcJamyoJLaUHMc2McCRVkU4b6VMAWVR2R00XRdwmXMx
STRIPE_SECRET_KEY=sk_test_51SkSHkCKXUJE51jnbEtXZ0nKiTHTa8ohDwLH8fZiDVEx6Ze0g5dg4fGJJgX1VgNHvF93GE3HTramT3oQrCaqOxid00OXTcZlsW
STRIPE_WEBHOOK_SECRET= STRIPE_WEBHOOK_SECRET=
STRIPE_STARTER_PRICE_ID=
STRIPE_PRO_PRICE_ID= # Price IDs créés dans Stripe Dashboard (wordly.art — test mode)
STRIPE_BUSINESS_PRICE_ID= STRIPE_PRICE_STARTER_MONTHLY=price_1TdF8FCKXUJE51jnNAeLqhF3
STRIPE_PRICE_STARTER_YEARLY=price_1TdF8GCKXUJE51jnBhVVrjrh
STRIPE_PRICE_PRO_MONTHLY=price_1TdF8GCKXUJE51jn9ChAAhKM
STRIPE_PRICE_PRO_YEARLY=price_1TdF8HCKXUJE51jnpsvBivDe
STRIPE_PRICE_BUSINESS_MONTHLY=price_1TdF8HCKXUJE51jn2K9EeBGJ
STRIPE_PRICE_BUSINESS_YEARLY=price_1TdF8ICKXUJE51jnmgWZxW4U

View File

@@ -0,0 +1,37 @@
"""Add subscription_ends_at and cancel_at_period_end to users
Revision ID: f6a7b8c9d0e1
Revises: e5b2c9d1f4a8
Create Date: 2026-05-31
Fixes critical bug: these columns were used in update_user() but never
persisted to the database, causing subscription end dates and cancellation
state to be lost on server restart.
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers
revision = "f6a7b8c9d0e1"
down_revision = "e5b2c9d1f4a8"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"users",
sa.Column("subscription_ends_at", sa.DateTime(), nullable=True),
)
op.add_column(
"users",
sa.Column("cancel_at_period_end", sa.Boolean(), nullable=False, server_default="false"),
)
def downgrade() -> None:
op.drop_column("users", "cancel_at_period_end")
op.drop_column("users", "subscription_ends_at")

345
backend_logs.txt Normal file
View File

@@ -0,0 +1,345 @@
🚀 Starting Document Translation API...
⏳ Waiting for database to be ready...
Connecting to postgres:5432...
Waiting for database... (1/30)
Waiting for database... (2/30)
Waiting for database... (3/30)
Waiting for database... (4/30)
Waiting for database... (5/30)
Waiting for database... (6/30)
Waiting for database... (7/30)
Waiting for database... (8/30)
Waiting for database... (9/30)
Waiting for database... (10/30)
Waiting for database... (11/30)
Waiting for database... (12/30)
Waiting for database... (13/30)
Waiting for database... (14/30)
Waiting for database... (15/30)
Waiting for database... (16/30)
Waiting for database... (17/30)
Waiting for database... (18/30)
Waiting for database... (19/30)
Waiting for database... (20/30)
Waiting for database... (21/30)
Waiting for database... (22/30)
Waiting for database... (23/30)
Waiting for database... (24/30)
Waiting for database... (25/30)
Waiting for database... (26/30)
Waiting for database... (27/30)
Waiting for database... (28/30)
Waiting for database... (29/30)
Waiting for database... (30/30)
📦 Running database migrations...
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
⏳ Waiting for Redis...
Waiting for Redis... (1/10)
Waiting for Redis... (2/10)
Waiting for Redis... (3/10)
Waiting for Redis... (4/10)
Waiting for Redis... (5/10)
Waiting for Redis... (6/10)
Waiting for Redis... (7/10)
Waiting for Redis... (8/10)
Waiting for Redis... (9/10)
Waiting for Redis... (10/10)
🎯 Starting uvicorn...
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started parent process [1]
{"positional_args": [["https://wordly.art", "http://localhost:3000", "http://localhost:3001", "http://127.0.0.1:3000", "http://127.0.0.1:3001"]], "event": "CORS: non-production \u2014 localhost dev ports merged into allowed origins: %s", "level": "info", "timestamp": "2026-05-17T17:11:22.381690Z"}
{"positional_args": [["https://wordly.art", "http://localhost:3000", "http://localhost:3001", "http://127.0.0.1:3000", "http://127.0.0.1:3001"]], "event": "CORS: non-production \u2014 localhost dev ports merged into allowed origins: %s", "level": "info", "timestamp": "2026-05-17T17:11:22.391687Z"}
INFO: Started server process [115]
INFO: Waiting for application startup.
{"event": "Starting Document Translation API...", "level": "info", "timestamp": "2026-05-17T17:11:22.412313Z"}
INFO: Started server process [118]
INFO: Waiting for application startup.
{"event": "Starting Document Translation API...", "level": "info", "timestamp": "2026-05-17T17:11:22.422764Z"}
{"event": "\u2705 Database tables initialized (async)", "level": "info", "timestamp": "2026-05-17T17:11:22.441699Z"}
{"event": "Database connection verified", "level": "info", "timestamp": "2026-05-17T17:11:22.442565Z"}
{"event": "File cleanup manager started", "level": "info", "timestamp": "2026-05-17T17:11:22.442668Z"}
{"event": "API ready to accept requests", "level": "info", "timestamp": "2026-05-17T17:11:22.442711Z"}
{"event": "\u2705 Database tables initialized (async)", "level": "info", "timestamp": "2026-05-17T17:11:22.452056Z"}
{"event": "Database connection verified", "level": "info", "timestamp": "2026-05-17T17:11:22.452952Z"}
{"event": "File cleanup manager started", "level": "info", "timestamp": "2026-05-17T17:11:22.453047Z"}
{"event": "API ready to accept requests", "level": "info", "timestamp": "2026-05-17T17:11:22.453088Z"}
{"event": "Redis async client connected (shared)", "level": "info", "timestamp": "2026-05-17T17:11:22.458535Z"}
INFO: Application startup complete.
{"files_deleted": 0, "bytes_freed_mb": 0.0, "orphaned_deleted": 0, "cleanup_run_timestamp": "2026-05-17T17:11:22.459996", "event": "cleanup_completed", "level": "info", "timestamp": "2026-05-17T17:11:22.460020Z"}
{"positional_args": [["https://wordly.art", "http://localhost:3000", "http://localhost:3001", "http://127.0.0.1:3000", "http://127.0.0.1:3001"]], "event": "CORS: non-production \u2014 localhost dev ports merged into allowed origins: %s", "level": "info", "timestamp": "2026-05-17T17:11:22.479626Z"}
{"event": "Redis async client connected (shared)", "level": "info", "timestamp": "2026-05-17T17:11:22.480471Z"}
INFO: Application startup complete.
{"files_deleted": 0, "bytes_freed_mb": 0.0, "orphaned_deleted": 0, "cleanup_run_timestamp": "2026-05-17T17:11:22.481779", "event": "cleanup_completed", "level": "info", "timestamp": "2026-05-17T17:11:22.481800Z"}
{"positional_args": [["https://wordly.art", "http://localhost:3000", "http://localhost:3001", "http://127.0.0.1:3000", "http://127.0.0.1:3001"]], "event": "CORS: non-production \u2014 localhost dev ports merged into allowed origins: %s", "level": "info", "timestamp": "2026-05-17T17:11:22.489773Z"}
INFO: Started server process [116]
INFO: Waiting for application startup.
{"event": "Starting Document Translation API...", "level": "info", "timestamp": "2026-05-17T17:11:22.509417Z"}
INFO: Started server process [117]
INFO: Waiting for application startup.
{"event": "Starting Document Translation API...", "level": "info", "timestamp": "2026-05-17T17:11:22.519375Z"}
{"event": "\u2705 Database tables initialized (async)", "level": "info", "timestamp": "2026-05-17T17:11:22.537873Z"}
{"event": "Database connection verified", "level": "info", "timestamp": "2026-05-17T17:11:22.538652Z"}
{"event": "File cleanup manager started", "level": "info", "timestamp": "2026-05-17T17:11:22.538740Z"}
{"event": "API ready to accept requests", "level": "info", "timestamp": "2026-05-17T17:11:22.538779Z"}
{"event": "\u2705 Database tables initialized (async)", "level": "info", "timestamp": "2026-05-17T17:11:22.547593Z"}
{"event": "Database connection verified", "level": "info", "timestamp": "2026-05-17T17:11:22.548456Z"}
{"event": "File cleanup manager started", "level": "info", "timestamp": "2026-05-17T17:11:22.548570Z"}
{"event": "API ready to accept requests", "level": "info", "timestamp": "2026-05-17T17:11:22.548626Z"}
{"event": "Redis async client connected (shared)", "level": "info", "timestamp": "2026-05-17T17:11:22.554183Z"}
INFO: Application startup complete.
{"files_deleted": 0, "bytes_freed_mb": 0.0, "orphaned_deleted": 0, "cleanup_run_timestamp": "2026-05-17T17:11:22.555618", "event": "cleanup_completed", "level": "info", "timestamp": "2026-05-17T17:11:22.555640Z"}
{"event": "Redis async client connected (shared)", "level": "info", "timestamp": "2026-05-17T17:11:22.564580Z"}
INFO: Application startup complete.
{"files_deleted": 0, "bytes_freed_mb": 0.0, "orphaned_deleted": 0, "cleanup_run_timestamp": "2026-05-17T17:11:22.566021", "event": "cleanup_completed", "level": "info", "timestamp": "2026-05-17T17:11:22.566042Z"}
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "0952ee8d", "level": "info", "timestamp": "2026-05-17T17:11:23.415144Z"}
{"event": "Redis sync client connected (shared)", "request_id": "0952ee8d", "level": "info", "timestamp": "2026-05-17T17:11:23.422831Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 8.304, "event": "request_completed", "request_id": "0952ee8d", "level": "info", "timestamp": "2026-05-17T17:11:23.423426Z"}
INFO: 127.0.0.1:39488 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "172.20.0.1", "event": "request_started", "request_id": "fec1023a", "level": "info", "timestamp": "2026-05-17T17:11:23.867885Z"}
{"event": "Redis sync client connected (shared)", "request_id": "fec1023a", "level": "info", "timestamp": "2026-05-17T17:11:23.875512Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 8.246, "event": "request_completed", "request_id": "fec1023a", "level": "info", "timestamp": "2026-05-17T17:11:23.876108Z"}
INFO: 172.20.0.1:34692 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "172.20.0.1", "event": "request_started", "request_id": "f2c60024", "level": "info", "timestamp": "2026-05-17T17:11:23.944065Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.226, "event": "request_completed", "request_id": "f2c60024", "level": "info", "timestamp": "2026-05-17T17:11:23.946282Z"}
INFO: 172.20.0.1:34708 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "ffb62786", "level": "info", "timestamp": "2026-05-17T17:11:25.665331Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.155, "event": "request_completed", "request_id": "ffb62786", "level": "info", "timestamp": "2026-05-17T17:11:25.666462Z"}
INFO: 172.20.0.6:50404 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "0bbf4bca", "level": "info", "timestamp": "2026-05-17T17:11:35.662038Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.334, "event": "request_completed", "request_id": "0bbf4bca", "level": "info", "timestamp": "2026-05-17T17:11:35.663357Z"}
INFO: 172.20.0.6:41622 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "e94bf663", "level": "info", "timestamp": "2026-05-17T17:11:45.661105Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.059, "event": "request_completed", "request_id": "e94bf663", "level": "info", "timestamp": "2026-05-17T17:11:45.662154Z"}
INFO: 172.20.0.6:37112 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "973ea3f0", "level": "info", "timestamp": "2026-05-17T17:11:53.462094Z"}
{"event": "Redis sync client connected (shared)", "request_id": "973ea3f0", "level": "info", "timestamp": "2026-05-17T17:11:53.469537Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 8.179, "event": "request_completed", "request_id": "973ea3f0", "level": "info", "timestamp": "2026-05-17T17:11:53.470256Z"}
INFO: 127.0.0.1:46364 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "56e70681", "level": "info", "timestamp": "2026-05-17T17:11:55.661476Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.001, "event": "request_completed", "request_id": "56e70681", "level": "info", "timestamp": "2026-05-17T17:11:55.662466Z"}
INFO: 172.20.0.6:57742 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "30e0ed4b", "level": "info", "timestamp": "2026-05-17T17:12:05.661054Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.328, "event": "request_completed", "request_id": "30e0ed4b", "level": "info", "timestamp": "2026-05-17T17:12:05.662357Z"}
INFO: 172.20.0.6:37922 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "5cdd34ee", "level": "info", "timestamp": "2026-05-17T17:12:15.661045Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.025, "event": "request_completed", "request_id": "5cdd34ee", "level": "info", "timestamp": "2026-05-17T17:12:15.662060Z"}
INFO: 172.20.0.6:44744 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "6ccf9698", "level": "info", "timestamp": "2026-05-17T17:12:23.507069Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.09, "event": "request_completed", "request_id": "6ccf9698", "level": "info", "timestamp": "2026-05-17T17:12:23.509145Z"}
INFO: 127.0.0.1:60030 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "cad7d1be", "level": "info", "timestamp": "2026-05-17T17:12:25.661643Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.535, "event": "request_completed", "request_id": "cad7d1be", "level": "info", "timestamp": "2026-05-17T17:12:25.663173Z"}
INFO: 172.20.0.6:45036 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "5cbf8014", "level": "info", "timestamp": "2026-05-17T17:12:35.661023Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.003, "event": "request_completed", "request_id": "5cbf8014", "level": "info", "timestamp": "2026-05-17T17:12:35.662015Z"}
INFO: 172.20.0.6:49096 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "db392fe6", "level": "info", "timestamp": "2026-05-17T17:12:45.661640Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 0.998, "event": "request_completed", "request_id": "db392fe6", "level": "info", "timestamp": "2026-05-17T17:12:45.662627Z"}
INFO: 172.20.0.6:60652 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "dad2a986", "level": "info", "timestamp": "2026-05-17T17:12:53.544924Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.107, "event": "request_completed", "request_id": "dad2a986", "level": "info", "timestamp": "2026-05-17T17:12:53.547020Z"}
INFO: 127.0.0.1:59876 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "f63b1413", "level": "info", "timestamp": "2026-05-17T17:12:55.661941Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.272, "event": "request_completed", "request_id": "f63b1413", "level": "info", "timestamp": "2026-05-17T17:12:55.663189Z"}
INFO: 172.20.0.6:39182 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "dd90b6ed", "level": "info", "timestamp": "2026-05-17T17:13:05.681172Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.037, "event": "request_completed", "request_id": "dd90b6ed", "level": "info", "timestamp": "2026-05-17T17:13:05.682191Z"}
INFO: 172.20.0.6:37982 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "3cb5dedd", "level": "info", "timestamp": "2026-05-17T17:13:15.660756Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.336, "event": "request_completed", "request_id": "3cb5dedd", "level": "info", "timestamp": "2026-05-17T17:13:15.662086Z"}
INFO: 172.20.0.6:54296 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "839cf9f7", "level": "info", "timestamp": "2026-05-17T17:13:23.589967Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 1.996, "event": "request_completed", "request_id": "839cf9f7", "level": "info", "timestamp": "2026-05-17T17:13:23.591952Z"}
INFO: 127.0.0.1:45676 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "d443cfbd", "level": "info", "timestamp": "2026-05-17T17:13:25.661474Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 0.981, "event": "request_completed", "request_id": "d443cfbd", "level": "info", "timestamp": "2026-05-17T17:13:25.662445Z"}
INFO: 172.20.0.6:44714 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "f4c6b76f", "level": "info", "timestamp": "2026-05-17T17:13:35.683441Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.238, "event": "request_completed", "request_id": "f4c6b76f", "level": "info", "timestamp": "2026-05-17T17:13:35.684647Z"}
INFO: 172.20.0.6:38144 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "9c3145d5", "level": "info", "timestamp": "2026-05-17T17:13:45.661379Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.012, "event": "request_completed", "request_id": "9c3145d5", "level": "info", "timestamp": "2026-05-17T17:13:45.662381Z"}
INFO: 172.20.0.6:55298 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "89d2d10e", "level": "info", "timestamp": "2026-05-17T17:13:53.629836Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.035, "event": "request_completed", "request_id": "89d2d10e", "level": "info", "timestamp": "2026-05-17T17:13:53.631858Z"}
INFO: 127.0.0.1:40654 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "42f74d0b", "level": "info", "timestamp": "2026-05-17T17:13:55.661606Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 0.966, "event": "request_completed", "request_id": "42f74d0b", "level": "info", "timestamp": "2026-05-17T17:13:55.662563Z"}
INFO: 172.20.0.6:41534 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "7c6e0f9c", "level": "info", "timestamp": "2026-05-17T17:14:05.683680Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.031, "event": "request_completed", "request_id": "7c6e0f9c", "level": "info", "timestamp": "2026-05-17T17:14:05.684696Z"}
INFO: 172.20.0.6:44916 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "c1bd1f3f", "level": "info", "timestamp": "2026-05-17T17:14:15.682417Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.658, "event": "request_completed", "request_id": "c1bd1f3f", "level": "info", "timestamp": "2026-05-17T17:14:15.684060Z"}
INFO: 172.20.0.6:44324 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "15cd7770", "level": "info", "timestamp": "2026-05-17T17:14:23.668443Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.171, "event": "request_completed", "request_id": "15cd7770", "level": "info", "timestamp": "2026-05-17T17:14:23.670604Z"}
INFO: 127.0.0.1:34284 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "e47e0b17", "level": "info", "timestamp": "2026-05-17T17:14:25.684397Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.248, "event": "request_completed", "request_id": "e47e0b17", "level": "info", "timestamp": "2026-05-17T17:14:25.685613Z"}
INFO: 172.20.0.6:37214 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "10bf57d4", "level": "info", "timestamp": "2026-05-17T17:14:35.661616Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.029, "event": "request_completed", "request_id": "10bf57d4", "level": "info", "timestamp": "2026-05-17T17:14:35.662632Z"}
INFO: 172.20.0.6:48092 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "58ae597f", "level": "info", "timestamp": "2026-05-17T17:14:45.661638Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.013, "event": "request_completed", "request_id": "58ae597f", "level": "info", "timestamp": "2026-05-17T17:14:45.662640Z"}
INFO: 172.20.0.6:44578 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "b2fe5d7c", "level": "info", "timestamp": "2026-05-17T17:14:53.707360Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.217, "event": "request_completed", "request_id": "b2fe5d7c", "level": "info", "timestamp": "2026-05-17T17:14:53.709576Z"}
INFO: 127.0.0.1:39056 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "8c452cc5", "level": "info", "timestamp": "2026-05-17T17:14:55.662077Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.038, "event": "request_completed", "request_id": "8c452cc5", "level": "info", "timestamp": "2026-05-17T17:14:55.663105Z"}
INFO: 172.20.0.6:58856 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "2ba84cfb", "level": "info", "timestamp": "2026-05-17T17:15:05.661058Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.311, "event": "request_completed", "request_id": "2ba84cfb", "level": "info", "timestamp": "2026-05-17T17:15:05.662359Z"}
INFO: 172.20.0.6:57554 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "6c58569d", "level": "info", "timestamp": "2026-05-17T17:15:15.660586Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.047, "event": "request_completed", "request_id": "6c58569d", "level": "info", "timestamp": "2026-05-17T17:15:15.661620Z"}
INFO: 172.20.0.6:43854 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "4c88cac5", "level": "info", "timestamp": "2026-05-17T17:15:23.746303Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.642, "event": "request_completed", "request_id": "4c88cac5", "level": "info", "timestamp": "2026-05-17T17:15:23.748930Z"}
INFO: 127.0.0.1:41870 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "be43eebf", "level": "info", "timestamp": "2026-05-17T17:15:25.662094Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.183, "event": "request_completed", "request_id": "be43eebf", "level": "info", "timestamp": "2026-05-17T17:15:25.663263Z"}
INFO: 172.20.0.6:41386 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "916f7fc5", "level": "info", "timestamp": "2026-05-17T17:15:35.661119Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.066, "event": "request_completed", "request_id": "916f7fc5", "level": "info", "timestamp": "2026-05-17T17:15:35.662176Z"}
INFO: 172.20.0.6:48524 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "ed93716b", "level": "info", "timestamp": "2026-05-17T17:15:45.661080Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 0.98, "event": "request_completed", "request_id": "ed93716b", "level": "info", "timestamp": "2026-05-17T17:15:45.662049Z"}
INFO: 172.20.0.6:40504 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "f9c4cca3", "level": "info", "timestamp": "2026-05-17T17:15:53.784245Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.799, "event": "request_completed", "request_id": "f9c4cca3", "level": "info", "timestamp": "2026-05-17T17:15:53.787026Z"}
INFO: 127.0.0.1:55384 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "dd82f907", "level": "info", "timestamp": "2026-05-17T17:15:55.661193Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.021, "event": "request_completed", "request_id": "dd82f907", "level": "info", "timestamp": "2026-05-17T17:15:55.662205Z"}
INFO: 172.20.0.6:59130 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "ef2ac592", "level": "info", "timestamp": "2026-05-17T17:16:05.661519Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.053, "event": "request_completed", "request_id": "ef2ac592", "level": "info", "timestamp": "2026-05-17T17:16:05.662560Z"}
INFO: 172.20.0.6:42932 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "a6daa069", "level": "info", "timestamp": "2026-05-17T17:16:15.662582Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.069, "event": "request_completed", "request_id": "a6daa069", "level": "info", "timestamp": "2026-05-17T17:16:15.663628Z"}
INFO: 172.20.0.6:51850 - "GET /metrics HTTP/1.1" 200 OK
{"files_deleted": 0, "bytes_freed_mb": 0.0, "orphaned_deleted": 0, "cleanup_run_timestamp": "2026-05-17T17:16:22.461453", "event": "cleanup_completed", "level": "info", "timestamp": "2026-05-17T17:16:22.461485Z"}
{"files_deleted": 0, "bytes_freed_mb": 0.0, "orphaned_deleted": 0, "cleanup_run_timestamp": "2026-05-17T17:16:22.482113", "event": "cleanup_completed", "level": "info", "timestamp": "2026-05-17T17:16:22.482146Z"}
{"files_deleted": 0, "bytes_freed_mb": 0.0, "orphaned_deleted": 0, "cleanup_run_timestamp": "2026-05-17T17:16:22.556190", "event": "cleanup_completed", "level": "info", "timestamp": "2026-05-17T17:16:22.556226Z"}
{"files_deleted": 0, "bytes_freed_mb": 0.0, "orphaned_deleted": 0, "cleanup_run_timestamp": "2026-05-17T17:16:22.566761", "event": "cleanup_completed", "level": "info", "timestamp": "2026-05-17T17:16:22.566798Z"}
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "3e2473aa", "level": "info", "timestamp": "2026-05-17T17:16:23.828059Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.547, "event": "request_completed", "request_id": "3e2473aa", "level": "info", "timestamp": "2026-05-17T17:16:23.830593Z"}
INFO: 127.0.0.1:45570 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "90063a81", "level": "info", "timestamp": "2026-05-17T17:16:25.661869Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 0.997, "event": "request_completed", "request_id": "90063a81", "level": "info", "timestamp": "2026-05-17T17:16:25.662855Z"}
INFO: 172.20.0.6:48240 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "3fcaffcf", "level": "info", "timestamp": "2026-05-17T17:16:35.661734Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.035, "event": "request_completed", "request_id": "3fcaffcf", "level": "info", "timestamp": "2026-05-17T17:16:35.662759Z"}
INFO: 172.20.0.6:53906 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "2fb8103d", "level": "info", "timestamp": "2026-05-17T17:16:45.660807Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.234, "event": "request_completed", "request_id": "2fb8103d", "level": "info", "timestamp": "2026-05-17T17:16:45.662026Z"}
INFO: 172.20.0.6:35962 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "a319dbcc", "level": "info", "timestamp": "2026-05-17T17:16:53.866139Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.473, "event": "request_completed", "request_id": "a319dbcc", "level": "info", "timestamp": "2026-05-17T17:16:53.868607Z"}
INFO: 127.0.0.1:36240 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "46e42991", "level": "info", "timestamp": "2026-05-17T17:16:55.662359Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.021, "event": "request_completed", "request_id": "46e42991", "level": "info", "timestamp": "2026-05-17T17:16:55.663369Z"}
INFO: 172.20.0.6:59572 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "2324cc27", "level": "info", "timestamp": "2026-05-17T17:17:05.660609Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.043, "event": "request_completed", "request_id": "2324cc27", "level": "info", "timestamp": "2026-05-17T17:17:05.661643Z"}
INFO: 172.20.0.6:49408 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "03673c90", "level": "info", "timestamp": "2026-05-17T17:17:15.661649Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.105, "event": "request_completed", "request_id": "03673c90", "level": "info", "timestamp": "2026-05-17T17:17:15.662745Z"}
INFO: 172.20.0.6:45116 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "c607cb87", "level": "info", "timestamp": "2026-05-17T17:17:23.907134Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 1.99, "event": "request_completed", "request_id": "c607cb87", "level": "info", "timestamp": "2026-05-17T17:17:23.909114Z"}
INFO: 127.0.0.1:41178 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "3672ae0c", "level": "info", "timestamp": "2026-05-17T17:17:25.661442Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 0.993, "event": "request_completed", "request_id": "3672ae0c", "level": "info", "timestamp": "2026-05-17T17:17:25.662424Z"}
INFO: 172.20.0.6:36692 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "25bdc6fa", "level": "info", "timestamp": "2026-05-17T17:17:35.660440Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.019, "event": "request_completed", "request_id": "25bdc6fa", "level": "info", "timestamp": "2026-05-17T17:17:35.661449Z"}
INFO: 172.20.0.6:41322 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "4337a01e", "level": "info", "timestamp": "2026-05-17T17:17:45.660875Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.002, "event": "request_completed", "request_id": "4337a01e", "level": "info", "timestamp": "2026-05-17T17:17:45.661866Z"}
INFO: 172.20.0.6:33176 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "c9b7b88c", "level": "info", "timestamp": "2026-05-17T17:17:53.952563Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.099, "event": "request_completed", "request_id": "c9b7b88c", "level": "info", "timestamp": "2026-05-17T17:17:53.954647Z"}
INFO: 127.0.0.1:36764 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "0286de08", "level": "info", "timestamp": "2026-05-17T17:17:55.661959Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 0.993, "event": "request_completed", "request_id": "0286de08", "level": "info", "timestamp": "2026-05-17T17:17:55.662942Z"}
INFO: 172.20.0.6:50744 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "a42235e3", "level": "info", "timestamp": "2026-05-17T17:18:05.660816Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.012, "event": "request_completed", "request_id": "a42235e3", "level": "info", "timestamp": "2026-05-17T17:18:05.661819Z"}
INFO: 172.20.0.6:55188 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/languages", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "a0762339", "level": "info", "timestamp": "2026-05-17T17:18:09.853583Z"}
{"method": "GET", "path": "/api/v1/languages", "status_code": 200, "duration_ms": 1.246, "event": "request_completed", "request_id": "a0762339", "level": "info", "timestamp": "2026-05-17T17:18:09.854823Z"}
INFO: 172.20.0.9:35236 - "GET /api/v1/languages HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/providers/available", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "6dc042b1", "level": "info", "timestamp": "2026-05-17T17:18:09.861169Z"}
{"method": "GET", "path": "/api/v1/providers/available", "status_code": 200, "duration_ms": 1.357, "event": "request_completed", "request_id": "6dc042b1", "level": "info", "timestamp": "2026-05-17T17:18:09.862519Z"}
INFO: 172.20.0.9:35250 - "GET /api/v1/providers/available HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/auth/me", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "68937668", "level": "info", "timestamp": "2026-05-17T17:18:09.866205Z"}
{"event": "Redis sync client connected (shared)", "request_id": "68937668", "level": "info", "timestamp": "2026-05-17T17:18:09.867317Z"}
{"event": "Token blocklist using Redis (persistent across restarts)", "request_id": "68937668", "level": "info", "timestamp": "2026-05-17T17:18:09.867370Z"}
{"method": "GET", "path": "/api/v1/auth/me", "status_code": 200, "duration_ms": 23.408, "event": "request_completed", "request_id": "68937668", "level": "info", "timestamp": "2026-05-17T17:18:09.889629Z"}
INFO: 172.20.0.9:35264 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
{"method": "POST", "path": "/api/v1/translate", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "af725d1a", "level": "info", "timestamp": "2026-05-17T17:18:14.569738Z"}
{"event": "Token blocklist using Redis (persistent across restarts)", "request_id": "af725d1a", "level": "info", "timestamp": "2026-05-17T17:18:14.587190Z"}
{"job_id": "tr_06f9aff68e80", "original_filename": "wiki.pdf", "file_size": 2154510, "file_hash": "40e509ae49741c8c0f0d280dbacd8deec1fdb0a2c6a9427d5159acfec77b3ec9", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "timestamp": "2026-05-17T17:18:14.675518Z", "event": "file_uploaded", "request_id": "af725d1a", "level": "info"}
{"job_id": "tr_06f9aff68e80", "ttl_seconds": 3600, "event": "file_tracked_in_redis", "request_id": "af725d1a", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "level": "info", "timestamp": "2026-05-17T17:18:14.675830Z"}
{"event": "[af725d1a] Created translation job tr_06f9aff68e80 for wiki.pdf", "request_id": "af725d1a", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "level": "info", "timestamp": "2026-05-17T17:18:14.676106Z"}
{"event": "google_provider_using_cloud_api", "request_id": "af725d1a", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "level": "info", "timestamp": "2026-05-17T17:18:14.678462Z"}
{"method": "POST", "path": "/api/v1/translate", "status_code": 202, "duration_ms": 112.516, "event": "request_completed", "request_id": "af725d1a", "level": "info", "timestamp": "2026-05-17T17:18:14.682243Z"}
INFO: 172.20.0.9:35266 - "POST /api/v1/translate HTTP/1.1" 202 Accepted
{"method": "GET", "path": "/api/v1/translations/tr_06f9aff68e80", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "d65b3cf1", "level": "info", "timestamp": "2026-05-17T17:18:14.717537Z"}
{"event": "Token blocklist using Redis (persistent across restarts)", "request_id": "d65b3cf1", "level": "info", "timestamp": "2026-05-17T17:18:14.726467Z"}
{"method": "GET", "path": "/api/v1/translations/tr_06f9aff68e80", "status_code": 200, "duration_ms": 23.756, "event": "request_completed", "request_id": "d65b3cf1", "level": "info", "timestamp": "2026-05-17T17:18:14.741294Z"}
INFO: 172.20.0.9:35274 - "GET /api/v1/translations/tr_06f9aff68e80 HTTP/1.1" 200 OK
{"pages": 6, "file": "input_517ddb52_wiki.pdf", "font": "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf", "event": "pdf_layout_start", "request_id": "af725d1a", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "level": "info", "timestamp": "2026-05-17T17:18:14.745197Z"}
{"total_blocks": 0, "translated_blocks": 0, "event": "pdf_blocks_processed", "request_id": "af725d1a", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "level": "info", "timestamp": "2026-05-17T17:18:14.837841Z"}
{"pages": 6, "processing_time_ms": 157.73, "output": "/app/outputs/translated_6c1429db_translated_517ddb52_wiki.pdf", "event": "pdf_layout_success", "request_id": "af725d1a", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "level": "info", "timestamp": "2026-05-17T17:18:14.840896Z"}
{"event": "Job tr_06f9aff68e80: usage recorded \u2014 1 page(s)", "request_id": "af725d1a", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "level": "info", "timestamp": "2026-05-17T17:18:14.850106Z"}
{"file": "/app/outputs/translated_6c1429db_translated_517ddb52_wiki.pdf", "event": "watermark_added_pdf", "request_id": "af725d1a", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "level": "info", "timestamp": "2026-05-17T17:18:14.950210Z"}
{"event": "Job tr_06f9aff68e80: watermark applied (free plan)", "request_id": "af725d1a", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "level": "info", "timestamp": "2026-05-17T17:18:14.950581Z"}
{"event": "Job tr_06f9aff68e80: Completed successfully", "request_id": "af725d1a", "user_id": "14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "level": "info", "timestamp": "2026-05-17T17:18:14.950779Z"}
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "c8e55a39", "level": "info", "timestamp": "2026-05-17T17:18:15.660768Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.143, "event": "request_completed", "request_id": "c8e55a39", "level": "info", "timestamp": "2026-05-17T17:18:15.661901Z"}
INFO: 172.20.0.6:36254 - "GET /metrics HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/translations/tr_06f9aff68e80", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "5966b6cb", "level": "info", "timestamp": "2026-05-17T17:18:16.714805Z"}
{"event": "Token blocklist using Redis (persistent across restarts)", "request_id": "5966b6cb", "level": "info", "timestamp": "2026-05-17T17:18:16.715435Z"}
{"method": "GET", "path": "/api/v1/translations/tr_06f9aff68e80", "status_code": 200, "duration_ms": 84.202, "event": "request_completed", "request_id": "5966b6cb", "level": "info", "timestamp": "2026-05-17T17:18:16.799005Z"}
INFO: 172.20.0.9:35286 - "GET /api/v1/translations/tr_06f9aff68e80 HTTP/1.1" 200 OK
{"method": "GET", "path": "/health", "client_ip": "127.0.0.1", "event": "request_started", "request_id": "a4d14070", "level": "info", "timestamp": "2026-05-17T17:18:23.990430Z"}
{"method": "GET", "path": "/health", "status_code": 200, "duration_ms": 2.151, "event": "request_completed", "request_id": "a4d14070", "level": "info", "timestamp": "2026-05-17T17:18:23.992566Z"}
INFO: 127.0.0.1:43562 - "GET /health HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "30358172", "level": "info", "timestamp": "2026-05-17T17:18:25.662049Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.162, "event": "request_completed", "request_id": "30358172", "level": "info", "timestamp": "2026-05-17T17:18:25.663199Z"}
INFO: 172.20.0.6:60524 - "GET /metrics HTTP/1.1" 200 OK
{"method": "PATCH", "path": "/api/v1/admin/users/14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "c030527f", "level": "info", "timestamp": "2026-05-17T17:18:26.073909Z"}
{"event": "admin_tier_change", "request_id": "c030527f", "level": "info", "timestamp": "2026-05-17T17:18:26.080927Z"}
{"method": "PATCH", "path": "/api/v1/admin/users/14cc7c0b-c67a-4bfd-8fee-35dd6c61761d", "status_code": 200, "duration_ms": 7.406, "event": "request_completed", "request_id": "c030527f", "level": "info", "timestamp": "2026-05-17T17:18:26.081310Z"}
INFO: 172.20.0.9:44762 - "PATCH /api/v1/admin/users/14cc7c0b-c67a-4bfd-8fee-35dd6c61761d HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/admin/users", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "d58944eb", "level": "info", "timestamp": "2026-05-17T17:18:26.112803Z"}
{"method": "GET", "path": "/api/v1/admin/users", "status_code": 200, "duration_ms": 4.981, "event": "request_completed", "request_id": "d58944eb", "level": "info", "timestamp": "2026-05-17T17:18:26.117774Z"}
INFO: 172.20.0.9:44776 - "GET /api/v1/admin/users HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/providers/available", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "0a0b4901", "level": "info", "timestamp": "2026-05-17T17:18:29.839411Z"}
{"method": "GET", "path": "/api/v1/providers/available", "status_code": 200, "duration_ms": 1.184, "event": "request_completed", "request_id": "0a0b4901", "level": "info", "timestamp": "2026-05-17T17:18:29.840582Z"}
INFO: 172.20.0.9:43648 - "GET /api/v1/providers/available HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/auth/me", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "2e460b02", "level": "info", "timestamp": "2026-05-17T17:18:29.841166Z"}
{"event": "Token blocklist using Redis (persistent across restarts)", "request_id": "2e460b02", "level": "info", "timestamp": "2026-05-17T17:18:29.841766Z"}
{"method": "GET", "path": "/api/v1/languages", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "60b84010", "level": "info", "timestamp": "2026-05-17T17:18:29.843340Z"}
{"method": "GET", "path": "/api/v1/languages", "status_code": 200, "duration_ms": 0.869, "event": "request_completed", "request_id": "60b84010", "level": "info", "timestamp": "2026-05-17T17:18:29.844200Z"}
INFO: 172.20.0.9:43670 - "GET /api/v1/languages HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/auth/me", "status_code": 200, "duration_ms": 21.452, "event": "request_completed", "request_id": "2e460b02", "level": "info", "timestamp": "2026-05-17T17:18:29.862620Z"}
INFO: 172.20.0.9:43662 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/auth/me", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "318229e6", "level": "info", "timestamp": "2026-05-17T17:18:33.244143Z"}
{"event": "Token blocklist using Redis (persistent across restarts)", "request_id": "318229e6", "level": "info", "timestamp": "2026-05-17T17:18:33.244597Z"}
{"method": "GET", "path": "/api/v1/auth/me", "status_code": 200, "duration_ms": 2.578, "event": "request_completed", "request_id": "318229e6", "level": "info", "timestamp": "2026-05-17T17:18:33.246715Z"}
INFO: 172.20.0.9:43680 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/providers/available", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "e52681bb", "level": "info", "timestamp": "2026-05-17T17:18:33.254917Z"}
{"method": "GET", "path": "/api/v1/providers/available", "status_code": 200, "duration_ms": 1.427, "event": "request_completed", "request_id": "e52681bb", "level": "info", "timestamp": "2026-05-17T17:18:33.256353Z"}
{"method": "GET", "path": "/api/v1/languages", "client_ip": "2a01:e0a:ffb:12c0:3055:a5de:f9c7:1b34", "event": "request_started", "request_id": "a0be60c5", "level": "info", "timestamp": "2026-05-17T17:18:33.256895Z"}
INFO: 172.20.0.9:43688 - "GET /api/v1/providers/available HTTP/1.1" 200 OK
{"method": "GET", "path": "/api/v1/languages", "status_code": 200, "duration_ms": 1.283, "event": "request_completed", "request_id": "a0be60c5", "level": "info", "timestamp": "2026-05-17T17:18:33.258180Z"}
INFO: 172.20.0.9:43704 - "GET /api/v1/languages HTTP/1.1" 200 OK
{"method": "GET", "path": "/metrics", "client_ip": "172.20.0.6", "event": "request_started", "request_id": "599da081", "level": "info", "timestamp": "2026-05-17T17:18:35.660845Z"}
{"method": "GET", "path": "/metrics", "status_code": 200, "duration_ms": 1.192, "event": "request_completed", "request_id": "599da081", "level": "info", "timestamp": "2026-05-17T17:18:35.662027Z"}
INFO: 172.20.0.6:48698 - "GET /metrics HTTP/1.1" 200 OK

View File

@@ -82,6 +82,8 @@ class User(Base):
stripe_customer_id = Column(String(255), nullable=True, index=True) stripe_customer_id = Column(String(255), nullable=True, index=True)
stripe_subscription_id = Column(String(255), nullable=True) stripe_subscription_id = Column(String(255), nullable=True)
subscription_ends_at = Column(DateTime, nullable=True)
cancel_at_period_end = Column(Boolean, default=False, nullable=False)
reset_token = Column(String(255), nullable=True) reset_token = Column(String(255), nullable=True)
reset_token_expires = Column(DateTime, nullable=True) reset_token_expires = Column(DateTime, nullable=True)

View File

@@ -283,6 +283,7 @@ export default function PricingPage() {
const [openFAQ, setOpenFAQ] = useState<number | null>(null); const [openFAQ, setOpenFAQ] = useState<number | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loadingPlanId, setLoadingPlanId] = useState<string | null>(null); const [loadingPlanId, setLoadingPlanId] = useState<string | null>(null);
const [loadingCreditIdx, setLoadingCreditIdx] = useState<number | null>(null);
const [toastMsg, setToastMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); const [toastMsg, setToastMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const [annualDiscountPercent, setAnnualDiscountPercent] = useState(ANNUAL_DISCOUNT_PERCENT); const [annualDiscountPercent, setAnnualDiscountPercent] = useState(ANNUAL_DISCOUNT_PERCENT);
/** Until false: don't show STATIC_PLANS (avoids flash of stale prices on refresh). */ /** Until false: don't show STATIC_PLANS (avoids flash of stale prices on refresh). */
@@ -401,6 +402,41 @@ export default function PricingPage() {
} }
}; };
const handleBuyCredits = async (packageIndex: number) => {
const token = localStorage.getItem("token");
if (!token) {
router.push(`/auth/login?redirect=/pricing`);
return;
}
setLoadingCreditIdx(packageIndex);
setToastMsg(null);
try {
const res = await fetch(`${API_BASE}/api/v1/auth/create-credits-checkout`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ package_index: packageIndex }),
});
const data = await res.json();
const url = data.data?.url ?? data.url;
if (!res.ok) {
throw new Error(data.message ?? data.error ?? t('pricing.toast.paymentError'));
}
if (url) {
window.location.replace(url);
} else {
setToastMsg({ type: 'ok', text: t('pricing.toast.demo', { planId: 'credits' }) });
}
} catch (error) {
const message = error instanceof Error ? error.message : t('pricing.toast.networkError');
setToastMsg({ type: 'err', text: message });
} finally {
setLoadingCreditIdx(null);
}
};
return ( return (
<div className="min-h-screen bg-background text-foreground"> <div className="min-h-screen bg-background text-foreground">
{/* ── Top navigation — breadcrumb bar ── */} {/* ── Top navigation — breadcrumb bar ── */}
@@ -774,8 +810,12 @@ export default function PricingPage() {
<div className="text-muted-foreground text-xs mb-3">{t('pricing.credits.unit')}</div> <div className="text-muted-foreground text-xs mb-3">{t('pricing.credits.unit')}</div>
<div className="text-xl font-bold text-foreground">{pkg.price} &euro;</div> <div className="text-xl font-bold text-foreground">{pkg.price} &euro;</div>
<div className="text-muted-foreground text-xs">{(pkg.price_per_credit * 100).toFixed(0)} {t('pricing.credits.centsPerCredit')}</div> <div className="text-muted-foreground text-xs">{(pkg.price_per_credit * 100).toFixed(0)} {t('pricing.credits.centsPerCredit')}</div>
<button className="mt-3 w-full py-1.5 rounded-lg bg-muted hover:bg-muted/80 text-foreground text-xs transition-all"> <button
{t('pricing.credits.buy')} onClick={() => handleBuyCredits(i)}
disabled={loadingCreditIdx === i}
className="mt-3 w-full py-1.5 rounded-lg bg-muted hover:bg-muted/80 text-foreground text-xs transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{loadingCreditIdx === i ? '...' : t('pricing.credits.buy')}
</button> </button>
</div> </div>
))} ))}

77
scripts/stripe_setup.py Normal file
View File

@@ -0,0 +1,77 @@
"""
Script de setup Stripe : crée les produits et prix dans Stripe Dashboard.
Lance avec : python scripts/stripe_setup.py
"""
import stripe
import sys
SK = "sk_test_51SkSHkCKXUJE51jnbEtXZ0nKiTHTa8ohDwLH8fZiDVEx6Ze0g5dg4fGJJgX1VgNHvF93GE3HTramT3oQrCaqOxid00OXTcZlsW"
stripe.api_key = SK
PLANS = [
{"id": "starter", "name": "Wordly Starter", "monthly_eur": 900, "yearly_eur": 8640},
{"id": "pro", "name": "Wordly Pro", "monthly_eur": 1900, "yearly_eur": 18240},
{"id": "business","name": "Wordly Business","monthly_eur": 4900, "yearly_eur": 47040},
]
results = {}
for plan in PLANS:
print(f"\n>> Creation produit {plan['name']}...")
# Search existing product first
existing = stripe.Product.search(query=f"name:'{plan['name']}'", limit=1)
if existing.data:
product = existing.data[0]
print(f" Produit existant: {product.id}")
else:
product = stripe.Product.create(
name=plan["name"],
metadata={"plan_id": plan["id"]}
)
print(f" Produit créé: {product.id}")
# Monthly price
monthly_prices = stripe.Price.list(product=product.id, active=True, limit=10)
monthly_price = None
yearly_price = None
for p in monthly_prices.data:
if p.recurring and p.recurring.interval == "month" and p.unit_amount == plan["monthly_eur"]:
monthly_price = p
if p.recurring and p.recurring.interval == "year" and p.unit_amount == plan["yearly_eur"]:
yearly_price = p
if not monthly_price:
monthly_price = stripe.Price.create(
product=product.id,
unit_amount=plan["monthly_eur"],
currency="eur",
recurring={"interval": "month"},
metadata={"plan_id": plan["id"], "billing": "monthly"}
)
print(f" Prix mensuel créé: {monthly_price.id}")
else:
print(f" Prix mensuel existant: {monthly_price.id}")
if not yearly_price:
yearly_price = stripe.Price.create(
product=product.id,
unit_amount=plan["yearly_eur"],
currency="eur",
recurring={"interval": "year"},
metadata={"plan_id": plan["id"], "billing": "yearly"}
)
print(f" Prix annuel créé: {yearly_price.id}")
else:
print(f" Prix annuel existant: {yearly_price.id}")
results[plan["id"]] = {
"monthly": monthly_price.id,
"yearly": yearly_price.id,
}
print("\n" + "="*60)
print("Variables à ajouter dans .env.production et .env :")
print("="*60)
for plan_id, ids in results.items():
print(f"STRIPE_PRICE_{plan_id.upper()}_MONTHLY={ids['monthly']}")
print(f"STRIPE_PRICE_{plan_id.upper()}_YEARLY={ids['yearly']}")
print("="*60)

View File

@@ -2,9 +2,12 @@
Stripe payment integration for subscriptions and credits Stripe payment integration for subscriptions and credits
""" """
import os import os
import logging
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from datetime import datetime from datetime import datetime
logger = logging.getLogger(__name__)
# Try to import stripe # Try to import stripe
try: try:
import stripe import stripe
@@ -347,13 +350,33 @@ async def handle_subscription_deleted(subscription: Dict):
async def handle_payment_failed(invoice: Dict): async def handle_payment_failed(invoice: Dict):
"""Handle failed payment""" """Handle failed payment — set subscription status to PAST_DUE"""
customer_id = invoice.get("customer") customer_id = invoice.get("customer")
if not customer_id: if not customer_id:
return return
# Find user by customer ID and update status # Find user by stripe_customer_id
# In production, query database by stripe_customer_id user = None
try:
from database.connection import get_sync_session
from database.models import User as DBUser
with get_sync_session() as session:
db_user = (
session.query(DBUser)
.filter(DBUser.stripe_customer_id == customer_id)
.first()
)
if db_user:
user_id = str(db_user.id)
db_user.subscription_status = SubscriptionStatus.PAST_DUE
session.commit()
logger.warning(
"Payment failed for customer %s (user %s) — status set to past_due",
customer_id, user_id,
)
except Exception as exc:
logger.error("handle_payment_failed DB error: %s", exc)
async def cancel_subscription(user_id: str) -> Dict[str, Any]: async def cancel_subscription(user_id: str) -> Dict[str, Any]: