- 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>
233 lines
10 KiB
Python
233 lines
10 KiB
Python
"""Сервис автоматического сканирования рисков на основе данных о ВС."""
|
||
|
||
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
|