From ec41267444df68caa274f4aef8aa2143c76619a4 Mon Sep 17 00:00:00 2001 From: core Date: Mon, 1 Jun 2026 20:47:24 +0500 Subject: [PATCH] =?UTF-8?q?ui:=20edit=20all=2064=20slots=20inline=20in=20b?= =?UTF-8?q?ulk=20mode;=20drop=20per-row=20"+=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Изменить" now renders all 64 slots as editable rows — filled ones for editing, empty ones with blank fields (user prefilled "admin", channel 1) for adding. Saving PUTs changed cameras and POSTs filled empty slots (auto id camN, lowest free). Removed the per-row "+ Добавить" buttons; empty slots in normal view just show "пусто". Bulk edit can now be entered with zero cameras. VERSION 0.0.134. Co-Authored-By: Claude Opus 4.8 (1M context) --- VERSION | 2 +- frontend/static/app.js | 100 ++++++++++++++++++++++++++--------------- 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/VERSION b/VERSION index 6067817..297b2ec 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.133 +0.0.134 diff --git a/frontend/static/app.js b/frontend/static/app.js index d7a9bf1..c4b2ca3 100644 --- a/frontend/static/app.js +++ b/frontend/static/app.js @@ -1180,13 +1180,21 @@ const CamSettings = { }, enterBulk() { - if (!this.cams.length) { alert("Нет камер для редактирования"); return; } - this.bulk = true; this.editing = true; + this.bulk = true; this.editing = true; // пустые слоты тоже редактируемы → можно с 0 камер this.setBulkButtons(); this.setBulkHeaders(true); this.load(); }, + // наименьший свободный id вида camN (id скрыт от пользователя, нужен как ключ записи/потока) + nextCamId(used) { + for (let n = 1; n <= 999; n++) { + const id = "cam" + n; + if (!used.has(id)) { used.add(id); return id; } + } + return "cam" + (used.size + 1); + }, + async exitBulk() { this.bulk = false; this.editing = false; this.setBulkButtons(); @@ -1212,20 +1220,29 @@ const CamSettings = { }, // ── строка камеры в режиме массового редактирования: те же колонки, но ячейки — - // редактируемые инпуты (НЕ отдельная форма). Статус-колонка на время правки = логин/пароль. + // редактируемые инпуты (НЕ отдельная форма). cam=null → пустой слот для новой камеры. + // Статус-колонка на время правки = логин/пароль. bulkRowHtml(slotNo, cam) { const v = (x) => this.esc(x); + const name = cam ? v(cam.name) : ""; + const ip = cam ? v(cam.ip) : ""; + const user = cam ? v(cam.user) : "admin"; + const password = cam ? v(cam.password) : ""; + const ch = cam ? channelFromPath(cam.main_path) : 1; + const enabled = cam ? cam.enabled : true; + const record = cam ? cam.record : true; + const storageId = cam ? cam.storage_id : ""; return ( `${slotNo}` + - `` + - `` + - `` + - `` + - `` + - `` + + `` + + `` + + `` + + `` + + `` + + `` + `` + - `` + - `` + + `` + + `` + `` + `` ); @@ -1240,37 +1257,49 @@ const CamSettings = { }, async saveBulk() { - const rows = document.querySelectorAll("#cam-rows tr[data-id]"); + const rows = document.querySelectorAll("#cam-rows tr.bulk-row"); const errors = []; + const used = new Set(this.cams.map((c) => c.id)); // занятые id (для генерации новых) for (const tr of rows) { - const id = tr.dataset.id; - const orig = this.byId[id] || {}; const get = (f) => { const el = tr.querySelector(`[data-f="${f}"]`); return el ? el.value : ""; }; + const ch = +get("channel") || 1; const payload = { name: get("name").trim(), ip: get("ip").trim(), user: get("user").trim(), password: get("password"), - main_path: mainPathForChannel(+get("channel") || 1), - sub_path: subPathForChannel(+get("channel") || 1), + main_path: mainPathForChannel(ch), + sub_path: subPathForChannel(ch), storage_id: get("storage_id"), enabled: tr.querySelector('[data-f="enabled"]').checked, record: tr.querySelector('[data-f="record"]').checked, }; - const changed = - payload.name !== orig.name || payload.ip !== orig.ip || - payload.user !== orig.user || payload.password !== (orig.password || "") || - payload.main_path !== orig.main_path || - (payload.sub_path || "") !== (orig.sub_path || "") || - (payload.storage_id || "") !== (orig.storage_id || "") || - payload.enabled !== orig.enabled || - payload.record !== orig.record; - if (!changed) continue; - const r = await fetch(`/api/cameras/${id}`, { - method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - if (!r.ok) { const d = await r.json().catch(() => ({})); errors.push(`${id}: ${d.detail || r.status}`); } + if (tr.dataset.id) { // существующая камера → PUT только при изменении + const id = tr.dataset.id; + const orig = this.byId[id] || {}; + const changed = + payload.name !== orig.name || payload.ip !== orig.ip || + payload.user !== orig.user || payload.password !== (orig.password || "") || + payload.main_path !== orig.main_path || + (payload.sub_path || "") !== (orig.sub_path || "") || + (payload.storage_id || "") !== (orig.storage_id || "") || + payload.enabled !== orig.enabled || + payload.record !== orig.record; + if (!changed) continue; + const r = await fetch(`/api/cameras/${id}`, { + method: "PUT", headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!r.ok) { const d = await r.json().catch(() => ({})); errors.push(`${id}: ${d.detail || r.status}`); } + } else { // пустой слот → создаём камеру, если заполнено имя/IP + if (!payload.name && !payload.ip) continue; + const id = this.nextCamId(used); + const r = await fetch("/api/cameras", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id, ...payload }), + }); + if (!r.ok) { const d = await r.json().catch(() => ({})); errors.push(`слот ${tr.dataset.slot}: ${d.detail || r.status}`); } + } } this.bulk = false; this.editing = false; this.setBulkButtons(); @@ -1376,14 +1405,15 @@ const CamSettings = { ); tb.innerHTML = ""; if (this.bulk) { - // массовое редактирование: те же строки/колонки, но ячейки — редактируемые инпуты - cams.forEach((c, i) => { + // массовое редактирование: ВСЕ 64 слота сразу редактируемые (пустые — для добавления) + for (let i = 0; i < this.SLOTS; i++) { + const c = cams[i] || null; const tr = document.createElement("tr"); tr.className = "bulk-row"; - tr.dataset.id = c.id; + if (c) tr.dataset.id = c.id; else tr.dataset.slot = i + 1; tr.innerHTML = this.bulkRowHtml(i + 1, c); tb.appendChild(tr); - }); + } return; } for (let i = 0; i < this.SLOTS; i++) { @@ -1423,7 +1453,7 @@ const CamSettings = { `${i + 1}` + `——————` + `пусто` + - ``; + ``; tb.appendChild(tr); } }