""" GET /api/health — «آیا این سرور واقعاً سالم است؟» چرا لازم بود: تا امروز تنها راهِ فهمیدنِ اینکه دیپلوی درست نشسته یا نه، این بود که کسی از بیرون اپ را دستی امتحان کند و اگر چیزی غلط بود به آدمی که به سرور دسترسی دارد پیام بدهد. یعنی خرابی را همیشه آدم کشف می‌کرد، نه ابزار. این اندپوینت همان بررسی‌ها را از بیرون و بدونِ دسترسیِ SSH ممکن می‌کند. مهم‌ترین بررسی «مهاجرت‌های اعمال‌نشده» است: شایع‌ترین حالتِ خرابِ دیپلوی این است که کد pull شده ولی migrate اجرا نشده — اپ بالا می‌آید و بعد زیرِ بارِ واقعی با خطای ستونِ ناموجود می‌شکند. عمداً هیچ سکرتی برنمی‌گرداند: فقط بله/خیرِ «تنظیم شده» و شمارشِ محتوا. """ from django.conf import settings from django.core.cache import cache from django.db import connection from django.db.migrations.executor import MigrationExecutor from drf_spectacular.utils import extend_schema from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.throttling import ScopedRateThrottle from rest_framework.views import APIView def _check_database() -> dict: try: with connection.cursor() as cursor: cursor.execute('SELECT 1') cursor.fetchone() return {'ok': True, 'engine': connection.vendor} except Exception as exc: # noqa: BLE001 return {'ok': False, 'error': type(exc).__name__} def _check_migrations() -> dict: """مهاجرتِ اعمال‌نشده یعنی دیپلوی نیمه‌کاره است.""" try: executor = MigrationExecutor(connection) targets = executor.loader.graph.leaf_nodes() pending = executor.migration_plan(targets) return { 'ok': not pending, 'pending': [f'{app}.{name}' for (app, name), _ in ((m.key, m) for m, _ in pending)], } except Exception as exc: # noqa: BLE001 return {'ok': False, 'error': type(exc).__name__} def _check_cache() -> dict: """با DEBUG=False کشِ مشترک اجباری است؛ throttle بدونش بی‌اثر می‌شود.""" try: cache.set('health:probe', 'ok', 10) return {'ok': cache.get('health:probe') == 'ok', 'backend': settings.CACHES['default']['BACKEND'].rsplit('.', 1)[-1]} except Exception as exc: # noqa: BLE001 return {'ok': False, 'error': type(exc).__name__} def _check_content() -> dict: """ محتوا روی این سرور هست یا نه — بدونِ این، اپ سالم به نظر می‌رسد ولی کاربر صفحه‌ی خالی می‌بیند. """ try: from content.models import Grade, Lesson, Question grades = Grade.objects.count() lessons = Lesson.objects.count() questions = Question.objects.count() return { 'ok': grades > 0 and lessons > 0, 'grades': grades, 'lessons': lessons, 'questions': questions, } except Exception as exc: # noqa: BLE001 return {'ok': False, 'error': type(exc).__name__} def _check_integrations() -> dict: """فقط «تنظیم شده یا نه» — هیچ کلیدی برنمی‌گردد.""" return { 'qdrant': bool(getattr(settings, 'QDRANT_API_KEY', '')), 'openai': bool(getattr(settings, 'OPENAI_API_KEY', '')), 'sms': bool(getattr(settings, 'KAVENEGAR_API_KEY', '')), } class HealthView(APIView): """ GET /api/health — وضعیتِ این نسخه‌ی مستقرشده. ۲۰۰ یعنی سالم، ۵۰۳ یعنی چیزی واقعاً خراب است (دیتابیس، کش، یا مهاجرتِ اعمال‌نشده). نبودِ محتوا خرابی حساب نمی‌شود ولی گزارش می‌شود. """ permission_classes = [AllowAny] authentication_classes = [] throttle_classes = [ScopedRateThrottle] throttle_scope = 'health' @extend_schema(tags=['System'], responses={200: dict, 503: dict}) def get(self, request): checks = { 'database': _check_database(), 'migrations': _check_migrations(), 'cache': _check_cache(), 'content': _check_content(), } critical_ok = all(checks[k]['ok'] for k in ('database', 'migrations', 'cache')) payload = { 'status': 'ok' if critical_ok else 'error', 'debug': bool(settings.DEBUG), 'checks': checks, 'integrations': _check_integrations(), } return Response(payload, status=200 if critical_ok else 503)