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
+23 -1
View File
@@ -27,8 +27,8 @@ class Position(Base):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), unique=True)
kind: Mapped[str] = mapped_column(String(12), default="technician")
# устарело: график теперь у сотрудника (User.schedule_id); значение перенесено сотрудникам при обновлении и больше не используется
schedule_id: Mapped[int | None] = mapped_column(ForeignKey("schedules.id"), nullable=True)
schedule: Mapped[ShiftSchedule | None] = relationship(lazy="joined")
class Department(Base):
@@ -55,6 +55,9 @@ class User(Base):
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
position_id: Mapped[int | None] = mapped_column(ForeignKey("positions.id"), nullable=True)
position: Mapped[Position | None] = relationship(lazy="joined")
# график работы сотрудника: должность выполняет тот, кто её занимает и сейчас на смене. Пусто — всегда на смене
schedule_id: Mapped[int | None] = mapped_column(ForeignKey("schedules.id"), nullable=True)
schedule: Mapped[ShiftSchedule | None] = relationship(lazy="joined")
department_id: Mapped[int | None] = mapped_column(ForeignKey("departments.id"), nullable=True)
department: Mapped[Department | None] = relationship(lazy="joined", foreign_keys=[department_id])
# карточка сотрудника
@@ -89,6 +92,8 @@ class Equipment(Base):
department_id: Mapped[int | None] = mapped_column(ForeignKey("departments.id"), nullable=True)
# кран (грузоподъёмное оборудование): при включённой карте кранов показывается на ней, а не на карте завода
is_crane: Mapped[bool] = mapped_column(Boolean, default=False)
# папка документации в Nextcloud (путь от корня документации в настройках сервера); пусто — не привязана
docs_path: Mapped[str] = mapped_column(String(500), default="")
class ServiceUnit(Base):
@@ -160,6 +165,7 @@ class ChecklistItem(Base):
max_value: Mapped[float | None] = mapped_column(Float, nullable=True)
required: Mapped[bool] = mapped_column(Boolean, default=True)
photo: Mapped[str] = mapped_column(String(10), default="none") # none|optional|required
photo_keep: Mapped[bool] = mapped_column(Boolean, default=False) # фото хранить более года (каталог keep/, очистка не трогает)
class OrderTemplate(Base):
@@ -232,6 +238,7 @@ class OrderItem(Base):
answered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
out_of_range: Mapped[bool] = mapped_column(Boolean, default=False)
photo: Mapped[str] = mapped_column(String(10), default="none")
photo_keep: Mapped[bool] = mapped_column(Boolean, default=False)
service_unit_id: Mapped[int | None] = mapped_column(ForeignKey("service_units.id"), nullable=True) # «узел обслужен»
is_meter: Mapped[bool] = mapped_column(Boolean, default=False) # пункт «наработка, м/ч» — обновляет счётчик машины
photos: Mapped[list["Photo"]] = relationship(
@@ -276,6 +283,20 @@ class Feedback(Base):
answered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
class DocIndex(Base):
"""Индекс документации для AI-помощника: имя файла и фрагмент текста (в нижнем регистре). Строится заново из Nextcloud."""
__tablename__ = "doc_index"
id: Mapped[int] = mapped_column(primary_key=True)
equipment_id: Mapped[int] = mapped_column(ForeignKey("equipment.id", ondelete="CASCADE"), index=True)
path: Mapped[str] = mapped_column(String(1000)) # путь файла от папки машины
name: Mapped[str] = mapped_column(String(300))
etag: Mapped[str] = mapped_column(String(200), default="")
size: Mapped[int] = mapped_column(Integer, default=0)
text: Mapped[str] = mapped_column(Text, default="") # извлечённый текст, нижний регистр, обрезан
note: Mapped[str] = mapped_column(String(200), default="") # почему текста нет (большой файл, ошибка чтения)
indexed_at: Mapped[datetime] = mapped_column(DateTime, default=config.now)
class Setting(Base):
__tablename__ = "settings"
key: Mapped[str] = mapped_column(String(64), primary_key=True)
@@ -464,6 +485,7 @@ class DailyTask(Base):
updated_at: Mapped[datetime] = mapped_column(DateTime, default=config.now)
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
labor_hours: Mapped[float | None] = mapped_column(Float, nullable=True) # трудоёмкость (норма), ч — для отчёта по сотрудникам
photo_keep: Mapped[bool] = mapped_column(Boolean, default=False) # фото к заданию хранить более года
class TaskEvent(Base):