klg-asutk-app/backend/app/services/risk_scanner.py
Yuriy 0150aba4f5 Consolidation: KLG ASUTK + PAPA integration
- Unify API: lib/api.ts uses /api/v1, inbox uses /api/inbox (rewrites)
- Remove localhost refs: openapi, inbox page
- Add rewrites: /api/inbox|tmc -> inbox-server, /api/v1 -> FastAPI
- Add stub routes: knowledge/insights, recommendations, search, log-error
- Transfer from PAPA: prompts (inspection, tmc), scripts, supabase, data/tmc-requests
- Fix inbox-server: ORDER BY created_at, package.json
- Remove redundant app/api/inbox/files route (rewrites handle it)
- knowledge/ in gitignore (large PDFs)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 17:18:31 +03:00

233 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Сервис автоматического сканирования рисков на основе данных о ВС."""
from datetime import datetime, timezone, timedelta
from sqlalchemy.orm import Session
from app.models import (
RiskAlert, MaintenanceTask, LimitedLifeComponent, LandingGearComponent,
DefectReport, AirworthinessCertificate, Aircraft
)
def scan_risks(db: Session) -> int:
"""Сканирует все ВС и создаёт предупреждения о рисках. Возвращает количество созданных предупреждений."""
created = 0
now = datetime.now(timezone.utc)
warning_days = 7
critical_days = 3
# 1. MaintenanceTask: next_due в прошлом или скоро
tasks = db.query(MaintenanceTask).join(Aircraft).filter(
MaintenanceTask.next_due.isnot(None)
).all()
for task in tasks:
if task.next_due:
days_until = (task.next_due - now).days
if days_until < 0:
severity = "critical"
title = f"Просрочено ТО: {task.task_number}"
message = f"Карта программы ТО {task.task_number} просрочена на {abs(days_until)} дней"
elif days_until <= critical_days:
severity = "critical"
title = f"Скоро ТО: {task.task_number}"
message = f"Карта программы ТО {task.task_number} должна быть выполнена через {days_until} дней"
elif days_until <= warning_days:
severity = "high"
title = f"Приближается ТО: {task.task_number}"
message = f"Карта программы ТО {task.task_number} должна быть выполнена через {days_until} дней"
else:
continue
# Проверяем, нет ли уже такого предупреждения
existing = db.query(RiskAlert).filter(
RiskAlert.entity_type == "maintenance_task",
RiskAlert.entity_id == task.id,
RiskAlert.is_resolved == False
).first()
if not existing:
alert = RiskAlert(
entity_type="maintenance_task",
entity_id=task.id,
aircraft_id=task.aircraft_id,
severity=severity,
title=title,
message=message,
due_at=task.next_due
)
db.add(alert)
created += 1
# 2. LimitedLifeComponent: expected_date
components = db.query(LimitedLifeComponent).join(Aircraft).filter(
LimitedLifeComponent.expected_date.isnot(None)
).all()
for comp in components:
if comp.expected_date:
days_until = (comp.expected_date - now).days
if days_until < 0:
severity = "critical"
title = f"Просрочен компонент: {comp.part_number}"
message = f"Компонент {comp.part_number} (SN: {comp.serial_number}) просрочен на {abs(days_until)} дней"
elif days_until <= critical_days:
severity = "critical"
title = f"Скоро срок компонента: {comp.part_number}"
message = f"Компонент {comp.part_number} должен быть заменён через {days_until} дней"
elif days_until <= warning_days:
severity = "high"
title = f"Приближается срок компонента: {comp.part_number}"
message = f"Компонент {comp.part_number} должен быть заменён через {days_until} дней"
else:
continue
existing = db.query(RiskAlert).filter(
RiskAlert.entity_type == "limited_life",
RiskAlert.entity_id == comp.id,
RiskAlert.is_resolved == False
).first()
if not existing:
alert = RiskAlert(
entity_type="limited_life",
entity_id=comp.id,
aircraft_id=comp.aircraft_id,
severity=severity,
title=title,
message=message,
due_at=comp.expected_date
)
db.add(alert)
created += 1
# 3. LandingGearComponent: due_at
landing_gear = db.query(LandingGearComponent).join(Aircraft).filter(
LandingGearComponent.due_at.isnot(None)
).all()
for lg in landing_gear:
if lg.due_at:
days_until = (lg.due_at - now).days
if days_until < 0:
severity = "critical"
title = f"Просрочено шасси: {lg.part_number}"
message = f"Компонент шасси {lg.part_number} просрочен на {abs(days_until)} дней"
elif days_until <= critical_days:
severity = "critical"
title = f"Скоро срок шасси: {lg.part_number}"
message = f"Компонент шасси {lg.part_number} должен быть заменён через {days_until} дней"
elif days_until <= warning_days:
severity = "high"
title = f"Приближается срок шасси: {lg.part_number}"
message = f"Компонент шасси {lg.part_number} должен быть заменён через {days_until} дней"
else:
continue
existing = db.query(RiskAlert).filter(
RiskAlert.entity_type == "landing_gear",
RiskAlert.entity_id == lg.id,
RiskAlert.is_resolved == False
).first()
if not existing:
alert = RiskAlert(
entity_type="landing_gear",
entity_id=lg.id,
aircraft_id=lg.aircraft_id,
severity=severity,
title=title,
message=message,
due_at=lg.due_at
)
db.add(alert)
created += 1
# 4. DefectReport: limit_date
defects = db.query(DefectReport).join(Aircraft).filter(
DefectReport.limit_date.isnot(None)
).all()
for defect in defects:
if defect.limit_date:
days_until = (defect.limit_date - now).days
if days_until < 0:
severity = "critical"
title = f"Просрочен дефект: {defect.wo_number or 'N/A'}"
message = f"Дефект {defect.wo_number or 'N/A'} просрочен на {abs(days_until)} дней"
elif days_until <= critical_days:
severity = "critical"
title = f"Скоро срок устранения дефекта: {defect.wo_number or 'N/A'}"
message = f"Дефект {defect.wo_number or 'N/A'} должен быть устранён через {days_until} дней"
elif days_until <= warning_days:
severity = "high"
title = f"Приближается срок устранения дефекта: {defect.wo_number or 'N/A'}"
message = f"Дефект {defect.wo_number or 'N/A'} должен быть устранён через {days_until} дней"
else:
continue
existing = db.query(RiskAlert).filter(
RiskAlert.entity_type == "defect_report",
RiskAlert.entity_id == defect.id,
RiskAlert.is_resolved == False
).first()
if not existing:
alert = RiskAlert(
entity_type="defect_report",
entity_id=defect.id,
aircraft_id=defect.aircraft_id,
severity=severity,
title=title,
message=message,
due_at=defect.limit_date
)
db.add(alert)
created += 1
# 5. AirworthinessCertificate: expiry_date (60 дней предупреждение)
certs = db.query(AirworthinessCertificate).join(Aircraft).filter(
AirworthinessCertificate.expiry_date.isnot(None),
AirworthinessCertificate.status == "valid"
).all()
for cert in certs:
if cert.expiry_date:
days_until = (cert.expiry_date - now).days
if days_until < 0:
severity = "critical"
title = f"Истёк сертификат лётной годности: {cert.certificate_number}"
message = f"Сертификат {cert.certificate_number} истёк {abs(days_until)} дней назад"
elif days_until <= 30:
severity = "critical"
title = f"Скоро истекает сертификат: {cert.certificate_number}"
message = f"Сертификат {cert.certificate_number} истекает через {days_until} дней"
elif days_until <= 60:
severity = "high"
title = f"Приближается срок сертификата: {cert.certificate_number}"
message = f"Сертификат {cert.certificate_number} истекает через {days_until} дней"
else:
continue
existing = db.query(RiskAlert).filter(
RiskAlert.entity_type == "airworthiness_certificate",
RiskAlert.entity_id == cert.id,
RiskAlert.is_resolved == False
).first()
if not existing:
alert = RiskAlert(
entity_type="airworthiness_certificate",
entity_id=cert.id,
aircraft_id=cert.aircraft_id,
severity=severity,
title=title,
message=message,
due_at=cert.expiry_date
)
db.add(alert)
created += 1
db.commit()
return created