fix(admin): починить панель отладки — она отдавала 500
All checks were successful
Deploy / test (push) Successful in 2m59s
Deploy / deploy (push) Successful in 2m15s

Моя же правка про зависшие проверки уронила /admin/debug: колонки tasks в
PostgreSQL — timestamptz, а модель объявляет их без зоны, и asyncpg отверг
переданное из Python время («can't subtract offset-naive and offset-aware»).

Возраст задачи теперь считает сама база (now() - coalesce(updated_at,
created_at)), так что расхождение объявления и реального типа колонки роли не
играет. Проверено на живых данных: находит все 4 задачи, висящие с 30 мая.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-09-05 20:17:28 +05:00
parent 39951d6a0f
commit 1b1ca3b7e3

View File

@@ -9,7 +9,7 @@ import io
import logging import logging
import os import os
import uuid import uuid
from datetime import UTC, datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
import httpx import httpx
@@ -929,20 +929,24 @@ async def debug_snapshot(db: AsyncSession = Depends(get_db)) -> dict:
# Пользователь видит вечное «обрабатывается» — молча и без следов в логах, # Пользователь видит вечное «обрабатывается» — молча и без следов в логах,
# поэтому такие задачи должны быть видны в отладке (нашлись экземпляры, # поэтому такие задачи должны быть видны в отладке (нашлись экземпляры,
# висевшие с мая). # висевшие с мая).
# tasks.created_at/updated_at — timestamptz, поэтому сравниваем с #
# осведомлённым о зоне временем (naive utcnow() дал бы TypeError) # Возраст считает сама база: колонки в PostgreSQL — timestamptz, а модель
task_stale_cutoff = datetime.now(UTC) - timedelta(hours=2) # объявляет их без зоны, и передача сюда Python-времени ломается на
# несовпадении (asyncpg отвергает aware-параметр, naive даёт неверный сдвиг).
stale_tasks = ( stale_tasks = (
await db.execute( await db.execute(
select(Task) text("""
.where( SELECT public_id, type, created_at,
Task.status == "processing", round(extract(epoch FROM now() - coalesce(updated_at, created_at))
func.coalesce(Task.updated_at, Task.created_at) < task_stale_cutoff, / 3600.0, 1) AS stuck_hours
) FROM tasks
.order_by(Task.created_at.desc()) WHERE status = 'processing'
.limit(20) AND coalesce(updated_at, created_at) < now() - interval '2 hours'
ORDER BY created_at DESC
LIMIT 20
""")
) )
).scalars().all() ).all()
return { return {
"generated_at": datetime.utcnow().isoformat(timespec="seconds"), "generated_at": datetime.utcnow().isoformat(timespec="seconds"),
@@ -980,9 +984,7 @@ async def debug_snapshot(db: AsyncSession = Depends(get_db)) -> dict:
"public_id": t.public_id, "public_id": t.public_id,
"type": t.type, "type": t.type,
"created_at": t.created_at, "created_at": t.created_at,
"stuck_hours": round( "stuck_hours": float(t.stuck_hours),
(datetime.now(UTC) - (t.updated_at or t.created_at)).total_seconds() / 3600, 1
),
} }
for t in stale_tasks for t in stale_tasks
], ],