feat: документация из Nextcloud, график у сотрудника, автоочистка фото, мониторинг, итог мастера, фильтр по местам; доп. сервисы в docker-compose

- docker-compose.yml: профили docs (Nextcloud), office (ONLYOFFICE Document Server), ai (Ollama + скачивание модели)
- автонастройка: адреса Nextcloud, ONLYOFFICE, портала и модели подставляются из .env или по именам сервисов
  во внутренней сети Docker; значения из интерфейса важнее
- локальная модель в своей сети (имя сервиса, частный IP) подключается без ключа API
- .env.example, docs/DEPLOY.md (раздел 7а), README
This commit is contained in:
darkorion
2026-10-13 13:27:20 +03:00
parent 6b8fca4f5a
commit e67c175f80
44 changed files with 2131 additions and 94 deletions
+62 -11
View File
@@ -333,8 +333,9 @@ def build():
["Наряд", "Одно задание: что проверить или обслужить, на каком оборудовании, кому, до какого срока и кто проверит."],
["Чек-лист", "Список задач внутри наряда. У каждой задачи свой тип ответа: статус, выбор из списка, число или текст; "
"у некоторых нужно приложить фото."],
["Должность и график", "Сотрудник привязан к должности, у должности есть график (смены). Наряд, назначенный на должность, "
"видят и могут взять только те, кто сейчас на смене."],
["Должность и график", "Сотрудник занимает должность и работает по своему графику (смены). Одну должность могут занимать "
"несколько сменщиков. Наряд, назначенный на должность, видят, берут и получают уведомление только те, "
"кто сейчас на смене, — сменщики дома не беспокоятся."],
["Расписание нарядов", "Наряд настраивается один раз и затем создаётся автоматически: каждый день, по дням недели, "
"раз в месяц или каждые N дней."],
["Проверка", "После сдачи наряд уходит проверяющему: он принимает его или возвращает на доработку с комментарием."],
@@ -562,7 +563,8 @@ def build():
pdf.bullets([
"для **числа** задайте единицу измерения и границы нормы — отклонения будут подсвечиваться и попадут в отчёт;",
"для **списка** — варианты ответа, по одному в строке;",
"для любой задачи — режим фото: без фото, по желанию, обязательно;",
"для любой задачи — режим фото: без фото, по желанию, обязательно; если фото нужно сохранить надолго (табличка, акт, "
"дефект), отметьте **«хранить более года»** — такие снимки лежат в отдельном каталоге и не удаляются автоочисткой сервера;",
"стрелками меняется порядок задач, галочкой «Обязательный» отмечаются пункты, без которых наряд не сдать.",
])
pdf.wide("d_checklist", "Редактор чек-листа")
@@ -708,6 +710,15 @@ def build():
"В приложении Android история открывается кнопкой «История машины».")
pdf.wide("d_history", "История машины с фильтрами")
pdf.phone("a_history", "История машины в приложении Android", width=70)
pdf.h2("Документация машины (Nextcloud)")
pdf.p("Если подключён Nextcloud, в карточке машины есть блок **«Документация»**: паспорта, руководства, схемы, хранящиеся "
"в Nextcloud по машинам. Кнопка **«Открыть»** показывает папку машины: слева файлы и вложенные папки, справа — документ. "
"**PDF, картинки и текстовые файлы** открываются прямо в портале, документы **Word, Excel, PowerPoint** — тоже, после перевода в PDF "
"на сервере ONLYOFFICE (первое открытие — несколько секунд). Любой файл можно **скачать**. Заходить в Nextcloud не нужно: "
"портал сам забирает файлы и показывает только документацию машин, которые видит сотрудник.")
pdf.p("**Привязывают папку администратор или модератор**: «Привязать папку» в блоке «Документация» открывает проводник по "
"документации — выберите папку машины и нажмите «Выбрать» или «Привязать эту папку». Рядом с папкой видно, к каким машинам "
"она уже привязана. «Сменить папку» и «Отвязать» — там же.")
pdf.h2("Сообщения об отказах извне")
pdf.p("Об аварии может сообщить и человек без учётной записи — подрядчик, охрана, смежная служба. Для этого есть **публичная "
"форма** по адресу **/report** (её удобно разместить на QR-коде у оборудования). В форме выбирается **место** — после этого "
@@ -832,16 +843,22 @@ def build():
"висит приглашение запустить мастер). Мастер проводит по шагам в порядке зависимостей данных:")
pdf.bullets([
"**Сервер** — проверить часовой пояс, каталог фото, время сервера.",
"**Графики** — типовые одним нажатием (пятидневка, сутки через трое, 2/2 день-ночь, круглосуточно) или в конструкторе.",
"**Графики** — типовые одним нажатием (пятидневка, сутки через трое, 2/2 день-ночь, круглосуточно) или в конструкторе. "
"График потом назначается каждому сотруднику.",
"**Должности → отделы → места → оборудование** — пакетным вводом: одна строка — одна запись, поля через «;». "
"Для оборудования: «Название; инв. №; место; наработка; отдел; кран» — отдел закрепляет машину за службой, слово «кран» "
"отправляет её в раздел «Краны».",
"**Сотрудники** — строки «ФИО; логин; должность; отдел; роль». Логины и временные пароли создаются сами и показываются "
"**Сотрудники** — строки «ФИО; логин; должность; отдел; график; роль». Логины и временные пароли создаются сами и показываются "
"таблицей для печати или CSV — **только один раз**.",
"**Карта завода** (и подсказка про отдельную карту кранов), **склады и начальные остатки инструмента**, **чек-листы и расписание**, **внешние обращения**, **резервные копии**.",
])
pdf.wide("d_setup", "Мастер первичной настройки")
pdf.note("Шаг считается выполненным, когда в нём есть записи, или если отметить «шаг выполнен». К мастеру можно возвращаться в любое время.", "info")
pdf.note("Шаг считается выполненным, когда в нём есть записи, или если отметить «шаг выполнен» — мастер сразу переходит к следующему. "
"К мастеру можно возвращаться в любое время.", "info")
pdf.p("Кнопка **«Завершить и проверить»** на последнем шаге открывает **проверку готовности**: мастер смотрит на введённые данные и "
"показывает, что мешает работе — сотрудники без должности, должности, которые никто не занимает, должности, где сейчас по "
"графикам никого нет на смене, машины без места, нет чек-листов или расписания, выключены автокопии или автоочистка фото. "
"Рядом с каждым пунктом — кнопка, чтобы поправить. После завершения плитка мастера ведёт сразу к этой проверке.")
pdf.h2("AI-помощник (включает администратор)")
pdf.p("В «Настройках сервера» администратор может включить **AI-помощника**: указать провайдера (OpenAI-совместимый — OpenAI, "
@@ -855,6 +872,12 @@ def build():
"Ключ API не показывается в настройках и не попадает в резервные копии.",
])
pdf.wide("d_assistant_settings", "Настройка AI-помощника")
pdf.p("**«Работа с документацией»** — отдельная опция помощника в той же карточке. Включённая, она раз в сутки собирает индекс "
"документации машин (имена файлов и начало текста PDF и документов Office), и помощник отвечает на вопросы вида "
"«где схема двигателя компрессора?» ссылкой на нужный файл и подсказкой, что в нём искать.")
pdf.note("Риски опции: имена файлов и фрагменты документов уходят провайдеру ИИ — с облачным провайдером документация покидает "
"предприятие, для закрытых документов используйте локальную модель. Запросы становятся в разы длиннее, расход токенов "
"растёт. Помощник видит только начало документов — ответ проверяйте в самом файле.", "warn")
pdf.wide("d_assistant", "Помощник: ответ по данным пользователя и подсказка «как сделать»")
pdf.phone("a_assistant", "Помощник в приложении Android (круглая кнопка справа внизу или пункт меню «Помощник»)", width=70)
@@ -883,9 +906,13 @@ def build():
"пользователя. Остальным администраторам мастер первичной настройки и резервные копии доступны как обычно.")
pdf.h2("Должности и графики работы")
pdf.p("**Должность** имеет тип (**техник** выполняет наряды, **инженер** проверяет и планирует) и **график**. По графику "
"система определяет, кто сейчас «на смене» и кому показывать наряды, назначенные на должность. Должность без графика "
"считается всегда на смене.")
pdf.p("**Должность** имеет тип (**техник** выполняет наряды, **инженер** проверяет и планирует). **График** задаётся не должности, "
"а **сотруднику** («Пользователи» → «Изменить» → «График работы»): должность выполняет тот, кто её занимает и сейчас на смене. "
"Например, «Дежурный механик» — четыре сменщика с графиком «сутки через трое», каждый со своим первым днём цикла: наряд или "
"задание на эту должность увидит и получит уведомление только тот, кто сегодня дежурит. Сотрудник без графика считается "
"всегда на смене.")
pdf.note("При обновлении портала графики, которые раньше были заданы должностям, автоматически перенесены их сотрудникам. "
"Для сменщиков поправьте графики (или первый день цикла), чтобы на смене был один человек.", "info")
pdf.p("Графики настраиваются **конструктором**: можно собрать любой цикл — дневные и ночные смены, отсыпные, сутки через трое. "
"Каждый шаг цикла — это «Смена» или «Выходной», число дней подряд, время начала и конца и метка (например, «день», "
"«ночь», «отсыпной»). Если конец смены раньше начала (20:00–08:00), смена ночная; одинаковое время (08:00–08:00) — сутки.")
@@ -901,6 +928,8 @@ def build():
pdf.p("Разделы **«Оборудование»** и **«Места»** — справочники для нарядов и отчётов. Оборудование можно привязать к месту "
"(например, «Цех №1»), указать инвентарный номер, **отдел** (кто его видит) и отметку **«Кран»**. Неиспользуемое оборудование "
"отключается флажком «Активно».")
pdf.p("Над списком оборудования (и кранов) — фильтр **«Место»**: выберите цех или участок, и в списке останутся машины этого "
"места и всех вложенных в него мест (например, «Цех №1» покажет и машины «Компрессорной», входящей в цех).")
pdf.h2("Обратная связь по порталу")
pdf.p("Внизу каждой страницы сайта — неприметная ссылка **«Предложить улучшение портала или сообщить об ошибке»**; в мобильной версии и "
@@ -914,6 +943,15 @@ def build():
pdf.p("Раздел **«Настройки сервера»** позволяет изменить каталог для хранения фотографий и максимальный размер снимка, а также "
"ключ для мгновенных уведомлений Android, включить публичную форму заявок и **«Краны — отдельная служба»**. Внизу виден "
"расход места: сколько фото хранится и сколько свободно на диске.")
pdf.p("**Автоочистка фото**: поле «удалять фото нарядов и заданий старше, дней» (например, 365). Раз в сутки сервер удаляет "
"фото старше этого срока; 0 — не удалять. Снимки с отметкой **«хранить более года»** (в чек-листе у пункта или в задании) "
"с самого начала лежат в отдельном каталоге **keep/** и не удаляются. В блоке «Состояние» видно, сколько фото будет удалено "
"при следующей очистке; кнопка «Очистить сейчас» запускает её сразу.")
pdf.note("Удалённые фото не восстановить, кроме как из резервной копии с фото. По умолчанию автоочистка выключена.", "warn")
pdf.h2("Мониторинг сервера (только администратор портала)")
pdf.p("«Администрирование → **Мониторинг сервера**»: круговые диаграммы загрузки **процессора**, занятой **памяти** и места на "
"**дисках** с данными портала и фото. Значения обновляются каждые 5 секунд, пока страница открыта; от 75 % диаграмма "
"становится жёлтой, от 90 % — красной. В Docker с лимитом памяти показывается память контейнера.")
pdf.wide("d_server", "Настройки сервера", width=150)
pdf.note("При смене каталога фото старые снимки автоматически **не переносятся** — скопируйте файлы вручную.", "warn")
@@ -935,6 +973,19 @@ def build():
pdf.note("Рабочие данные в демо-режиме не изменяются и не удаляются.", "info")
# ================= 7 =================
pdf.h2("Подключение Nextcloud и ONLYOFFICE")
pdf.p("«Настройки сервера» → **«Документация из Nextcloud»** (только администратор портала).")
pdf.bullets([
"В Nextcloud заведите служебного пользователя (например, portal-docs), дайте ему **доступ только на чтение** к папке "
"с документацией и создайте для него **пароль приложения** (Настройки → Безопасность).",
"В портале укажите адрес Nextcloud (лучше внутренний, в локальной сети), учётную запись, пароль приложения и корневую папку "
"документации. Кнопка «Проверить» покажет, сколько папок видно в корне.",
"Для документов Office укажите адрес **ONLYOFFICE Document Server** в локальной сети, его **секрет JWT** (если включён) и "
"**внутренний адрес портала** — по нему ONLYOFFICE забирает исходник по одноразовой ссылке. Без ONLYOFFICE документы "
"Office только скачиваются.",
"Сотрудникам Nextcloud не нужен: портал сам ходит в него и проверяет, кто какую машину видит. Пароль приложения и секрет "
"JWT не показываются в настройках и не попадают в резервные копии.",
])
pdf.h1("14. Частые вопросы")
faq = [
("Я не вижу машину (или кран), которая точно есть.",
@@ -944,8 +995,8 @@ def build():
"Вы в демонстрационном режиме — это тестовые данные. Нажмите **«Выйти из демо»** в полосе. Если кнопки нет или не помогает, "
"попросите администратора выключить демо для вас."),
("Я не вижу наряд, который мне должны были назначить.",
"Наряд, назначенный на должность, виден только сотрудникам этой должности **на смене**. Проверьте график смены и должность в "
"настройках; если вы вне смены — наряд появится, когда смена начнётся. Нажмите обновление списка. Если наряд назначен "
"Наряд, назначенный на должность, виден только сотрудникам этой должности **на смене** по их графику. Проверьте свой график "
"и должность в карточке; если вы вне смены — наряд появится, когда смена начнётся. Нажмите обновление списка. Если наряд назначен "
"другому человеку или уже взят коллегой, вы его не увидите."),
("Пишет «Нет связи» или «не отправлено» и не исчезает.",
"Проверьте интернет на телефоне. Данные сохранены и не пропадут: они уйдут сами. Для немедленной попытки нажмите кнопку "