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 3a6e1f444c
commit 0f239d5015
44 changed files with 2131 additions and 94 deletions
+109
View File
@@ -0,0 +1,109 @@
"""Мониторинг сервера: загрузка процессора, память, место на дисках. Без внешних библиотек.
Linux (в том числе Docker): /proc/stat, /proc/meminfo; если контейнеру задан лимит памяти (cgroup v2 memory.max) —
показываем память контейнера. Windows (локальный запуск): GetSystemTimes и GlobalMemoryStatusEx через ctypes."""
import os
import shutil
import sys
import time
from pathlib import Path
SAMPLE_SEC = 0.25
def _linux_cpu_times() -> tuple[int, int]:
with open("/proc/stat") as f:
vals = [int(x) for x in f.readline().split()[1:]]
idle = vals[3] + (vals[4] if len(vals) > 4 else 0) # idle + iowait
return idle, sum(vals)
def _win_cpu_times() -> tuple[int, int]:
import ctypes
from ctypes import wintypes
idle, kernel, user = wintypes.FILETIME(), wintypes.FILETIME(), wintypes.FILETIME()
ctypes.windll.kernel32.GetSystemTimes(ctypes.byref(idle), ctypes.byref(kernel), ctypes.byref(user))
v = lambda t: (t.dwHighDateTime << 32) | t.dwLowDateTime
return v(idle), v(kernel) + v(user) # время ядра уже включает простой
def cpu_percent() -> float | None:
get = _win_cpu_times if sys.platform == "win32" else _linux_cpu_times
try:
i1, t1 = get()
time.sleep(SAMPLE_SEC)
i2, t2 = get()
except (OSError, ValueError, AttributeError):
return None
dt = t2 - t1
return round(100.0 * (1 - (i2 - i1) / dt), 1) if dt > 0 else 0.0
def _cgroup_memory() -> tuple[int, int] | None:
try:
limit = Path("/sys/fs/cgroup/memory.max").read_text().strip()
if limit == "max":
return None
return int(Path("/sys/fs/cgroup/memory.current").read_text()), int(limit)
except (OSError, ValueError):
return None
def memory() -> dict | None:
"""{used, total, scope: host|container} в байтах."""
if sys.platform == "win32":
try:
import ctypes
class MS(ctypes.Structure):
_fields_ = [("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong), ("ullTotalPhys", ctypes.c_ulonglong),
("ullAvailPhys", ctypes.c_ulonglong), ("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong),
("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong), ("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
m = MS()
m.dwLength = ctypes.sizeof(MS)
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m))
return {"used": m.ullTotalPhys - m.ullAvailPhys, "total": m.ullTotalPhys, "scope": "host"}
except (OSError, AttributeError):
return None
cg = _cgroup_memory()
if cg:
return {"used": cg[0], "total": cg[1], "scope": "container"}
try:
info = {}
with open("/proc/meminfo") as f:
for line in f:
k, v = line.split(":", 1)
info[k] = int(v.split()[0]) * 1024
total = info["MemTotal"]
return {"used": total - info.get("MemAvailable", info.get("MemFree", 0)), "total": total, "scope": "host"}
except (OSError, KeyError, ValueError):
return None
def disks(paths: dict[str, Path]) -> list[dict]:
"""Место на дисках, где лежат база, фото и т. п. Один и тот же диск показывается один раз."""
out, seen = [], {}
for label, p in paths.items():
p = Path(p)
while not p.exists() and p.parent != p:
p = p.parent
try:
u = shutil.disk_usage(p)
except OSError:
continue
key = (u.total, os.stat(p).st_dev)
if key in seen:
seen[key]["labels"].append(label)
continue
seen[key] = {"labels": [label], "path": str(p), "used": u.used, "total": u.total, "free": u.free}
out.append(seen[key])
return out
def snapshot(paths: dict[str, Path]) -> dict:
try:
load = os.getloadavg()
except (OSError, AttributeError):
load = None
return {"cpu": cpu_percent(), "cpu_count": os.cpu_count(), "load": load, "memory": memory(), "disks": disks(paths),
"platform": sys.platform}