This commit is contained in:
2026-05-30 00:15:16 +05:00
parent d3afd698b6
commit 43dee9f796
5 changed files with 58 additions and 2 deletions
+5 -1
View File
@@ -2,7 +2,7 @@ FROM python:3.12-slim
# ffmpeg/ffprobe — движок записи и перекодирования # ffmpeg/ffprobe — движок записи и перекодирования
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \ && apt-get install -y --no-install-recommends ffmpeg openssl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
@@ -12,6 +12,10 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY backend/app ./app COPY backend/app ./app
COPY frontend ./frontend COPY frontend ./frontend
COPY backend/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 8090 EXPOSE 8090
# entrypoint генерирует self-signed TLS-сертификат при отсутствии, затем запускает CMD/команду
ENTRYPOINT ["/entrypoint.sh"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8090"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8090"]
+15
View File
@@ -280,3 +280,18 @@ async def delete_camera(cam_id: str, request: Request) -> dict:
await runtime.supervisor.remove_camera(cam_id) await runtime.supervisor.remove_camera(cam_id)
await go2rtc.remove_camera_streams(runtime.config, cam_id) await go2rtc.remove_camera_streams(runtime.config, cam_id)
return {"ok": True} return {"ok": True}
@router.post("/clear")
async def clear_cameras(request: Request) -> dict:
"""Удаляет ВСЕ камеры: чистит cameras.yaml, останавливает запись и потоки go2rtc.
Архив (nvr.db + файлы .mp4) НЕ трогает — очищается только список камер."""
require_role(request, "admin")
ids = [c.id for c in runtime.config.cameras]
runtime.config.cameras = []
save_config(runtime.config)
for cam_id in ids:
await runtime.supervisor.remove_camera(cam_id)
await go2rtc.remove_camera_streams(runtime.config, cam_id)
return {"ok": True, "removed": len(ids)}
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# Гарантирует наличие self-signed TLS-сертификата перед стартом uvicorn.
# Если cert.pem/key.pem отсутствуют (свежий деплой) — генерирует их;
# иначе использует существующие. Затем запускает переданную команду (CMD).
set -e
CERT_DIR=/app/certs
if [ ! -f "$CERT_DIR/cert.pem" ] || [ ! -f "$CERT_DIR/key.pem" ]; then
echo "[entrypoint] TLS cert not found in $CERT_DIR — generating self-signed (CN=${NVR_TLS_CN:-localhost})"
mkdir -p "$CERT_DIR"
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout "$CERT_DIR/key.pem" -out "$CERT_DIR/cert.pem" \
-days 3650 -subj "/CN=${NVR_TLS_CN:-localhost}"
fi
exec "$@"
+1 -1
View File
@@ -12,7 +12,7 @@ services:
ports: ports:
- "443:8090" # дашборд на https://<сервер> - "443:8090" # дашборд на https://<сервер>
volumes: volumes:
- ./certs:/app/certs:ro # self-signed TLS-сертификат - ./certs:/app/certs # self-signed TLS-сертификат (rw: entrypoint генерирует при отсутствии)
- ./VERSION:/app/VERSION:ro # номер версии (показывается в UI) - ./VERSION:/app/VERSION:ro # номер версии (показывается в UI)
- ./cameras.yaml:/app/cameras.yaml # rw: редактирование камер из UI - ./cameras.yaml:/app/cameras.yaml # rw: редактирование камер из UI
- ./go2rtc.yaml:/app/go2rtc.yaml # rw: бэкенд регенерирует live-потоки - ./go2rtc.yaml:/app/go2rtc.yaml # rw: бэкенд регенерирует live-потоки
+21
View File
@@ -1100,6 +1100,8 @@ const CamSettings = {
if (be) be.onclick = () => this.bulk ? this.saveBulk() : this.enterBulk(); if (be) be.onclick = () => this.bulk ? this.saveBulk() : this.enterBulk();
const bc = document.getElementById("bulk-cancel"); const bc = document.getElementById("bulk-cancel");
if (bc) bc.onclick = () => this.exitBulk(); if (bc) bc.onclick = () => this.exitBulk();
const cc = document.getElementById("clear-cams");
if (cc) cc.onclick = () => this.clearAll();
await this.load(); await this.load();
connectStatusWS(() => { if (!this.editing) this.load(); }); connectStatusWS(() => { if (!this.editing) this.load(); });
}, },
@@ -1108,10 +1110,12 @@ const CamSettings = {
const be = document.getElementById("bulk-edit"); const be = document.getElementById("bulk-edit");
const bc = document.getElementById("bulk-cancel"); const bc = document.getElementById("bulk-cancel");
const add = document.getElementById("add-cam"); const add = document.getElementById("add-cam");
const cc = document.getElementById("clear-cams");
if (be) be.textContent = this.bulk ? "Сохранить" : "Изменить"; if (be) be.textContent = this.bulk ? "Сохранить" : "Изменить";
if (be) be.classList.toggle("primary", this.bulk); if (be) be.classList.toggle("primary", this.bulk);
if (bc) bc.hidden = !this.bulk; if (bc) bc.hidden = !this.bulk;
if (add) add.hidden = this.bulk; // во время массового редактирования не добавляем if (add) add.hidden = this.bulk; // во время массового редактирования не добавляем
if (cc) cc.hidden = this.bulk;
}, },
enterBulk() { enterBulk() {
@@ -1127,6 +1131,23 @@ const CamSettings = {
await this.load(); await this.load();
}, },
// удалить ВСЕ камеры (архив не трогается) — POST /api/cameras/clear
async clearAll() {
if (this.bulk) return;
const n = this.cams.length;
if (!n) { alert("Камер нет"); return; }
if (!confirm(`Удалить ВСЕ камеры (${n})?\nЗаписи архива НЕ затрагиваются. Действие необратимо.`)) return;
const r = await fetch("/api/cameras/clear", { method: "POST" });
if (!r.ok) {
const d = await r.json().catch(() => ({}));
alert("Не удалось очистить: " + (d.detail || r.status));
return;
}
const d = await r.json().catch(() => ({}));
await this.load();
alert(`Удалено камер: ${d.removed != null ? d.removed : n}`);
},
// ── строка камеры в режиме массового редактирования ── // ── строка камеры в режиме массового редактирования ──
bulkRowHtml(slotNo, cam) { bulkRowHtml(slotNo, cam) {
const v = (x) => this.esc(x); const v = (x) => this.esc(x);