From 8987ce0f6715a1db0691936f8c959d5344b03431 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 13 Jul 2026 08:19:39 +0200 Subject: [PATCH] =?UTF-8?q?controller:=20drill=20F-11=20=E2=80=94=20inline?= =?UTF-8?q?=20two-step=20confirm=20(felhomConfirm=20+=20data-confirm)=20re?= =?UTF-8?q?places=20every=20native=20confirm();=20native=5Fconfirm=5Fgate.?= =?UTF-8?q?py=20enforces=20zero;=20stale-data=20delete=20keeps=20its=20dou?= =?UTF-8?q?ble=20acknowledgement=20inline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01GzammAMzsJTgpQHqxwM2bC --- controller/README.md | 8 ++- .../internal/web/templates/app_info.html | 13 ++-- .../internal/web/templates/backups.html | 33 +++++----- controller/internal/web/templates/debug.html | 64 ++++++++++--------- controller/internal/web/templates/deploy.html | 15 +++-- controller/internal/web/templates/layout.html | 27 ++++++++ controller/internal/web/templates/style.css | 4 ++ controller/scripts/native_confirm_gate.py | 39 +++++++++++ 8 files changed, 141 insertions(+), 62 deletions(-) create mode 100644 controller/scripts/native_confirm_gate.py diff --git a/controller/README.md b/controller/README.md index bc05af8..9edb3b1 100644 --- a/controller/README.md +++ b/controller/README.md @@ -119,9 +119,13 @@ backups, monitoring and notifications. All Proxmox/disk operations are delegated **unified drive view**: server-rendered registry cards enriched in place from the agent `/api/disks` (role/durable-id/actions, joined on mount path), plus read-only **Rendszermeghajtók** and **Nem regisztrált meghajtók** groups; agent-down degrades to a single warn note. Consequential - actions use an in-page `.confirm-overlay` (`openDialog`), never native `confirm()`/`prompt()`. + actions use an in-page `.confirm-overlay` (`openDialog`) or the LIGHT inline two-step + (`felhomConfirm` in layout.html, v0.123.0: the trigger swaps in place to "kérdés + Igen/Mégse"; + form buttons opt in via `data-confirm="…"`) — never native `confirm()`/`prompt()` (OS-modals + freeze browser automation; drill F-11). **Enforcement:** `scripts/template_id_gate.py` (JS element-ID integrity) + `scripts/emoji_gate.py` - (no emoji — run after any template change; the Go `TestNoEmojiInTemplates` mirrors it). + (no emoji) + `scripts/native_confirm_gate.py` (zero native confirm/prompt) — run after any + template change; the Go `TestNoEmojiInTemplates` mirrors the emoji gate. - **Europe/Budapest timezone** — All scheduled jobs, timestamps, and UI labels use Hungarian timezone. ### Module Map diff --git a/controller/internal/web/templates/app_info.html b/controller/internal/web/templates/app_info.html index 9c7341c..fc2a463 100644 --- a/controller/internal/web/templates/app_info.html +++ b/controller/internal/web/templates/app_info.html @@ -71,7 +71,7 @@ {{range .MigrateTargets}}{{end}} - + @@ -98,14 +98,15 @@ function appMigWatch(){ } tick(); } -function appMigrate(app,label){ +function appMigrate(btn,app,label){ var sel=document.getElementById('app-migrate-target'); var target=sel?sel.value:''; if(!target){ alert('Válassz céltárhelyet.'); return; } - if(!confirm('Áthelyezed a(z) '+label+' adatait ide: '+target+'?\n\nAz alkalmazás rövid időre leáll. A régi adatok csak sikeres áthelyezés után törlődnek.')) return; - fetch('/api/storage/migrate-app',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({app:app,target:target})}) - .then(function(r){return r.json();}).then(function(d){ if(d.ok){ appMigWatch(); } else { alert('Hiba: '+(d.error||'ismeretlen')); } }) - .catch(function(e){ alert('Hiba: '+e); }); + felhomConfirm(btn,'Áthelyezed a(z) '+label+' adatait ide: '+target+'? Az alkalmazás rövid időre leáll. A régi adatok csak sikeres áthelyezés után törlődnek.',function(){ + fetch('/api/storage/migrate-app',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({app:app,target:target})}) + .then(function(r){return r.json();}).then(function(d){ if(d.ok){ appMigWatch(); } else { alert('Hiba: '+(d.error||'ismeretlen')); } }) + .catch(function(e){ alert('Hiba: '+e); }); + }); } (function(){ fetch('/api/storage/migrate/status').then(function(r){return r.json();}).then(function(d){ if(d.data&&d.data.job){ appMigWatch(); } }).catch(function(){}); })(); diff --git a/controller/internal/web/templates/backups.html b/controller/internal/web/templates/backups.html index c5b0b31..860264d 100644 --- a/controller/internal/web/templates/backups.html +++ b/controller/internal/web/templates/backups.html @@ -182,9 +182,9 @@ {{if .Enabled}} -
{{$.CSRFField}} + {{$.CSRFField}} - +
{{end}} @@ -469,9 +469,9 @@
{{if .Tier2LastRun}} -
{{$.CSRFField}} + {{$.CSRFField}} - +
{{end}} Beállítás @@ -673,19 +673,20 @@ function startBackupPolling() { // Whole-guest (appliance) backup — manual trigger via the quiesce loop (controller-owned quiesce: // stop stacks → agent vzdump → resume). Returns immediately; we poll /api/guest-backup/status. function triggerGuestBackup() { - if (!confirm('Elindítja a teljes rendszermentést most? A mentés ideje alatt az alkalmazások rövid időre leállhatnak.')) return; var btn = document.getElementById('wg-backup-btn'); - var out = document.getElementById('wg-backup-result'); - btn.disabled = true; - out.innerHTML = 'Mentés indítása…'; - fetch('/api/guest-backup/trigger', { method: 'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()) }) - .then(r => r.json()) - .then(data => { - if (!data.ok) { out.innerHTML = '
' + (data.error || 'Hiba') + '
'; btn.disabled = false; return; } - out.innerHTML = 'Mentés folyamatban…'; - pollGuestBackup(out, btn); - }) - .catch(e => { out.innerHTML = '
Hiba: ' + e + '
'; btn.disabled = false; }); + felhomConfirm(btn, 'Elindítja a teljes rendszermentést most? A mentés ideje alatt az alkalmazások rövid időre leállhatnak.', function () { + var out = document.getElementById('wg-backup-result'); + btn.disabled = true; + out.innerHTML = 'Mentés indítása…'; + fetch('/api/guest-backup/trigger', { method: 'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()) }) + .then(r => r.json()) + .then(data => { + if (!data.ok) { out.innerHTML = '
' + (data.error || 'Hiba') + '
'; btn.disabled = false; return; } + out.innerHTML = 'Mentés folyamatban…'; + pollGuestBackup(out, btn); + }) + .catch(e => { out.innerHTML = '
Hiba: ' + e + '
'; btn.disabled = false; }); + }); } function pollGuestBackup(out, btn) { diff --git a/controller/internal/web/templates/debug.html b/controller/internal/web/templates/debug.html index edad6c0..1a644f4 100644 --- a/controller/internal/web/templates/debug.html +++ b/controller/internal/web/templates/debug.html @@ -457,7 +457,7 @@ function renderWatchdogStatus(paths) { var simBadge = p.simulated ? ' SIM' : ''; var action = ''; if (p.status === 'connected' && !p.simulated) { - action = ''; + action = ''; } else if (p.simulated) { action = ''; } @@ -468,16 +468,17 @@ function renderWatchdogStatus(paths) { html += ''; document.getElementById('watchdog-status').innerHTML = html; } -function simulateDisconnect(path) { - if (!confirm('Biztosan szimulálja a leválasztást?\n\nEz leállítja az érintett alkalmazásokat.')) return; - fetch('/api/debug/storage/simulate-disconnect', { - method: 'POST', - headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()), - body: JSON.stringify({path: path}) - }).then(function(r){return r.json()}).then(function(data) { - if (!data.ok) alert('Hiba: ' + (data.error || 'ismeretlen')); - loadWatchdogStatus(); - }).catch(function(e) { alert('Hiba: ' + e.message); }); +function simulateDisconnect(btn, path) { + felhomConfirm(btn, 'Biztosan szimulálja a leválasztást? Ez leállítja az érintett alkalmazásokat.', function () { + fetch('/api/debug/storage/simulate-disconnect', { + method: 'POST', + headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()), + body: JSON.stringify({path: path}) + }).then(function(r){return r.json()}).then(function(data) { + if (!data.ok) alert('Hiba: ' + (data.error || 'ismeretlen')); + loadWatchdogStatus(); + }).catch(function(e) { alert('Hiba: ' + e.message); }); + }); } function simulateReconnect(path) { fetch('/api/debug/storage/simulate-reconnect', { @@ -560,30 +561,31 @@ function triggerDR() { result.textContent = 'Írja be: RESET'; return; } - if (!confirm('FIGYELEM! Ez újraindítja a controllert setup módba.\n\nBiztosan folytatja?')) return; var btn = document.getElementById('btn-dr-trigger'); - var result = document.getElementById('btn-dr-trigger-result'); - btn.disabled = true; - btn.textContent = 'Folyamatban...'; - fetch('/api/debug/dr/trigger-setup', { - method: 'POST', - headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()), - body: JSON.stringify({confirm: 'RESET'}) - }).then(function(r){return r.json()}).then(function(data) { - if (data.ok) { - result.className = 'debug-result debug-result-ok'; - result.textContent = 'Controller újraindulása...'; - } else { + felhomConfirm(btn, 'FIGYELEM! Ez újraindítja a controllert setup módba. Biztosan folytatja?', function () { + var result = document.getElementById('btn-dr-trigger-result'); + btn.disabled = true; + btn.textContent = 'Folyamatban...'; + fetch('/api/debug/dr/trigger-setup', { + method: 'POST', + headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()), + body: JSON.stringify({confirm: 'RESET'}) + }).then(function(r){return r.json()}).then(function(data) { + if (data.ok) { + result.className = 'debug-result debug-result-ok'; + result.textContent = 'Controller újraindulása...'; + } else { + result.className = 'debug-result debug-result-error'; + result.textContent = data.error || 'Hiba'; + btn.disabled = false; + btn.textContent = btn.dataset.label; + } + }).catch(function(e) { result.className = 'debug-result debug-result-error'; - result.textContent = data.error || 'Hiba'; + result.textContent = 'Hiba: ' + e.message; btn.disabled = false; btn.textContent = btn.dataset.label; - } - }).catch(function(e) { - result.className = 'debug-result debug-result-error'; - result.textContent = 'Hiba: ' + e.message; - btn.disabled = false; - btn.textContent = btn.dataset.label; + }); }); } diff --git a/controller/internal/web/templates/deploy.html b/controller/internal/web/templates/deploy.html index f5ff45a..2e9d0c3 100644 --- a/controller/internal/web/templates/deploy.html +++ b/controller/internal/web/templates/deploy.html @@ -793,14 +793,15 @@ function copyPostDeployCred(btn, val) { } function deleteStaleData(stackName, stalePath, btn) { - if (!confirm('Biztosan törölni szeretnéd a korábbi adatokat?\n\nTárhely: ' + stalePath + '\n\nFIGYELEM: Ez a művelet visszavonhatatlan!\nElőtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.')) { - return; - } - // Second confirmation - if (!confirm('UTOLSÓ FIGYELMEZTETÉS!\n\nA törlés visszavonhatatlan. Biztosan folytatod?')) { - return; - } + // Two-step inline confirm (the irreversible delete keeps its double acknowledgement). + felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () { + felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () { + deleteStaleDataRun(stackName, stalePath, btn); + }); + }); +} +function deleteStaleDataRun(stackName, stalePath, btn) { btn.disabled = true; btn.textContent = 'Törlés folyamatban...'; diff --git a/controller/internal/web/templates/layout.html b/controller/internal/web/templates/layout.html index f83d4aa..fa2fc0f 100644 --- a/controller/internal/web/templates/layout.html +++ b/controller/internal/web/templates/layout.html @@ -10,6 +10,33 @@ diff --git a/controller/internal/web/templates/style.css b/controller/internal/web/templates/style.css index fd6dff0..c566237 100644 --- a/controller/internal/web/templates/style.css +++ b/controller/internal/web/templates/style.css @@ -3270,3 +3270,7 @@ span.mono, .mono { font-family: var(--font-data); } .drive-agent-extra .drive-actions { margin: 0; } + +/* Inline two-step confirm (drill F-11) — the in-place replacement for native confirm() */ +.inline-confirm { display: inline-flex; align-items: center; gap: .5rem; flex-wrap: wrap; } +.inline-confirm-q { font-size: .8rem; color: var(--warn); } diff --git a/controller/scripts/native_confirm_gate.py b/controller/scripts/native_confirm_gate.py new file mode 100644 index 0000000..3e2de5c --- /dev/null +++ b/controller/scripts/native_confirm_gate.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +"""Native-confirm gate (drill F-11) — native confirm()/prompt() dialogs are OS-modals that block +browser automation (CDP freezes) and are banned from the UI; consequential actions use the inline +felhomConfirm helper (layout.html) or the .confirm-overlay dialog. + +Run from controller/: python scripts/native_confirm_gate.py +Exit 1 if any native confirm(/prompt( call remains in the templates. +""" +import io, os, re, sys + +ROOTS = [ + os.path.join("internal", "web", "templates"), + os.path.join("internal", "setup", "templates"), +] + +# A bare confirm(/prompt( CALL with an argument: not preceded by an identifier character, so felhomConfirm( and +# onRestoreConfirmChange( never match. window.confirm( still matches ('.' is not identifier). +NATIVE = re.compile(r"(?