From 64991f02286a547767dcd43fec6ec78a7cae07f2 Mon Sep 17 00:00:00 2001 From: Sergey Korotonozhko <_serezhka_@mail.ru> Date: Wed, 20 May 2026 20:08:25 +0300 Subject: [PATCH] v1.4.0: fix #43, #31, #29, #49, #33 + shared templating --- py_service/CHANGELOG.md | 120 ++ .../app/__pycache__/auth.cpython-38.pyc | Bin 0 -> 3114 bytes .../app/__pycache__/config.cpython-38.pyc | Bin 0 -> 1303 bytes .../documents_router.cpython-38.pyc | Bin 0 -> 7946 bytes py_service/app/auth.py | 8 +- py_service/app/config.py | 3 +- py_service/app/dependencies.py | 16 + py_service/app/documents_router.py | 282 ++++ py_service/app/main.py | 125 +- py_service/app/models/models.py | 32 + .../__pycache__/service_pages.cpython-38.pyc | Bin 0 -> 47362 bytes py_service/app/routers/service_pages.py | 1081 +++++++++++++++ .../docs_permissions.cpython-38.pyc | Bin 0 -> 2779 bytes py_service/app/services/docs_permissions.py | 34 +- py_service/app/static/css/service-theme.css | 168 +++ py_service/app/static/css/service.css | 1234 +++++++++++++++++ py_service/app/static/img/aegisone_logo.svg | 3 + py_service/app/static/img/favicon.svg | 1 + .../img/favicons/android-chrome-192x192.png | Bin 0 -> 31700 bytes .../static/img/favicons/apple-touch-icon.png | Bin 0 -> 29337 bytes .../app/static/img/favicons/favicon-16x16.png | Bin 0 -> 1318 bytes .../app/static/img/favicons/favicon-32x32.png | Bin 0 -> 2380 bytes .../app/static/img/favicons/favicon.ico | Bin 0 -> 9662 bytes .../app/static/img/favicons/logo_service.png | Bin 0 -> 115315 bytes .../app/static/img/favicons/site.webmanifest | 1 + py_service/app/static/js/theme.js | 47 + py_service/app/templates/base.html | 24 +- py_service/app/templates/dashboard.html | 146 +- py_service/app/templates/documents/edit.html | 29 + py_service/app/templates/documents/list.html | 30 + .../app/templates/documents/permissions.html | 39 + .../app/templates/documents/tech_access.html | 36 + py_service/app/templates/documents/view.html | 16 + py_service/app/templates/login.html | 4 +- py_service/app/templates/page.html | 165 +++ .../app/templates/pages/assignments.html | 31 + py_service/app/templates/pages/blog.html | 50 + py_service/app/templates/pages/cases.html | 47 + py_service/app/templates/pages/ceo.html | 45 + py_service/app/templates/pages/charts.html | 158 +++ py_service/app/templates/pages/checklist.html | 32 + .../app/templates/pages/coefficients.html | 107 ++ py_service/app/templates/pages/customers.html | 90 ++ py_service/app/templates/pages/docs.html | 14 + .../app/templates/pages/docs_manage.html | 17 + py_service/app/templates/pages/docs_view.html | 8 + .../app/templates/pages/formula_test.html | 145 ++ py_service/app/templates/pages/ideas.html | 55 + py_service/app/templates/pages/incidents.html | 55 + py_service/app/templates/pages/objects.html | 67 + py_service/app/templates/pages/passports.html | 16 + .../app/templates/pages/questionnaire.html | 16 + .../templates/pages/questionnaire_config.html | 59 + .../templates/pages/questionnaire_step.html | 53 + py_service/app/templates/pages/quickmenu.html | 38 + py_service/app/templates/pages/reports.html | 37 + py_service/app/templates/pages/sla.html | 54 + py_service/app/templates/pages/tasks.html | 46 + py_service/app/templates/pages/users.html | 67 + py_service/app/templating.py | 10 + py_service/app/version.py | 15 + py_service/scripts/bump_version.py | 73 + py_service/version.txt | 1 + 63 files changed, 5005 insertions(+), 45 deletions(-) create mode 100644 py_service/CHANGELOG.md create mode 100644 py_service/app/__pycache__/auth.cpython-38.pyc create mode 100644 py_service/app/__pycache__/config.cpython-38.pyc create mode 100644 py_service/app/__pycache__/documents_router.cpython-38.pyc create mode 100644 py_service/app/dependencies.py create mode 100644 py_service/app/documents_router.py create mode 100644 py_service/app/routers/__pycache__/service_pages.cpython-38.pyc create mode 100644 py_service/app/routers/service_pages.py create mode 100644 py_service/app/services/__pycache__/docs_permissions.cpython-38.pyc create mode 100644 py_service/app/static/css/service-theme.css create mode 100644 py_service/app/static/css/service.css create mode 100644 py_service/app/static/img/aegisone_logo.svg create mode 100644 py_service/app/static/img/favicon.svg create mode 100644 py_service/app/static/img/favicons/android-chrome-192x192.png create mode 100644 py_service/app/static/img/favicons/apple-touch-icon.png create mode 100644 py_service/app/static/img/favicons/favicon-16x16.png create mode 100644 py_service/app/static/img/favicons/favicon-32x32.png create mode 100644 py_service/app/static/img/favicons/favicon.ico create mode 100644 py_service/app/static/img/favicons/logo_service.png create mode 100644 py_service/app/static/img/favicons/site.webmanifest create mode 100644 py_service/app/static/js/theme.js create mode 100644 py_service/app/templates/documents/edit.html create mode 100644 py_service/app/templates/documents/list.html create mode 100644 py_service/app/templates/documents/permissions.html create mode 100644 py_service/app/templates/documents/tech_access.html create mode 100644 py_service/app/templates/documents/view.html create mode 100644 py_service/app/templates/page.html create mode 100644 py_service/app/templates/pages/assignments.html create mode 100644 py_service/app/templates/pages/blog.html create mode 100644 py_service/app/templates/pages/cases.html create mode 100644 py_service/app/templates/pages/ceo.html create mode 100644 py_service/app/templates/pages/charts.html create mode 100644 py_service/app/templates/pages/checklist.html create mode 100644 py_service/app/templates/pages/coefficients.html create mode 100644 py_service/app/templates/pages/customers.html create mode 100644 py_service/app/templates/pages/docs.html create mode 100644 py_service/app/templates/pages/docs_manage.html create mode 100644 py_service/app/templates/pages/docs_view.html create mode 100644 py_service/app/templates/pages/formula_test.html create mode 100644 py_service/app/templates/pages/ideas.html create mode 100644 py_service/app/templates/pages/incidents.html create mode 100644 py_service/app/templates/pages/objects.html create mode 100644 py_service/app/templates/pages/passports.html create mode 100644 py_service/app/templates/pages/questionnaire.html create mode 100644 py_service/app/templates/pages/questionnaire_config.html create mode 100644 py_service/app/templates/pages/questionnaire_step.html create mode 100644 py_service/app/templates/pages/quickmenu.html create mode 100644 py_service/app/templates/pages/reports.html create mode 100644 py_service/app/templates/pages/sla.html create mode 100644 py_service/app/templates/pages/tasks.html create mode 100644 py_service/app/templates/pages/users.html create mode 100644 py_service/app/templating.py create mode 100644 py_service/app/version.py create mode 100644 py_service/scripts/bump_version.py create mode 100644 py_service/version.txt diff --git a/py_service/CHANGELOG.md b/py_service/CHANGELOG.md new file mode 100644 index 0000000..5695508 --- /dev/null +++ b/py_service/CHANGELOG.md @@ -0,0 +1,120 @@ +# Changelog — AegisOne Service Portal + +## 1.4.0 (20.05.2026) + +## 1.3.0 (20.05.2026) +### Новые функции +- П.49: Подключены favicon (ico, png, webmanifest, apple-touch-icon) + лого на странице логина +- П.44: Сохранение scroll сайдбара при навигации (sessionStorage) +- П.47: Липкий тулбар редактора документов (верх/низ) +- П.33: Кастомный модал подтверждения выхода из опросника (вместо системного confirm) +- П.33: Добавлен POST /service/questionnaire/save/{step} — сохранение черновика +### Исправления и улучшения +- П.35: Версия ПО отображается на всех страницах, не только дашборде +- П.40: Звёзды избранного выровнены в одну строку с пунктами меню +- П.42: discover_docs() вызывается при каждом открытии документа — новые .md сразу видны +- П.43: POST /admin/permissions использует discover_docs вместо load_permissions +- П.45: Футер сайдбара: имя → выход → версия +- П.46: Иконки EasyMDE видны на тёмной теме ([data-theme="dark"] overrides) +- П.48: Создание "Пример из практики" — добавлен action="/service/cases/create" +- П.29: SLA — action="/service/sla/create" у формы + safe type conversions в edit +- П.31: Идеи — защита от None description ((idea.description or '')[:100]) +- П.49: Назначения — a.object_name → a.object.name, a.user_name → a.user.full_name +- П.26: Заголовок "Назначения" → "Назначение сотрудников" +- П.24: Объекты — список клиентов в select (c.id/c.name вместо c[0]/c[1]) +- П.24: Объекты — editObject защита от None customer_id + safe int() в backend +- П.23: Валидация клиентов: * у обязательных полей, KPP (9 цифр), маска телефона "8 (___) ___-__-__" +- П.36: Portainer URL https://81.177.141.34:9443 + +## 1.2.1 (20.05.2026) +### Новые функции +- П.38: Стилизация логина — "One" цветом #00ADEF +- П.45: Футер сайдбара: имя → выход → версия +- П.44: Сохранение scroll сайдбара при навигации (sessionStorage) +- П.47: Липкий тулбар редактора документов +- П.49: Подключены favicon (ico, png, webmanifest, apple-touch-icon) +### Исправления и улучшения +- П.35: Версия ПО отображается на всех страницах, не только дашборде +- П.40: Звёзды избранного выровнены в одну строку с пунктами меню +- П.42: discover_docs() вызывается при каждом открытии документа — новые .md сразу видны +- П.43: Управление документами — POST использует discover_docs вместо load_permissions +- П.46: Иконки EasyMDE видны на тёмной теме +- П.48: Создание "Пример из практики" — добавлен action="/service/cases/create" +- П.29: SLA — добавлен action="/service/sla/create" у формы, safe type conversions +- П.31: Идеи — защита от None description (|default('')) +- П.49: Назначения — a.object_name → a.object.name, a.user_name → a.user.full_name +- П.26: Заголовок "Назначения" → "Назначение сотрудников" +- П.24: Объекты — список клиентов в select (c.id/c.name вместо c[0]/c[1]) +- П.36: Portainer URL через https://81.177.141.34:9443 +- П.33: Опросник — добавлен POST /service/questionnaire/save/{step}, кастомный модал + +## 1.2.0 (20.05.2026) +### Доработки +- П.21: Кнопка "Настроить" на дашборде — выбор отображаемых показателей (localStorage) +- П.22: Исправлена ошибка Internal Server Error при редактировании пользователя (добавлен action у формы) +- П.23: Исправлена ошибка Method Not Allowed при создании клиента; ИНН обязателен (10/12 цифр) +- П.24: Тип объекта переведён на русский (select вместо input) +- П.25: Исправлена ошибка Method Not Allowed при создании/редактировании объекта +- П.26: "Назначения" → "Назначение сотрудников" +- П.27: Статус SLA по умолчанию — "Переговоры" (negotiation) +- П.28: Уровни SLA отображаются на русском: Базовый/Оптимальный/Максимальный +- П.29: Проверена работа создания и редактирования SLA контрактов +- П.30: Расширены графики: группировка (день/неделя/месяц), название, сохранение/экспорт PNG +- П.31: Исправлена ошибка Internal Server Error при смене статуса идеи +- П.32: Быстрое меню заменено на звёздочки (★) — избранное в localStorage +- П.33: Кнопка "Выйти" в опроснике с подтверждением сохранения черновика +- П.34: Убрана надпись "Главная" под каждым меню +- П.35: Версия показывается полностью только на дашборде, в остальных — "v" +- П.36: Ссылка Portainer изменена на IP:порт (через PORTAINER_URL) +- П.37: Документация: левое меню (page.html), авто-загрузка .md из /py_service/docs + +## 1.1.0 (18.05.2026) +### Баги +- discover_docs: миграция существующих permissions без technician — добавляется technician: {view: false} + engineer: {edit: false, cancel: false} +- favicon: исправлен тип (image/x-icon → image/svg+xml) и расширение (favicon.ico → favicon.svg) +### Доработки +- П.15: Визуальная система формул — коэффициенты сгруппированы по formula_ref в аккордеоны +- П.15: Информация о формуле (📐) — модалка с текстом формулы и описанием результата +- П.15: Песочница формул (/service/coefficients/test) — подстановка параметров и расчёт для 7 формул: + risk_score, complexity, object_index, engineer, ecs, shs, ceo_shs +- П.15: Иконка 🧪 у каждой группы ведёт в песочницу с предвыбором группы +- П.15: В сайдбар добавлена ссылка "Тест формул" (owner) +- CSS: добавлены стили аккордеона и панели теста формул + +## 1.0.3 (18.05.2026) +### Баги +- П.18: Ченджлог не открывался — CHANGELOG.md исключался .dockerignore (*.md убран) +- П.20: Ошибка при сохранении пользователя — исправлена логика is_active (без дефолта), phone/email не сбрасываются +### Доработки +- П.12: Авто-добавление документов из /py_service/docs через discover_docs() в lifespan +- П.13: Владелец редактирует .md файлы через /service/documents/{slug}/edit (EasyMDE) +- П.14: Система прав документов: + - Владелец видит/создаёт/редактирует/удаляет все документы + - Владелец назначает engineer'у просмотр/редактирование через /service/documents/admin/permissions + - Engineer видит только назначенные документы + - Engineer даёт technician'у просмотр своих документов через /service/documents/tech-access + - Technician видит только те документы, что открыл engineer +- П.15: Удаление коэффициентов (кнопка ✕); предложен редизайн с группировкой по formula_ref +- П.16: Гибкие графики для владельца (/service/charts) — Chart.js: задачи/SHS/KPI/MRR/объекты, типы: bar/line/pie/doughnut +- П.17: "Service Portal" увеличен с 10px до 12px +- П.19: Роль убрана из футера сайдбара, "Выйти" под ФИО +- Удалены дублирующие ручки /service/docs* (старая система), везде /service/documents/ + +## 1.0.2 (18.05.2026) +### Доработки +- П.3: Логотип AegisOne в sidebare (sidebar-logo-text, 2rem) +- П.5: Версия кликабельна — модалка с историей изменений (changelog) +- П.6: Текст логотипа увеличен (sidebar-logo-text, 2rem) +- П.7: Верхнее меню упрощено — только логотип и кнопка темы +- П.8: Быстрое меню — настройка своих и техниковских ссылок (owner/engineer) +- П.9: Система идей — CRUD + статусы (normal/in_progress/completed) +- П.11: Ссылки на Portainer и Gitea в левом меню (owner) + +## 1.0.1 (18.05.2026) + +## 1.0.0 +### Новые функции +- Первый запуск сервисного портала на FastAPI +- Базовые страницы: логин, дашборд, документация +- Авторизация (owner/engineer/technician) +- Дашборд с основной статистикой (объекты, задачи, инциденты, SLA, SHS) diff --git a/py_service/app/__pycache__/auth.cpython-38.pyc b/py_service/app/__pycache__/auth.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d50a770dbc4c45dbcc5f42be3aa6d84aeb40450e GIT binary patch literal 3114 zcmZuz+ix6K8J{z^y?A$RuN~L9G~FU7EL7%FE&?G`r%BXG)J3(Uv?EKS$(*zC*t3_O zIUCz(Bhi=wQi>2f8`Y~m^{s@2hPFu`c;#XCbzXV$e}E7We%~3dZ(&B~_MOXj`+eUz z*Q(XBfp7PR6YK9x7{)(naQt&Ico`-6Cn|1m78_pPGLPx8>6vKF*lJsz#c0lo?Y84N zx^KsByWkbtMX#vGPF!l2y>ffPn`l?O3TUoZJEpLTCXokgWYyQi|^VxLC}r2YFiao<9PoyA^|B%58y zGgk`Lmz|Ii!-J3d4$3PiNduKK_SnGKGl$v}@0n+bVhxRG zBb3`c<+*D?vew(oT+s=;TzG|>qLT!%%3Lmp&}0(sH$F0C9nHVKbNNTBZwDPNwk}7> z#%dBN;RoVolyo~{wYTju7`&O>wAkaRqboJ8Xl!Zi_Y^fu)b5lziUo!l&vVS8otCI)Q7L|Yk{8|Ebgy()Ir2G)>m z*h6#3UIVgTHE!Djdv9`R^gPaPI|D~eZ8%zONsw$#OY)iL_Ep&FZbEg0MWTXsFRNdA z^V-T4|LPlWzH-IC{@T*zH?C&|u_eO364J&dNuJJ}u-oq_S)jfd@yyv=6H>_Yw15!c zRf0Thd^*~zwq&<`$i#D#Fo*-0%^zp>`r=#u;>yaE*MGLM+;rtRI_mJe%!L5mR*Rx6 z>BmYwN7LFnM=qMaAt9yT>dFvaB4m+M5?WxeDQ2@Nv(9S&F`twR6(+v|DsXs`Pk7u% zq%IZhqpzW(i~$>%oc-3+{$XTwX5{gl+nq9{wllzdC_K1mLs>vW=CtB|vevXuG4=`v z20N`(O(M!A_YbCDfEWpZ$6ys!W|G8uN@(Jyg{D-(8B)B_X19^zNz{R*EP%;Y*1Ds? zWUae7n1+aru^L!e7#ib|R?1q+!NVx?#6pQ?F)JtU#6b*Ox3^^z%;ZI?h`)TEszV#d zuVVO=*7=@OZmfl|2;^AduYN$wKn-y982|Vb?Z_t6xF*8yY)ZojbjXXF0$Y4~o zQT~n&JW0N^cC1`Qd4bHttzBd&sLqsm(nv$L=W_eVKsvr)Ac`&Nsp7yKIP0YyGr6V8 zCr<_<7T8jqZ+qgZ5D){VaW$ex5!Y-D#v>q>s}tFmng(rwfhr1GM*1{(>3{ z(g%%~8niZjG};;68Qopbxv?LHNMD&Fr0mLl${qc05vM;ApY#FvJsRCj|4N)5r+=p1 z4OC!D3_r&7e+0kRrcq1R@%?8YK1wfb@FcLH4arX&z$ zb|04?avw#04pg1c2pC6bMfQ%`k@D(!j`DGB`d#UF&`P>#yz2O#^y${r0qB*Y#1BFh z;g%v;W~G4M3O??ET&!;r3o_h^#C8POFVp7C<#4N3}zznmtmk9(=*NU znN9bQoX4bogXA~i+)*A|5L;>?p!Z?al@=}gI#n7fWF&7IPp5@8US4r>&`1p-o%OUH z&(el3p~|d8N&Se*>>J%~tPv#XR?0z0&dd>G^C(o>Uzru*W+gR_siu{jp%aTg2J3z- jenP>RG*A_h6N|`=6{tE(-nXXgx?Qtpt97?tp1b%z!(IIO literal 0 HcmV?d00001 diff --git a/py_service/app/__pycache__/config.cpython-38.pyc b/py_service/app/__pycache__/config.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e29c035f0128d038617ccd287af780f1ec6be832 GIT binary patch literal 1303 zcmZux-EP}96sBZZmTft9e!8Mt(VJcwgPnTK&^8E$x{go;^=!j-3XC^|Dl)bLiBd_r zKz!@IL2vg0dxt&=fnDutudu5g%1xS~k)Xqm{79a2zC-G~(WoJK{{B?Id|X24AJe)0 zIM8_spPYlCh*}B4rnM6*wXtm(TuMr*gPj69NjY_~YoXVOmZ|#@QI|QJ60R_JgII-D z{g*|G`P`mao?2JZjOFNTLEur;ag_6yfQZZjh=vNc5B2gGES!AZ(ejwy+ zc%K0?9`TysDZ4_*?V0XS_n+4k{*cSNM#k&RGJ2B$y(*qFaU}NJwdJkmyL3`IJ zuLpT1)j}}&KKVJ6S3JrW$A^a@Tf{Qs?D!gxJe2Zfl0{*%gayaH9zFf#W|sC(`}6** z{wyHp)8YEZXc_Va>!hrsvW^Hj&C-rk0wmV8SumIebMhwmBW~>=r0cU_Hai=S$b3G; zb;>@lB+FCAm9F$B6B3Nx>M~{TRtsGnj$e~u@HQA?hh~w~-syNSBd2FmT+f+EV=3c| zOWm9V)8EgEO}A`il_yz9rLLb(hU5NeF}i-RR4Rv5pZB`GXV1F5qh9yv(eaBXPkOpB z8Bgc^vr#ZL`FwW}tm}htaO)tLV?b?)tONo(>*lq<-9g4L;>FtOGX7y*Ua5=D^HIC3 zJwkYxGD38XkTj#K#9*J0_p313GT4J9n>vIoyX)cv#IhMEwVAS3hEwgIZ7{dz}j4nZSbd!*f^Gt=x$ViB&7~U)Rm@82= z-MTTh8G{5&Xb`lUhXM*Fe8V-2fx;ruG{$)S7U&ik*+dzPaUe~vX8AYoL+cT|zFo|K z8|ezBR)XJIg@M_?WT4Onf$kPu?PA?sF15d4ipZ~b{k~ah5<#|j2)13-b|!TthyOwn zzbkUYyZIFjxr(EkWy1UgboFAzBb8-|6h_~zOS8+Je!Y`zeSb3tM;ev9ZMLOW%dx%y D+f6d6 literal 0 HcmV?d00001 diff --git a/py_service/app/__pycache__/documents_router.cpython-38.pyc b/py_service/app/__pycache__/documents_router.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8156b04d63a3aebdcbe29825cb7facf249814b5 GIT binary patch literal 7946 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 literal 0 HcmV?d00001 diff --git a/py_service/app/auth.py b/py_service/app/auth.py index 0067996..7555e1b 100644 --- a/py_service/app/auth.py +++ b/py_service/app/auth.py @@ -1,23 +1,21 @@ import time import secrets +import bcrypt from datetime import datetime from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func -from passlib.context import CryptContext from app.models.models import User, LoginAttempt, AuditLog -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - BRUTE_FORCE_WINDOW = 180 BRUTE_FORCE_MAX_ATTEMPTS = 3 def hash_password(password: str) -> str: - return pwd_context.hash(password) + return bcrypt.hashpw(password.encode()[:72], bcrypt.gensalt()).decode() def verify_password(password: str, hashed: str) -> bool: - return pwd_context.verify(password, hashed) + return bcrypt.checkpw(password.encode()[:72], hashed.encode()) def generate_csrf_token() -> str: diff --git a/py_service/app/config.py b/py_service/app/config.py index 95be857..c77065a 100644 --- a/py_service/app/config.py +++ b/py_service/app/config.py @@ -10,9 +10,10 @@ class Settings(BaseSettings): SESSION_TTL: int = 3600 APP_ENV: str = "development" LOG_LEVEL: str = "debug" - DOCS_DIR: str = str(Path(__file__).parent.parent.parent / "docs") + DOCS_DIR: str = str(Path(__file__).parent.parent / "docs") PERMISSIONS_DIR: str = str(Path(__file__).parent.parent.parent / "permissions") UPLOADS_DIR: str = str(Path(__file__).parent.parent.parent / "uploads") + PORTAINER_URL: str = "https://81.177.141.34:9443" class Config: env_file = ".env" diff --git a/py_service/app/dependencies.py b/py_service/app/dependencies.py new file mode 100644 index 0000000..51a37a2 --- /dev/null +++ b/py_service/app/dependencies.py @@ -0,0 +1,16 @@ +from fastapi import Request, Depends +from fastapi.responses import HTMLResponse, RedirectResponse + + +def get_current_user(request: Request) -> dict | None: + return request.session.get("user") + + +def require_role(*roles: str): + def dependency(request: Request, user: dict = Depends(get_current_user)): + if not user: + return RedirectResponse(url="/service/login", status_code=302) + if roles and user["role"] not in roles: + return HTMLResponse(content="Доступ запрещён", status_code=403) + return user + return dependency diff --git a/py_service/app/documents_router.py b/py_service/app/documents_router.py new file mode 100644 index 0000000..5e9e8fa --- /dev/null +++ b/py_service/app/documents_router.py @@ -0,0 +1,282 @@ +import os +import re +import markdown +from pathlib import Path +from fastapi import APIRouter, Request, Depends, Form +from fastapi.responses import HTMLResponse, RedirectResponse +from sqlalchemy.ext.asyncio import AsyncSession +from app.database import get_db +from app.config import get_settings +from app.services.docs_permissions import load_permissions, save_permissions, discover_docs, can_access_doc, get_allowed_docs +from app.dependencies import get_current_user, require_role + +settings = get_settings() +router = APIRouter(prefix="/service/documents", tags=["documents"]) +from app.templating import templates + + +def transliterate_slug(text: str) -> str: + translit_map = { + 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh', + 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', + 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts', + 'ч': 'ch', 'ш': 'sh', 'щ': 'shch', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', + 'я': 'ya', ' ': '_', + } + result = ''.join(translit_map.get(c.lower(), c) for c in text) + result = re.sub(r'[^a-zA-Z0-9_\-]', '', result) + result = re.sub(r'_+', '_', result) + return result.strip('_').lower() + + +def render_markdown(content: str) -> str: + md = markdown.Markdown(extensions=['tables', 'fenced_code', 'toc', 'attr_list']) + return md.convert(content) + + +@router.get("/") +async def documents_list( + request: Request, + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + docs = get_allowed_docs(user["role"]) + return templates.TemplateResponse("documents/list.html", { + "request": request, "user": user, "docs": docs, + }) + + +@router.get("/{slug}") +async def document_view( + slug: str, + request: Request, + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + if not can_access_doc(user["role"], slug, "view"): + return HTMLResponse(content="Доступ запрещён", status_code=403) + + docs_dir = settings.DOCS_DIR + perms = discover_docs() + doc = perms.get(slug) + if not doc: + return HTMLResponse(content="Документ не найден", status_code=404) + + filepath = os.path.join(docs_dir, doc["filename"]) + if not os.path.exists(filepath): + return HTMLResponse(content="Файл не найден", status_code=404) + + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + + html_content = render_markdown(content) + return templates.TemplateResponse("documents/view.html", { + "request": request, "user": user, "doc": doc, "content": html_content, + "can_edit": can_access_doc(user["role"], slug, "edit"), + "can_download": user["role"] == "owner", + }) + + +@router.get("/{slug}/edit") +async def document_edit( + slug: str, + request: Request, + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + if not can_access_doc(user["role"], slug, "edit"): + return HTMLResponse(content="Доступ запрещён", status_code=403) + + docs_dir = settings.DOCS_DIR + perms = discover_docs() + doc = perms.get(slug) + if not doc: + return HTMLResponse(content="Документ не найден", status_code=404) + + filepath = os.path.join(docs_dir, doc["filename"]) + if not os.path.exists(filepath): + return HTMLResponse(content="Файл не найден", status_code=404) + + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + + return templates.TemplateResponse("documents/edit.html", { + "request": request, "user": user, "doc": doc, "content": content, + }) + + +@router.post("/{slug}/edit") +async def document_update( + slug: str, + request: Request, + content: str = Form(...), + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + if not can_access_doc(user["role"], slug, "edit"): + return HTMLResponse(content="Доступ запрещён", status_code=403) + + docs_dir = settings.DOCS_DIR + perms = discover_docs() + doc = perms.get(slug) + if not doc: + return HTMLResponse(content="Документ не найден", status_code=404) + + filepath = os.path.join(docs_dir, doc["filename"]) + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + + return RedirectResponse(url=f"/service/documents/{slug}", status_code=302) + + +@router.get("/tech-access") +async def tech_access_page( + request: Request, + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + if user["role"] != "engineer": + return HTMLResponse(content="Доступ только для инженеров", status_code=403) + + perms = discover_docs() + editable_docs = [ + doc for slug, doc in perms.items() + if doc.get("permissions", {}).get("engineer", {}).get("edit", False) + ] + return templates.TemplateResponse("documents/tech_access.html", { + "request": request, "user": user, "docs": editable_docs, "perms": perms, + }) + + +@router.post("/tech-access") +async def tech_access_update( + request: Request, + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + if user["role"] != "engineer": + return HTMLResponse(content="Доступ только для инженеров", status_code=403) + + form = await request.form() + perms = discover_docs() + + for slug in perms.keys(): + if not perms[slug].get("permissions", {}).get("engineer", {}).get("edit", False): + continue + technician_view = form.get(f"{slug}_technician_view") == "on" + perms[slug]["permissions"]["technician"] = {"view": technician_view} + + save_permissions(perms) + return RedirectResponse(url="/service/documents/tech-access", status_code=302) + + +@router.get("/{slug}/download") +async def document_download( + slug: str, + request: Request, + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + if user["role"] != "owner": + return HTMLResponse(content="Скачивание доступно только владельцу", status_code=403) + + from fastapi.responses import FileResponse + docs_dir = settings.DOCS_DIR + perms = discover_docs() + doc = perms.get(slug) + if not doc: + return HTMLResponse(content="Документ не найден", status_code=404) + + filepath = os.path.join(docs_dir, doc["filename"]) + if not os.path.exists(filepath): + return HTMLResponse(content="Файл не найден", status_code=404) + + return FileResponse(filepath, filename=doc["filename"], media_type="text/markdown") + + +@router.get("/admin/permissions") +async def permissions_page( + request: Request, + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + if user["role"] != "owner": + return HTMLResponse(content="Доступ запрещён", status_code=403) + + perms = discover_docs() + return templates.TemplateResponse("documents/permissions.html", { + "request": request, "user": user, "perms": perms, + }) + + +@router.post("/admin/permissions") +async def permissions_update( + request: Request, + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + if user["role"] != "owner": + return HTMLResponse(content="Доступ запрещён", status_code=403) + + form = await request.form() + perms = discover_docs() + + for slug in perms.keys(): + engineer_view = form.get(f"{slug}_engineer_view") == "on" + engineer_edit = form.get(f"{slug}_engineer_edit") == "on" + technician_view = form.get(f"{slug}_technician_view") == "on" + + if engineer_edit and not engineer_view: + engineer_view = True + + perms[slug]["permissions"]["engineer"] = { + "view": engineer_view, + "edit": engineer_edit, + "cancel": engineer_edit, + } + perms[slug]["permissions"]["technician"] = { + "view": technician_view, + } + + save_permissions(perms) + return RedirectResponse(url="/service/documents/admin/permissions", status_code=302) + + +@router.post("/admin/create") +async def document_create( + request: Request, + title: str = Form(...), + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + if user["role"] != "owner": + return HTMLResponse(content="Доступ запрещён", status_code=403) + + slug = transliterate_slug(title) + if not slug: + return HTMLResponse(content="Некорректное название", status_code=400) + + docs_dir = settings.DOCS_DIR + os.makedirs(docs_dir, exist_ok=True) + filename = f"{slug}.md" + filepath = os.path.join(docs_dir, filename) + + if os.path.exists(filepath): + return HTMLResponse(content="Документ уже существует", status_code=400) + + with open(filepath, "w", encoding="utf-8") as f: + f.write(f"# {title}\n\n") + + perms = load_permissions() + perms[slug] = { + "title": title, + "filename": filename, + "sort_order": len(perms), + "permissions": { + "engineer": {"view": False, "edit": False, "cancel": False}, + "technician": {"view": False}, + }, + } + save_permissions(perms) + + return RedirectResponse(url=f"/service/documents/{slug}/edit", status_code=302) diff --git a/py_service/app/main.py b/py_service/app/main.py index 054663c..26c0fa0 100644 --- a/py_service/app/main.py +++ b/py_service/app/main.py @@ -3,20 +3,90 @@ from contextlib import asynccontextmanager from fastapi import FastAPI, Request, Depends from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.base import BaseHTTPMiddleware from sqlalchemy.ext.asyncio import AsyncSession from app.config import get_settings from app.database import get_db, async_session from app.auth import audit_log +from app.templating import templates from app.services.docs_permissions import discover_docs +from app.documents_router import router as documents_router +from app.dependencies import get_current_user, require_role +from app.version import get_app_version settings = get_settings() +APP_VERSION = get_app_version() + + +SEED_COEFFICIENTS = [ + ("risk_score.no_archive", 25, "Нет архива видеонаблюдения", "calc_risk_score()"), + ("risk_score.no_power_backup", 20, "Нет резервного питания", "calc_risk_score()"), + ("risk_score.no_regulations", 15, "Нет регламента обслуживания", "calc_risk_score()"), + ("risk_score.system_failures", 20, "Частые сбои систем", "calc_risk_score()"), + ("risk_score.no_documentation", 10, "Нет документации", "calc_risk_score()"), + ("risk_score.bound_low", 20, "Граница низкого риска", "risk_label()"), + ("risk_score.bound_medium", 50, "Граница среднего риска", "risk_label()"), + ("risk_score.bound_high", 75, "Граница высокого риска", "risk_label()"), + ("risk_multiplier.low", 1.0, "Множитель низкого риска (0-20)", "risk_multiplier()"), + ("risk_multiplier.medium", 1.3, "Множитель среднего риска (21-50)", "risk_multiplier()"), + ("risk_multiplier.high", 1.6, "Множитель высокого риска (51-75)", "risk_multiplier()"), + ("risk_multiplier.critical", 2.0, "Множитель критического риска (76-100)", "risk_multiplier()"), + ("complexity.video_0_20", 10, "Видеонаблюдение до 20 камер", "calc_complexity()"), + ("complexity.video_20_100", 20, "Видеонаблюдение 20-100 камер", "calc_complexity()"), + ("complexity.video_100plus", 35, "Видеонаблюдение 100+ камер", "calc_complexity()"), + ("complexity.access_0_5", 10, "СКУД до 5 точек", "calc_complexity()"), + ("complexity.access_5_20", 20, "СКУД 5-20 точек", "calc_complexity()"), + ("complexity.access_20plus", 30, "СКУД 20+ точек", "calc_complexity()"), + ("complexity.fire_simple", 15, "Пожарная сигнализация простая", "calc_complexity()"), + ("complexity.fire_medium", 25, "Пожарная сигнализация средняя", "calc_complexity()"), + ("complexity.fire_complex", 40, "Пожарная сигнализация сложная (>5000 м²)", "calc_complexity()"), + ("complexity.it", 10, "IT-инфраструктура", "calc_complexity()"), + ("infra.server_none", 15, "Нет сервера", "calc_infrastructure_load()"), + ("infra.server_weak", 10, "Слабый сервер", "calc_infrastructure_load()"), + ("infra.network_unstable", 20, "Нестабильная сеть", "calc_infrastructure_load()"), + ("infra.network_partial", 10, "Частичная сеть", "calc_infrastructure_load()"), + ("infra.power_none", 20, "Нет UPS", "calc_infrastructure_load()"), + ("infra.power_weak", 10, "Слабый UPS", "calc_infrastructure_load()"), + ("history.none", 30, "Нет обслуживания", "calc_service_history()"), + ("history.irregular", 20, "Нерегулярное обслуживание", "calc_service_history()"), + ("history.formal", 10, "Формальный подрядчик", "calc_service_history()"), + ("history.sla", 0, "Есть SLA", "calc_service_history()"), + ("object_index.risk_weight", 0.4, "Вес Risk Score", "calc_object_index()"), + ("object_index.complexity_weight", 0.3, "Вес Complexity", "calc_object_index()"), + ("object_index.infra_weight", 0.2, "Вес Infra Load", "calc_object_index()"), + ("object_index.history_weight", 0.1, "Вес Service History", "calc_object_index()"), + ("sla_price.base_cost", 15000, "Базовая стоимость инженера", "calc_sla_price()"), + ("sla_price.region_default", 1.0, "Региональный коэффициент по умолчанию", "calc_sla_price()"), +] + + +async def seed_initial_data(): + from sqlalchemy import text, select, func + from app.models.models import FormulaCoefficient, User + + async with async_session() as db: + try: + count = await db.execute(select(func.count(FormulaCoefficient.id))) + if count.scalar() == 0: + from app.models.models import FormulaCoefficient as FC + for key, value, desc, ref in SEED_COEFFICIENTS: + db.add(FC(key=key, value=value, description=desc, formula_ref=ref)) + await db.commit() + + await db.execute(text("DELETE FROM users WHERE login = 'admin'")) + await db.commit() + except Exception: + await db.rollback() @asynccontextmanager async def lifespan(app: FastAPI): + from app.models.models import Base + from app.database import engine + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + await seed_initial_data() async with async_session() as db: from app.calculations.engine import reload_coefficients await reload_coefficients(db) @@ -26,10 +96,7 @@ async def lifespan(app: FastAPI): app = FastAPI(title="AegisOne Service Portal", lifespan=lifespan) -app.add_middleware(SessionMiddleware, secret_key=settings.SECRET_KEY, max_age=settings.SESSION_TTL) - -templates = Jinja2Templates(directory="app/templates") -templates.env.globals["SITE_NAME"] = "AegisOne Engineering" +app.mount("/service/static", StaticFiles(directory="app/static"), name="static") class RoleGuardMiddleware(BaseHTTPMiddleware): @@ -38,28 +105,28 @@ class RoleGuardMiddleware(BaseHTTPMiddleware): if path.startswith("/service/") and not any( path.startswith(p) for p in ["/service/login", "/service/static", "/service/api/"] ): - user = request.session.get("user") - if not user: - return RedirectResponse(url="/service/login", status_code=302) + if "session" in request.scope: + user = request.session.get("user") + if not user: + return RedirectResponse(url="/service/login", status_code=302) response = await call_next(request) return response app.add_middleware(RoleGuardMiddleware) +app.add_middleware(SessionMiddleware, secret_key=settings.SECRET_KEY, max_age=settings.SESSION_TTL) + +app.include_router(documents_router) + +from app.routers.service_pages import router as service_pages_router +app.include_router(service_pages_router) -def get_current_user(request: Request) -> dict | None: - return request.session.get("user") - - -def require_role(*roles: str): - def dependency(request: Request, user: dict = Depends(get_current_user)): - if not user: - return RedirectResponse(url="/service/login", status_code=302) - if roles and user["role"] not in roles: - return HTMLResponse(content="Доступ запрещён", status_code=403) - return user - return dependency +@app.get("/") +async def root(request: Request): + if request.session.get("user"): + return RedirectResponse(url="/service/dashboard", status_code=302) + return RedirectResponse(url="/service/login", status_code=302) @app.get("/service/login") @@ -100,7 +167,7 @@ async def logout(request: Request, db: AsyncSession = Depends(get_db)): @app.get("/service/dashboard") async def dashboard(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): - from sqlalchemy import select, func + from sqlalchemy import select, func, Integer from app.models.models import Object, Task, Incident, SLAContract, SHSRecord stats = {} @@ -130,8 +197,8 @@ async def dashboard(request: Request, db: AsyncSession = Depends(get_db), user: stats["my_tasks"] = result.scalar() or 0 result = await db.execute( - select(func.count(Task.id), func.sum(Task.sla_compliant.cast(int))) - .where(Task.closed_at.isnot(None)) + select(func.count(Task.id), func.sum(Task.sla_compliant.cast(Integer))) + .where(Task.closed_at != None) ) row = result.first() total = row[0] or 0 @@ -182,6 +249,18 @@ async def api_tasks(db: AsyncSession = Depends(get_db), user: dict = Depends(get ]) +@app.get("/service/api/changelog") +async def api_changelog(): + import os, markdown + base = os.path.dirname(os.path.abspath(__file__)) + changelog_path = os.path.join(base, "..", "CHANGELOG.md") + if not os.path.isfile(changelog_path): + return JSONResponse({"html": "

Файл изменений не найден.

"}) + with open(changelog_path, encoding="utf-8") as f: + html = markdown.markdown(f.read(), extensions=["fenced_code", "tables"]) + return JSONResponse({"html": html}) + + @app.get("/health") async def health(): return {"status": "ok"} diff --git a/py_service/app/models/models.py b/py_service/app/models/models.py index fd3c9db..495825e 100644 --- a/py_service/app/models/models.py +++ b/py_service/app/models/models.py @@ -463,3 +463,35 @@ class Case(Base): is_active = Column(Boolean, nullable=False, default=True) created_at = Column(DateTime, nullable=False, server_default="NOW()") updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate="NOW()") + + +class Idea(Base): + __tablename__ = "ideas" + + id = Column(Integer, primary_key=True, autoincrement=True) + title = Column(String(500), nullable=False) + description = Column(Text, default="") + status = Column(String(20), nullable=False, default="normal") + created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL")) + created_at = Column(DateTime, nullable=False, server_default="NOW()") + updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate="NOW()") + + __table_args__ = ( + CheckConstraint("status IN ('normal','in_progress','completed')", name="ck_ideas_status"), + ) + + creator = relationship("User") + + +class QuickMenuItem(Base): + __tablename__ = "quick_menu_items" + + id = Column(Integer, primary_key=True, autoincrement=True) + role = Column(String(20), nullable=False) + label = Column(String(100), nullable=False) + url = Column(String(500), nullable=False) + sort_order = Column(Integer, nullable=False, default=0) + created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL")) + updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate="NOW()") + + creator = relationship("User") diff --git a/py_service/app/routers/__pycache__/service_pages.cpython-38.pyc b/py_service/app/routers/__pycache__/service_pages.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93273196baa2843e72ad2f311c3a044d8cac7b84 GIT binary patch literal 47362 zcmc(|37lQUbtnAxtzWNd2}ua?07(`lUO7cAtcrdcDvQ})csm-Y3jWvLHo50 z7RC!;Fm?j5h2659I7*yE#$d~UF)>M;O!8$~nMo$OlP_Oldy+7dcx-30<%RkGPu=Y; zt;SB~v-Eq_b?er8s_N7^r_MR|_U`UX2>%AY**)>?e+q@Z$%o*-0X$rVU-g;(P)LPT zxEQkdH&P6j!d5uUc(fQPMXjjBW5rl0ZpBLpD^W^X$x_NnmC{yP-o=ZVQkT_L%39e{ zx796aiDFNw*Xoscve;MZxB5#1)<9{2wV*U;4VD&K3rmZvMWr*WGbB%{xVW^$T2fkS zEtThV@yyaPYnj9|#j{Gwt>vY&t+VC1tC%aDW1S=MZ1LREdDeNQ71oNSBpqqApdJ zN$f30dabwQI~vX>^UDr1JX>u)96lW0AIT?>d%L;@d9PL1JsdwAwyr_$Yt{AhL=JC$!5jZ%5Ze~aYbKX3jqH7@ygOa4N0{mfU-!?#ev5C)ItDd#~iy^W?6on&iGsdQxxB?ZK50jXLY&W_9pLOdV49OS#*n zl?OVNYl7#LdZ*;SL-IG~%|ET)CHe1^e!aUne;Yg>R1Zn+yCnB}=E;3nJuJEJmcG1q zp4^Y9_epL`a=(9`+#gUMl-&17?nmdz{g`@Oa^EYtKQvG7C)AUY`)$CiA68GPkEo9V zhtP3l=*^`d0xHX#lNV&g!lSL`;SMG*4wd4wyAHouM&gDy&Za``rnwj@2Ky}Y8jTb^}Y5|vOlkslk1<5 z|DV;rNd9+7EC1S@U)`$y?QqmRE1dRx9DAkuK5*<0)G4W9L|Xh|YYjYcnsCC8)Q=_q zKFR-+*8HZ$rV?*F8CD@FGs<4#DcreKD@2epawLW@ieF4>P;3=X@y;~_+eNMfen}~p zcUn$$E;rYiit_0rafBKCx}>K4P7kuJHMK$tCA#tJky2w$sovI7?eIdGKK%Nn+_=;4 zf!1;zT3dipgHo#Clv>!S6vpbF3^#KfAdp{Wk^jpN(1P&I@oJ$j{xkOUk^{Y2%i8%j`y(Tcu3H zX=hJUnNBmf7iDgfGSf~wx6fPV4wSi5%Dl@dbJx6O?nW6)%DmetbI-hG?nRlmNtp+o zGH;)^%rMHlL&`kllo^?~%s!MEl``*f%BXqEbzxYC{vd*?{~@^oVUy&lrbE}2b?l3JVuB0KZP=e%lM!Q?+vYPghQd%DYn@)6t+`p zq?WH0N_oV)RDSj#FScHf@7D(L)ZZBM>BSt(cZcJ_w7x9swtcHDOJP3{w#%3fW4pgg)aU#%7@ z<)NgVsOF0(ZO8W4%cFKe;i;ClqZK`D$41I(*iP;&*YacNO01T@zcv)Hv!+C$T&#>J zJ2O!!l=F&l)Sej2*M`+TM?%2C0KU&es?z*--h92hijk|Z+0|Z{ZII_Kx-@m^wT40&fHgsWk1u%|n z9|8RO0orN}&?%3gw>xY3l0Ddz z9=JJQuCqP@DlfgNFTrY<{lv*fW?z{7%Iu4?$8#qioIP>!Aw*x9eXf2V-_DqX+~v7) zWq3r7ju#H*UweA<#kmWZFj+yX!}~@?57Z}-y3wTS{1`^bK~!IRdK0r&AE=_`;r%0p zVqNEv>Sk4y(K-PuxsiEeeGsi~{KkVH*|c$EZuaxD$7f%{e{|PcOw=2u^_2}$+M z?EkZv)-RlV@Z@2E?r{Pd!3oU3!)W8UjGS*VWX{jY!}aSs6}vo#ZF?}Ux$5$@Llu1h zFp%~E!ZOEJXTI=m}YH@_oLb<+-g`Y;D=cG86 zEE+-pc%MMOkJrzXo}j7cW!@iZT?B8%`<>X5fa`(Rp5D0rB8+>qQkpF0?=RFIV9JF^ zDU|o?5k73j!*~I>P0PDY_2noplY~8nKILXVh6yCtAIe?5VcUl78?G^%Du^05JpckR z#S5Gy*;Lo=MK6F7r4?>;jsu(ECFFil5{VL!{e_e7m!c!e}n+r-6^BeZobfrAD zVQ-#^X1YIyroVzodk}w#G+IZQTYc^Jy+idn0EUP^*bCVatlr}o#BuCy>55=Q)&Pup zLgtQDKk5&dc=ctJ;lv*I2h8MG&;`N+JAtL$;ntJ>R81|#xsqGG7i+#Sn$PXXj}&X; zxxF}N@(jS{F!|hnlh3bYqSk;)Tk$sr5 z>viQ&ZG0-Z{n}e{7+(D`6mjAh>dkxhuquJOy9#B6bR3_YIkQe*4R%?F`H|k3A#x(V zB765iY-?{49I&*{>ZXlT3(#(koh=lx^>RDQDt~`{9VcTicPQQt%g!u}lrd+oJ)K*< zDYq`SP3K2O$A|akfxS>{Xlk)bM6}hl0d?(^x`0Ji;{*Z*S&hSSZH_18y4<0BVQjpX zSHl_<9S_!_A%j0Y%)wylfjwHc?scehWllix0v6@1y zZ*AVVa&7K1ZmY}Qg2zpK+=}N%p4zywo6>sv6SN1nQKM3)QYv9-B9pi0Z6 zXPg(vS}Tl>6zgZRx=*3;6R7M9+)^hWW%DmdR0ccLYiF>o4-8kiQLSEyg9@w_CNUgs zXMDtM#|35ENe5Zm*^1Di!v(G?J7uUgl!@j?t9G`N}XNRunX<6~+Fs;`=J) zdeu&Ax6@W!*NgcoiCz$4lc3D<)lqc3>0cC2+wrURAgG0A!bd|h5y}(yhK3@$rg~PY z^(#y3SKgglxns-9n}?!0f_!$gTsdT?sx`fzyu`7Pz7nYtoLV+AIk`a#A5q=Fa~e2n zcyeScUtK@>fSp9aVU%0LCRtS|tgl8mDsevJU(EU4h`&`HnvGB+Tnilrw|FEv9c@Hs z!i~sL+%Gqx(n~NTNs{^GSYo95_2nou6p@&rnQnL#pOr?Z|;cc6ik9wZ4j} z(b3xd+``oaf>5|8oC#|ZAgDj0_SrGA5Trh9bzN@iFoDQ2ui;lEiO>kmglgd$M7=Z7 zT4W;D2p>R)XX1z_@Jk-W(@tEE?+8st8j+b4fSx`Y!eGL?hB9_Ce}8_Ij953S(rXde z@k8Tzo!2`VPl3T8_5FaIse;%amPb2MPbb zhQS?;9vB8KUcV4|s@&J1a4fw1jZ9SULHtyYJBu3#+NvF|94hDav8Vy3G(nh-5(w@; z&LDLM1Rw-^BisxS03jX#LbR5cNvd!yg_(|EegTY`%u#3(sHloPj+_8ZeAiT;tRvHp z_2act(T+=>gn0lB4I)ISOGBlDe#fM-;Zp0)O zI|^}`#31gMzX^r4D6y1EJF$!x>r&aH5tTgk?}J4=#jD=pBx9NZDWJvOJFkXbfG$Irr8c}8cOP6 zl(M_sN`~2nUz6U)Iu(OFgIf?-U7Vm{1NizXq;Or~Bq5da6`Y-T0Ktd^GLJlF4A-gm zBJ1nxF_>c!J6hL8yQ>QBwq6Apps>y$>YxNA1?;$fq+T1>Z)b~Ka&{C#3+z~hLGLft z(Lu83cEX%`c5EDnqD~`eDABUzbdhziA}dwS9}n6{d^=8IHOj{)r5Q_AKv*AcM zg)kM$gfsZ{{4kNsgnGj3!@7zbQwv(wA|yVwrj;+d;8Hk_2_6rNM^oRa7C zQCuq>;QO?~%B4clbex9->h4xOMqh2Zj{^$2z`$Fst&40w-*V0Cw=sAJgHZ%lS{5FmTwjgn z*=MlEUMBN&^24WFctZnb8I)O(Sx*JXskmU~?nH@iWd)8gHpzgiPWFqY07z>D*g$e- z`Y4Sn&JNejqKi%*QmyOF!EYP}F=I7{fU9j;I2Ou=`@H{Ith^XkU3g7cA4Hxu+-HP> zO)HMb(pz!tK4wtPS**OMJCidY6Nu-$)dno6_cWptn6(3p5sLvox%ddrO}LClj*1I* zN;q?S8=7jF;GvZEAg*hCFPo7khwfz{FqLiFb}o>sY=xV%PtBl5kfAENcac@$o=_il zr9OnTQv=?f9Cc)tRedRI;|dyzx6BsBtQ6e=IciG~TkD?a;bft`?up(;dZLGfl3T+}-_g+I5fz!v1WNRKB?fB! zZW+WDsOW(R`_oj~M(M6bYG$yJZe$u=Ds}|p2#4+)S8sQOkE!Pb6u_Sqbc6r3eSwjv@cpMfHK3Os36Lq^i2%<7?2GG ze^M+QfP5YUG4zs9N!;H;`DnIg$7TF_8B=+Z+FfpUhC!Mp3~8!)`sjYXPMGr%;GEZhoPHs09dycClXQ#qM@2|{5fP?SfyCr9C=L96g@CGX?GX%VdV zC4Q2Kk}A_rBj3~+?cmo=(Ia-$BPeGTdI1u&<9tvQ&LHVoDfwaD&21*T7 zl#_NC%H8)}(Q=9gDJ1t42hksa?~jn-I>0 z9p2wM9RXZGIC61R2N3uqO9}_@AjlN`F{F0H0`n0nq#gjCH%F-KKu$1g?inVmRy?pP z6F$)lj@Xc%Er3lLRO$ms0N0CN_(VxNvpUaWHG|}a@(7cdAxw@S?Npz)FrCw|jTkYg z`#s3qM28TSc=+m;L}5XK0cgjX1<72~ZfxnN$l+l5S3MXp)$|SIUGZGL0Ci z3c+<gbyZD0lz zNNrH)ha{+tC@2eq!@;D0f=KSt@5J~7ZA}fB1Ka_YIlhz5)4%XIo&AJ={fEOlX%6pC zumT=OJ~aJfd}yIL!I$B^ovcjCMXX-Pr<99zT26NTZprH)Va?g`4|qt<$^}T(d=ZD1 zH6<8yE3^6sb*AG%E%iOZoSPW1_m*^J7&zZhA}um0NAhV-W}>|6Ac9b2P)=s7I3UQr z@M@gU`ZGumAVddvoz7&^y*#btOlBvV5n_#^kO-Lu`HPv>8-;C+;H(5~T8=0(ks^zd zb2ox>G3LY)UM%UwTv19yWh5=4x^UWNA5Y`F=k5lL++knij8?tEcJ%EssFC2ayUf6b zd1EW*ul^L01E-oyM}uiR)yzrg;8ubEFEh1M8Oi<;zI~Q~$Gf_ymg~r%5<#`zk!OSv zt;~j?XFFVJ!?4c8I3g8xq`DLVG3<)KL6`B5y3gWmfE{#aw(X!xAs_LF%^5m!e*b76)ZJ_bUs>bRe8 zZ%B^Cdx1BwDjEr3mSiI-M`N*(P>BN({qodcBdMQ3`$@BYrjuxWS)lb7P3sES=I&5A zau}iy+;08GKzNk zHY_%Y8j?p=!ZU1CNS;*dX6FF|?VWCTGxWNyE&D=j$GAM2)Gg=;Q{AA z#RMrx-c$G=0sTFIcn{Ct&mm`r{r+=5ecLvVkSGE&fo_M>*S(AIL;JYab>-A|~%{e*uo&LnXlVj1Gf#pF-R zeMY91p6J3aJJZdrGe#uI<1vmjF!L_y;*}=SV{5%A-B;_c^-c^N4$I#Kl+xgaq<3O) zW}ynPENbbU04sh}p8O3svlxAG`;(|GnOUl0wKL6i0jSXgSrwIZnDx{yIc1G#gu;}m zMdrK>0LYxVU4fMP1;{BnDqoW8cT-V&JjWL(2k} z3$h+=;`@gg+{|DD1D}f~RutCQNTVrABFjB&#CT`%?F<7s(GzBInCqysE~vS?@j~j%VU32&Wa-GmfB?}MLG*XM%n-#G7Zg@1u4*%@JD}<0jEs= zCIkO44KS4=o;AWknN7)FVRoWbbF6lj@l#@GheulR=|FHI3_JN}$XVrHHyq4boUp*1 zU&iy)Ssg*|E}bTX5kr0t_`C4nA`DEz=>@IZYLFQo1-&$!K$&KM`e&HSxwOGbaHjyr z$DI+hFE_gSImUcgwFy>F@O{f-9Sp+A9V~HR0GA4kUPRidtOp~gZMCCmPW32(a!uSQp{T*_Ml0+!NyNo{EX!M0I3G1&SJ#bfs?47V#8o4}DwFYr}Gd7#bfs~Y6 zGQGNHM0=Q1hjb+2x-39C-r$jrF+)1yHtpUZc{+8j)CPHtzTXiZ;&eO;Z3N)G#B{u+ zzK=Qe;f<+pYPFOHi>zO%f!%hW6c||w^vMuSCt7OxYp+)crA_eY1kYrL9V(ijZ9)Rj?PV7r7&mglVM#{G29;|W|XfMsvM7)=-}>>3Y`d*nUv zc<(NQtY6}6dnnrO=Nv;>6^be)NFm3I4;Vpz1`=xMS5SN8RBGkj>sFT5tyEOUQIck3 zksI}vBJ{bSiy+zMe7CyF`LRl^AnMJ>5~kJPVXLn)_-zK{S|l^1+(pI(_X*njRX!eL zz;(lG;R=>M@t`Q(fU}1_G$r17BTx6v5RmoTSZoJ_?F^*mUqj4_%Lw)FA`V3&2;AS# z_btmkLr|ELmxue-X?VC-etI6R{0IoqYMirPBvsW2vv1THZ+F@A6t|CJo*)gN+A$7}ywGWFs@^U3Cj`%*RG%WvP;(G_mE*q6K;MM<_kzAN|m+Yodnw@OlA$ z+_BXnMgwHi#6FSY!4aIG45^L%;q~(t9J-MuX8vL|-hZ+@4*XhnqFe4idl2j84Q8MQ z-hP%9(6*4NrbK^jfOo7Q0eTSYRAL4pAt_u{{k=nU~R#~Li~6$ z3X;KP^t=(Ge@})d`%s?f*Db%8TZt?X zDl0g&APsyB3r}`o3tNj#$-ODT>k6^gWvhm*s2X`dKZ*pGp2LzBdhT`ZMJtNOI*fQ`O-MH`=KKlLa26Be()SI5rH4@SUheZls6S2UzEGnJNtPEsT)@ zgnC$^RHy}*XF)VQd=;(MO9uJ8%Ir5Wc$mRq1`jb1f^jKh{S5L9b}|t9@odDb?qJuU zRysO@3qgVmq%PEy4O<1~81OEp73p?U>sUdDZeL|YtF^?un~^OPSr1zH zqVPuizbbqgFlm$>6?P5ZG5MP=o9%I|?oPz+Q^tg&m`cs@>%n-C^jYJg^U98)C^q_ax9qw3cGx^zzvq_Qckk_}%h5)f{yp3OpA4>(8Yj_4 z;eIPAoz5#wj;nPA_6K1gd>O>}?j_L*kvlELT8!a0i2wSZP{-6#j|2^tZl^`#$ehIQ zHTIYF>gn;L`$2iZ6gJ5h}htj%IWo4M&sHPjvyW}#|`=XNAV8VY!PCH{X-$lV;l0~_j+An5>Mc@>i9@r$dZuNmJ zDt*|@gW*I7TmYp^|B5=MmURYptui(SlV2_^_g@SV58QX*!QI>Q3Yl)AHeZ%PrkDf9 zD0D_!_4qt-(=k`v)B-<8h0h0_KtfJ~e?fPANt6CKA6rI z-iZHz%lGj-7hDXl)dm+2=P8u!v<$JJpz94ni%Sl~C5aJ{d0 zbm?fY6v+&*0{;tbX<0ci4TrqAAXmr+RsAF1@%+X5@$%^(FjZa&z*^wzsB8##DnOQ% zD(ll*j_ryFG6|=JwSn0PVa*etggw zmNQ_gSH1{)KchsAj+tSDXx@O4ScyV;vRTj9f!GB%%G1RhaS1D>fYoR8FD%2g@9E`%ej8%Iji8{Izz z8?BL2Za1PZwxQ*%-Q$6Fv;w=LDFR7AjXvPHP!`CoGkHKTVoz6}TdP^oagaMap_L~R* z!|zF^Is$XgVF-&4O=b2np1Tfldl6<7M#6v>I6v0|2^PVkTm1CgwT@?!QCJm%!yg6_ zP6O-=Os#Xq7~9TH>WTpVAoiq{-5q$MHES6YPxmfX2D9tLapZ9`>kAoN!r)Q{moaz? z0}AfuqR*k%TWh<5v*W@ZjK)yV{YQeU3qPUTzlVrRNJ2g%=9e9q#F)b*LX}mRL@;>5 zBqD5K5~-Jy6ebaxKG4nCNJ|DViE)ofya7xjnM7&<`AlLO3?b5R$(MniU>Dw|Ns1vv zZ3t5cGp26%#4~l5AuIe^BU&D4r1YOOpyv(JEhBQn*mPH|e`WykqXoiXreMo}>knRj zh?EFd8~+0%H}asrWo(oI(UiFm+sGL2x}dfo=NAt_?WJ1DDje^C7SO_$F^9{Ex-1~) zLpV|WI^Nb7BAERYjz=KL9PIt+$>3!iw=aQ0+>Uov$|+dIrG>cr0Z(@)UKk0_U?)!0 z^o&T(7#5IS(Eq?dh|F&@_Fe}6o56P&e3wBPf!*sep?k01bs>p()8 z;d)%C=JK(!?vTj?3m*W#eG_z8$ySx;CkWbN3-=6R9xoN}JeP>bb# z+Ifb{Zh6k2wG76Ygggl5MMs0Z5SbD-E!2kn{-`q>aTrrAYjhzzi*QiOpG^iJaW?uD z(OUqCZa~8HCa2C}YtGZT>b$_yil(QPYE|If`Ay~EG@S3$n#YrYS?Xz;r7U5lE|5O1 zZDcV^u|VI}N#CAB-+H~it#4{^LsM_yfIQHf&8B^46c?JD>Y~7!VcQo=+m|%0j!V&Q zR2R{1pV#hXP3^v=sogD2<9=&XyIY%bUf$HhD;x-2`FJV-q5dWabqj>9av;>Lu3i$7 zne0~EB-T8e+ok8N%%oc9{mZxPx`yl zokiTUFF25SGN!kNah(wkO)Z!AH$ZK@=df68GiM@nCsbiS1*rh;`Fq~ga~GbdqN?bj z;Kp6%NdGZ0&VB|VqD7pHkb5(L{wjq8^1j0>IfJI6>o@He^1sQ~SMD1Rel%x@1(>fp zhgp)bGy`7s=_~@^KzZ-KVyYo9x*5aJV1~)yQTDKpv3>-GAp%!GQ#*1#TXOY2gfs+vu$^Uqj}C=)xg|z3r zl18V}jzb^xmvsE>XM7TZ&Ro6%#Y-`_f8Mi;Y<0H9_w=tuW}0rk;Kjzw@F#Ggbplsf z;NZ99U>89_3+xd`ilew=p<_O)S003qF;^@kAuQvuMWi5_vK1$r&x(rno)zRxO=b$U ztbVD})q0}u7R&YIGFpThnl<3Olc!-RX)Or!3XTV)GnRk5nG2M_xTMJVEkMNXS(Qx66$)E8;T)941qjU1bXMq++VS}2LM z=#r4=3DPWkH&_m)Lm$gYi)k%sj`pB}X$i1@jHcR-;gnDxGu))i#{8!C0Rh2~Ryo%kK_8zLk#;Fy^x*%Z+{_?|}6TgM|z}j{v?qM7^$+i2byw{kLV)>i&V z9$|Lb#4Uquk*H?`QgVZKF|Ml}1L$SpEG~4Y-53ik7Y#@1bL%SssDMF^6z!ztNT%H5 zH)inA#Bb1w02gTP&4szQfOndRbB6(5!@0u%{jP6tQW5t|u%guQcWyVJ>!%Oaup^`% zJke_n6^^MSg`ESoS&a8Vn4n##Z+IGkX@_yvL1N2xgOL$;&+Dc?rKnLO$+ zwB0&+w{h$fphiU-`0$xW<$E8XO+iKUK%#mLZ{`dx118%CjD(vxI(HJ4aN>+~U?wt6 zI`(FdbITu#=-co|b~#mxDUHTub1Nvj%zcVLvBvy7=4I~8eiV384MYDW0}r0=50jvWxgT1LNLl_$TjeDPy?>qV7pzn$s zxRpM^Dt9t=Cu7`!LylJYIZT*g;9+f}|HF-FcbV#qCFPq%&VtyFvr0kJTN$Iw-uS0z zQ!AH#`8e3`aO>`MhAIg74uAI!dI;E2FTV)`4OqQ7#pXi3edw7irx`>#Azv|uiSgoC zP_3Bh3qpn5(s2nUs^UPZ@QXsFafzk87-X1ETE>fsn=r{ILK`g4UM$(Z$Ga#m#g>l; zwtS-5`IlhL8!<*JQz%y_;d~nsA@Gf6ea3T>u;~Qj8AATvnuVz@YIigY?;rK+`w# zaTkMI80=;st6>jgg4%a5=A-uDefK%&xWVdz-#y&Z5trkVLKgn8!RGJadLTjI-fTP0 zAjDcW@#9nsSbZ;=4!8qyuL!DO4!5!C9SpW3pzsyacpPIw@61i1l4owkKmqHOpOr#Y z4MVj;mgP{bl$ph$`WmJMVH-`_19}vcDrr-pNi*nEztEWd6KN%NplpAWt&5wV=L_?@ zG%V;=%rp4h;+;RA0BzUAiL193WX(-Ti8wJ_`L|yMY9?T8)N_)@dUXyo^ zP5yvgD)bJn8o^>hh2G9bA>e#`=J?{taS~@3`1tH{W+wq^4mv3<%ks@)G@snevz9kS zU;2IYaa+qUS`b{12Ms}SB`)d3Xph!2K87Y$zWgdQOj8`aD27$mIdf1kVYNRD|JuDhiZx7iYlNmGcU2 z0NSh+4>psF6j*uW;(k1Inu4I|rFRuTpGTIWn?4XP(3Ud_OVEpR$i&$2n;;Xdl+!6F z&q_>gjLn@_3+TEGStw{YhmIPoEL-Mo0vYfy=7pR987Pft1Jq(GMwR;PaG{lC?Drr> zRa-*rJwaG615cF*^%&yu9<+RFk$34MzH4!y$;Ths$Z|@eWT}(Zamn%;2yJMv$AO~h zR5m&RyAv=#DLml@Q|uACg7yedixq?IT945`Cz2)X;p)!#SB0A%cuNdTEjIe6Om#Up+84O-TTP$~&nx)nulXoPDmO_h)BG1q zYo@%+_24~x(SmpenmQ@S=P^!03VB13Sa>ZR+rFIxJ-0LXH{g>MlN;X2aN0Rd?HHc| zXKKb79+bD64%7tN@JRZ1-~rBkg^9ue2EP;Zddu0OJ75uyB=ICO=KI3s+|dveS6Q5| zu*_E!_r$pTqPcxT#Pl#;Hl#MZ;-1xE!a(EXCuP?3+b~w6JJT{(B5#JE6t4Jl5Y`H@ z5HG-0bX5@ScQbY?gXZZ&bK96IWjs>F zv^c_MPd}*~+(&|wy6TOAsNRQyb5H8X!Mr=EG1IQ}z-zpTHRBwT^Ojv#M!np1E)@&K z@|ZE!q-r_XvWTM>EM~sW)ls6nhN2F`7&jo4gRwanH(ZOF!vs6mtq(MEsF8pkFvJ%% zTsdN`WNPAI0S*PZrUw zfIl=81w9aL+gbMuKU@dEIFgl1$5Hov2 zw+4*Y#9{C^aPvJ(?L3`lJ~_U)LWi=Q7PILb>#<@)RhpX$b_UqoVohhI@Wl{(Zm2wF zrOWuhlAw$EHXU}*$GPdal}S8MV!t?IbiWgbFU2Mlhr)* z3cUxq9oFOM%qia+K@)h_;cjDpVXRGzBBN~Dqr|ly5%>-~80JOH?3ZB;M!;ae_RbTD zaMx0N5~TMhj>`#i-#!gczE*oSh(BBr~UOLDAcF@-#qU1FrQgK zLfCi0z0D(n)*t8RRW1b5CB|tcU2^FOOl!a)WG6t(Ma=i2BEhv!A~Gi>3AqK7Q>X?a zZp|UFFwzN#h$RJ>RsMp3gIPE{utA0)CgW5vaU>s(T8X^tWet#1z$AS>IfX!(7=+FS zjgVp#Pg*wZPZ)cH!2$$Rk#|npAW!A9W6BP1G`Hk3F@O-XeaG{~$zgui>{!~s{s7Wj ztD0ebMZS=dJcM+Cfrp_Wel9YTARSo{f`iIT6C6bR&{5uSX4Bj_Q|j;sU+2tHNtj!F z6}^MQq#KuNp^V`YCeeqvPFZL3wp-RDQ5}CW$4?97Sw=H1FfOOuh!-^X_R62$;5%# zm*b{_0YY*qGcKT)Ng#_e2mA%#KK!3yP7nT=q-Hw9a1W9D3gRZKqiyq&j?+W?n^{hd zD;WYq0jw&XvO)+$@KlNanksz z3;1M73qk3@@K~Fpm;1O8T4==1i`tCzuY7c6>~a97`kR2$&&^(>%0=kdi}*`RU_)H# zuP6x=Fvx& z^8t`Ugpz-X23gTHgy#$cPsnB^MG#xjt(<5x7t?y1+ld{8KOF~{!ixSQp>HngxOt72 zEaM?d-;X{9M7m;}Lj}~iXMy;>(=0&j35P3$KVIz4_-pjnf1evBr98I0; zclNVXzgkD<<|K*SS=!n>OFBB`Iit65w((BptlGq2bNm$hh`JXdIT7&TQNLl8U$KftLJ1Jn28Gfd`+ z#ybY-MB+i`ZWVW}IN%<9UueGw&mM-R#x)_@J1g9Udt$UG#Av*IO-pS;65)xC^dVgE z?!vRQQpP2M>A2GDg}z@u9q ziQE@MRbiod@Q2797S56N8}0CcV_`$E2;#m9Z|uxgeted&x*`=t#iZ#Ipf@6+sZ|~< zL7$il;PqRJ73dRHuUPLF8_HPSBU%^0 zJ$P_2(fpE0SC)bE0N;QBzXd4=)NG?X3tG>CUc+&*d4oz~j2i6!XHgy{3x+7==Z0>8LxYlwFgHsX% zhlI!F#%=n_HS?GduDWKs;%i!i>65oub zb|OBTCpg4ak$ErI;Uf&B#QPX~KZC;z-owD(E{OnNBeNqUqPOllvNT-+YIA?Okia&F zY!6&fh%|+g^ol& z?9c_rqK3&9k;w$>d>kXhK&A;Cs{@2GTZINDiEt?a#Bq^Cm^#=0a^W!G5+Dw|0z)yF zwV8`3b74y8%2y4Kwav~*hqil~o7m(}koiIk9E?1PVyFC1rTLT7gizRN@2tEI4~8y> zT31ppuDSPc*x2wG&Itj{9-I)EC{d0fXC&1Va3yW%a7WRKzRW}aau({H{U}%=P}H!) zg8@0eQ_SnR7fybF9}#=~Bgh6K>;N*Ja`-FhFc*_Ur+d%!Gg#e+`F(R#N0}wm@r^0{ zhg^DHVCHjGaZP!wz_;bTM?cX4y1fDD@{r#`@TO2hC_=?>0QVPBYHG1Zg80DQP=zqM z3Br_hc@X|{Jj?}Qpz^SL(g`Z(m#*;L9mt09xj9n`{1QGjhdAstd8_KuE07XK@vRBk zbn@;x1vw*pv(?fyI;e~vhGdd*D)=#@FS8_WW|Abf_5HN}l)qG&c3qapJM#wag^ZgH z&MQvi`|bzo2XE|=={+RUePsjgLz#2^6yeHg)gNU*vi($23=8gg+o?D{LwL{Ij>U}{ zwZG+dvAA5Pe3*s&gSXdApkOl*DU(-|50DyOOWI{YJBe==a+}#{EFFGvzJ||3O%*2f zM_Ho6z&j`)<@C^$!6R}|B&3^FD~4WH&DQt3*n%d5ONCRvjZ4J{DY;a7;CPBR4s`vq zhKI-}F|Jb`7Y@H2*|c!DHMwkG!>_s)K@EsY!C{>vfW&nHKK(%PvDtodWsVOsnJWQ! z&mBP-{R?RDwkd-zxSQ6ooq7(hW{;3GJ^3N}S$hd?1R$n(0e(10tkRqAb4?QeEYC;W z%0N0iIv$W$#NPC#g0$ZKo~u+MCaJd)TpVa9(ihf8(bs@8=)kUGtLsj`wLXVRI#WZ~ zLh!pife*&<%B5+`z#_oeG6{Z-6k7%toq;WLRnwM%$OyI$B09ofUSPsP6I%)CCRMge z48$u{7r0%RtC_2966?wgJ&jT8XBcpk=wD)NhJnBQ9X4&wl@CX`A^mX%M;LTi>iSu} zXj!~%Q$h&e$telYsLtxe=l;U_Q>Y|hgHnLzd9zov_TwZRP&2^yh9TO4)*VD|9_pf3 zG=3%r_Ls7bT{VMNcFXz1uC!@HZgGjr$G726gLZS?XLUcr(fas58@TYifIt>G%)^LT zo+~QQlF##1i%e!W)8^!(GSUTMr1twuc|}E$F&G7h-{Bnx^AmEBG=D__P zO*i85@m8F6c*ZRqo?ErOf)1O3US+p7!_9}PYfVwZ5DbO)tBf6E!0~B;sy_(??Hqhd zlAr>^k6Us?TKBNC0tv_(VeVd#1-@H?v!{={_gJSjX;hAz4jwX?-W*VL^nY;<9x>8e zmBRPaY4`=WPb9^ZhV#+PW(MK!-ifuq|D9aM*CMbJW%@4Rx5j^sY2FLhH=RE*GAA-S z{=^V92%XURmP~(^b^a;>T!o9yHhhG(AXjHQnAeBrni_t27E|-&h)+sMI47&ge@L~v%%0U7*pfsZ&Yo@T{O zn3(tC`Ub)~`NRr@c4B}{vIzNT(F=Mh;8_H@3{zwLfx)s*XsDNuMU0+*vIJ%ioCJ6r zj#S^ypk=|NXcWs)K1+a5t`Umr&l3>-H|z)qIH&z7;h=v5wX}ob#@##re*%O4O)i4t z3_6WG$&q7NpAHQCIS#znbH5A0qDTp*`i-#G zRAV?wh@)9&eFcineuN7EGs<~8fv?+gQJkOxqAZM;>C_FVWIM73$O?Rrm!v ze?39kFwG^99mg3W?QerwC=KD}FIh}#mpf3Ef_(9qY ztp)1FT?ef=X#DY3l=__$S=^g;Lpn?U**$pfg`G}hrjKuo=j<8y|5o5ual^O3jk%;Y z{8RVL3__Eym)ITxoL;#W>Vm|;f66NOn})xk$XIx~TV=qnJ~$1x8gH@}3mecJ^m-FH zZEyM;{nDF(#(;jc(LJ-M(FY!uJsDuX*asorUwtxC9tT8>6PWL*ZuIGy=><)Finl-} z?m;F#aYh4rjX2K?gihZl4?>MzK>xNz7xWcp;cMH6PnYy`f$H_s+@3xezVEgt!{wLJ zS3m)7T56_`)I6D40A0ymex%B0ePlkC;P#YrG6=gTwle6<$$(nmKeGijs1~-QEK+AQ zr;J6XyU~L!=)q#?!QVSQNJZrj zZ+iLn&?cSJMDr>r{HUybE`XyZI- z<2Ri)($Yo`z6)-4LO)`i+gK5(wM(rGJZ04?zYXyz->uGf4zJa_s4YX1@NE3iIR@u4 zIFG?{2E6MuXd(f3Y4FlL$`bBBo*!}i_Tnc_pF{wqXl~$A5mDM5aHn)~K;dZja0%Mv-3}*Ke}lnXL;g{)1A3s-XuXAW`d0PJ zDD1ZRr>AT4$4zbi39`;T(z)=Zw03%InLx)ql-<^XH-|iC1eQ>#5eYO-kI6yEq5T~y zm{~v?HXxk~n#o|3RunEg#oV;5aSx>bj^XtN+xi(=m>;kA1zXUc1e`Fvp8y=-k^=`H zzWrVuI@|6anVei-s({z6I)COl(s^B z15(;(^5`=%PLW(b%*XdKxE;O2hdkgyUF4d7!!$8svl)Ap0r^w7EqER|TiA^^({CAm zLhM?q`YWts6Y7J@rplqJu)lWaXHYtkBJrSVK zb6-4%>O#pJY&T+I{1=Z&=Y~^Id`sZ7o(cJ+r|26;kQSpyC%g@{zK?}-C^^?#`oZjR zd^!!lDi8NKuqMI(AEy#xTjHnvBoSu{T+_S(wL7#YUY?{^OOeYwXCwMmfFd9{AI0iu z=4P>${A>v)RkH{ausyAU60SliatE{O z73e%n@F3!FNLemj1P1n&O0lg)TG6C9qT*XR-b zK88Q^s1+-X)W$`b3*HbbMSPG&+^A@v{E(I0k8fN-K&DYXq)D0R1Ou){&6RBRJ2}=v zAyu5HSIsSp2wRB=tVPxPijb|2=SvT)hk?`jk?I5Z9ID^?q8tW2+B@ABS zf+Yc|&txFHJ=JCmtA93QIR@u2AnRlV0V^0=$v~Fy`HZb*FvMUD0}E(Z57csqlA48|GI zVo=kdOpKPqB1a$O+B*NK9Dw;|Rq*LKlplVk2;Ma#= zDiyx0I~88tg&*JXIfFRI^zGJyKe$11JfL3z;|1k2C zSXV?;tQLhcKf)8120)"}, + "infra": {"name": "Нагрузка на инфраструктуру", "formula": "Нагрузка = server + network + power (по состояниям)", "result": "0–50 баллов"}, + "history": {"name": "История обслуживания", "formula": "Присваивается вес: none→max, sla→min", "result": "Зависит от типа"}, + "object_index": {"name": "Индекс объекта", "formula": "I = risk×0.4 + complexity×0.3 + infra×0.2 + history×0.1", "result": "0–100 → Класс A/B/C/D"}, + "engineer": {"name": "Оценка инженера", "formula": "ES = SLA×0.25 + отклик×0.20 + решение×0.20 + диагноз×0.15 + переоткрытия×0.10 + риски×0.10 → grade", "result": "0–100 → Junior/Middle/Strong/Senior"}, + "ecs": {"name": "Контроль инженера (ECS)", "formula": "ECS = SLA×0.30 + задачи×0.25 + инциденты×0.20 + команда×0.15 + координация×0.10", "result": "0–100"}, + "shs": {"name": "SHS (Service Health Score)", "formula": "SHS = SLA×0.22 + выручка×0.18 + удержание×0.18 + инженеры×0.15 + инциденты×0.12 + продажи×0.10 + эффективность×0.05", "result": "0–100 → Crisis/Risk/Stable/Growth"}, + "ceo_shs": {"name": "CEO SHS", "formula": "CEO SHS = MRR×0.25 + SLA×0.20 + удержание×0.20 + продукт-ть×0.15 + конверсия×0.10 + инциденты×0.10", "result": "0–100"}, + "ssi": {"name": "SLA Stability Index", "formula": "SSI = SLA_compliance × (1 - Breach_Severity)", "result": "0–100"}, + "isi": {"name": "Incident Stability Index", "formula": "ISI = max(0, min(100, 100 - weighted_rate×20))", "result": "0–100"}, + "bonus": {"name": "Бонус инженера", "formula": "% премии = f(оценка): ≥90→20%, 80–89→10%, <80→0%", "result": "0%–20%"}, + "retention": {"name": "Удержание клиентов", "formula": "Удержание = base - lost_key×10% (per key client)", "result": "0–100%"}, + "rules": {"name": "Правила автоматизации", "formula": "Пороги: SHS<70→Crisis, SLA<90%→Warning, Retention<90%→Critical", "result": "Триггеры алертов"}, +} + +router = APIRouter() +from app.templating import templates + +def now_str(): + return datetime.now().strftime("%d.%m.%Y %H:%M") + +def ctx(request, user, **kw): + c = {"request": request, "user": user, "now": now_str()} + c.update(kw) + return c + + +async def load_quick_menu(db: AsyncSession, role: str) -> list: + result = await db.execute( + select(QuickMenuItem).where(QuickMenuItem.role == role).order_by(QuickMenuItem.sort_order, QuickMenuItem.id) + ) + return result.scalars().all() + + +# ===================== USERS ===================== + +@router.get("/service/users") +async def users_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + result = await db.execute(select(User).order_by(User.role, User.full_name)) + return templates.TemplateResponse("pages/users.html", ctx(request, user=user, users=result.scalars().all(), active_page="users", title="Сотрудники")) + +@router.post("/service/users/create") +async def users_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + from app.auth import hash_password + new_user = User( + login=form.get("login"), + password_hash=hash_password(form.get("password")), + full_name=form.get("full_name"), role=form.get("role", "technician"), + phone=form.get("phone", ""), email=form.get("email", ""), + is_active=form.get("is_active", "1") == "1", + ) + db.add(new_user); await db.flush() + await audit_log(db, user["id"], "user_create", f"Создан пользователь {form.get('login')}", request.client.host or "") + return RedirectResponse(url="/service/users", status_code=302) + +@router.post("/service/users/edit") +async def users_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + u = await db.get(User, int(form.get("id"))) + if not u: return RedirectResponse(url="/service/users", status_code=302) + u.full_name = form.get("full_name", u.full_name) + u.role = form.get("role", u.role) + u.phone = form.get("phone", u.phone) + u.email = form.get("email", u.email) + u.is_active = form.get("is_active") == "1" + from app.auth import hash_password + pwd = form.get("password", "") + if pwd: u.password_hash = hash_password(pwd) + await db.flush() + await audit_log(db, user["id"], "user_edit", f"Изменён пользователь {u.login}", request.client.host or "") + return RedirectResponse(url="/service/users", status_code=302) + +@router.post("/service/users/delete") +async def users_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + u = await db.get(User, int((await request.form()).get("id"))) + if u and u.role != "owner": await db.delete(u); await db.flush() + return RedirectResponse(url="/service/users", status_code=302) + + +# ===================== CUSTOMERS ===================== + +@router.get("/service/customers") +async def customers_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer")), search: str = Query(""), error: str = Query("")): + sql = select(Customer) + if search: sql = sql.where(or_(Customer.name.ilike(f"%{search}%"), Customer.inn.ilike(f"%{search}%"))) + result = await db.execute(sql.order_by(Customer.name)) + customers = result.scalars().all() + data = [] + for c in customers: + oc = (await db.execute(select(func.count(Object.id)).where(Object.customer_id == c.id))).scalar() or 0 + sc = (await db.execute(select(func.count(SLAContract.id)).where(and_(SLAContract.customer_id == c.id, SLAContract.status == "active")))).scalar() or 0 + data.append({"c": c, "objects_count": oc, "active_contracts": sc}) + err_msg = {"inn_invalid": "ИНН должен содержать 10 или 12 цифр"}.get(error, "") + return templates.TemplateResponse("pages/customers.html", ctx(request, user=user, customers=data, search=search, active_page="customers", title="Клиенты", error=err_msg)) + +@router.post("/service/customers/create") +async def customers_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + form = await request.form() + inn = form.get("inn", "").strip() + if not inn or not inn.isdigit() or len(inn) not in (10, 12): + return RedirectResponse(url="/service/customers?error=inn_invalid", status_code=302) + c = Customer(name=form.get("name"), inn=inn, kpp=form.get("kpp",""), legal_address=form.get("legal_address",""), + contact_person=form.get("contact_person",""), contact_phone=form.get("contact_phone",""), contact_email=form.get("contact_email",""), + status=form.get("status","active"), notes=form.get("notes",""), created_by=user["id"]) + db.add(c); await db.flush() + return RedirectResponse(url="/service/customers", status_code=302) + +@router.post("/service/customers/edit") +async def customers_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + form = await request.form() + c = await db.get(Customer, int(form.get("id"))) + if not c: return RedirectResponse(url="/service/customers", status_code=302) + for f in ("name","inn","kpp","legal_address","contact_person","contact_phone","contact_email","status","notes"): + if form.get(f) is not None: setattr(c, f, form.get(f)) + await db.flush() + return RedirectResponse(url="/service/customers", status_code=302) + +@router.post("/service/customers/delete") +async def customers_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + c = await db.get(Customer, int((await request.form()).get("id"))) + if c: await db.delete(c); await db.flush() + return RedirectResponse(url="/service/customers", status_code=302) + + +# ===================== OBJECTS ===================== + +@router.get("/service/objects") +async def objects_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer")), search: str = Query(""), customer_id: int = Query(None)): + sql = select(Object).options(joinedload(Object.customer)) + if search: sql = sql.where(or_(Object.name.ilike(f"%{search}%"), Object.address.ilike(f"%{search}%"))) + if customer_id: sql = sql.where(Object.customer_id == customer_id) + result = await db.execute(sql.order_by(Object.name)) + objects = result.unique().scalars().all() + cust_res = await db.execute(select(Customer).where(Customer.status == "active").order_by(Customer.name)) + customers = cust_res.scalars().all() + return templates.TemplateResponse("pages/objects.html", ctx(request, user=user, objects=objects, customers_list=customers, search=search, customer_id=customer_id, active_page="objects", title="Объекты")) + +@router.post("/service/objects/create") +async def objects_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + form = await request.form() + o = Object(name=form.get("name"), customer_id=int(form.get("customer_id")) if form.get("customer_id") else None, + object_type=form.get("object_type",""), address=form.get("address",""), contact_person=form.get("contact_person",""), + contact_phone=form.get("contact_phone",""), status=form.get("status","active"), notes=form.get("notes",""), created_by=user["id"]) + db.add(o); await db.flush() + return RedirectResponse(url="/service/objects", status_code=302) + +@router.post("/service/objects/edit") +async def objects_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + form = await request.form() + try: + oid = int(form.get("id")) + except (ValueError, TypeError): + return RedirectResponse(url="/service/objects", status_code=302) + o = await db.get(Object, oid) + if not o: return RedirectResponse(url="/service/objects", status_code=302) + for f in ("name","customer_id","object_type","address","contact_person","contact_phone","status","notes"): + val = form.get(f) + if val is not None: + if f == "customer_id": + try: + val = int(val) if val else None + except (ValueError, TypeError): + val = None + setattr(o, f, val) + await db.flush() + return RedirectResponse(url="/service/objects", status_code=302) + +@router.post("/service/objects/delete") +async def objects_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + o = await db.get(Object, int((await request.form()).get("id"))) + if o: await db.delete(o); await db.flush() + return RedirectResponse(url="/service/objects", status_code=302) + + +# ===================== ASSIGNMENTS ===================== + +@router.get("/service/assignments") +async def assignments_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + objects = (await db.execute(select(Object).where(Object.status == "active").order_by(Object.name))).scalars().all() + users = (await db.execute(select(User).where(and_(User.is_active, User.role.in_(["engineer","technician"]))).order_by(User.full_name))).scalars().all() + result = await db.execute( + select(ObjectAssignment).options( + joinedload(ObjectAssignment.object), + joinedload(ObjectAssignment.user) + ).where(ObjectAssignment.unassigned_at.is_(None)).order_by(ObjectAssignment.assigned_at.desc()) + ) + assignments = result.unique().scalars().all() + return templates.TemplateResponse("pages/assignments.html", ctx(request, user=user, objects=objects, users=users, assignments=assignments, active_page="assignments", title="Назначение сотрудников")) + +@router.post("/service/assignments/create") +async def assignments_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + u = await db.get(User, int(form.get("user_id"))) + a = ObjectAssignment(object_id=int(form.get("object_id")), user_id=int(form.get("user_id")), role=u.role if u else "technician") + db.add(a); await db.flush() + return RedirectResponse(url="/service/assignments", status_code=302) + +@router.post("/service/assignments/unassign") +async def assignments_unassign(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + a = await db.get(ObjectAssignment, int((await request.form()).get("id"))) + if a: a.unassigned_at = datetime.now(); await db.flush() + return RedirectResponse(url="/service/assignments", status_code=302) + + +# ===================== SLA ===================== + +@router.get("/service/sla") +async def sla_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer")), object_id: int = Query(None)): + sql = select(SLAContract).options(joinedload(SLAContract.object), joinedload(SLAContract.customer)) + if object_id: sql = sql.where(SLAContract.object_id == object_id) + result = await db.execute(sql.order_by(SLAContract.created_at.desc())) + contracts = result.unique().scalars().all() + objects = (await db.execute(select(Object).where(Object.status.in_(["active","audit"])).order_by(Object.name))).scalars().all() + return templates.TemplateResponse("pages/sla.html", ctx(request, user=user, contracts=contracts, objects=objects, sel_object_id=object_id, active_page="sla", title="SLA контракты")) + +@router.post("/service/sla/create") +async def sla_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + form = await request.form() + try: + oid = int(form.get("object_id")) if form.get("object_id") else None + except (ValueError, TypeError): + oid = None + if not oid: + return RedirectResponse(url="/service/sla", status_code=302) + try: + price = float(form.get("sla_price_monthly", 0)) if form.get("sla_price_monthly") else 0 + except (ValueError, TypeError): + price = 0 + try: + start = datetime.strptime(form.get("start_date"), "%Y-%m-%d").date() if form.get("start_date") else None + except (ValueError, TypeError): + start = None + try: + end = datetime.strptime(form.get("end_date"), "%Y-%m-%d").date() if form.get("end_date") else None + except (ValueError, TypeError): + end = None + sc = SLAContract(object_id=oid, service_level=form.get("service_level", "start"), + sla_price_monthly=price, start_date=start, end_date=end, status=form.get("status", "negotiation")) + db.add(sc); await db.flush() + return RedirectResponse(url="/service/sla", status_code=302) + +@router.post("/service/sla/edit") +async def sla_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + form = await request.form() + try: + sc_id = int(form.get("id")) + except (ValueError, TypeError): + return RedirectResponse(url="/service/sla", status_code=302) + sc = await db.get(SLAContract, sc_id) + if not sc: return RedirectResponse(url="/service/sla", status_code=302) + for f in ("object_id","service_level","sla_price_monthly","start_date","end_date","status"): + val = form.get(f) + if val is not None: + if f == "object_id": + try: + val = int(val) if val else None + except (ValueError, TypeError): + val = None + elif f == "sla_price_monthly": + try: + val = float(val) if val else 0 + except (ValueError, TypeError): + val = 0 + elif f in ("start_date","end_date"): + try: + val = datetime.strptime(val,"%Y-%m-%d").date() if val else None + except (ValueError, TypeError): + val = None + setattr(sc, f, val) + await db.flush() + return RedirectResponse(url="/service/sla", status_code=302) + + +# ===================== CEO Dashboard ===================== + +@router.get("/service/ceo") +async def ceo_dashboard(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + shs_history = (await db.execute(select(SHSRecord).order_by(SHSRecord.id.desc()).limit(12))).scalars().all() + latest_shs = shs_history[0] if shs_history else None + mrr = float((await db.execute(select(func.sum(SLAContract.sla_price_monthly)).where(SLAContract.status == "active"))).scalar() or 0) + active_objects = (await db.execute(select(func.count(Object.id)).where(Object.status == "active"))).scalar() or 0 + active_contracts = (await db.execute(select(func.count(SLAContract.id)).where(SLAContract.status == "active"))).scalar() or 0 + active_engineers = (await db.execute(select(func.count(User.id)).where(and_(User.is_active, User.role.in_(["engineer","technician"]))))).scalar() or 0 + compl_res = await db.execute(select(func.count(Task.id), func.sum(Task.sla_compliant.cast(Integer))).where(Task.closed_at != None)) + total_c, compliant_c = compl_res.first() or (0,0) + sla_comp = round((compliant_c / total_c) * 100, 1) if total_c > 0 else 100 + incidents_30 = (await db.execute(select(func.count(Incident.id)).where(Incident.created_at > datetime.now().replace(day=1)))).scalar() or 0 + kpi_data = (await db.execute( + select(EngineerKPI, User.full_name).join(User, EngineerKPI.engineer_id == User.id).order_by(EngineerKPI.period_end.desc()).limit(10) + )).all() + avg_es = (await db.execute(select(func.avg(EngineerKPI.engineer_score)).where(EngineerKPI.period_end > datetime.now().replace(day=1)))).scalar() or 0 + return templates.TemplateResponse("pages/ceo.html", ctx(request, user=user, shs_history=shs_history, latest_shs=latest_shs, mrr=mrr, + active_objects=active_objects, active_contracts=active_contracts, active_engineers=active_engineers, + sla_compliance=sla_comp, incidents_30=incidents_30, kpi_data=kpi_data, avg_es=round(float(avg_es),2), + active_page="ceo", title="CEO дашборд")) + + +# ===================== COEFFICIENTS ===================== + +@router.get("/service/coefficients") +async def coefficients_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + coeffs = (await db.execute(select(FormulaCoefficient).order_by(FormulaCoefficient.key))).scalars().all() + groups = defaultdict(list) + for c in coeffs: + prefix = c.formula_ref.split(".")[0] if c.formula_ref else "_other" + groups[prefix].append(c) + grouped = {} + for prefix in sorted(groups.keys()): + info = FORMULA_GROUPS.get(prefix, {"name": prefix, "formula": "", "result": ""}) + grouped[prefix] = {"info": info, "coeffs": groups[prefix]} + return templates.TemplateResponse("pages/coefficients.html", ctx(request, user=user, groups=grouped, active_page="coefficients", title="Коэффициенты")) + +@router.post("/service/coefficients/edit") +async def coefficients_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + c = await db.get(FormulaCoefficient, int(form.get("id"))) + if c: + try: c.value = float(form.get("value")) + except: c.value = form.get("value") + c.updated_by = user["id"]; await db.flush() + return RedirectResponse(url="/service/coefficients", status_code=302) + +@router.post("/service/coefficients/toggle") +async def coefficients_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + c = await db.get(FormulaCoefficient, int((await request.form()).get("id"))) + if c: c.is_active = not c.is_active; c.updated_by = user["id"]; await db.flush() + return RedirectResponse(url="/service/coefficients", status_code=302) + +@router.post("/service/coefficients/create") +async def coefficients_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + db.add(FormulaCoefficient(key=form.get("key"), value=float(form.get("value")), description=form.get("description",""), + formula_ref=form.get("formula_ref",""), updated_by=user["id"])) + await db.flush() + return RedirectResponse(url="/service/coefficients", status_code=302) + +@router.post("/service/coefficients/delete") +async def coefficients_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + c = await db.get(FormulaCoefficient, int((await request.form()).get("id"))) + if c: await db.delete(c); await db.flush() + return RedirectResponse(url="/service/coefficients", status_code=302) + + +# ===================== FORMULA TEST ===================== + +FORMULA_SANDBOX = { + "risk_score": { + "name": "Оценка рисков", + "inputs": [ + {"key": "no_archive", "label": "Нет архивации", "type": "checkbox"}, + {"key": "no_power_backup", "label": "Нет резервного питания", "type": "checkbox"}, + {"key": "no_regulations", "label": "Нет регламентов", "type": "checkbox"}, + {"key": "system_failures", "label": "Отказы системы", "type": "checkbox"}, + {"key": "no_documentation", "label": "Нет документации", "type": "checkbox"}, + ], + }, + "complexity": { + "name": "Сложность объекта", + "inputs": [ + {"key": "cameras", "label": "Кол-во камер", "type": "number", "default": 20}, + {"key": "access_points", "label": "Кол-во СКУД точек", "type": "number", "default": 5}, + {"key": "fire_type", "label": "Тип пожарной системы", "type": "select", "options": {"none": "Нет", "simple": "Простая", "medium": "Средняя", "complex": "Комплексная"}}, + {"key": "has_it", "label": "Есть IT-инфраструктура", "type": "checkbox"}, + ], + }, + "object_index": { + "name": "Индекс объекта", + "inputs": [ + {"key": "risk_score", "label": "Риск (0–100)", "type": "number", "default": 50}, + {"key": "complexity_score", "label": "Сложность (0–150)", "type": "number", "default": 50}, + {"key": "infra_score", "label": "Инфраструктура (0–50)", "type": "number", "default": 10}, + {"key": "history_score", "label": "История (0–30)", "type": "number", "default": 15}, + ], + }, + "engineer": { + "name": "Оценка инженера (Engineer Score)", + "inputs": [ + {"key": "sla_compliance", "label": "SLA Compliance", "type": "number", "default": 90}, + {"key": "response_time_score", "label": "Response Time Score", "type": "number", "default": 80}, + {"key": "resolution_time_score", "label": "Resolution Time Score", "type": "number", "default": 80}, + {"key": "diagnosis_accuracy", "label": "Diagnosis Accuracy", "type": "number", "default": 85}, + {"key": "reopen_rate_score", "label": "Reopen Rate Score", "type": "number", "default": 90}, + {"key": "risk_coverage_score", "label": "Risk Coverage Score", "type": "number", "default": 75}, + ], + }, + "ecs": { + "name": "Контроль инженера (ECS)", + "inputs": [ + {"key": "sla_control", "label": "SLA Control", "type": "number", "default": 85}, + {"key": "task_dist", "label": "Task Distribution", "type": "number", "default": 80}, + {"key": "incident_red", "label": "Incident Reduction", "type": "number", "default": 70}, + {"key": "team_perf", "label": "Team Performance", "type": "number", "default": 75}, + {"key": "response_coord", "label": "Response Coordination", "type": "number", "default": 80}, + ], + }, + "shs": { + "name": "Service Health Score (SHS)", + "inputs": [ + {"key": "sla_stability", "label": "SLA Stability", "type": "number", "default": 85}, + {"key": "revenue_stability", "label": "Revenue Stability", "type": "number", "default": 80}, + {"key": "retention", "label": "Retention", "type": "number", "default": 90}, + {"key": "engineer_performance", "label": "Engineer Performance", "type": "number", "default": 75}, + {"key": "incident_stability", "label": "Incident Stability", "type": "number", "default": 80}, + {"key": "sales_flow", "label": "Sales Flow", "type": "number", "default": 70}, + {"key": "operational_efficiency", "label": "Operational Efficiency", "type": "number", "default": 75}, + ], + }, + "ceo_shs": { + "name": "CEO SHS", + "inputs": [ + {"key": "mrr_growth", "label": "MRR Growth", "type": "number", "default": 80}, + {"key": "sla_compliance", "label": "SLA Compliance", "type": "number", "default": 90}, + {"key": "retention", "label": "Retention", "type": "number", "default": 85}, + {"key": "productivity", "label": "Productivity", "type": "number", "default": 75}, + {"key": "conversion", "label": "Conversion", "type": "number", "default": 70}, + {"key": "incident_stability", "label": "Incident Stability", "type": "number", "default": 80}, + ], + }, +} + + +@router.get("/service/coefficients/test") +async def formula_test_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")), group: str = Query("")): + coeffs = (await db.execute(select(FormulaCoefficient).order_by(FormulaCoefficient.key))).scalars().all() + groups = defaultdict(list) + for c in coeffs: + prefix = c.formula_ref.split(".")[0] if c.formula_ref else "_other" + groups[prefix].append(c) + formula_list = [] + for prefix in sorted(groups.keys()): + info = FORMULA_GROUPS.get(prefix, {"name": prefix, "formula": "", "result": ""}) + sb = FORMULA_SANDBOX.get(prefix) + formula_list.append({ + "key": prefix, + "name": info["name"], + "formula": info["formula"], + "result": info["result"], + "sandbox": sb, + "coeffs": groups[prefix], + }) + selected = group if group else (formula_list[0]["key"] if formula_list else "") + return templates.TemplateResponse("pages/formula_test.html", ctx(request, user=user, formulas=formula_list, selected=selected, active_page="coefficients", title="Тест формул")) + + +@router.post("/service/coefficients/test/{formula_name}") +async def formula_test_calc(request: Request, formula_name: str, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + try: + if formula_name == "risk_score": + result = await calc_engine.calc_risk_score( + db, + no_archive=form.get("no_archive") == "1", + no_power_backup=form.get("no_power_backup") == "1", + no_regulations=form.get("no_regulations") == "1", + system_failures=form.get("system_failures") == "1", + no_documentation=form.get("no_documentation") == "1", + ) + label = await calc_engine.risk_label(db, result) + return JSONResponse({"result": f"{result:.1f} баллов — {label}"}) + + elif formula_name == "complexity": + result = await calc_engine.calc_complexity( + db, + cameras=int(form.get("cameras", 0)), + access_points=int(form.get("access_points", 0)), + fire_type=form.get("fire_type", "none"), + has_it=form.get("has_it") == "1", + ) + label = calc_engine.complexity_label(result) + return JSONResponse({"result": f"{result:.1f} баллов — {label}"}) + + elif formula_name == "object_index": + # These are pre-calculated scores, not the same as coefficient-based. + # We use the weighted formula directly. + risk = float(form.get("risk_score", 50)) + complexity = float(form.get("complexity_score", 50)) + infra = float(form.get("infra_score", 10)) + history = float(form.get("history_score", 15)) + result = await calc_engine.calc_object_index(db, risk, complexity, infra, history) + cls = calc_engine.object_class(result) + label = calc_engine.object_class_label(result) + return JSONResponse({"result": f"Индекс: {result} — Класс {cls} ({label})"}) + + elif formula_name == "engineer": + result = await calc_engine.calc_engineer_score( + db, + sla_compliance=float(form.get("sla_compliance", 90)), + response_time_score=float(form.get("response_time_score", 80)), + resolution_time_score=float(form.get("resolution_time_score", 80)), + diagnosis_accuracy=float(form.get("diagnosis_accuracy", 85)), + reopen_rate_score=float(form.get("reopen_rate_score", 90)), + risk_coverage_score=float(form.get("risk_coverage_score", 75)), + ) + grade = await calc_engine.engineer_grade(db, result) + return JSONResponse({"result": f"Engineer Score: {result} — {grade}"}) + + elif formula_name == "ecs": + result = await calc_engine.calc_engineer_control_score( + db, + sla_control=float(form.get("sla_control", 85)), + task_dist=float(form.get("task_dist", 80)), + incident_red=float(form.get("incident_red", 70)), + team_perf=float(form.get("team_perf", 75)), + response_coord=float(form.get("response_coord", 80)), + ) + return JSONResponse({"result": f"ECS: {result} баллов"}) + + elif formula_name == "shs": + result = await calc_engine.calc_shs( + db, + sla_stability=float(form.get("sla_stability", 85)), + revenue_stability=float(form.get("revenue_stability", 80)), + retention=float(form.get("retention", 90)), + engineer_performance=float(form.get("engineer_performance", 75)), + incident_stability=float(form.get("incident_stability", 80)), + sales_flow=float(form.get("sales_flow", 70)), + operational_efficiency=float(form.get("operational_efficiency", 75)), + ) + zone = await calc_engine.shs_zone(db, result) + return JSONResponse({"result": f"SHS: {result} — {zone['label']} ({zone['zone']})"}) + + elif formula_name == "ceo_shs": + result = await calc_engine.calc_ceo_shs( + db, + mrr_growth=float(form.get("mrr_growth", 80)), + sla_compliance=float(form.get("sla_compliance", 90)), + retention=float(form.get("retention", 85)), + productivity=float(form.get("productivity", 75)), + conversion=float(form.get("conversion", 70)), + incident_stability=float(form.get("incident_stability", 80)), + ) + return JSONResponse({"result": f"CEO SHS: {result} баллов"}) + + return JSONResponse({"result": "Формула не поддерживается в песочнице"}) + except Exception as e: + return JSONResponse({"result": f"Ошибка: {str(e)}"}) + + +# ===================== TASKS ===================== + +@router.get("/service/tasks") +async def tasks_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician")), status_filter: str = Query(""), search: str = Query("")): + sql = select(Task).options(joinedload(Task.object), joinedload(Task.assignee)) + if user["role"] == "technician": sql = sql.where(Task.assigned_to == user["id"]) + if status_filter: sql = sql.where(Task.status == status_filter) + if search: sql = sql.where(or_(Task.title.ilike(f"%{search}%"))) + result = await db.execute(sql.order_by(Task.created_at.desc())) + tasks = result.unique().scalars().all() + objects = (await db.execute(select(Object).where(Object.status == "active").order_by(Object.name))).scalars().all() + users = (await db.execute(select(User).where(and_(User.is_active, User.role.in_(["engineer","technician"]))).order_by(User.full_name))).scalars().all() + return templates.TemplateResponse("pages/tasks.html", ctx(request, user=user, tasks=tasks, objects=objects, users=users, status_filter=status_filter, search=search, active_page="tasks", title="Задачи")) + +@router.post("/service/tasks/create") +async def tasks_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + form = await request.form() + db.add(Task(title=form.get("title"), description=form.get("description",""), object_id=int(form.get("object_id")), + assigned_to=int(form.get("assigned_to")) if form.get("assigned_to") else None, + priority=form.get("priority","P3"), + deadline=datetime.strptime(form.get("deadline"),"%Y-%m-%d").date() if form.get("deadline") else None, + created_by=user["id"])) + await db.flush() + return RedirectResponse(url="/service/tasks", status_code=302) + +@router.post("/service/tasks/close") +async def tasks_close(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician"))): + t = await db.get(Task, int((await request.form()).get("id"))) + if t and (user["role"] in ("owner","engineer") or t.assigned_to == user["id"]): + t.status = "completed"; t.closed_at = datetime.now(); await db.flush() + return RedirectResponse(url="/service/tasks", status_code=302) + + +# ===================== REPORTS ===================== + +@router.get("/service/reports") +async def reports_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician")), type_filter: str = Query("")): + sql = select(Report).options(joinedload(Report.object)) + if user["role"] in ("engineer","technician"): sql = sql.where(Report.created_by == user["id"]) + if type_filter: sql = sql.where(Report.report_type == type_filter) + result = await db.execute(sql.order_by(Report.created_at.desc())) + reports = result.unique().scalars().all() + objects = (await db.execute(select(Object).where(Object.status.in_(["active","audit"])).order_by(Object.name))).scalars().all() + return templates.TemplateResponse("pages/reports.html", ctx(request, user=user, reports=reports, objects=objects, type_filter=type_filter, active_page="reports", title="Отчёты")) + +@router.post("/service/reports/create") +async def reports_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician"))): + form = await request.form() + db.add(Report(object_id=int(form.get("object_id")), report_type=form.get("report_type","inspection"), title=form.get("title"), + description=form.get("description",""), findings=form.get("findings",""), recommendations=form.get("recommendations",""), + work_done=form.get("work_done",""), cause_classification=form.get("cause_classification",""), + status=form.get("status","draft"), result_status=form.get("result_status","ok"), created_by=user["id"])) + await db.flush() + return RedirectResponse(url="/service/reports", status_code=302) + + +# ===================== INCIDENTS ===================== + +@router.get("/service/incidents") +async def incidents_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician")), severity: str = Query(""), status_filter: str = Query("")): + sql = select(Incident).options(joinedload(Incident.object), joinedload(Incident.assignee)) + if user["role"] == "technician": sql = sql.where(Incident.assigned_to == user["id"]) + if severity: sql = sql.where(Incident.severity == severity) + if status_filter: sql = sql.where(Incident.status == status_filter) + result = await db.execute(sql.order_by(Incident.created_at.desc())) + incidents = result.unique().scalars().all() + objects = (await db.execute(select(Object).where(Object.status == "active").order_by(Object.name))).scalars().all() + users = (await db.execute(select(User).where(User.is_active).order_by(User.full_name))).scalars().all() + return templates.TemplateResponse("pages/incidents.html", ctx(request, user=user, incidents=incidents, objects=objects, users=users, severity=severity, status_filter=status_filter, active_page="incidents", title="Инциденты")) + +@router.post("/service/incidents/create") +async def incidents_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + form = await request.form() + db.add(Incident(title=form.get("title"), description=form.get("description",""), object_id=int(form.get("object_id")), + assigned_to=int(form.get("assigned_to")) if form.get("assigned_to") else None, + severity=form.get("severity","P3"), reported_by=user["id"])) + await db.flush() + return RedirectResponse(url="/service/incidents", status_code=302) + +@router.post("/service/incidents/resolve") +async def incidents_resolve(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician"))): + form = await request.form() + i = await db.get(Incident, int(form.get("id"))) + if i: i.status = "resolved"; i.resolution = form.get("resolution",""); i.resolved_at = datetime.now(); await db.flush() + return RedirectResponse(url="/service/incidents", status_code=302) + + +# ===================== QUESTIONNAIRE ===================== + +@router.get("/service/questionnaire") +async def questionnaire_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + result = await db.execute( + select(QuestionnaireSession, User.full_name).join(User, QuestionnaireSession.created_by == User.id).order_by(QuestionnaireSession.created_at.desc()).limit(20) + ) + return templates.TemplateResponse("pages/questionnaire.html", ctx(request, user=user, sessions=result.all(), active_page="questionnaire", title="Опросник")) + +@router.get("/service/questionnaire/step/{step}") +async def questionnaire_step(request: Request, step: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer")), session_id: int = Query(None)): + items = (await db.execute( + select(QuestionnaireItem).where(and_(QuestionnaireItem.is_active, QuestionnaireItem.step == step)).order_by(QuestionnaireItem.sort_order, QuestionnaireItem.id) + )).scalars().all() + return templates.TemplateResponse("pages/questionnaire_step.html", ctx(request, user=user, items=items, step=step, session_id=session_id, active_page="questionnaire", title=f"Опросник — шаг {step}")) + + +@router.post("/service/questionnaire/save/{step}") +async def questionnaire_save(step: int, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + form = await request.form() + session_id = form.get("session_id") + if session_id: + sess = await db.get(QuestionnaireSession, int(session_id)) + else: + sess = QuestionnaireSession(created_by=user["id"], status="draft", current_step=step) + db.add(sess); await db.flush() + sess.current_step = step + items = (await db.execute( + select(QuestionnaireItem).where(and_(QuestionnaireItem.is_active, QuestionnaireItem.step == step)) + )).scalars().all() + for item in items: + val = form.get(item.question_key) + if val is not None: + existing = (await db.execute( + select(QuestionnaireAnswer).where(and_( + QuestionnaireAnswer.session_id == sess.id, + QuestionnaireAnswer.question_key == item.question_key + )) + )).scalar_one_or_none() + if existing: + existing.answer_value = val + else: + db.add(QuestionnaireAnswer(session_id=sess.id, step=step, question_key=item.question_key, answer_value=val)) + await db.flush() + next_step = step + 1 + if step >= 5 or form.get("action") == "complete": + sess.status = "completed" + await db.flush() + return RedirectResponse(url="/service/questionnaire", status_code=302) + return RedirectResponse(url=f"/service/questionnaire/step/{next_step}", status_code=302) + + +# ===================== QUESTIONNAIRE CONFIG ===================== + +@router.get("/service/questionnaire-config") +async def questionnaire_config(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")), step: int = Query(None)): + sql = select(QuestionnaireItem) + if step: sql = sql.where(QuestionnaireItem.step == step) + items = (await db.execute(sql.order_by(QuestionnaireItem.step, QuestionnaireItem.sort_order, QuestionnaireItem.id))).scalars().all() + return templates.TemplateResponse("pages/questionnaire_config.html", ctx(request, user=user, items=items, sel_step=step, active_page="questionnaire-config", title="Настройка опросника")) + +@router.post("/service/questionnaire-config/create") +async def questionnaire_config_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + db.add(QuestionnaireItem(step=int(form.get("step",1)), section=form.get("section",""), question_key=form.get("question_key"), + label=form.get("label"), type=form.get("type","text"), options="{}", required=form.get("required","0")=="1", + sort_order=int(form.get("sort_order",0)), help_text=form.get("help_text",""))) + await db.flush() + return RedirectResponse(url="/service/questionnaire-config", status_code=302) + +@router.post("/service/questionnaire-config/edit") +async def questionnaire_config_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + qi = await db.get(QuestionnaireItem, int(form.get("id"))) + if qi: + for f in ("step","section","question_key","label","type","options","required","sort_order","help_text"): + val = form.get(f) + if val is not None: + if f in ("step","sort_order"): val = int(val) + elif f == "required": val = val == "1" + setattr(qi, f, val) + await db.flush() + return RedirectResponse(url="/service/questionnaire-config", status_code=302) + +@router.post("/service/questionnaire-config/toggle") +async def questionnaire_config_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + qi = await db.get(QuestionnaireItem, int((await request.form()).get("id"))) + if qi: qi.is_active = not qi.is_active; await db.flush() + return RedirectResponse(url="/service/questionnaire-config", status_code=302) + +@router.post("/service/questionnaire-config/delete") +async def questionnaire_config_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + qi = await db.get(QuestionnaireItem, int((await request.form()).get("id"))) + if qi: await db.delete(qi); await db.flush() + return RedirectResponse(url="/service/questionnaire-config", status_code=302) + + +# ===================== PASSPORTS ===================== + +@router.get("/service/passports") +async def passports_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))): + result = await db.execute( + select(ObjectPassport).options( + joinedload(ObjectPassport.object), + joinedload(ObjectPassport.creator) + ).order_by(ObjectPassport.created_at.desc()) + ) + return templates.TemplateResponse("pages/passports.html", ctx(request, user=user, passports=result.unique().scalars().all(), active_page="passports", title="Паспорта объектов")) + + +# ===================== CHECKLIST ===================== + +@router.get("/service/checklist") +async def checklist_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("technician"))): + result = await db.execute( + select(Task).options(joinedload(Task.object)).where( + and_(Task.assigned_to == user["id"], Task.status.in_(["open","in_progress"])) + ).order_by(Task.created_at.desc()) + ) + return templates.TemplateResponse("pages/checklist.html", ctx(request, user=user, tasks=result.unique().scalars().all(), active_page="checklist", title="Чек-лист")) + +@router.post("/service/checklist/complete") +async def checklist_complete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("technician"))): + form = await request.form() + t = await db.get(Task, int(form.get("task_id"))) + if t and t.assigned_to == user["id"]: + checks = {k: form.get(k) == "1" for k in ("arrival_fixed","photo_before","power_check","cameras_check","acs_check","fire_check","fault_found","fault_fixed","system_test","photo_after")} + t.status = "completed"; t.closed_at = datetime.now(); t.checklist_data = checks; t.result_notes = form.get("notes","") + await db.flush() + return RedirectResponse(url="/service/checklist", status_code=302) + + +# ===================== BLOG ===================== + +@router.get("/service/blog") +async def blog_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + result = await db.execute(select(BlogPost, User.full_name).join(User, BlogPost.author_id == User.id).order_by(BlogPost.created_at.desc())) + return templates.TemplateResponse("pages/blog.html", ctx(request, user=user, posts=result.all(), active_page="blog", title="Управление блогом")) + +@router.post("/service/blog/create") +async def blog_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + db.add(BlogPost(title=form.get("title"), slug=form.get("slug"), category=form.get("category",""), + status=form.get("status","draft"), excerpt=form.get("excerpt",""), content=form.get("content",""), author_id=user["id"])) + await db.flush() + return RedirectResponse(url="/service/blog", status_code=302) + +@router.post("/service/blog/edit") +async def blog_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + bp = await db.get(BlogPost, int(form.get("id"))) + if bp: + for f in ("title","slug","category","status","excerpt","content"): + if form.get(f) is not None: setattr(bp, f, form.get(f)) + if form.get("status") == "published" and not bp.published_at: bp.published_at = datetime.now() + await db.flush() + return RedirectResponse(url="/service/blog", status_code=302) + +@router.post("/service/blog/delete") +async def blog_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + bp = await db.get(BlogPost, int((await request.form()).get("id"))) + if bp: await db.delete(bp); await db.flush() + return RedirectResponse(url="/service/blog", status_code=302) + + +# ===================== QUICK MENU ===================== + +@router.get("/service/api/quick-menu") +async def quick_menu_api(request: Request, db: AsyncSession = Depends(get_db)): + user = request.session.get("user") + if not user: + return JSONResponse([]) + items = await load_quick_menu(db, user["role"]) + return JSONResponse([{"id": i.id, "label": i.label, "url": i.url} for i in items]) + + +@router.get("/service/quick-menu") +async def quick_menu_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + if user["role"] not in ("owner", "engineer"): + return HTMLResponse("Доступ запрещён", status_code=403) + items = await load_quick_menu(db, user["role"]) + return templates.TemplateResponse("pages/quickmenu.html", ctx(request, user=user, items=items, active_page="quickmenu", title="Быстрое меню")) + + +@router.post("/service/quick-menu/save") +async def quick_menu_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + if user["role"] not in ("owner", "engineer"): + return HTMLResponse("Доступ запрещён", status_code=403) + form = await request.form() + await db.execute(delete(QuickMenuItem).where(QuickMenuItem.role == user["role"], QuickMenuItem.created_by == user["id"])) + labels = form.getlist("label[]") + urls = form.getlist("url[]") + for i, (label, url) in enumerate(zip(labels, urls)): + if label.strip() and url.strip(): + db.add(QuickMenuItem(role=user["role"], label=label.strip(), url=url.strip(), sort_order=i, created_by=user["id"])) + await db.flush() + return RedirectResponse(url="/service/quick-menu", status_code=302) + + +@router.get("/service/quick-menu/tech") +async def quick_menu_tech_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("engineer"))): + items = await load_quick_menu(db, "technician") + return templates.TemplateResponse("pages/quickmenu.html", ctx(request, user=user, items=items, editing_tech=True, active_page="quickmenu-tech", title="Меню для техников")) + + +@router.post("/service/quick-menu/tech-save") +async def quick_menu_tech_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("engineer"))): + form = await request.form() + await db.execute(delete(QuickMenuItem).where(QuickMenuItem.role == "technician")) + labels = form.getlist("label[]") + urls = form.getlist("url[]") + for i, (label, url) in enumerate(zip(labels, urls)): + if label.strip() and url.strip(): + db.add(QuickMenuItem(role="technician", label=label.strip(), url=url.strip(), sort_order=i, created_by=user["id"])) + await db.flush() + return RedirectResponse(url="/service/quick-menu/tech", status_code=302) + + +# ===================== IDEAS ===================== + +@router.get("/service/ideas") +async def ideas_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + result = await db.execute(select(Idea).order_by(Idea.status, Idea.updated_at.desc())) + return templates.TemplateResponse("pages/ideas.html", ctx(request, user=user, ideas=result.scalars().all(), active_page="ideas", title="Идеи")) + + +@router.post("/service/ideas/create") +async def ideas_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + db.add(Idea(title=form.get("title"), description=form.get("description",""), created_by=user["id"])) + await db.flush() + return RedirectResponse(url="/service/ideas", status_code=302) + + +@router.post("/service/ideas/edit") +async def ideas_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + try: + idea_id = int(form.get("id")) + except (ValueError, TypeError): + return RedirectResponse(url="/service/ideas", status_code=302) + idea = await db.get(Idea, idea_id) + if idea: + idea.title = form.get("title", idea.title) + idea.description = form.get("description", idea.description) + await db.flush() + return RedirectResponse(url="/service/ideas", status_code=302) + + +@router.post("/service/ideas/status") +async def ideas_status(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + try: + idea_id = int(form.get("id")) + except (TypeError, ValueError): + return RedirectResponse(url="/service/ideas", status_code=302) + idea = await db.get(Idea, idea_id) + if idea: + statuses = {"normal": "in_progress", "in_progress": "completed", "completed": "normal"} + idea.status = statuses.get(idea.status, "normal") + await db.flush() + return RedirectResponse(url="/service/ideas", status_code=302) + + +@router.post("/service/ideas/delete") +async def ideas_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + idea = await db.get(Idea, int((await request.form()).get("id"))) + if idea: + await db.delete(idea) + await db.flush() + return RedirectResponse(url="/service/ideas", status_code=302) + + +# ===================== CEO CALC STUBS ===================== + +@router.post("/service/ceo/calc") +async def ceo_calc(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + # SHS stub — until the calculation pipeline is designed + return JSONResponse({"status": "stub", "message": "Расчёт SHS будет доработан после согласования формул с заказчиком"}) + + +@router.post("/service/ceo/kpi") +async def ceo_kpi(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + # KPI stub — until the calculation pipeline is designed + return JSONResponse({"status": "stub", "message": "Расчёт KPI будет доработан после согласования формул с заказчиком"}) + + +# ===================== CHARTS ===================== + +import calendar +from collections import defaultdict + + +@router.get("/service/charts") +async def charts_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + return templates.TemplateResponse("pages/charts.html", ctx(request, user=user, active_page="charts", title="Графики и диаграммы")) + + +@router.get("/service/api/charts/data") +async def charts_data( + metric: str = Query("tasks"), + group: str = Query("month"), + db: AsyncSession = Depends(get_db), + user: dict = Depends(require_role("owner")), +): + from app.models.models import Task, Incident, SHSRecord, EngineerKPI, SLAContract, Object + import datetime + + labels = [] + values = [] + datasets = [] + + if metric == "tasks": + result = await db.execute( + select(func.date_trunc(group, Task.created_at).label("period"), func.count(Task.id)) + .where(Task.created_at != None) + .group_by(text("period")) + .order_by(text("period")) + ) + rows = result.all() + labels = [r[0].strftime("%Y-%m") if r[0] else "" for r in rows] + values = [r[1] for r in rows] + datasets = [ + {"label": "Все задачи", "data": values}, + ] + # Also get by status + for status in ("open", "in_progress", "completed"): + r2 = await db.execute( + select(func.date_trunc(group, Task.created_at).label("period"), func.count(Task.id)) + .where(Task.status == status, Task.created_at != None) + .group_by(text("period")) + .order_by(text("period")) + ) + rows2 = r2.all() + if rows2: + datasets.append({"label": status, "data": [r[1] for r in rows2]}) + + elif metric == "incidents": + for sev in ("P1", "P2", "P3"): + result = await db.execute( + select(func.date_trunc(group, Incident.created_at).label("period"), func.count(Incident.id)) + .where(Incident.severity == sev, Incident.created_at != None) + .group_by(text("period")) + .order_by(text("period")) + ) + rows = result.all() + if rows: + if not labels: + labels = [r[0].strftime("%Y-%m") if r[0] else "" for r in rows] + datasets.append({"label": sev, "data": [r[1] for r in rows]}) + + elif metric == "shs": + result = await db.execute( + select(SHSRecord).order_by(SHSRecord.id.asc()) + ) + records = result.scalars().all() + labels = [r.period_end.strftime("%Y-%m") if r.period_end else "" for r in records] + datasets = [ + {"label": "SHS Score", "data": [float(r.shs_score) for r in records]}, + {"label": "SLA Stability", "data": [float(r.sla_stability) for r in records]}, + {"label": "Revenue Stability", "data": [float(r.revenue_stability) for r in records]}, + ] + + elif metric == "kpi": + result = await db.execute( + select(EngineerKPI).order_by(EngineerKPI.period_end.asc()).limit(50) + ) + kpis = result.scalars().all() + labels = [r.period_end.strftime("%Y-%m") for r in kpis] + datasets = [ + {"label": "Engineer Score", "data": [float(r.engineer_score or 0) for r in kpis]}, + {"label": "SLA Compliance", "data": [float(r.sla_compliance or 0) for r in kpis]}, + ] + + elif metric == "mrr": + result = await db.execute( + select(SLAContract.status, func.sum(SLAContract.sla_price_monthly)) + .group_by(SLAContract.status) + ) + rows = result.all() + labels = [r[0] for r in rows] + values = [float(r[1] or 0) for r in rows] + datasets = [{"label": "MRR", "data": values}] + + elif metric == "objects": + result = await db.execute( + select(Object.status, func.count(Object.id)).group_by(Object.status) + ) + rows = result.all() + labels = [r[0] for r in rows] + values = [r[1] for r in rows] + datasets = [{"label": "Объекты", "data": values}] + + return JSONResponse({"labels": labels, "datasets": datasets}) + + +# ===================== CASES ===================== + +@router.get("/service/cases") +async def cases_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + cases = (await db.execute(select(Case).order_by(Case.sort_order, Case.id))).scalars().all() + return templates.TemplateResponse("pages/cases.html", ctx(request, user=user, cases=cases, active_page="cases", title="Примеры из практики")) + +@router.post("/service/cases/create") +async def cases_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + db.add(Case(title=form.get("title"), text=form.get("text"), effect=form.get("effect",""), sort_order=int(form.get("sort_order",0)), is_active=form.get("is_active","1")=="1")) + await db.flush() + return RedirectResponse(url="/service/cases", status_code=302) + +@router.post("/service/cases/edit") +async def cases_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + form = await request.form() + c = await db.get(Case, int(form.get("id"))) + if c: + for f in ("title","text","effect","sort_order","is_active"): + val = form.get(f) + if val is not None: + if f == "sort_order": val = int(val) + elif f == "is_active": val = val == "1" + setattr(c, f, val) + await db.flush() + return RedirectResponse(url="/service/cases", status_code=302) + +@router.post("/service/cases/delete") +async def cases_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): + c = await db.get(Case, int((await request.form()).get("id"))) + if c: await db.delete(c); await db.flush() + return RedirectResponse(url="/service/cases", status_code=302) + + + diff --git a/py_service/app/services/__pycache__/docs_permissions.cpython-38.pyc b/py_service/app/services/__pycache__/docs_permissions.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b844caf7e4a3eac2121f10f3e2fafd2369de0aa GIT binary patch literal 2779 zcmb_e&u=706|U;8{xRd3@p#vP4Ft(73uL0ifenEiMpj51ODn($>p)hcM5ErWvbV=Q z-Lq9~J6ZLdcCRbof^zU-Mx6G>pTdblU31DU61Np1e6QLTwu3m)t$y|D)vKyk?|tu8 z|E}9@Gdyp6(VhH#g|RQFbMXn$c^jYkGl*o8=WNupe8kcBa&PL7d`>by7t>%AaHfLC zY!oUsi59U6CoL(K-l(lsWN^x5D5EE0)Il#suO-{)bi^-! zoK?Xhtl5%(#60#U`z^c6{=)WLdyi${24dH8{X96T_FsGJ8Mm=2(o({xL1ewsSTTdW zHdLQxrZTqw!FzXqb7ybw&ilK2rV1;_Tr2W2kuaL?KVy0ghCSa%X0z9g z(#Kh<%d)1N;cm=-?giMPR!Cn|5^ zJn+=x4BiN&FNF-8H+U6JqMFq%#4>&=IQ!_#EB62oc%!xZA-0O$amVGMM{|Tc zn$8BEUMESP2zjp|*GQpnl4EtzOEhlCG6o;@x&9uwOCTXPk{6w8Zop~e8o&Y0SqK7z z`4)x0LLSWFmYOf3C40>0Uga%f8sBE0+?)Hgzi3GgVBN0Twf~q;R_3A>wRafl+qGD9 z2xv=h(Ot56PzSYN^=kf@!{PJ>r#^B?WL!qblK52AycR?D@U|3l+HrNsuuJ~2Hy86r zw(1D0{xxGY_@SdOvUx1qbqx96nw4twhfJkZAx8@OD&NQ{x+<3fm@TpBr`%F*7n zL3vUreaQls^C;B$ntJb%Ngw(X0&bBD>u55(} z^h;kqy7j+KaX!hV%m4oXx5(5DkmdlIZqh5J>n+HBh3|sR1K)wphLqvRf$%uVkCZ|6 z{uwRcO8C|4ef5;Tq$O{FP!_~|9Ut)gbtukTO&ZQPxZAD!QwVIBq!s=S8ld|$|Lh6S z63iUXCt%JXj{1xmPoSqtj`+HIiTW>nK=~b1aMWrSXD zze_?&P+Z1YS~XVFt=dG}HAmhSLX4-f-E#* dict: "filename": filename, "sort_order": len(perms), "permissions": { - "engineer": {"view": False, "edit": False, "cancel": False} + "engineer": {"view": False, "edit": False, "cancel": False}, + "technician": {"view": False}, }, } - save_permissions(perms) + # migrate existing entries missing technician key + changed = False + for slug, doc in perms.items(): + if "permissions" not in doc: + doc["permissions"] = {} + perms_doc = doc["permissions"] + if "technician" not in perms_doc: + perms_doc["technician"] = {"view": False} + changed = True + # ensure engineer has edit/cancel defaults + eng = perms_doc.setdefault("engineer", {}) + if "edit" not in eng: + eng["edit"] = False + changed = True + if "cancel" not in eng: + eng["cancel"] = False + changed = True + if changed: + save_permissions(perms) return perms @@ -52,14 +71,21 @@ def get_allowed_docs(role: str) -> list[dict]: doc for doc in perms.values() if doc.get("permissions", {}).get("engineer", {}).get("view", False) ] + if role == "technician": + return [ + doc for doc in perms.values() + if doc.get("permissions", {}).get("technician", {}).get("view", False) + ] return [] def can_access_doc(role: str, slug: str, action: str = "view") -> bool: if role == "owner": return True + perms = load_permissions() + doc = perms.get(slug, {}) if role == "engineer": - perms = load_permissions() - doc = perms.get(slug, {}) return doc.get("permissions", {}).get("engineer", {}).get(action, False) + if role == "technician": + return doc.get("permissions", {}).get("technician", {}).get("view", False) return False diff --git a/py_service/app/static/css/service-theme.css b/py_service/app/static/css/service-theme.css new file mode 100644 index 0000000..ce9912a --- /dev/null +++ b/py_service/app/static/css/service-theme.css @@ -0,0 +1,168 @@ +@charset "UTF-8"; + +:root { + --bg-body: #0D1B2A; + --bg-card: #1B2838; + --bg-card-hover: #1e3044; + --bg-sidebar: #0A1628; + --bg-input: #1e2a3e; + --border: #253248; + --border-focus: #00ADEF; + --text-primary: #E8EDF2; + --text-secondary: #A0B4C8; + --text-muted: #6B8299; + --accent: #00ADEF; + --accent-hover: #33c4f5; + --accent-dim: rgba(0, 173, 239, 0.15); + --success: #22c55e; + --warning: #f59e0b; + --danger: #ef4444; + --info: #6366f1; + --font: 'Inter', 'Segoe UI', sans-serif; + --radius: 10px; + --shadow: 0 2px 12px rgba(0,0,0,0.4); +} + +[data-theme="light"] { + --bg-body: #F5F7FA; + --bg-card: #FFFFFF; + --bg-card-hover: #f0f4f8; + --bg-sidebar: #FFFFFF; + --bg-input: #FFFFFF; + --border: #e2e8f0; + --border-focus: #00ADEF; + --text-primary: #1a1a2e; + --text-secondary: #5a6a7a; + --text-muted: #8899aa; + --accent: #00ADEF; + --accent-hover: #0095cc; + --accent-dim: rgba(0, 173, 239, 0.1); + --success: #16a34a; + --warning: #d97706; + --danger: #dc2626; + --info: #4f46e5; + --shadow: 0 2px 12px rgba(10, 42, 77, 0.1); +} + +[data-theme="dark"] { + --bg-body: #0D1B2A; + --bg-card: #1B2838; + --bg-card-hover: #1e3044; + --bg-sidebar: #0A1628; + --bg-input: #1e2a3e; + --border: #253248; + --border-focus: #00ADEF; + --text-primary: #E8EDF2; + --text-secondary: #A0B4C8; + --text-muted: #6B8299; + --accent: #00ADEF; + --accent-hover: #33c4f5; + --accent-dim: rgba(0, 173, 239, 0.15); + --success: #22c55e; + --warning: #f59e0b; + --danger: #ef4444; + --info: #6366f1; + --shadow: 0 2px 12px rgba(0,0,0,0.4); +} + +body { + background: var(--bg-body); + color: var(--text-primary); + transition: background 0.3s, color 0.3s; +} + +.theme-switcher { + display: flex; + align-items: center; + gap: 6px; +} + +.theme-switcher button { + background: var(--accent-dim); + border: 1px solid var(--border); + border-radius: 6px; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 16px; + color: var(--text-secondary); + transition: all 0.2s; +} + +.theme-switcher button:hover { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +.theme-label { + font-size: 11px; + color: var(--text-muted); + white-space: nowrap; +} + +/* EasyMDE dark theme overrides */ +[data-theme="dark"] .EasyMDEContainer .editor-toolbar { + background: var(--bg-card); + border-color: var(--border); +} +[data-theme="dark"] .EasyMDEContainer .CodeMirror { + background: var(--bg-input); + color: var(--text-primary); + border-color: var(--border); +} +[data-theme="dark"] .editor-toolbar button { + color: var(--text-secondary); + border-color: transparent; +} +[data-theme="dark"] .editor-toolbar button:hover { + background: var(--accent-dim); + border-color: var(--accent); + color: var(--text-primary); +} +[data-theme="dark"] .editor-toolbar button.active { + background: var(--accent-dim); + border-color: var(--accent); + color: var(--accent); +} +[data-theme="dark"] .CodeMirror-cursor { + border-color: var(--accent) !important; +} +[data-theme="dark"] .CodeMirror-selected { + background: var(--accent-dim) !important; +} +[data-theme="dark"] .editor-preview { + background: var(--bg-card); + color: var(--text-primary); +} +[data-theme="dark"] .editor-preview-side { + background: var(--bg-card); + color: var(--text-primary); + border-color: var(--border); +} +[data-theme="dark"] .editor-statusbar { + color: var(--text-muted); +} + +/* Sticky editor toolbar */ +.EasyMDEContainer .editor-toolbar { + position: sticky; + top: 0; + z-index: 10; + background: var(--bg-card); + border-radius: var(--radius) var(--radius) 0 0; +} + +/* Sticky bottom actions on edit page */ +.edit-form-actions { + position: sticky; + bottom: 0; + z-index: 10; + background: var(--bg-card); + padding: 12px 0; + border-top: 1px solid var(--border); + margin-top: 8px; +} diff --git a/py_service/app/static/css/service.css b/py_service/app/static/css/service.css new file mode 100644 index 0000000..8f16812 --- /dev/null +++ b/py_service/app/static/css/service.css @@ -0,0 +1,1234 @@ +@charset "UTF-8"; +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Montserrat:wght@600;700;800&display=swap'); + +:root { + --bg-body: #0a0e1a; + --bg-card: #131a2e; + --bg-card-hover: #1a2340; + --bg-sidebar: #0d1322; + --bg-input: #1a2340; + --border: #1e2a4a; + --border-focus: #00ADEF; + --text-primary: #e8edf5; + --text-secondary: #8899bb; + --text-muted: #556688; + --accent: #00ADEF; + --accent-hover: #33c4f5; + --accent-dim: rgba(0, 173, 239, 0.15); + --success: #22c55e; + --warning: #f59e0b; + --danger: #ef4444; + --info: #6366f1; + --font: 'Inter', 'Segoe UI', sans-serif; + --radius: 10px; + --shadow: 0 2px 12px rgba(0,0,0,0.3); +} + +/* ---------- SCROLLBARS ---------- */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: rgba(255,255,255,0.03); border-radius: 3px; } +::-webkit-scrollbar-thumb { background: rgba(0,173,239,0.3); border-radius: 3px; transition: background 0.2s; } +::-webkit-scrollbar-thumb:hover { background: rgba(0,173,239,0.5); } +* { scrollbar-width: thin; scrollbar-color: rgba(0,173,239,0.3) rgba(255,255,255,0.03); } + +* { margin:0; padding:0; box-sizing:border-box; } + +html, body { + font-family: var(--font); + background: var(--bg-body); + color: var(--text-primary); + font-size: 14px; + line-height: 1.5; + min-height: 100vh; +} + +a { color: var(--accent); text-decoration:none; } +a:hover { color: var(--accent-hover); } +img { max-width:100%; height:auto; } + +/* ---------- LOGIN PAGE ---------- */ +.login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background: linear-gradient(135deg, #0a0e1a 0%, #0d1a30 50%, #0a0e1a 100%); +} + +.login-box { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 36px 32px; + width: 100%; + max-width: 400px; + box-shadow: var(--shadow); +} + +.login-brand { + text-align: center; + margin-bottom: 24px; +} + +.login-brand__img { + display: block; + height: 56px; + margin: 0 auto 16px; +} + +.login-brand__title { + font-family: 'Montserrat', sans-serif; + font-weight: 800; + font-size: 24px; + color: var(--text-primary); + letter-spacing: -0.02em; + line-height: 1.1; +} + +.login-brand__subtitle { + font-family: 'Inter', sans-serif; + font-weight: 500; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.2em; + color: var(--accent); + margin-top: 4px; +} + +.login-divider { + border: none; + border-top: 1px solid var(--border); + margin: 20px 0; +} + +.login-box h1 { + font-size: 20px; + font-weight: 700; + margin-bottom: 24px; + color: var(--text-primary); + text-align: center; +} + +.login-box__footnote { + margin-top: 24px; + font-size: 11px; + color: var(--text-muted); + text-align: center; + line-height: 1.5; +} + +.form-group { + margin-bottom: 18px; +} + +.form-group label { + display: block; + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 6px; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 10px 14px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-family: var(--font); + font-size: 14px; + transition: border 0.2s; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--border-focus); + box-shadow: 0 0 0 3px var(--accent-dim); +} + +.form-group textarea { + min-height: 80px; + resize: vertical; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 20px; + border: none; + border-radius: 6px; + font-family: var(--font); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.btn-primary { + background: var(--accent); + color: #fff; +} + +.btn-primary:hover { + background: var(--accent-hover); +} + +.btn-secondary { + background: var(--bg-input); + color: var(--text-primary); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + border-color: var(--accent); +} + +.btn-success { + background: var(--success); + color: #fff; +} + +.btn-danger { + background: var(--danger); + color: #fff; +} + +.btn-warning { + background: var(--warning); + color: #fff; +} + +.btn-sm { + padding: 5px 12px; + font-size: 12px; +} + +.btn-block { + width: 100%; + justify-content: center; +} + +.alert { + padding: 12px 16px; + border-radius: 6px; + font-size: 13px; + margin-bottom: 16px; +} + +.alert-error { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); + border: 1px solid rgba(239, 68, 68, 0.3); +} + +.alert-success { + background: rgba(34, 197, 94, 0.15); + color: var(--success); + border: 1px solid rgba(34, 197, 94, 0.3); +} + +.alert-warning { + background: rgba(245, 158, 11, 0.15); + color: var(--warning); + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.alert-info { + background: rgba(99, 102, 241, 0.15); + color: var(--info); + border: 1px solid rgba(99, 102, 241, 0.3); +} + +/* ---------- LAYOUT ---------- */ +.layout { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 240px; + background: var(--bg-sidebar); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + position: fixed; + top: 0; + left: 0; + height: 100vh; + z-index: 100; + transition: transform 0.3s; +} + +.sidebar-header { + padding: 20px; + border-bottom: 1px solid var(--border); +} + +.sidebar-header .logo { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); +} + +.sidebar-header .logo span { + color: var(--accent); +} + +.sidebar-nav { + flex: 1; + padding: 12px 0; + overflow-y: auto; +} + +.sidebar-nav a { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 20px; + color: var(--text-secondary); + font-size: 13px; + font-weight: 500; + transition: all 0.2s; +} + +.sidebar-nav a:hover, +.sidebar-nav a.active { + color: var(--text-primary); + background: var(--accent-dim); +} + +.sidebar-nav a.active { + border-right: 3px solid var(--accent); +} + +.sidebar-section { + padding: 8px 20px 4px; + font-size: 10px; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1px; +} + +.sidebar-footer { + padding: 16px 20px; + border-top: 1px solid var(--border); + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} + +.sidebar-logo-text { font-size: 2rem !important; line-height: 1.1; } + +.version-pill { + display: inline-block; + margin-top: 4px; + padding: 2px 10px; + background: var(--accent-dim); + color: var(--accent); + border: 1px solid rgba(0,173,239,0.2); + border-radius: 12px; + font-size: 11px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} +.version-pill:hover { + background: rgba(0,173,239,0.25); + border-color: var(--accent); +} + +.status-completed { color: var(--success); font-weight: 600; } + +.sidebar-footer .user-name { + color: var(--text-primary); + font-weight: 600; +} + +.sidebar-footer .user-role { + color: var(--text-muted); + font-size: 11px; +} + +.sidebar-toggle { + display: none; + position: fixed; + top: 12px; + left: 12px; + z-index: 101; + background: var(--bg-card); + border: 1px solid var(--border); + color: var(--text-primary); + padding: 8px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 18px; +} + +.main { + flex: 1; + margin-left: 240px; + padding: 24px; + min-height: 100vh; +} + +.main-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.main-header h1 { + font-size: 24px; + font-weight: 700; +} + +.main-header .breadcrumb { + font-size: 12px; + color: var(--text-muted); +} + +.main-header .breadcrumb a { + color: var(--text-secondary); +} + +.main-header .breadcrumb a:hover { + color: var(--accent); +} + +/* ---------- CARDS ---------- */ +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; + margin-bottom: 16px; + transition: box-shadow 0.2s; +} + +.card:hover { + box-shadow: var(--shadow); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 14px; +} + +.card-header h2, +.card-header h3 { + font-size: 15px; + font-weight: 600; +} + +.card-value { + font-size: 28px; + font-weight: 700; +} + +.card-label { + font-size: 12px; + color: var(--text-secondary); + margin-top: 2px; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +/* ---------- KPI VALUES ---------- */ +.value-green { color: var(--success); } +.value-yellow { color: var(--warning); } +.value-red { color: var(--danger); } +.value-accent { color: var(--accent); } +.value-info { color: var(--info); } + +/* ---------- TABLES ---------- */ +.table-wrap { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +table th { + text-align: left; + padding: 10px 12px; + font-size: 11px; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +table td { + padding: 10px 12px; + font-size: 13px; + border-bottom: 1px solid var(--border); + vertical-align: middle; +} + +table tr:hover td { + background: rgba(0, 173, 239, 0.03); +} + +table .actions { + white-space: nowrap; + text-align: right; +} + +table .actions a, +table .actions button { + margin-left: 6px; +} + +/* ---------- BADGES ---------- */ +.badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; +} + +.badge-critical { background: rgba(239,68,68,0.2); color: var(--danger); } +.badge-high { background: rgba(245,158,11,0.2); color: var(--warning); } +.badge-medium { background: rgba(99,102,241,0.2); color: var(--info); } +.badge-low { background: rgba(136,153,187,0.2); color: var(--text-secondary); } +.badge-open { background: rgba(99,102,241,0.2); color: var(--info); } +.badge-progress { background: rgba(245,158,11,0.2); color: var(--warning); } +.badge-done { background: rgba(34,197,94,0.2); color: var(--success); } +.badge-cancelled { background: rgba(239,68,68,0.2); color: var(--danger); } +.badge-active { background: rgba(34,197,94,0.2); color: var(--success); } +.badge-inactive { background: rgba(136,153,187,0.2); color: var(--text-secondary); } +.badge-draft { background: rgba(136,153,187,0.2); color: var(--text-secondary); } +.badge-risk { background: rgba(245,158,11,0.2); color: var(--warning); } +.badge-crisis { background: rgba(239,68,68,0.2); color: var(--danger); } +.badge-stable { background: rgba(99,102,241,0.2); color: var(--info); } +.badge-growth { background: rgba(34,197,94,0.2); color: var(--success); } +.badge-default { background: rgba(136,153,187,0.2); color: var(--text-secondary); } + +/* ---------- MODAL ---------- */ +.modal-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); + display: none; + align-items: center; + justify-content: center; + z-index: 200; +} + +.modal-overlay.open { + display: flex; +} + +.modal { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 28px; + width: 100%; + max-width: 540px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 8px 40px rgba(0,0,0,0.5); +} + +.modal h2 { + font-size: 18px; + font-weight: 700; + margin-bottom: 20px; +} + +.modal-close { + float: right; + background: none; + border: none; + color: var(--text-muted); + font-size: 20px; + cursor: pointer; +} + +/* ---------- TABS ---------- */ +.tabs { + display: flex; + border-bottom: 1px solid var(--border); + margin-bottom: 20px; + gap: 0; +} + +.tabs a { + padding: 10px 20px; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + border-bottom: 2px solid transparent; + margin-bottom: -1px; + transition: all 0.2s; +} + +.tabs a:hover, +.tabs a.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +/* ---------- PAGINATION ---------- */ +.pagination { + display: flex; + gap: 4px; + justify-content: center; + margin-top: 20px; +} + +.pagination a, +.pagination span { + padding: 6px 12px; + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-secondary); + font-size: 13px; +} + +.pagination a:hover { + border-color: var(--accent); + color: var(--accent); +} + +.pagination .current { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +/* ---------- SEARCH ---------- */ +.search-box { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +.search-box input { + flex: 1; + padding: 8px 14px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-family: var(--font); + font-size: 13px; +} + +.search-box input:focus { + outline: none; + border-color: var(--border-focus); +} + +/* ---------- PROGRESS ---------- */ +.progress-bar { + height: 6px; + background: rgba(255,255,255,0.08); + border-radius: 3px; + overflow: hidden; + margin-top: 8px; +} + +.progress-fill { + height: 100%; + border-radius: 3px; + transition: width 0.4s; +} + +/* ---------- FORM INLINE ---------- */ +.form-row { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 14px; +} + +.form-check { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; +} + +.form-check input[type="checkbox"], +.form-check input[type="radio"] { + width: auto; +} + +/* ---------- FILTER BAR ---------- */ +.filter-bar { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 16px; + align-items: center; +} + +.filter-bar select, +.filter-bar input { + padding: 6px 10px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-family: var(--font); + font-size: 12px; +} + +/* ---------- QUESTIONNAIRE ---------- */ +.questionnaire-step { + display: none; +} + +.questionnaire-step.active { + display: block; +} + +.step-indicators { + display: flex; + gap: 0; + margin-bottom: 28px; + overflow-x: auto; +} + +.step-dot { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + border-bottom: 2px solid var(--border); + white-space: nowrap; + cursor: default; +} + +.step-dot.completed { + color: var(--success); + border-bottom-color: var(--success); +} + +.step-dot.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +/* ---------- SHS ALERT ---------- */ +.shs-alert { + padding: 12px 16px; + border-radius: 6px; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 10px; + font-size: 13px; + font-weight: 600; +} + +.shs-alert.warning { + background: rgba(245,158,11,0.15); + color: var(--warning); + border: 1px solid rgba(245,158,11,0.3); +} + +.shs-alert.critical { + background: rgba(239,68,68,0.15); + color: var(--danger); + border: 1px solid rgba(239,68,68,0.3); +} + +/* ---------- RESPONSIVE ---------- */ +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + } + .sidebar.open { + transform: translateX(0); + } + .sidebar-toggle { + display: block; + } + .main { + margin-left: 0; + padding: 16px; + padding-top: 56px; + } + .card-grid { + grid-template-columns: 1fr; + } + .form-row { + grid-template-columns: 1fr; + } + .login-box { + margin: 16px; + padding: 24px; + } + .tabs { + overflow-x: auto; + } + .step-indicators { + flex-wrap: nowrap; + } +} + +/* ---------- PRINT ---------- */ +@media print { + .sidebar, .sidebar-toggle, .main-header .actions { display: none; } + .main { margin-left: 0; } + .card { break-inside: avoid; } +} + +/* ---------- TOAST ---------- */ +.toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 999; +} + +.toast { + padding: 12px 20px; + border-radius: 6px; + margin-bottom: 8px; + font-size: 13px; + font-weight: 500; + box-shadow: 0 4px 20px rgba(0,0,0,0.4); + animation: slideIn 0.3s ease; +} + +.toast-success { background: var(--success); color: #fff; } +.toast-error { background: var(--danger); color: #fff; } +.toast-warning { background: var(--warning); color: #fff; } +.toast-info { background: var(--info); color: #fff; } + +@keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +/* ---------- TOOLTIPS ---------- */ +[data-tooltip] { + position: relative; +} +[data-tooltip]::after { + content: attr(data-tooltip); + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%) translateY(4px); + background: #1a1a2e; + color: #e8edf5; + padding: 6px 10px; + border-radius: 6px; + font-size: 11px; + font-weight: 500; + white-space: nowrap; + pointer-events: none; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s, transform 0.2s, visibility 0.2s; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); +} +[data-tooltip]::before { + content: ''; + position: absolute; + bottom: calc(100% + 2px); + left: 50%; + transform: translateX(-50%) translateY(4px); + border: 5px solid transparent; + border-top-color: #1a1a2e; + pointer-events: none; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s, transform 0.2s, visibility 0.2s; + z-index: 1000; +} +[data-tooltip]:hover::after, +[data-tooltip]:hover::before { + opacity: 1; + visibility: visible; + transform: translateX(-50%) translateY(0); +} + +/* ---------- EMPTY STATE ---------- */ +.empty-state { + text-align: center; + padding: 48px 20px; + color: var(--text-muted); +} + +.empty-state svg { + width: 60px; + height: 60px; + margin-bottom: 16px; + opacity: 0.4; +} + +.empty-state h3 { + font-size: 16px; + margin-bottom: 6px; +} + +.empty-state p { + font-size: 13px; +} + +/* ---------- LOADING ---------- */ +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ---------- NAVBAR ---------- */ +.navbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24px; + height: 56px; + background: var(--bg-sidebar); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 50; +} + +.navbar__brand a { + font-family: 'Montserrat', sans-serif; + font-weight: 700; + font-size: 16px; + color: var(--text-primary); +} + +.navbar__menu { + display: flex; + align-items: center; + gap: 20px; + list-style: none; + margin: 0; + padding: 0; +} + +.navbar__menu a { + color: var(--text-secondary); + font-size: 13px; + font-weight: 500; + transition: color 0.2s; +} + +.navbar__menu a:hover { + color: var(--text-primary); +} + +.navbar__logout { + color: var(--text-muted) !important; +} + +/* ---------- DOCUMENTS ---------- */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 24px; +} + +.docs-list { + display: grid; + gap: 8px; + margin-bottom: 24px; +} + +.doc-item { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px 20px; + transition: all 0.2s; +} + +.doc-item:hover { + border-color: var(--accent); + background: var(--bg-card-hover); +} + +.doc-item a { + font-size: 15px; + font-weight: 500; + color: var(--text-primary); +} + +.doc-item a:hover { + color: var(--accent); +} + +.docs-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.doc-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 24px; + flex-wrap: wrap; + gap: 16px; +} + +.doc-header h1 { + font-size: 24px; + font-weight: 700; +} + +.doc-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.doc-content { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 28px; + line-height: 1.7; +} + +.doc-content h1, .doc-content h2, .doc-content h3 { + margin-top: 24px; + margin-bottom: 12px; + color: var(--text-primary); +} + +.doc-content h1 { font-size: 22px; } +.doc-content h2 { font-size: 18px; } +.doc-content h3 { font-size: 16px; } + +.doc-content p { + margin-bottom: 12px; + color: var(--text-secondary); +} + +.doc-content code { + background: var(--bg-input); + padding: 2px 6px; + border-radius: 4px; + font-size: 13px; + color: var(--accent); +} + +.doc-content pre { + background: var(--bg-input); + padding: 16px; + border-radius: 6px; + overflow-x: auto; + margin-bottom: 16px; +} + +.doc-content pre code { + background: none; + padding: 0; +} + +.doc-content table { + margin-bottom: 16px; +} + +.doc-content ul, .doc-content ol { + margin-bottom: 12px; + padding-left: 24px; + color: var(--text-secondary); +} + +.doc-content li { + margin-bottom: 4px; +} + +.doc-content blockquote { + border-left: 3px solid var(--accent); + padding-left: 16px; + margin: 16px 0; + color: var(--text-muted); +} + +.doc-content a { + color: var(--accent); +} + +.doc-content a:hover { + color: var(--accent-hover); +} + +.form-actions { + display: flex; + gap: 10px; + margin-top: 16px; +} + +/* ---------- PERMISSIONS TABLE ---------- */ +.permissions-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +.permissions-table th { + text-align: left; + padding: 12px 16px; + font-size: 12px; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid var(--border); + background: var(--bg-sidebar); +} + +.permissions-table td { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + vertical-align: middle; +} + +.permissions-table tr:hover td { + background: rgba(0, 173, 239, 0.05); +} + +.permissions-table input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--accent); + cursor: pointer; +} + +/* ---------- ACCORDION ---------- */ +.accordion { + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 8px; + overflow: hidden; +} +.accordion-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: var(--bg-sidebar); + cursor: pointer; + user-select: none; + transition: background 0.2s; +} +.accordion-header:hover { + background: var(--bg-card-hover); +} +.accordion-header-left { + display: flex; + align-items: center; + gap: 10px; +} +.accordion-header-right { + display: flex; + align-items: center; + gap: 8px; +} +.accordion-arrow { + font-size: 10px; + color: var(--text-muted); + transition: transform 0.2s; + width: 12px; +} +.accordion-title { + font-weight: 600; + font-size: 14px; +} +.accordion-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + padding: 0 6px; + border-radius: 11px; + background: var(--accent-dim); + color: var(--accent); + font-size: 11px; + font-weight: 700; +} +.accordion-formula, .accordion-action { + cursor: pointer; + font-size: 16px; + opacity: 0.6; + transition: opacity 0.2s; + line-height: 1; +} +.accordion-formula:hover, .accordion-action:hover { + opacity: 1; +} +.accordion-body { + display: none; + border-top: 1px solid var(--border); +} +.accordion-body .table-wrap { + margin: 0; +} + +@media (max-width: 768px) { + .navbar { + flex-direction: column; + height: auto; + padding: 12px 16px; + gap: 12px; + } + .navbar__menu { + flex-wrap: wrap; + justify-content: center; + } + .doc-header { + flex-direction: column; + } + .container { + padding: 16px; + } +} + diff --git a/py_service/app/static/img/aegisone_logo.svg b/py_service/app/static/img/aegisone_logo.svg new file mode 100644 index 0000000..596c7dc --- /dev/null +++ b/py_service/app/static/img/aegisone_logo.svg @@ -0,0 +1,3 @@ + + AegisOne + diff --git a/py_service/app/static/img/favicon.svg b/py_service/app/static/img/favicon.svg new file mode 100644 index 0000000..74e806c --- /dev/null +++ b/py_service/app/static/img/favicon.svg @@ -0,0 +1 @@ +AO diff --git a/py_service/app/static/img/favicons/android-chrome-192x192.png b/py_service/app/static/img/favicons/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..46d5e3e81501b910043fa647f79d51bf2ed54de7 GIT binary patch literal 31700 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%0D>>tWn6BKmJCN6_Swtp1&BT1;QvX6P&bo%B=S_H zDrhafO4--=_V?x8RHXhMXmL?K`?5(4&*1!F_!I0{OviylE01ol?<(2L*ryyA`+yXf zXB|m#0B=Vn67}HT^f_*S6>KR&720rp5T&#m;Y@dS@R7Z)A*Td~Yf+ld?bv|5QZ*VV zzj2=;phntsN$;G(M-FNiL6Gq%`_K^2|0T|YrJv4I?iR9BHc`D9^(6?Hk+&DYFnyVJcpw3*K0E z<3yLW+2{{;Bx6tu9OdFcEFfpH;)QVH*>iIe6G~Y>n-G*`DI|+vFE9MS6oS_}6S}cP zsBhmOO<1PMr;^ccDD(}>2R{F@m|_4-^E9!23BYddN)miRnih7kLYDf8Y)cf7;x(Q} zjrM8Nk!1YO@?jm!tSsclB;7}S=6wFvC+^g-eO`h@7&eP#miT%;H>NB3ucgopI;6Eo zaQ(UxJ$0d<@LMCRd;+S@*sXcusgy`Ci9Xw)R*>U({X$JA{xnA*#w3xnim%a|0VzRO zRQDZ(lOk=BkKPJO(WXhzcGd9{w(;2QYk=y^0T6ZyK~T{-eU{>8zeEsQa-zX}8Ee6< zb#ZuiWUPrLiwjxc1tDLyNq!D)fzhY9KCYw?kdhZaWM+^1j)qAMe#@x$u!1rgZVQox zG?0@{wTk`Sg(uzDhv`ccZAB|72d1PJO4FU=F~~SFuUf+~E|GbP$R|Q0ajdKVn;m?N zmA~{*x*TcSm0+j8I;MtJ2Sedvv=jPh9}M2JIvj#?G~QAXedxvyYqct-=d4`I#f8yk z#dU(<0z|Y-wpNPMl&0ssPsjC1uRtWmdzyftI9JNJg6s?N;)y8c0j(J6?BiW@N(dzD z7gdTgUe=8$6^BDqUs|Ihx#b0}h42Tn%G#ZKwNe*tfh|(e8^6bwf^G5Cxwh!(s8T<; zwuOG2(h>Yc_U-?ChF^||BQxj5svmT=nJIBU2Y$|sYSv2Ty%CT>=SA24W;|R$nl4rp zpm>cG$$WVK)>46#AK>W&opJ=Wo^9S<8T2>He9doW&=DA=G@MQ)32 zPcyH(zBQ>w29vg+tQ+?~!vlUfIjZ>Wek9Htj}?*-)gGCQ92V4fR)LxCit zPNl{EdF%|lH5(WO)fOJke=7`=i|j-2=qOH!ApSV{o6~?xW@PDcS1H)_aWfAWJD=X`G{4qZ%Wbe3 z?OCPlL0y5C+Z|}dRa*s7KHhI>1M@LLhPrD9-MIA%(=4)b3O=Q^ObYxVp2l_}I7i_A zLy%;Du>StZV|_fKtY9m$lNI}eREYwQ+F&zFZI=xQtneU1+NqX1)~V?UGLO#35XXCL z2O`IfHb1wGjd({u=<6Gibc;TGQ#ru3Blm4J`?Ir58XRNcz9JkM&;t&;V>deZ$um=7 zibcSxS&7M`boRtDn08VI#+{(3T8RG7^$`E7q)p@XzJNI4;;7B7 zQpd1zA@%4R&V@d>0!`w*aT~YsSAZH^7HdacTKgC~=%5OS1Eam01}x5$!hooIIXkZT zp=PY;Ecee&D@ib(=XTwk(~P4RI-mHp7s9LRXcVdSZa|;6>E?ECAUp2io-90(KcgC9 zEb#rd(vavRB(hLg@;~rZVi?_gd7)8dfRyTeIXmPn&u^L!&BS$QK>0lwG3ZqbRNeUC z7$Kh(;ZNXulq4eMwP~xK*82*&Yuld}9avw%by$j?zZ}#72VB{Yhjob}(MFZtOA>$| z&C~``V#+)&tdb`Rr>8*d+6d&FHZxKmC*6sd3{eP1S=5$4club|{36$SSLz^M?=%{m z89}$HsHmqmIWch_7FWT9=VDp129CYTsYW;g_}n z1_cG(maoM=GX4JdPcqa?_5S_;!8jB)Xh9#T{=_btfa4Y;|JNVR>qql8SWWM()eL9TM7UG literal 0 HcmV?d00001 diff --git a/py_service/app/static/img/favicons/apple-touch-icon.png b/py_service/app/static/img/favicons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6c9b56e403e64d58e9df92fe62d8899e3f99f933 GIT binary patch literal 29337 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 literal 0 HcmV?d00001 diff --git a/py_service/app/static/img/favicons/favicon-16x16.png b/py_service/app/static/img/favicons/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..7d47fb9fff1a90f2a98e1c96a3b8158d09a0ff82 GIT binary patch literal 1318 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>bcqASna_%D!gU`io)xXNGkjp=j*~hP7ZeP;m3ljFjZ!)C9}U6-IpRB^8u;HcU0?U2oRCCO0!9Hzg712A~uKWR0|evR%TXC9u{%1E!jD_47>hoP2Q6O1TcHi7(g4%yVo%6_zwz=)o&7ttKn_{ zs!Yo-Vq5VH4cD_<05q@vjZ;R-Fh0kj1g zw(CBIRkkDB1vDzLsFDE~wxGZV$HLn88F>XLYCyioVFJm*%vt-MXW7HF{9=d|x!Kv7 zx%nasuEETC2Xvdl%oD(%fGE(NxC7!^ppO_f|4uF}2U@|f=_k-Yh=P>l|Q-(-S%y@Jk0ZFF5J4fY~SV;{TsQ{RvqeCyMB4o@fx1AUGvVDdS+anwQ^grh{WtQ zbC*@6Y?`YfP`P-)9$nd8n`*bpGtJ75Ju{(u(bOdu;#P46ZS*|Sc6e!n?cxmXC9MtX zruFPt+Eut&M0xY<%?D1**fnL3fow$`qkN_ZM>v8Y}^?a)pk_@eZZLH?e20g zCGCA4ki%Z$>Fdh=idC4OPiVU0KcHtB1w36GLnJOICjfznps4Wl2~#GeB{49jiAhFU zRgLME+SW2VD<|`@2?rY&b9-xNOGkTKSF^jTv!#oNo0pgK z`|IZ$m&?c7*R!2#IItk0UqE{Qg$*A#`aSg8R=kktzp>*-OUscZPo`YiQqtCQW=+oL zIWc+j_8hqIu&5CN<^{egJvS-t-Z7W;PxBJ|>JhprD z?(O@>nfOfjwz}^;qf#k(=J#JDjnmJ5n{8Zv?++`txbCym2hU?SmE8RFMpQd|ZQSNE z+4T&}%?j^-@k*QL&fA^(dfVF?riAw&-`&`mee~4b>TjR83>_K`&hPun-g3}~AnC}Q!>*kacj7fzWzK=1B0ilpUXO@geCwBDI|OV literal 0 HcmV?d00001 diff --git a/py_service/app/static/img/favicons/favicon-32x32.png b/py_service/app/static/img/favicons/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..be0b50057e5f95127d7bcc6953215ca2aca0e5ed GIT binary patch literal 2380 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 literal 0 HcmV?d00001 diff --git a/py_service/app/static/img/favicons/favicon.ico b/py_service/app/static/img/favicons/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7d97fd7cacb2eb81c6ee5b0761c3da6209774565 GIT binary patch literal 9662 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 ZqW+*^Y6Mr{fT-V8j#6#XX&&J*=I^${djtRg literal 0 HcmV?d00001 diff --git a/py_service/app/static/img/favicons/logo_service.png b/py_service/app/static/img/favicons/logo_service.png new file mode 100644 index 0000000000000000000000000000000000000000..02fb822553918be6b540c9e7bf04fd177b12145d GIT binary patch literal 115315 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�QeQDifY&2@s^ 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 literal 0 HcmV?d00001 diff --git a/py_service/app/static/img/favicons/site.webmanifest b/py_service/app/static/img/favicons/site.webmanifest new file mode 100644 index 0000000..7bafe96 --- /dev/null +++ b/py_service/app/static/img/favicons/site.webmanifest @@ -0,0 +1 @@ +{"name":"AegisOne Service Portal","short_name":"AegisOne","icons":[{"src":"/service/static/img/favicons/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/service/static/img/favicons/apple-touch-icon.png","sizes":"180x180","type":"image/png"},{"src":"/service/static/img/favicons/favicon-16x16.png","sizes":"16x16","type":"image/png"},{"src":"/service/static/img/favicons/favicon-32x32.png","sizes":"32x32","type":"image/png"},{"src":"/service/static/img/favicons/favicon.ico","sizes":"48x48","type":"image/x-icon"}]} diff --git a/py_service/app/static/js/theme.js b/py_service/app/static/js/theme.js new file mode 100644 index 0000000..23c3408 --- /dev/null +++ b/py_service/app/static/js/theme.js @@ -0,0 +1,47 @@ +(function() { + var STORAGE_KEY = 'aegisone_service_theme'; + var themes = ['system', 'dark', 'light']; + + function getSystemTheme() { + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + + function applyTheme(theme) { + var resolved = theme === 'system' ? getSystemTheme() : theme; + document.documentElement.setAttribute('data-theme', resolved); + var label = document.getElementById('theme-label'); + if (label) { + var names = { system: 'Авто', dark: 'Тёмная', light: 'Светлая' }; + label.textContent = names[theme] || 'Авто'; + } + var btn = document.getElementById('theme-btn'); + if (btn) { + var icons = { system: '🖥', dark: '🌙', light: '☀️' }; + btn.textContent = icons[theme] || '🖥'; + } + } + + function getNextTheme(current) { + var idx = themes.indexOf(current); + return themes[(idx + 1) % themes.length]; + } + + function toggleTheme() { + var current = localStorage.getItem(STORAGE_KEY) || 'system'; + var next = getNextTheme(current); + localStorage.setItem(STORAGE_KEY, next); + applyTheme(next); + } + + var saved = localStorage.getItem(STORAGE_KEY) || 'system'; + applyTheme(saved); + + window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function() { + var current = localStorage.getItem(STORAGE_KEY) || 'system'; + if (current === 'system') { + applyTheme('system'); + } + }); + + window.toggleTheme = toggleTheme; +})(); diff --git a/py_service/app/templates/base.html b/py_service/app/templates/base.html index 977dd14..2898691 100644 --- a/py_service/app/templates/base.html +++ b/py_service/app/templates/base.html @@ -5,12 +5,32 @@ {% block title %}Service Portal — AegisOne Engineering{% endblock %} - - + + + + + + + + {% block extra_head %}{% endblock %} +{% if user %} + +{% endif %} {% block content %}{% endblock %} + {% block extra_scripts %}{% endblock %} diff --git a/py_service/app/templates/dashboard.html b/py_service/app/templates/dashboard.html index 10314e5..3e3881e 100644 --- a/py_service/app/templates/dashboard.html +++ b/py_service/app/templates/dashboard.html @@ -5,8 +5,8 @@

Дашборд

- +
{{ now }} @@ -117,10 +125,132 @@ {% endif %}
+
+ +
+ + + + + + {% endblock %} diff --git a/py_service/app/templates/documents/edit.html b/py_service/app/templates/documents/edit.html new file mode 100644 index 0000000..161ff4c --- /dev/null +++ b/py_service/app/templates/documents/edit.html @@ -0,0 +1,29 @@ +{% extends "page.html" %} +{% block breadcrumb %}Документация / Редактирование{% endblock %} +{% block page_actions %}← Назад{% endblock %} +{% block page_content %} +
+
+
+ +
+
+ + Отмена +
+
+
+ + + +{% endblock %} diff --git a/py_service/app/templates/documents/list.html b/py_service/app/templates/documents/list.html new file mode 100644 index 0000000..8ff60c2 --- /dev/null +++ b/py_service/app/templates/documents/list.html @@ -0,0 +1,30 @@ +{% extends "page.html" %} +{% block breadcrumb %}Документация{% endblock %} +{% block page_content %} +
+

Документация

+
+ {% for doc in docs %} + + {% else %} +

Нет доступных документов

+ {% endfor %} +
+ {% if user.role == "owner" %} +
+ Управление доступами + +
+ + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/py_service/app/templates/documents/permissions.html b/py_service/app/templates/documents/permissions.html new file mode 100644 index 0000000..823032f --- /dev/null +++ b/py_service/app/templates/documents/permissions.html @@ -0,0 +1,39 @@ +{% extends "page.html" %} +{% block breadcrumb %}Документация / Управление доступами{% endblock %} +{% block page_actions %}← Назад{% endblock %} +{% block page_content %} +
+
+ + + + + + + + + + + {% for slug, doc in perms.items()|sort(attribute='value.sort_order') %} + + + + + + + {% endfor %} + +
ДокументEngineer: ПросмотрEngineer: РедактированиеTechnician: Просмотр
{{ doc.title }} + + + + + +
+
+ + Отмена +
+
+
+{% endblock %} diff --git a/py_service/app/templates/documents/tech_access.html b/py_service/app/templates/documents/tech_access.html new file mode 100644 index 0000000..85857b7 --- /dev/null +++ b/py_service/app/templates/documents/tech_access.html @@ -0,0 +1,36 @@ +{% extends "page.html" %} +{% block breadcrumb %}Документация / Доступ техников{% endblock %} +{% block page_actions %}← Назад{% endblock %} +{% block page_content %} +
+

Вы можете открыть техникам просмотр документов, к которым у вас есть доступ на редактирование.

+
+ + + + + + + + + {% for slug, doc in perms.items() %} + {% if doc.permissions.engineer.edit %} + + + + + {% endif %} + {% else %} + + {% endfor %} + +
ДокументTechnician: Просмотр
{{ doc.title }} + +
Нет документов с правом редактирования
+
+ + Отмена +
+
+
+{% endblock %} diff --git a/py_service/app/templates/documents/view.html b/py_service/app/templates/documents/view.html new file mode 100644 index 0000000..f8876b9 --- /dev/null +++ b/py_service/app/templates/documents/view.html @@ -0,0 +1,16 @@ +{% extends "page.html" %} +{% block breadcrumb %}Документация{% endblock %} +{% block page_actions %} + {% if can_edit %} + Редактировать + {% endif %} + {% if can_download %} + Скачать + {% endif %} + ← Назад +{% endblock %} +{% block page_content %} +
+
{{ content | safe }}
+
+{% endblock %} \ No newline at end of file diff --git a/py_service/app/templates/login.html b/py_service/app/templates/login.html index 57e0b8c..c6a6a37 100644 --- a/py_service/app/templates/login.html +++ b/py_service/app/templates/login.html @@ -3,8 +3,8 @@ {% block content %} + + +{% endblock %} diff --git a/py_service/app/templates/pages/docs.html b/py_service/app/templates/pages/docs.html new file mode 100644 index 0000000..bc411e6 --- /dev/null +++ b/py_service/app/templates/pages/docs.html @@ -0,0 +1,14 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Документация

+ {% if error %}
{{ error }}
{% endif %} + {% if docs %} +
{% for d in docs %} + + {% endfor %}
+ {% else %} +

Нет доступных документов

+ {% endif %} +
+{% endblock %} diff --git a/py_service/app/templates/pages/docs_manage.html b/py_service/app/templates/pages/docs_manage.html new file mode 100644 index 0000000..e795fec --- /dev/null +++ b/py_service/app/templates/pages/docs_manage.html @@ -0,0 +1,17 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Управление документами

+

Управление доступом к документам для инженеров. Отметьте документы, которые будут видны.

+
+
+ {% for d in docs %} + + + + {% endfor %}
ДокументДоступен инженерам
{{ d.title }}
+
+
+
+
+{% endblock %} diff --git a/py_service/app/templates/pages/docs_view.html b/py_service/app/templates/pages/docs_view.html new file mode 100644 index 0000000..68cfe41 --- /dev/null +++ b/py_service/app/templates/pages/docs_view.html @@ -0,0 +1,8 @@ +{% extends "page.html" %} +{% block breadcrumb %}Документация{% endblock %} +{% block page_actions %}← Назад{% endblock %} +{% block page_content %} +
+
{{ content|safe }}
+
+{% endblock %} diff --git a/py_service/app/templates/pages/formula_test.html b/py_service/app/templates/pages/formula_test.html new file mode 100644 index 0000000..0a9b8b0 --- /dev/null +++ b/py_service/app/templates/pages/formula_test.html @@ -0,0 +1,145 @@ +{% extends "page.html" %} +{% block page_content %} +
+
+

🧪 Тест формул

+ ← Коэффициенты +
+
+
+ + +
+ + {% for f in formulas %} +
+
+
Формула:
+
{{ f.formula }}
+
Результат: {{ f.result }}
+
+ + {% if f.sandbox %} +
+

Параметры расчёта

+
+ {% for inp in f.sandbox.inputs %} +
+ + {% if inp.type == "checkbox" %} + + {% elif inp.type == "select" %} + + {% else %} + + {% endif %} +
+ {% endfor %} +
+ + +
+ {% else %} +
+

Для этой формулы пока нет тестовой песочницы. Используйте коэффициенты на странице управления коэффициентами.

+
+ {% endif %} + +

Связанные коэффициенты

+
+ {% for c in f.coeffs %} + + + + {% endfor %}
КлючЗначениеОписание
{{ c.key }}{{ c.value }}{{ c.description }}
+
+
+ {% endfor %} +
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/py_service/app/templates/pages/ideas.html b/py_service/app/templates/pages/ideas.html new file mode 100644 index 0000000..ed1c1ef --- /dev/null +++ b/py_service/app/templates/pages/ideas.html @@ -0,0 +1,55 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Система идей

+ +
+
+ {% for idea in ideas %} + + + + + + + {% endfor %}
IDНазваниеОписаниеСтатусДата
{{ idea.id }}{{ idea.title }}{{ (idea.description or '')[:100] }}{% if (idea.description or '')|length > 100 %}…{% endif %}{{ {'normal':'Обычная','in_progress':'В работе','completed':'Выполнено'}.get(idea.status, idea.status) }}{{ idea.updated_at.strftime('%d.%m.%Y') if idea.updated_at else '' }} +
+ + +
+ +
+ + +
+
+
+
+ + + + +{% endblock %} diff --git a/py_service/app/templates/pages/incidents.html b/py_service/app/templates/pages/incidents.html new file mode 100644 index 0000000..f4c9c81 --- /dev/null +++ b/py_service/app/templates/pages/incidents.html @@ -0,0 +1,55 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Инциденты

+ {% if user.role in ['owner','engineer'] %} + + {% endif %} +
+
+ + +
+
+ {% for i in incidents %} + + + + + + + {% endfor %}
ВажностьНазваниеОбъектИсполнительСтатусСоздан
{{ i.severity }}{{ i.title }}{{ i.object.name if i.object else '' }}{{ i.assignee.full_name if i.assignee else '' }}{{ {'open':'Открыт','in_progress':'В работе','resolved':'Решён','closed':'Закрыт'}[i.status] }}{{ i.created_at.strftime('%d.%m.%Y') if i.created_at else '' }}{% if i.status in ['open','in_progress'] %} + + {% endif %} +
+
+
+ + + +{% endblock %} diff --git a/py_service/app/templates/pages/objects.html b/py_service/app/templates/pages/objects.html new file mode 100644 index 0000000..e2a67be --- /dev/null +++ b/py_service/app/templates/pages/objects.html @@ -0,0 +1,67 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Объекты

+ +
+
+ + + +
+
+ {% for o in objects %} + + + + + {% endfor %}
НазваниеКлиентТипАдресСтатус
{{ o.name }}{{ o.customer.name if o.customer else '' }}{{ o.object_type }}{{ o.address }}{{ {'active':'Активен','inactive':'Неактивен','audit':'Аудит','prospective':'Перспективный'}[o.status] }} + + {% if user.role == 'owner' %} +
+ {% endif %} +
+
+
+ + +{% endblock %} diff --git a/py_service/app/templates/pages/passports.html b/py_service/app/templates/pages/passports.html new file mode 100644 index 0000000..faaaaaf --- /dev/null +++ b/py_service/app/templates/pages/passports.html @@ -0,0 +1,16 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Паспорта объектов

+
+ {% for p in passports %} + + + + + + + {% endfor %}
ОбъектАвторObject IndexRisk ScoreSLA PriceДата
{{ p.object.name if p.object else '' }}{{ p.creator.full_name if p.creator else '' }}{{ "%.2f"|format(p.object_index) if p.object_index else '—' }}{{ "%.2f"|format(p.risk_score) if p.risk_score else '—' }}{{ "%.2f"|format(p.sla_price_monthly) if p.sla_price_monthly else '—' }} ₽{{ p.created_at.strftime('%d.%m.%Y') if p.created_at else '' }}
+
+
+{% endblock %} diff --git a/py_service/app/templates/pages/questionnaire.html b/py_service/app/templates/pages/questionnaire.html new file mode 100644 index 0000000..b486704 --- /dev/null +++ b/py_service/app/templates/pages/questionnaire.html @@ -0,0 +1,16 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Опросники

+ + Новый опросник +
+
+ {% for s, author in sessions %} + + + + + {% endfor %}
IDКлиентАвторСтатусШагДата
#{{ s.id }}{{ s.client_name }}{{ author }}{{ {'draft':'Черновик','completed':'Завершён','cancelled':'Отменён'}[s.status] }}{{ s.current_step }}/5{{ s.created_at.strftime('%d.%m.%Y') if s.created_at else '' }}
+
+
+{% endblock %} diff --git a/py_service/app/templates/pages/questionnaire_config.html b/py_service/app/templates/pages/questionnaire_config.html new file mode 100644 index 0000000..3ce1e2d --- /dev/null +++ b/py_service/app/templates/pages/questionnaire_config.html @@ -0,0 +1,59 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Настройка опросника

+ +
+
Все{% for s in range(1,6) %}Шаг {{ s }}{% endfor %}
+
+ {% for qi in items %} + + + + + {% endfor %}
ШагРазделКлючВопросТипОбязат.ПорядокАктивен
{{ qi.step }}{{ qi.section }}{{ qi.question_key }}{{ qi.label }}{{ qi.type }}{{ '✓' if qi.required else '—' }}{{ qi.sort_order }}
+ +
+
+
+
+ + +{% endblock %} diff --git a/py_service/app/templates/pages/questionnaire_step.html b/py_service/app/templates/pages/questionnaire_step.html new file mode 100644 index 0000000..0aa819e --- /dev/null +++ b/py_service/app/templates/pages/questionnaire_step.html @@ -0,0 +1,53 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Шаг {{ step }} из 5

+
{% for s in range(1,6) %}Шаг {{ s }}{% endfor %}
+
+
+ {% if session_id %}{% endif %} + {% for item in items %} +
+ + {% if item.type == 'text' %} + {% elif item.type == 'number' %} + {% elif item.type == 'textarea' %} + {% elif item.type in ('select','radio') %} + + {% elif item.type == 'checkbox' %}
{% for opt in (item.options or {}).get('options', []) %} + {% endfor %}
+ {% endif %} + {% if item.help_text %}{{ item.help_text }}{% endif %} +
+ {% endfor %} +
+ {% if step > 1 %}← Назад{% endif %} + {% if step < 5 %}{% else %}{% endif %} + +
+
+
+ + +{% endblock %} diff --git a/py_service/app/templates/pages/quickmenu.html b/py_service/app/templates/pages/quickmenu.html new file mode 100644 index 0000000..0cd65d7 --- /dev/null +++ b/py_service/app/templates/pages/quickmenu.html @@ -0,0 +1,38 @@ +{% extends "page.html" %} +{% block page_content %} +
+

{% if editing_tech %}Меню для техников{% else %}Быстрое меню{% endif %}

+ Настройте ссылки, отображаемые в верхней панели навигации +
+
+
+ + {% for item in items %} + + + + + + + {% endfor %} + +
#НазваниеСсылка
{{ loop.index }}
+ {% if items|length == 0 %} +

Нет элементов

Добавьте ссылки для быстрого доступа

+ {% endif %} +
+ Отмена
+
+
+ +{% endblock %} diff --git a/py_service/app/templates/pages/reports.html b/py_service/app/templates/pages/reports.html new file mode 100644 index 0000000..a2711cd --- /dev/null +++ b/py_service/app/templates/pages/reports.html @@ -0,0 +1,37 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Отчёты

+ +
+
+ +
+
+ {% for r in reports %} + + + + + + {% endfor %}
ТипНазваниеОбъектСтатусРезультатДата
{{ {'inspection':'Осмотр','service':'Обслуживание','emergency':'Авария','audit':'Аудит','monthly':'Ежемесячный'}[r.report_type] }}{{ r.title }}{{ r.object.name if r.object else '' }}{{ {'draft':'Черновик','final':'Готов','cancelled':'Отменён'}[r.status] }}{{ {'ok':'OK','fixed':'Исправлено','partial':'Частично','revisit':'Повторный'}[r.result_status] }}{{ r.created_at.strftime('%d.%m.%Y') if r.created_at else '' }}
+
+
+ +{% endblock %} diff --git a/py_service/app/templates/pages/sla.html b/py_service/app/templates/pages/sla.html new file mode 100644 index 0000000..a10c82d --- /dev/null +++ b/py_service/app/templates/pages/sla.html @@ -0,0 +1,54 @@ +{% extends "page.html" %} +{% block page_content %} +
+

SLA контракты

+ +
+
+ +
+
+ {% for sc in contracts %} + + + + + + + {% endfor %}
ОбъектУровеньЦена/месПериодСтатус
{{ sc.object.name if sc.object else '' }}{{ {'start':'Базовый','business':'Оптимальный','enterprise':'Максимальный'}.get(sc.service_level, sc.service_level) }}{{ "%.2f"|format(sc.sla_price_monthly) }} ₽{{ sc.start_date.strftime('%d.%m.%Y') if sc.start_date else '' }} — {{ sc.end_date.strftime('%d.%m.%Y') if sc.end_date else '∞' }}{{ {'active':'Активен','expired':'Истёк','cancelled':'Отменён','negotiation':'Переговоры'}.get(sc.status, sc.status) }}
+
+
+ + +{% endblock %} diff --git a/py_service/app/templates/pages/tasks.html b/py_service/app/templates/pages/tasks.html new file mode 100644 index 0000000..1976906 --- /dev/null +++ b/py_service/app/templates/pages/tasks.html @@ -0,0 +1,46 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Задачи

+ {% if user.role in ['owner','engineer'] %} + + {% endif %} +
+
+ + + +
+
+ {% for t in tasks %} + + + + + + + {% endfor %}
ПриоритетНазваниеОбъектИсполнительСтатусСрок
{{ t.priority }}{{ t.title }}{{ t.object.name if t.object else '' }}{{ t.assignee.full_name if t.assignee else '' }}{{ {'open':'Открыта','in_progress':'В работе','completed':'Завершена','cancelled':'Отменена'}[t.status] }}{{ t.deadline.strftime('%d.%m.%Y') if t.deadline else '' }}{% if t.status in ['open','in_progress'] and (user.role in ['owner','engineer'] or t.assigned_to == user.id) %} +
+ {% endif %} +
+
+
+ +{% endblock %} diff --git a/py_service/app/templates/pages/users.html b/py_service/app/templates/pages/users.html new file mode 100644 index 0000000..bcdfebf --- /dev/null +++ b/py_service/app/templates/pages/users.html @@ -0,0 +1,67 @@ +{% extends "page.html" %} +{% block page_content %} +
+

Сотрудники

+ +
+
+ + {% for u in users %} + + + + + + + {% endfor %}
ЛогинФИОРольТелефонEmailСтатусСоздан
{{ u.login }}{{ u.full_name }}{{ {'owner':'Владелец','engineer':'Инженер','technician':'Техник'}[u.role] }}{{ u.phone }}{{ u.email }}{{ 'Активен' if u.is_active else 'Неактивен' }}{{ u.created_at.strftime('%d.%m.%Y') if u.created_at else '' }} + + {% if u.role != 'owner' %} +
+ + +
{% endif %} +
+
+
+ + +{% endblock %} diff --git a/py_service/app/templating.py b/py_service/app/templating.py new file mode 100644 index 0000000..99c752e --- /dev/null +++ b/py_service/app/templating.py @@ -0,0 +1,10 @@ +from fastapi.templating import Jinja2Templates +from app.config import get_settings +from app.version import get_app_version + +settings = get_settings() + +templates = Jinja2Templates(directory="app/templates") +templates.env.globals["SITE_NAME"] = "AegisOne Engineering" +templates.env.globals["app_version"] = get_app_version() +templates.env.globals["PORTAINER_URL"] = settings.PORTAINER_URL diff --git a/py_service/app/version.py b/py_service/app/version.py new file mode 100644 index 0000000..8078fae --- /dev/null +++ b/py_service/app/version.py @@ -0,0 +1,15 @@ +import os +from pathlib import Path + +VERSION_FILE = Path(__file__).parent.parent / "version.txt" + + +def read_version() -> str: + try: + return VERSION_FILE.read_text().strip() + except (FileNotFoundError, OSError): + return "0.0.1" + + +def get_app_version() -> str: + return read_version() diff --git a/py_service/scripts/bump_version.py b/py_service/scripts/bump_version.py new file mode 100644 index 0000000..8abe1de --- /dev/null +++ b/py_service/scripts/bump_version.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Auto-increment version and update changelog on deploy.""" + +import os +import sys +import json +from datetime import datetime +from pathlib import Path + +BASE = Path(__file__).resolve().parent.parent +VERSION_FILE = BASE / "version.txt" +CHANGELOG_FILE = BASE / "CHANGELOG.md" + + +def read_version() -> tuple: + try: + parts = VERSION_FILE.read_text().strip().split(".") + return tuple(int(p) for p in parts[:3]) + except (FileNotFoundError, ValueError): + return (0, 0, 1) + + +def write_version(major: int, minor: int, patch: int): + VERSION_FILE.write_text(f"{major}.{minor}.{patch}\n") + + +def bump_version(bump_type: str = "patch") -> str: + major, minor, patch = read_version() + if bump_type == "major": + major += 1; minor = 0; patch = 0 + elif bump_type == "minor": + minor += 1; patch = 0 + else: + patch += 1 + write_version(major, minor, patch) + return f"{major}.{minor}.{patch}" + + +def add_changelog_entry(version: str, features: list = None, fixes: list = None): + date = datetime.now().strftime("%d.%m.%Y") + lines = [f"\n## {version} ({date})"] + if features: + lines.append("### Новые функции\n" + "\n".join(f"- {f}" for f in features)) + if fixes: + lines.append("### Исправления и улучшения\n" + "\n".join(f"- {f}" for f in fixes)) + entry = "\n".join(lines) + "\n" + changelog = CHANGELOG_FILE.read_text(encoding="utf-8") if CHANGELOG_FILE.exists() else "# Changelog — AegisOne Service Portal\n" + # Insert after the title line + if changelog.startswith("# Changelog"): + header_end = changelog.find("\n", changelog.find("\n", 2) + 1) + if header_end == -1: + changelog += entry + else: + changelog = changelog[:header_end] + entry + changelog[header_end:] + else: + changelog = f"# Changelog — AegisOne Service Portal\n{entry}\n" + changelog + CHANGELOG_FILE.write_text(changelog, encoding="utf-8") + + +if __name__ == "__main__": + bump = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1] in ("major", "minor", "patch") else "patch" + features = [] + fixes = [] + if len(sys.argv) > 2: + try: + parsed = json.loads(sys.argv[2]) + features = parsed.get("features", []) + fixes = parsed.get("fixes", []) + except (json.JSONDecodeError, TypeError): + pass + version = bump_version(bump) + add_changelog_entry(version, features, fixes) + print(f"Version bumped to {version}") diff --git a/py_service/version.txt b/py_service/version.txt new file mode 100644 index 0000000..88c5fb8 --- /dev/null +++ b/py_service/version.txt @@ -0,0 +1 @@ +1.4.0