diff --git a/.env.example b/.env.example
index 199abcd..e2da8e5 100644
--- a/.env.example
+++ b/.env.example
@@ -5,7 +5,7 @@ CAMERA_cam2=http://IP:PORT/pfad
CAMERA_cam3=http://IP:PORT/pfad
# Optional: Konfigurations-URL pro Kamera (Button "Setup" im Frontend), z.B. ESP32-CAM Auflösung
-#SETUP_cam1=http://IP/control?var=framesize&val=11
+#SETUP_cam2=http://IP/control?var=framesize&val=11
PORT=8000
# Optional: Login (nur sinnvoll mit HTTPS in Zoraxy)
diff --git a/frontend.html b/frontend.html
index cb10bd0..f22c9c9 100644
--- a/frontend.html
+++ b/frontend.html
@@ -61,6 +61,7 @@ main{padding:22px;max-width:1800px;margin:0 auto}
@@ -72,6 +73,7 @@ const app = document.getElementById("app");
document.getElementById("home").href = ROOT;
const banner = document.getElementById("banner");
const tiles = {};
+let setupNames = [];
let order = [], visible = true;
const el = (tag, cls, text) => {
@@ -88,6 +90,15 @@ const ICONS = {
};
const icon = k => { const s = document.createElementNS("http://www.w3.org/2000/svg", "svg"); s.setAttribute("viewBox", "0 0 24 24"); s.setAttribute("fill", "none"); s.setAttribute("stroke", "currentColor"); s.setAttribute("stroke-width", "2"); s.setAttribute("stroke-linecap", "round"); s.setAttribute("stroke-linejoin", "round"); s.innerHTML = ICONS[k]; return s; };
const btn = (label, ic, fn, cls) => { const b = el("button", "btn" + (cls ? " " + cls : "")); b.title = label; b.append(icon(ic), el("span", "", label)); b.onclick = fn; return b; };
+const setupAllBtn = document.getElementById("setupAll");
+setupAllBtn.prepend(icon("cog"));
+setupAllBtn.onclick = async () => {
+ setupAllBtn.disabled = true;
+ const label = setupAllBtn.querySelector("span");
+ const res = await Promise.all(setupNames.map(runSetup));
+ label.textContent = res.every(Boolean) ? "Gesendet ✓" : "Fehler ✗";
+ setTimeout(() => { label.textContent = "Setup alle"; setupAllBtn.disabled = false; }, 2500);
+};
// mode: "live" = MJPEG-Stream, "thumb" = Einzelbild im Intervall
function makeTile(name, mode, hasSetup) {
@@ -115,13 +126,7 @@ function makeTile(name, mode, hasSetup) {
const setup = btn("Setup", "cog", async () => {
setup.disabled = true;
const label = setup.querySelector("span");
- try {
- const r = await fetch(ROOT + name + "/setup", { method: "POST" });
- if (!r.ok) throw new Error(r.status);
- label.textContent = "Gesendet ✓";
- } catch (e) {
- label.textContent = "Fehler ✗";
- }
+ label.textContent = (await runSetup(name)) ? "Gesendet ✓" : "Fehler ✗";
setTimeout(() => { label.textContent = "Setup"; setup.disabled = false; }, 2500);
}, "alt");
actions.append(setup);
@@ -140,6 +145,18 @@ function makeTile(name, mode, hasSetup) {
return t;
}
+async function runSetup(name) {
+ try {
+ const r = await fetch(ROOT + name + "/setup", { method: "POST" });
+ if (!r.ok) throw new Error(r.status);
+ // Kamera startet den Stream mit neuer Auflösung; Bild kurz danach neu laden
+ setTimeout(() => tiles[name] && connect(tiles[name]), 800);
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
function connect(o) {
clearTimeout(o.retry);
clearInterval(o.timer);
@@ -192,6 +209,8 @@ async function poll() {
if (names.join() !== order.join()) build(data.cameras);
data.cameras.forEach(c => setOnline(tiles[c.name], c.online));
const n = data.cameras.filter(c => c.online).length;
+ setupNames = data.cameras.filter(c => c.setup).map(c => c.name);
+ setupAllBtn.hidden = !setupNames.length;
document.getElementById("count").innerHTML = "" + n + " / " + data.cameras.length + " online";
} catch (e) {
banner.style.display = "block";