diff --git a/.claude/settings.json b/.claude/settings.json index 5b67b20..5197e91 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -10,7 +10,9 @@ "WebSearch", "Bash(DEEPSEEK_API_KEY=sk-176db424442e40728c04c04a4170c224 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 \" *)" ] } } diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d08e785..b50544d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -26,7 +26,11 @@ "Bash(docker exec *)", "Bash(git commit -m ' *)", "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)" ] } } diff --git a/.env.production b/.env.production index fae5c35..9ddedd1 100644 --- a/.env.production +++ b/.env.production @@ -15,6 +15,7 @@ LOG_LEVEL=INFO # ---- Domaine ---- DOMAIN=wordly.art NEXT_PUBLIC_API_URL=https://wordly.art +FRONTEND_URL=https://wordly.art BACKEND_PORT=8000 FRONTEND_PORT=3000 @@ -91,8 +92,15 @@ GRAFANA_USER=admin GRAFANA_PASSWORD=WordlyGrafana2026! # ---- 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_STARTER_PRICE_ID= -STRIPE_PRO_PRICE_ID= -STRIPE_BUSINESS_PRICE_ID= + +# Price IDs créés dans Stripe Dashboard (wordly.art — test mode) +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 diff --git a/alembic/versions/f6a7b8c9d0e1_add_subscription_ends_at_cancel_at_period_end.py b/alembic/versions/f6a7b8c9d0e1_add_subscription_ends_at_cancel_at_period_end.py new file mode 100644 index 0000000..d97ed00 --- /dev/null +++ b/alembic/versions/f6a7b8c9d0e1_add_subscription_ends_at_cancel_at_period_end.py @@ -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") diff --git a/backend_logs.txt b/backend_logs.txt new file mode 100644 index 0000000..761a9d4 --- /dev/null +++ b/backend_logs.txt @@ -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 diff --git a/database/models.py b/database/models.py index efed78a..c8bae67 100644 --- a/database/models.py +++ b/database/models.py @@ -82,6 +82,8 @@ class User(Base): stripe_customer_id = Column(String(255), nullable=True, index=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_expires = Column(DateTime, nullable=True) diff --git a/frontend/src/app/pricing/page.tsx b/frontend/src/app/pricing/page.tsx index b82c7c6..a0d15ec 100644 --- a/frontend/src/app/pricing/page.tsx +++ b/frontend/src/app/pricing/page.tsx @@ -283,6 +283,7 @@ export default function PricingPage() { const [openFAQ, setOpenFAQ] = useState(null); const [isLoggedIn, setIsLoggedIn] = useState(false); const [loadingPlanId, setLoadingPlanId] = useState(null); + const [loadingCreditIdx, setLoadingCreditIdx] = useState(null); const [toastMsg, setToastMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); const [annualDiscountPercent, setAnnualDiscountPercent] = useState(ANNUAL_DISCOUNT_PERCENT); /** 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 (
{/* ── Top navigation — breadcrumb bar ── */} @@ -774,8 +810,12 @@ export default function PricingPage() {
{t('pricing.credits.unit')}
{pkg.price} €
{(pkg.price_per_credit * 100).toFixed(0)} {t('pricing.credits.centsPerCredit')}
-
))} diff --git a/scripts/stripe_setup.py b/scripts/stripe_setup.py new file mode 100644 index 0000000..df53703 --- /dev/null +++ b/scripts/stripe_setup.py @@ -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) diff --git a/services/payment_service.py b/services/payment_service.py index bc3ee92..92ebce2 100644 --- a/services/payment_service.py +++ b/services/payment_service.py @@ -2,9 +2,12 @@ Stripe payment integration for subscriptions and credits """ import os +import logging from typing import Optional, Dict, Any from datetime import datetime +logger = logging.getLogger(__name__) + # Try to import stripe try: import stripe @@ -347,13 +350,33 @@ async def handle_subscription_deleted(subscription: 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") if not customer_id: return - - # Find user by customer ID and update status - # In production, query database by stripe_customer_id + + # Find user 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]: