From 7060f26e8650b237945b2d4eba2960f82c24b571 Mon Sep 17 00:00:00 2001 From: core Date: Mon, 1 Jun 2026 18:21:49 +0500 Subject: [PATCH] cameras: surface per-camera REC (record-to-disk) toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `enabled` already gated recording only (go2rtc/live work regardless), but it was a buried "Вкл" checkbox. Make it an explicit REC control: - New "Запись" column with an inline REC switch in each camera row. - Relabel the add/edit form toggle "Вкл" -> "REC" + tooltip. - New POST /api/cameras/{id}/record {enabled} that flips recording through the supervisor only (no go2rtc re-sync, so live view doesn't blip). Off = view-only (live via go2rtc); on = record segments to disk. VERSION -> 0.0.124. Co-Authored-By: Claude Opus 4.8 (1M context) --- VERSION | 2 +- backend/app/api/cameras.py | 21 +++++++++++++++++++++ frontend/settings.html | 2 +- frontend/static/app.js | 29 +++++++++++++++++++++++------ 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/VERSION b/VERSION index dd5a402..8624408 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.123 +0.0.124 diff --git a/backend/app/api/cameras.py b/backend/app/api/cameras.py index 8a74543..bb17a73 100644 --- a/backend/app/api/cameras.py +++ b/backend/app/api/cameras.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import re +from dataclasses import replace from fastapi import APIRouter, HTTPException, Query, Request, WebSocket, WebSocketDisconnect from fastapi.responses import StreamingResponse @@ -298,3 +299,23 @@ async def clear_cameras(request: Request) -> dict: await runtime.supervisor.remove_camera(cam_id) await go2rtc.remove_camera_streams(runtime.config, cam_id) return {"ok": True, "removed": len(ids)} + + +@router.post("/{cam_id}/record") +async def set_record(cam_id: str, request: Request) -> dict: + """Включает/выключает запись камеры на диск (тумблер REC). + + enabled=False — запись не идёт, но go2rtc/live не трогаем → просмотр работает. + Меняет только рекордер (без пересоздания go2rtc-потоков, чтобы live не мигал).""" + require_role(request, "admin") + cam = runtime.config.camera(cam_id) + if not cam: + raise HTTPException(404, "camera not found") + body = await request.json() + on = bool(body.get("enabled", True)) + idx = next(i for i, c in enumerate(runtime.config.cameras) if c.id == cam_id) + new = replace(cam, enabled=on) + runtime.config.cameras[idx] = new + save_config(runtime.config) + await runtime.supervisor.replace_camera(new) # старт/стоп записи; go2rtc не трогаем + return {"ok": True, "enabled": on} diff --git a/frontend/settings.html b/frontend/settings.html index 8d8318a..ce9cc47 100644 --- a/frontend/settings.html +++ b/frontend/settings.html @@ -55,7 +55,7 @@ - +
IDКамераIPПотокиСтатус
IDКамераIPПотокиЗаписьСтатус
diff --git a/frontend/static/app.js b/frontend/static/app.js index ee114e2..032bcaa 100644 --- a/frontend/static/app.js +++ b/frontend/static/app.js @@ -1178,7 +1178,7 @@ const CamSettings = { bulkRowHtml(slotNo, cam) { const v = (x) => this.esc(x); return ( - `` + + `` + `
#${slotNo} · ID ${v(cam.id)}
` + `
` + `` + @@ -1187,7 +1187,7 @@ const CamSettings = { `` + `` + `` + - `` + + `` + `
` + `` ); @@ -1235,7 +1235,7 @@ const CamSettings = { const v = (x) => this.esc(x); const isEdit = !!cam; return ( - `` + + `` + `
` + `` + `` + @@ -1244,7 +1244,7 @@ const CamSettings = { `` + `` + `` + - `` + + `` + `
` + `
` + `` + @@ -1344,6 +1344,7 @@ const CamSettings = { `${c.name}` + `${c.ip}` + `${streams}` + + `` + `${STATE_RU[st] || st}` + `` + ` ` + @@ -1354,14 +1355,14 @@ const CamSettings = { const logTr = document.createElement("tr"); logTr.className = "logrow"; const hid = openLogs.has("log-" + c.id) ? "" : "hidden"; - logTr.innerHTML = `
`; + logTr.innerHTML = `
`; tb.appendChild(logTr); } else { tr.className = "slot-empty"; tr.dataset.slot = i + 1; tr.innerHTML = `${i + 1}` + - `———` + + `————` + `пусто` + ``; tb.appendChild(tr); @@ -1395,6 +1396,22 @@ const CamSettings = { box.hidden = false; } }; + + tb.onchange = async (e) => { // тумблер REC в строке — старт/стоп записи на диск + const cb = e.target.closest(".rec-toggle"); + if (!cb) return; + const id = cb.dataset.id; + cb.disabled = true; + try { + const r = await fetch(`/api/cameras/${id}/record`, { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: cb.checked }), + }); + if (!r.ok) { cb.checked = !cb.checked; const d = await r.json().catch(() => ({})); alert("Не удалось: " + (d.detail || r.status)); } + else if (this.byId[id]) { this.byId[id].enabled = cb.checked; } + } catch (err) { cb.checked = !cb.checked; alert("Сбой запроса"); } + finally { cb.disabled = false; } + }; }, };