From d011dcdbba4d9ac512ce41299495affc70325eff Mon Sep 17 00:00:00 2001 From: Yuriy Date: Mon, 16 Feb 2026 06:56:30 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD,?= =?UTF-8?q?=20=D0=B8=D0=BA=D0=BE=D0=BD=D0=BA=D0=B8=20REFLY,=20=D0=B4=D0=B5?= =?UTF-8?q?=D0=BC=D0=BE-=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5,=20UI=20(?= =?UTF-8?q?=D0=92=D0=A1/=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D0=B8/=D1=87=D0=B5=D0=BA-=D0=BB=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D1=8B/=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA?= =?UTF-8?q?=D0=B8/=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D1=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app/aircraft/page.tsx | 28 +++++--- app/checklists/page.tsx | 13 +--- app/documents/page.tsx | 2 +- app/settings/page.tsx | 53 ++++++++++++-- app/users/page.tsx | 6 +- backend/app/api/routes/aircraft.py | 18 +++-- backend/app/api/routes/airworthiness_core.py | 31 ++++++++ backend/app/demo/seed_document_templates.py | 8 +++ backend/app/demo/seed_full_demo.py | 70 +++++++++++++++++- backend/app/main.py | 12 ++++ backend/app/schemas/aircraft.py | 2 +- components/Sidebar.tsx | 8 ++- components/ui/StatusBadge.tsx | 16 ++++- docs/ops/AI_PROXY_SETUP.md | 48 +++++++++++++ icons/refly-icons.tsx | 75 +++++++++++++++++--- 15 files changed, 340 insertions(+), 50 deletions(-) create mode 100644 docs/ops/AI_PROXY_SETUP.md diff --git a/app/aircraft/page.tsx b/app/aircraft/page.tsx index 910d607..465f5a2 100644 --- a/app/aircraft/page.tsx +++ b/app/aircraft/page.tsx @@ -11,9 +11,15 @@ export default function AircraftPage() { const [search, setSearch] = useState(''); const [isAddOpen, setIsAddOpen] = useState(false); const { data, isLoading, mutate } = useAircraftData({ q: search || undefined, page, limit: 25 }); - const aircraft = data?.items || (Array.isArray(data) ? data : []); - const total = data?.total || aircraft.length; - const pages = data?.pages || 1; + const aircraft = Array.isArray(data?.items) ? data.items : (Array.isArray(data) ? data : []); + const total = data?.total ?? aircraft.length; + const pages = data?.pages ?? 1; + + const typeLabel = (a: any) => { + const t = a?.aircraft_type; + if (!t) return '—'; + return [t.manufacturer, t.model].filter(Boolean).join(' ') || t.icao_code || '—'; + }; const handleAdd = async (d: any) => { try { await aircraftApi.create(d); mutate(); setIsAddOpen(false); } catch (e: any) { alert(e.message); } }; const handleDelete = async (id: string) => { if (!confirm('Удалить ВС?')) return; try { await aircraftApi.delete(id); mutate(); } catch (e: any) { alert(e.message); } }; @@ -31,19 +37,19 @@ export default function AircraftPage() {
- - + + {aircraft.map((a: any) => ( - - - - - - + + + + + +
РегистрацияТипМодельОператорНалёт (ч)СтатусРегистрацияТипОператорСерийный №Налёт (ч)Статус Действия
{a.registration_number || a.registrationNumber}{a.aircraft_type || a.aircraftType}{a.model || '—'}{a.operator || a.operator_name || '—'}{a.flight_hours || a.flightHours || '—'}{a.registration_number ?? a.registrationNumber ?? '—'}{typeLabel(a)}{a.operator_name ?? a.operator ?? '—'}{a.serial_number ?? '—'}{a.total_time ?? a.flight_hours ?? a.flightHours ?? '—'} diff --git a/app/checklists/page.tsx b/app/checklists/page.tsx index 314b863..4456258 100644 --- a/app/checklists/page.tsx +++ b/app/checklists/page.tsx @@ -10,7 +10,7 @@ import { RequireRole } from '@/lib/auth-context'; export default function ChecklistsPage() { const [domain, setDomain] = useState(); - const { data, isLoading, mutate } = useChecklistsData({ domain }); + const { data, isLoading, mutate } = useChecklistsData({ domain: domain === 'part_m_ru' ? undefined : domain }); const templates = data?.items || []; const [exp, setExp] = useState(null); const [editTemplate, setEditTemplate] = useState(null); @@ -18,7 +18,7 @@ export default function ChecklistsPage() { const gen = async (src: string) => { const n = prompt('Название:'); if (!n) return; await checklistsApi.generate(src, n); mutate(); }; return ( - @@ -26,14 +26,7 @@ export default function ChecklistsPage() { {!isLoading && templates.length > 0 ? (
diff --git a/app/documents/page.tsx b/app/documents/page.tsx index 13d33f9..d71ad89 100644 --- a/app/documents/page.tsx +++ b/app/documents/page.tsx @@ -7,7 +7,7 @@ export default function DocumentsPage() { { title: 'Входящие документы', desc: 'PDF и DOCX файлы', href: '/inbox', icon: '📥' }, { title: 'Вложения аудитов', desc: 'Фото и протоколы', href: '/audits', icon: '🔍' }, { title: 'Сертификаты', desc: 'Сертификаты ЛГ', href: '/airworthiness', icon: '📜' }, - { title: 'Нормативные документы', desc: 'ФАП, ICAO, EASA', href: '/regulations', icon: '📚' }, + { title: 'Нормативные документы', desc: 'Part-M RU · ФАП', href: '/regulations', icon: '📚' }, { title: 'Чек-листы', desc: 'Шаблоны проверок', href: '/checklists', icon: '✅' }, { title: 'Шаблоны документов', desc: 'Заявки, акты, письма, формы', href: '/templates', icon: '📋' }, ]; diff --git a/app/settings/page.tsx b/app/settings/page.tsx index d3f11b4..0f20a23 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -2,16 +2,21 @@ import { useState, useEffect } from 'react'; import { PageLayout } from '@/components/ui'; import { apiFetch } from '@/lib/api/api-client'; +import { useAuth } from '@/lib/auth-context'; + +const ROLE_LABELS: Record = { admin: 'Администратор', authority_inspector: 'Инспектор', operator_manager: 'Менеджер оператора', operator_user: 'Оператор', mro_manager: 'Менеджер ТОиР', mro_specialist: 'Специалист ТОиР', mro_user: 'Специалист ТОиР' }; export default function SettingsPage() { + const { user } = useAuth(); const [prefs, setPrefs] = useState(null); const [saving, setSaving] = useState(false); useEffect(() => { - apiFetch('/notification-preferences').then(setPrefs); + apiFetch('/notification-preferences').catch(() => null).then(setPrefs); }, []); const save = async () => { + if (!prefs) return; setSaving(true); await apiFetch('/notification-preferences', { method: 'PUT', body: JSON.stringify(prefs) }); setSaving(false); @@ -27,11 +32,49 @@ export default function SettingsPage() {
); - if (!prefs) return
; - return ( - +
+
+

👤 Профиль пользователя

+
+
Имя{user?.display_name ?? 'Dev User'}
+
Email{user?.email ?? 'dev@local'}
+
Роль{user?.role ? ROLE_LABELS[user.role] ?? user.role : 'Администратор'}
+
Организация{user?.organization_name ?? '—'}
+
+
+ +
+

🖥️ Настройки системы

+
+
Название системыREFLY АСУ ТК
+
Версия2.0.0-beta
+
Нормативная базаPart-M RU
+
ЯзыкРусский
+
Часовой поясEurope/Moscow (UTC+3)
+
+
+ +
+

🔗 Интеграции

+
+
AI-помощник (Claude)⚠️ Настройка
+
ФГИС ЕС ОрВДНе подключено
+
Keycloak SSOПодключено (dev)
+
MinIO (документы)Подключено
+
+
+ +
+

ℹ️ О системе

+

REFLY АСУ ТК v2.0.0-beta

+

Part-M RU · Гармонизировано с ICAO/EASA

+

© 2025–2026 REFLY Aviation Technologies

+
+ + {prefs && ( + <>

📢 Типы уведомлений

@@ -78,6 +121,8 @@ export default function SettingsPage() { className="btn-primary px-6 py-2 rounded text-sm disabled:opacity-50"> {saving ? '⏳ Сохранение...' : '💾 Сохранить настройки'} + + )}
); diff --git a/app/users/page.tsx b/app/users/page.tsx index cb78bdd..810d12e 100644 --- a/app/users/page.tsx +++ b/app/users/page.tsx @@ -4,8 +4,8 @@ import UserEditModal from '@/components/UserEditModal'; import { PageLayout, DataTable, FilterBar, StatusBadge } from '@/components/ui'; import { useUsersData } from '@/hooks/useSWRData'; -const RL: Record = { admin:'Администратор', authority_inspector:'Инспектор', operator_manager:'Менеджер оператора', operator_user:'Оператор', mro_manager:'Менеджер ТОиР', mro_user:'Специалист ТОиР' }; -const RC: Record = { admin:'bg-green-500', authority_inspector:'bg-blue-500', operator_manager:'bg-orange-500', operator_user:'bg-orange-400', mro_manager:'bg-purple-500', mro_user:'bg-purple-400' }; +const RL: Record = { admin:'Администратор', authority_inspector:'Инспектор', operator_manager:'Менеджер оператора', operator_user:'Оператор', mro_manager:'Менеджер ТОиР', mro_specialist:'Специалист ТОиР', mro_user:'Специалист ТОиР' }; +const RC: Record = { admin:'bg-green-500', authority_inspector:'bg-blue-500', operator_manager:'bg-orange-500', operator_user:'bg-orange-400', mro_manager:'bg-purple-500', mro_specialist:'bg-purple-400', mro_user:'bg-purple-400' }; export default function UsersPage() { const [rf, setRf] = useState(); @@ -14,7 +14,7 @@ export default function UsersPage() { return ( ({ value: v, label: l }))]} className="mb-4" /> - {u.display_name} }, { key: 'email', header: 'Email', render: (u: any) => {u.email || '—'} }, diff --git a/backend/app/api/routes/aircraft.py b/backend/app/api/routes/aircraft.py index 840c9aa..c534c82 100644 --- a/backend/app/api/routes/aircraft.py +++ b/backend/app/api/routes/aircraft.py @@ -28,11 +28,17 @@ def _serialize_aircraft(a: Aircraft, db: Session) -> AircraftOut: return AircraftOut.model_validate({ "id": a.id, "registration_number": a.registration_number, - "aircraft_type": { - "id": a.aircraft_type.id, "manufacturer": a.aircraft_type.manufacturer, - "model": a.aircraft_type.model, "created_at": a.aircraft_type.created_at, - "updated_at": a.aircraft_type.updated_at, - } if a.aircraft_type else None, + "aircraft_type": ( + { + "id": a.aircraft_type.id, + "manufacturer": a.aircraft_type.manufacturer, + "model": a.aircraft_type.model, + "created_at": a.aircraft_type.created_at, + "updated_at": a.aircraft_type.updated_at, + } + if a.aircraft_type + else None + ), "operator_id": a.operator_id, "operator_name": operator_name, "serial_number": a.serial_number, "manufacture_date": getattr(a, 'manufacture_date', None), @@ -88,7 +94,7 @@ def list_aircraft( items = [] for a in items_raw: try: - if a.aircraft_type: items.append(_serialize_aircraft(a, db)) + items.append(_serialize_aircraft(a, db)) except Exception as e: logger.error(f"Serialization error for aircraft {a.id}: {e}") return {"items": items, "total": total, "page": page, "per_page": per_page, diff --git a/backend/app/api/routes/airworthiness_core.py b/backend/app/api/routes/airworthiness_core.py index f803341..cbfdf38 100644 --- a/backend/app/api/routes/airworthiness_core.py +++ b/backend/app/api/routes/airworthiness_core.py @@ -34,6 +34,37 @@ _maint_programs: dict = {} _components: dict = {} +def seed_airworthiness_core_demo(aircraft_id: Optional[str] = None) -> None: + """Заполнить демо-данными ДЛГ и Life Limits при первом запуске (если пусто).""" + if _directives: + return + now = datetime.now(timezone.utc).isoformat() + demo_ads = [ + {"number": "AD-2025-0142-R1", "title": "Замена болтов крепления двигателя", "status": "open", "compliance_deadline": "2026-04-15", "aircraft_types": ["B737", "SSJ100"], "effective_date": "2025-06-01", "compliance_type": "mandatory"}, + {"number": "AD-2025-0089", "title": "Инспекция лонжерона крыла зона 3", "status": "complied", "compliance_date": "2025-11-20", "aircraft_types": ["B737"], "effective_date": "2025-03-01", "compliance_type": "mandatory"}, + {"number": "AD-2026-0012", "title": "Модификация системы предупреждения TCAS", "status": "open", "compliance_deadline": "2026-06-01", "aircraft_types": ["B737", "A320"], "effective_date": "2026-01-15", "compliance_type": "mandatory"}, + {"number": "AD-2025-0201", "title": "Замена топливного насоса", "status": "complied", "compliance_date": "2026-01-15", "aircraft_types": ["SSJ100"], "effective_date": "2025-08-01", "compliance_type": "mandatory"}, + {"number": "AD-2025-0178", "title": "Инспекция фюзеляжа секции 41-43", "status": "open", "compliance_deadline": "2026-03-30", "aircraft_types": ["B737"], "effective_date": "2025-07-01", "compliance_type": "mandatory"}, + {"number": "AD-2024-0315-R2", "title": "Обновление ПО FADEC двигателя", "status": "overdue", "compliance_deadline": "2025-12-01", "aircraft_types": ["CFM56"], "effective_date": "2024-10-01", "compliance_type": "mandatory"}, + ] + for d in demo_ads: + did = str(uuid.uuid4()) + _directives[did] = {"id": did, "created_at": now, "issuing_authority": "FATA", "ata_chapter": None, "repetitive": False, "description": "", "affected_parts": [], **d} + logger.info("seed_airworthiness_core: %s directives", len(demo_ads)) + + if not _life_limits and aircraft_id: + demo_ll = [ + {"component_name": "Двигатель CFM56-5B", "part_number": "CFM-56-5B", "serial_number": "SN-001", "limit_type": "combined", "flight_hours_limit": 30000.0, "cycles_limit": 20000, "current_hours": 24580.0, "current_cycles": 15200, "aircraft_id": aircraft_id}, + {"component_name": "Шасси основное левое", "part_number": "LG-32-001", "serial_number": "SN-102", "limit_type": "cycles", "cycles_limit": 40000, "current_cycles": 38800, "aircraft_id": aircraft_id}, + {"component_name": "APU GTCP131-9A", "part_number": "APU-131-9A", "serial_number": "SN-201", "limit_type": "flight_hours", "flight_hours_limit": 15000.0, "current_hours": 6500.0, "aircraft_id": aircraft_id}, + {"component_name": "Лопатки турбины ВД", "part_number": "HPT-7680", "serial_number": "SN-305", "limit_type": "cycles", "cycles_limit": 8000, "current_cycles": 7680, "aircraft_id": aircraft_id}, + ] + for ll in demo_ll: + lid = str(uuid.uuid4()) + _life_limits[lid] = {"id": lid, "created_at": now, "calendar_limit_months": None, "install_date": None, "last_overhaul_date": None, "notes": None, **ll} + logger.info("seed_airworthiness_core: %s life limits", len(demo_ll)) + + # =================================================================== # 1. ДИРЕКТИВЫ ЛЁТНОЙ ГОДНОСТИ (AD / ДЛГ) # =================================================================== diff --git a/backend/app/demo/seed_document_templates.py b/backend/app/demo/seed_document_templates.py index 27c71fa..a525dba 100644 --- a/backend/app/demo/seed_document_templates.py +++ b/backend/app/demo/seed_document_templates.py @@ -544,7 +544,15 @@ def _templates_data() -> list[dict]: def seed_document_templates(): db = SessionLocal() try: + # Удалить шаблоны ICAO / EASA / FAA — оставляем только российские (Part-M RU, ФАП) + deleted = db.query(DocumentTemplate).filter(DocumentTemplate.standard.in_(["EASA", "FAA", "ICAO"])).delete(synchronize_session=False) + if deleted: + logger.info("Removed %s non-Russian document templates (EASA/FAA/ICAO)", deleted) + db.commit() + for d in _templates_data(): + if d.get("standard") in ("EASA", "FAA", "ICAO"): + continue if db.query(DocumentTemplate).filter(DocumentTemplate.code == d["code"]).first(): continue db.add( diff --git a/backend/app/demo/seed_full_demo.py b/backend/app/demo/seed_full_demo.py index 9e02324..ca3134b 100644 --- a/backend/app/demo/seed_full_demo.py +++ b/backend/app/demo/seed_full_demo.py @@ -15,6 +15,7 @@ from app.models import ( CertApplication, CertApplicationStatus, RiskAlert, + AirworthinessCertificate, ) from app.models.aircraft_db import Aircraft from app.models.personnel_plg import PLGSpecialist, PLGQualification @@ -212,9 +213,11 @@ def seed_full_demo(): if plg_org: specialists_data = [ ("Петрова Елена В.", "ПЕТРОВА-001", "Инспектор ЛГ", "I", "LIC-2024-001", "2026-12-01"), - ("Волков Алексей Н.", "ВОЛКОВ-001", "Инженер ТОиР", "B2", "LIC-2024-002", "2026-08-15"), + ("Волков Алексей Н.", "ВОЛКОВ-001", "Инженер по ТО", "B2", "LIC-2024-002", "2026-08-15"), ("Морозова Ольга С.", "МОРОЗОВА-001", "Авиатехник B1", "B1", "LIC-2024-003", "2026-03-20"), - ("Козлов Дмитрий И.", "КОЗЛОВ-001", "Пилот CAT.A", "CAT-A", "LIC-2024-004", "2027-01-10"), + ("Козлов Дмитрий И.", "КОЗЛОВ-001", "Инженер CAT.A", "CAT-A", "LIC-2024-004", "2027-01-10"), + ("Николаев Павел Р.", "НИКОЛАЕВ-001", "Инспектор Росавиации", "I", "LIC-2024-005", "2026-09-30"), + ("Сидорова Анна М.", "СИДОРОВА-001", "Диспетчер ТОиР", "D", "LIC-2024-006", "2026-11-15"), ] for full_name, personnel_number, position, category, license_no, expires in specialists_data: if db.query(PLGSpecialist).filter(PLGSpecialist.personnel_number == personnel_number).first(): @@ -248,6 +251,69 @@ def seed_full_demo(): db.commit() logger.info("seed_full_demo: personnel PLG checked/created") + # ─── 7. Сертификаты лётной годности (СЛГ) ───────────────────────── + issuer = db.query(User).filter(User.role.in_(["admin", "authority_inspector"])).first() + issuer_id = str(issuer.id) if issuer else None + certs_data = [ + ("KLG-2025-001", "RA-89060", "2026-08-15", "valid"), + ("KLG-2025-002", "RA-89061", "2026-05-20", "valid"), + ("KLG-2024-015", "RA-73801", "2026-02-28", "expiring_soon"), + ("KLG-2025-003", "RA-12345", "2027-01-10", "valid"), + ] + for cert_num, reg, valid_until, status in certs_data: + if db.query(AirworthinessCertificate).filter(AirworthinessCertificate.certificate_number == cert_num).first(): + continue + ac_id = _get_aircraft_id_by_reg(db, reg) + if not ac_id: + ac_id = _get_first_aircraft_id(db) + if not ac_id or not issuer_id: + continue + exp_d = datetime.strptime(valid_until, "%Y-%m-%d").date() + issue_d = exp_d - timedelta(days=365) + db.add( + AirworthinessCertificate( + aircraft_id=ac_id, + certificate_number=cert_num, + certificate_type="standard", + issue_date=datetime(issue_d.year, issue_d.month, issue_d.day, tzinfo=timezone.utc), + expiry_date=datetime(exp_d.year, exp_d.month, exp_d.day, tzinfo=timezone.utc), + issuing_authority="Росавиация", + issued_by_user_id=issuer_id, + status=status, + ) + ) + db.commit() + logger.info("seed_full_demo: airworthiness certificates checked/created") + + # ─── 8. Наряды на работу (Work Orders, in-memory) ────────────────── + try: + from app.api.routes.work_orders import _work_orders + aircraft_regs = [r[0] for r in db.query(Aircraft.registration_number).limit(5).all()] + reg1 = aircraft_regs[0] if aircraft_regs else "RA-89060" + reg2 = aircraft_regs[1] if len(aircraft_regs) > 1 else "RA-89061" + reg3 = aircraft_regs[2] if len(aircraft_regs) > 2 else "RA-73801" + reg4 = aircraft_regs[3] if len(aircraft_regs) > 3 else "RA-12345" + wos_demo = [ + ("WO-2026-001", reg1, "Периодическое ТО A-Check", "in_progress", "urgent"), + ("WO-2026-002", reg2, "Замена колеса основной стойки", "open", "normal"), + ("WO-2026-003", reg3, "Устранение течи гидросистемы", "completed", "urgent"), + ("WO-2026-004", reg1, "Плановая замена фильтров двигателя", "open", "normal"), + ("WO-2026-005", reg4, "Внеплановое ТО после bird strike", "in_progress", "urgent"), + ] + for wo_num, reg, title, status, priority in wos_demo: + if any(w.get("wo_number") == wo_num for w in _work_orders.values()): + continue + wid = str(uuid.uuid4()) + _work_orders[wid] = { + "id": wid, "wo_number": wo_num, "aircraft_reg": reg, "title": title, + "wo_type": "scheduled" if "Планов" in title else "unscheduled", + "description": title, "status": status, "priority": priority, + "created_at": datetime.now(timezone.utc).isoformat(), + } + logger.info("seed_full_demo: work orders (in-memory) populated") + except Exception as e: + logger.warning("seed_full_demo: work orders skip %s", e) + except Exception as e: db.rollback() logger.exception("seed_full_demo failed: %s", e) diff --git a/backend/app/main.py b/backend/app/main.py index 8c69f4b..921e976 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -78,6 +78,18 @@ async def lifespan(app: FastAPI): except Exception as e: import logging logging.getLogger(__name__).warning("Full demo seed skipped: %s", e) + try: + from app.db.session import SessionLocal + from app.models.aircraft_db import Aircraft + from app.api.routes.airworthiness_core import seed_airworthiness_core_demo + db = SessionLocal() + ac = db.query(Aircraft).filter(Aircraft.registration_number == "RA-89060").first() or db.query(Aircraft).first() + aircraft_id = str(ac.id) if ac else None + db.close() + seed_airworthiness_core_demo(aircraft_id) + except Exception as e: + import logging + logging.getLogger(__name__).warning("Airworthiness core demo seed skipped: %s", e) # Планировщик рисков (передаём app для shutdown hook) setup_scheduler(app) yield diff --git a/backend/app/schemas/aircraft.py b/backend/app/schemas/aircraft.py index f54d6c3..c1bc3dc 100644 --- a/backend/app/schemas/aircraft.py +++ b/backend/app/schemas/aircraft.py @@ -35,7 +35,7 @@ class AircraftUpdate(BaseModel): class AircraftOut(TimestampOut): id: str registration_number: str - aircraft_type: AircraftTypeOut + aircraft_type: AircraftTypeOut | None = None operator_id: str operator_name: str | None = None # Название организации-оператора serial_number: str | None = None # Серийный номер ВС diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 45e3f9e..de29db5 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -15,6 +15,8 @@ import { sidebarIcons, commonIcons } from '@/icons/refly-icons'; import type { SidebarKey } from '@/icons/refly-icons'; import { Icon } from '@/components/Icon'; +const HEADER_ICON_KEY: SidebarKey = 'aircraft'; + interface MenuItem { name: string; path: string; iconKey: SidebarKey; roles?: UserRole[]; } const menuItems: MenuItem[] = [ @@ -73,7 +75,9 @@ export default function Sidebar() { {/* Header */}
-
✈️
+
+ +
REFLY
КОНТРОЛЬ ЛЁТНОЙ ГОДНОСТИ
@@ -92,7 +96,7 @@ export default function Sidebar() { {visibleItems.map((item) => { const active = pathname === item.path; return ( - setMobileOpen(false)} className={`flex items-center px-6 py-3 text-white no-underline transition-colors ${active ? 'bg-white/[0.15] border-l-[3px] border-accent-blue' : 'border-l-[3px] border-transparent hover:bg-white/[0.07]'}`}> diff --git a/components/ui/StatusBadge.tsx b/components/ui/StatusBadge.tsx index aad6c43..9dca8e1 100644 --- a/components/ui/StatusBadge.tsx +++ b/components/ui/StatusBadge.tsx @@ -1,9 +1,13 @@ 'use client'; +import { statusIcons, type StatusKey } from '@/icons/refly-icons'; +import { Icon } from '@/components/Icon'; + interface Props { status: string; colorMap?: Record; labelMap?: Record; + showIcon?: boolean; } const defaults: Record = { @@ -17,8 +21,16 @@ const defaults: Record = { overdue: 'bg-red-600', deferred: 'bg-yellow-600', }; -export default function StatusBadge({ status, colorMap, labelMap }: Props) { +export default function StatusBadge({ status, colorMap, labelMap, showIcon = true }: Props) { const color = colorMap?.[status] || defaults[status] || 'bg-gray-400'; const label = labelMap?.[status] || status; - return {label}; + const StatusIcon = statusIcons[status as StatusKey]; + return ( + + {showIcon && StatusIcon && ( + + )} + {label} + + ); } diff --git a/docs/ops/AI_PROXY_SETUP.md b/docs/ops/AI_PROXY_SETUP.md new file mode 100644 index 0000000..1aef22f --- /dev/null +++ b/docs/ops/AI_PROXY_SETUP.md @@ -0,0 +1,48 @@ +# Настройка прокси Anthropic через papa-app (Railway) + +KLG backend в Yandex Cloud (РФ) не может вызывать Anthropic API из-за блокировки по IP. +Запросы идут через papa-app на Railway (вне РФ). + +## Что сделано в коде + +- **papa-app:** `POST /api/proxy/anthropic` — принимает тело запроса и заголовок `x-proxy-secret`, подставляет `ANTHROPIC_API_KEY` и вызывает `https://api.anthropic.com/v1/messages`. +- **KLG backend:** при заданных `AI_PROXY_URL` и `AI_PROXY_SECRET` использует прокси вместо прямого вызова Anthropic. + +## 1. Railway (papa-app) + +В проекте papa-app в Railway Dashboard → Variables: + +| Переменная | Значение | Секретность | +|------------|----------|-------------| +| `ANTHROPIC_API_KEY` | Ваш ключ Anthropic (sk-ant-...) | Secret | +| `PROXY_SECRET` | `klg-refly-proxy-2026` (или свой) | Secret | + +После сохранения Railway пересоберёт и задеплоит приложение. + +## 2. Сервер KLG (158.160.22.166) + +```bash +cd ~/klg-asutk-app +git pull origin main + +# Добавить в .env (если ещё не добавлено) +grep -q AI_PROXY_URL .env || echo 'AI_PROXY_URL=https://papa-app-production.up.railway.app/api/proxy/anthropic' >> .env +grep -q AI_PROXY_SECRET .env || echo 'AI_PROXY_SECRET=klg-refly-proxy-2026' >> .env + +docker compose build backend && docker compose up -d backend +``` + +Значение `AI_PROXY_SECRET` должно совпадать с `PROXY_SECRET` в Railway. + +## 3. Проверка + +```bash +# Прокси (после деплоя papa-app и установки переменных) +curl -s -X POST https://papa-app-production.up.railway.app/api/proxy/anthropic \ + -H "Content-Type: application/json" \ + -H "x-proxy-secret: klg-refly-proxy-2026" \ + -d '{"model":"claude-3-5-sonnet-20241022","max_tokens":50,"messages":[{"role":"user","content":"Hi"}]}' +``` + +Ожидается JSON с полем `content` (ответ модели) или сообщение об ошибке от Anthropic. +401 — неверный `x-proxy-secret`, 500 — не задан `ANTHROPIC_API_KEY` в Railway. diff --git a/icons/refly-icons.tsx b/icons/refly-icons.tsx index ee3968b..664c386 100644 --- a/icons/refly-icons.tsx +++ b/icons/refly-icons.tsx @@ -36,7 +36,6 @@ import { XCircle, MessageSquareWarning, Clock, - ClockAlert, BadgeCheck, PlaneOff, CircleDot, @@ -139,6 +138,66 @@ export const ReflyRegulator = (props: ReflyIconProps) => ( ); +/** Aircraft (ВС) */ +export const ReflyAircraft = (props: ReflyIconProps) => ( + + + + + + + +); + +/** Audits: shield + magnifier */ +export const ReflyAudits = (props: ReflyIconProps) => ( + + + + + +); + +/** Documents */ +export const ReflyDocuments = (props: ReflyIconProps) => ( + + + + + + + +); + +/** Defects */ +export const ReflyDefects = (props: ReflyIconProps) => ( + + + + + + + + +); + +/** Risks */ +export const ReflyRisks = (props: ReflyIconProps) => ( + + + + + +); + +/** Maintenance */ +export const ReflyMaintenance = (props: ReflyIconProps) => ( + + + + +); + /* ----------------------- ICON MAPS ----------------------- */ @@ -175,19 +234,19 @@ export type SidebarKey = export const sidebarIcons: Record> = { dashboard: LayoutDashboard, organizations: Building2, - aircraft: Plane, + aircraft: ReflyAircraft, applications: ClipboardList, checklists: CheckSquare, - audits: SearchCheck, - risks: AlertTriangle, + audits: ReflyAudits, + risks: ReflyRisks, users: Users, airworthiness: ReflyAirworthiness, calendar: Calendar, "airworthiness-core": ReflyContinuedAirworthiness, - maintenance: Wrench, - defects: Bug, + maintenance: ReflyMaintenance, + defects: ReflyDefects, modifications: GitBranch, - documents: FileText, + documents: ReflyDocuments, inbox: Inbox, regulations: BookOpen, monitoring: Activity, @@ -232,7 +291,7 @@ export const statusIcons: Record