v0.126.4: edge-safe error statuses + native-alert ban — writeDiskJSON maps 502/504→500 (CF swallows those bodies; the M1 refusal reached the operator as a JSON SyntaxError popup), M1 refusal = typed errLastUsableDrive → 409; all 29 native alert() swept to showAlert + native_confirm_gate now bans alert( (F-11 class complete)
This commit is contained in:
@@ -1,5 +1,21 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.126.4 — edge-safe error statuses + the native-alert ban (2026-07-13)
|
||||||
|
|
||||||
|
Two defects surfaced by the agent-0.87.0 wizard leg's decommission attempt (the M1 refusal —
|
||||||
|
correct policy — reached the operator as a JSON SyntaxError popup):
|
||||||
|
|
||||||
|
- **502/504 never leave the origin:** Cloudflare replaces origin 502/504 bodies with its own
|
||||||
|
HTML error page, so every `writeDiskJSON(StatusBadGateway…)` refusal/error rendered as
|
||||||
|
"<!DOCTYPE … is not valid JSON" in the browser. `writeDiskJSON` now maps 502/504 → 500 at the
|
||||||
|
single choke point (JSON body crosses the edge intact); the M1 last-usable-drive refusal
|
||||||
|
became the typed `errLastUsableDrive` sentinel → **409** (policy verdict, not gateway
|
||||||
|
failure). Unit tests + red-proofs for both.
|
||||||
|
- **Native `alert()` banned** (the F-11 OS-modal class, now complete): the decommission error
|
||||||
|
path's `alert()` froze browser automation exactly as F-11 predicted. All 29 native `alert(`
|
||||||
|
calls across 5 templates swept to the existing `showAlert` modal (layout.html);
|
||||||
|
`native_confirm_gate.py` extended to ban `alert(` alongside confirm/prompt.
|
||||||
|
|
||||||
### v0.126.3 — storage wizard on a CLAIMED box: the init/attach POST no longer dies on CSRF (2026-07-13)
|
### v0.126.3 — storage wizard on a CLAIMED box: the init/attach POST no longer dies on CSRF (2026-07-13)
|
||||||
|
|
||||||
First live hit during the agent-0.87.0 drill wizard leg: /api/storage/init → "CSRF token missing
|
First live hit during the agent-0.87.0 drill wizard leg: /api/storage/init → "CSRF token missing
|
||||||
|
|||||||
@@ -84,6 +84,14 @@ func (s *Server) ProbeAgentChannel(ctx context.Context) (constructionErr bool, e
|
|||||||
|
|
||||||
// writeDiskJSON writes the standard {ok,data,error} envelope used by the disk API.
|
// writeDiskJSON writes the standard {ok,data,error} envelope used by the disk API.
|
||||||
func writeDiskJSON(w http.ResponseWriter, status int, ok bool, errMsg string, data interface{}) {
|
func writeDiskJSON(w http.ResponseWriter, status int, ok bool, errMsg string, data interface{}) {
|
||||||
|
// Cloudflare replaces origin 502/504 bodies with its OWN HTML error page (no passthru on
|
||||||
|
// this plan) — the page JS then fails to parse JSON and the customer sees a SyntaxError
|
||||||
|
// alert instead of the Hungarian message (live-hit 2026-07-13: the M1 decommission refusal
|
||||||
|
// surfaced as "<!DOCTYPE ... is not valid JSON"). App-level JSON errors must never leave
|
||||||
|
// the origin as 502/504; 500 carries the body through the edge intact.
|
||||||
|
if status == http.StatusBadGateway || status == http.StatusGatewayTimeout {
|
||||||
|
status = http.StatusInternalServerError
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
resp := map[string]interface{}{"ok": ok}
|
resp := map[string]interface{}{"ok": ok}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// v0.126.4 — two defects surfaced by the 0.87.0 wizard leg's decommission attempt:
|
||||||
|
// 1. writeDiskJSON emitted app-level errors as 502 — Cloudflare replaces origin 502/504
|
||||||
|
// bodies with its own HTML error page, so the browser saw "<!DOCTYPE ..." instead of the
|
||||||
|
// Hungarian refusal and threw a JSON SyntaxError alert.
|
||||||
|
// 2. The M1 last-usable-drive refusal (a POLICY verdict) was indistinguishable from a real
|
||||||
|
// agent-gateway failure.
|
||||||
|
// COMPANION red-proofs: (1) remove the 502→500 map in writeDiskJSON → the status assertions
|
||||||
|
// fail; (2) return a non-sentinel fmt.Errorf from the mustBlock branch → the errors.Is
|
||||||
|
// assertion fails.
|
||||||
|
|
||||||
|
func TestWriteDiskJSON_EdgeSafeStatus(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
in int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{http.StatusBadGateway, http.StatusInternalServerError}, // CF would swallow 502
|
||||||
|
{http.StatusGatewayTimeout, http.StatusInternalServerError}, // CF would swallow 504
|
||||||
|
{http.StatusConflict, http.StatusConflict}, // policy refusals pass as-is
|
||||||
|
{http.StatusBadRequest, http.StatusBadRequest},
|
||||||
|
{http.StatusOK, http.StatusOK},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
writeDiskJSON(rr, c.in, false, "hiba szöveg", nil)
|
||||||
|
if rr.Code != c.want {
|
||||||
|
t.Errorf("writeDiskJSON(%d): got status %d, want %d (502/504 must never leave the origin — CF replaces their body)", c.in, rr.Code, c.want)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rr.Body.String(), `"hiba szöveg"`) {
|
||||||
|
t.Errorf("writeDiskJSON(%d): JSON body lost", c.in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The M1 refusal is the TYPED sentinel (so the handler can map it to 409, which crosses the
|
||||||
|
// CF edge with its body intact), and it must refuse BEFORE any side-effect: no soft-mark, no
|
||||||
|
// agent call.
|
||||||
|
func TestFinalizeDecommission_LastDriveRefusalIsTypedAndSideEffectFree(t *testing.T) {
|
||||||
|
s := testServer(t)
|
||||||
|
_ = s.settings.AddStoragePath(settings.StoragePath{Path: "/mnt/only", Schedulable: true, IsDefault: true})
|
||||||
|
agent := &mockAgent{}
|
||||||
|
|
||||||
|
err := s.finalizeDecommissionWith(context.Background(), agent, "/mnt/only", "")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("decommissioning the last usable drive must be refused (M1)")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, errLastUsableDrive) {
|
||||||
|
t.Fatalf("refusal must be the typed errLastUsableDrive sentinel (409 mapping), got: %v", err)
|
||||||
|
}
|
||||||
|
if s.settings.IsDecommissioned("/mnt/only") {
|
||||||
|
t.Error("refusal must precede the soft-mark side-effect")
|
||||||
|
}
|
||||||
|
if len(agent.decommissionCalls) != 0 {
|
||||||
|
t.Errorf("agent must not be called on refusal, got %v", agent.decommissionCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -419,7 +419,13 @@ func (s *Server) handleStorageDecommission(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := s.finalizeDecommission(r.Context(), req.Where, ""); err != nil {
|
if err := s.finalizeDecommission(r.Context(), req.Where, ""); err != nil {
|
||||||
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
// The M1 policy refusal is a 409 (CONFLICT with current state), not a gateway
|
||||||
|
// failure — and 409 passes the CF edge with its JSON body intact.
|
||||||
|
status := http.StatusBadGateway
|
||||||
|
if errors.Is(err, errLastUsableDrive) {
|
||||||
|
status = http.StatusConflict
|
||||||
|
}
|
||||||
|
writeDiskJSON(w, status, false, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"decommissioned": true, "where": req.Where, "stopped_apps": stopped})
|
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"decommissioned": true, "where": req.Where, "stopped_apps": stopped})
|
||||||
@@ -429,6 +435,11 @@ func (s *Server) handleStorageDecommission(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errLastUsableDrive is the M1 refusal (never leave zero default) — a POLICY verdict, not a
|
||||||
|
// gateway failure. The decommission handler maps it to 409 so the JSON refusal reaches the
|
||||||
|
// browser intact (a 502 would be swallowed by the Cloudflare edge error page).
|
||||||
|
var errLastUsableDrive = errors.New("ez az egyetlen használható tárhely — a leszerelés megtagadva (előbb adj hozzá vagy állíts be másik alapértelmezett meghajtót)")
|
||||||
|
|
||||||
// finalizeDecommission resolves the agent client then soft-marks + decommissions (see *With).
|
// finalizeDecommission resolves the agent client then soft-marks + decommissions (see *With).
|
||||||
func (s *Server) finalizeDecommission(ctx context.Context, where, migratedTo string) error {
|
func (s *Server) finalizeDecommission(ctx context.Context, where, migratedTo string) error {
|
||||||
agent, err := s.agentClient()
|
agent, err := s.agentClient()
|
||||||
@@ -448,7 +459,7 @@ func (s *Server) finalizeDecommissionWith(ctx context.Context, agent diskAgent,
|
|||||||
// flips IsDefault/Schedulable on `where`).
|
// flips IsDefault/Schedulable on `where`).
|
||||||
promote, mustBlock := defaultPromotionTarget(s.settings.GetStoragePaths(), where, migratedTo)
|
promote, mustBlock := defaultPromotionTarget(s.settings.GetStoragePaths(), where, migratedTo)
|
||||||
if mustBlock {
|
if mustBlock {
|
||||||
return fmt.Errorf("ez az egyetlen használható tárhely — a leszerelés megtagadva (előbb adj hozzá vagy állíts be másik alapértelmezett meghajtót)")
|
return errLastUsableDrive
|
||||||
}
|
}
|
||||||
if err := s.settings.SetDecommissioned(where, migratedTo); err != nil {
|
if err := s.settings.SetDecommissioned(where, migratedTo); err != nil {
|
||||||
return fmt.Errorf("nyilvántartás frissítése sikertelen: %w", err)
|
return fmt.Errorf("nyilvántartás frissítése sikertelen: %w", err)
|
||||||
|
|||||||
@@ -101,11 +101,11 @@ function appMigWatch(){
|
|||||||
function appMigrate(btn,app,label){
|
function appMigrate(btn,app,label){
|
||||||
var sel=document.getElementById('app-migrate-target');
|
var sel=document.getElementById('app-migrate-target');
|
||||||
var target=sel?sel.value:'';
|
var target=sel?sel.value:'';
|
||||||
if(!target){ alert('Válassz céltárhelyet.'); return; }
|
if(!target){ showAlert('Válassz céltárhelyet.'); return; }
|
||||||
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(){
|
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})})
|
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')); } })
|
.then(function(r){return r.json();}).then(function(d){ if(d.ok){ appMigWatch(); } else { showAlert('Hiba: '+(d.error||'ismeretlen')); } })
|
||||||
.catch(function(e){ alert('Hiba: '+e); });
|
.catch(function(e){ showAlert('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(){}); })();
|
(function(){ fetch('/api/storage/migrate/status').then(function(r){return r.json();}).then(function(d){ if(d.data&&d.data.job){ appMigWatch(); } }).catch(function(){}); })();
|
||||||
|
|||||||
@@ -475,9 +475,9 @@ function simulateDisconnect(btn, path) {
|
|||||||
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
|
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
|
||||||
body: JSON.stringify({path: path})
|
body: JSON.stringify({path: path})
|
||||||
}).then(function(r){return r.json()}).then(function(data) {
|
}).then(function(r){return r.json()}).then(function(data) {
|
||||||
if (!data.ok) alert('Hiba: ' + (data.error || 'ismeretlen'));
|
if (!data.ok) showAlert('Hiba: ' + (data.error || 'ismeretlen'));
|
||||||
loadWatchdogStatus();
|
loadWatchdogStatus();
|
||||||
}).catch(function(e) { alert('Hiba: ' + e.message); });
|
}).catch(function(e) { showAlert('Hiba: ' + e.message); });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function simulateReconnect(path) {
|
function simulateReconnect(path) {
|
||||||
@@ -486,9 +486,9 @@ function simulateReconnect(path) {
|
|||||||
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
|
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
|
||||||
body: JSON.stringify({path: path})
|
body: JSON.stringify({path: path})
|
||||||
}).then(function(r){return r.json()}).then(function(data) {
|
}).then(function(r){return r.json()}).then(function(data) {
|
||||||
if (!data.ok) alert('Hiba: ' + (data.error || 'ismeretlen'));
|
if (!data.ok) showAlert('Hiba: ' + (data.error || 'ismeretlen'));
|
||||||
loadWatchdogStatus();
|
loadWatchdogStatus();
|
||||||
}).catch(function(e) { alert('Hiba: ' + e.message); });
|
}).catch(function(e) { showAlert('Hiba: ' + e.message); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Section 5: Hub ──
|
// ── Section 5: Hub ──
|
||||||
|
|||||||
@@ -165,14 +165,14 @@
|
|||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
if (!data.ok) {
|
if (!data.ok) {
|
||||||
checkbox.checked = !enable;
|
checkbox.checked = !enable;
|
||||||
alert(data.error || 'Hiba történt');
|
showAlert(data.error || 'Hiba történt');
|
||||||
} else {
|
} else {
|
||||||
setTimeout(function(){ location.reload(); }, 500);
|
setTimeout(function(){ location.reload(); }, 500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
checkbox.checked = !enable;
|
checkbox.checked = !enable;
|
||||||
alert('Hálózati hiba');
|
showAlert('Hálózati hiba');
|
||||||
}
|
}
|
||||||
checkbox.disabled = false;
|
checkbox.disabled = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,13 +207,13 @@
|
|||||||
function storageMigrateAll(source,label){
|
function storageMigrateAll(source,label){
|
||||||
var sel=document.getElementById('migrate-target-'+source);
|
var sel=document.getElementById('migrate-target-'+source);
|
||||||
var target=sel?sel.value:'';
|
var target=sel?sel.value:'';
|
||||||
if(!target){ alert('Válassz céltárolót a legördülő menüből.'); return; }
|
if(!target){ showAlert('Válassz céltárolót a legördülő menüből.'); return; }
|
||||||
openDialog({title:'Összes adat áthelyezése', confirmLabel:'Áthelyezés',
|
openDialog({title:'Összes adat áthelyezése', confirmLabel:'Áthelyezés',
|
||||||
message:'Áthelyezed a(z) '+label+' ÖSSZES adatát ide: '+target+'?\n\nAz alkalmazások az áthelyezés alatt rövid időre leállnak. A forrás adatai csak az ellenőrzés és a sikeres újraindítás után törlődnek.',
|
message:'Áthelyezed a(z) '+label+' ÖSSZES adatát ide: '+target+'?\n\nAz alkalmazások az áthelyezés alatt rövid időre leállnak. A forrás adatai csak az ellenőrzés és a sikeres újraindítás után törlődnek.',
|
||||||
onConfirm:function(){
|
onConfirm:function(){
|
||||||
fetch('/api/storage/migrate',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({source:source,target:target})})
|
fetch('/api/storage/migrate',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({source:source,target:target})})
|
||||||
.then(function(r){return r.json();}).then(function(d){ if(d.ok){ migWatch(); } else { alert('Hiba: '+(d.error||'ismeretlen')); } })
|
.then(function(r){return r.json();}).then(function(d){ if(d.ok){ migWatch(); } else { showAlert('Hiba: '+(d.error||'ismeretlen')); } })
|
||||||
.catch(function(e){ alert('Hiba: '+e); });
|
.catch(function(e){ showAlert('Hiba: '+e); });
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
// Resume view: if a migration is STILL IN PROGRESS when the page loads, show the panel and watch.
|
// Resume view: if a migration is STILL IN PROGRESS when the page loads, show the panel and watch.
|
||||||
@@ -408,9 +408,9 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
|||||||
window.registerDrive=async function(where){
|
window.registerDrive=async function(where){
|
||||||
try{
|
try{
|
||||||
var r=await fetch('/api/storage/register',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({where:where})});
|
var r=await fetch('/api/storage/register',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({where:where})});
|
||||||
var j=await r.json(); if(!j.ok){ alert('Regisztráció sikertelen: '+(j.error||'')); return; }
|
var j=await r.json(); if(!j.ok){ showAlert('Regisztráció sikertelen: '+(j.error||'')); return; }
|
||||||
location.reload();
|
location.reload();
|
||||||
}catch(e){ alert('Hiba: '+e.message); }
|
}catch(e){ showAlert('Hiba: '+e.message); }
|
||||||
};
|
};
|
||||||
window.confirmEject=function(where){
|
window.confirmEject=function(where){
|
||||||
// The confirm name is the drive BASENAME (matches the server's path.Base(where) check); `where` may
|
// The confirm name is the drive BASENAME (matches the server's path.Base(where) check); `where` may
|
||||||
@@ -530,12 +530,12 @@ function doStorageDisconnect(path) {
|
|||||||
body: JSON.stringify({where: path})
|
body: JSON.stringify({where: path})
|
||||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
alert('A meghajtó biztonságosan eltávolítható.');
|
showAlert('A meghajtó biztonságosan eltávolítható.');
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert('Hiba: ' + (data.error || 'ismeretlen'));
|
showAlert('Hiba: ' + (data.error || 'ismeretlen'));
|
||||||
}
|
}
|
||||||
}).catch(function(e) { alert('Hiba: ' + e); });
|
}).catch(function(e) { showAlert('Hiba: ' + e); });
|
||||||
}
|
}
|
||||||
function storageReconnect(path) {
|
function storageReconnect(path) {
|
||||||
var actionsDiv = document.getElementById('storage-actions-' + path);
|
var actionsDiv = document.getElementById('storage-actions-' + path);
|
||||||
@@ -548,11 +548,11 @@ function storageReconnect(path) {
|
|||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert('Hiba: ' + (data.error || 'ismeretlen'));
|
showAlert('Hiba: ' + (data.error || 'ismeretlen'));
|
||||||
if (actionsDiv) actionsDiv.innerHTML = '<button class="btn btn-xs btn-primary" onclick="storageReconnect(\'' + path + '\')">Csatlakoztatás</button>';
|
if (actionsDiv) actionsDiv.innerHTML = '<button class="btn btn-xs btn-primary" onclick="storageReconnect(\'' + path + '\')">Csatlakoztatás</button>';
|
||||||
}
|
}
|
||||||
}).catch(function(e) {
|
}).catch(function(e) {
|
||||||
alert('Hiba: ' + e);
|
showAlert('Hiba: ' + e);
|
||||||
if (actionsDiv) actionsDiv.innerHTML = '<button class="btn btn-xs btn-primary" onclick="storageReconnect(\'' + path + '\')">Csatlakoztatás</button>';
|
if (actionsDiv) actionsDiv.innerHTML = '<button class="btn btn-xs btn-primary" onclick="storageReconnect(\'' + path + '\')">Csatlakoztatás</button>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -564,12 +564,12 @@ function storageRestartApps(path) {
|
|||||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
var r2 = data.restarted || [];
|
var r2 = data.restarted || [];
|
||||||
if (r2.length) alert('Elindítva: ' + r2.join(', '));
|
if (r2.length) showAlert('Elindítva: ' + r2.join(', '));
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert('Hiba: ' + (data.error || 'ismeretlen'));
|
showAlert('Hiba: ' + (data.error || 'ismeretlen'));
|
||||||
}
|
}
|
||||||
}).catch(function(e) { alert('Hiba: ' + e); });
|
}).catch(function(e) { showAlert('Hiba: ' + e); });
|
||||||
}
|
}
|
||||||
// H2: decommission a drive (non-destructive). If a migrate target is picked in the inline select →
|
// H2: decommission a drive (non-destructive). If a migrate target is picked in the inline select →
|
||||||
// migrate-then-decommission; otherwise decommission-anyway with type-to-confirm.
|
// migrate-then-decommission; otherwise decommission-anyway with type-to-confirm.
|
||||||
@@ -594,9 +594,9 @@ function doStorageDecommission(body) {
|
|||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||||
if (data.ok) { alert('Leszerelés elindítva.'); location.reload(); }
|
if (data.ok) { showAlert('Leszerelés elindítva.'); location.reload(); }
|
||||||
else { alert('Hiba: ' + (data.error || 'ismeretlen')); }
|
else { showAlert('Hiba: ' + (data.error || 'ismeretlen')); }
|
||||||
}).catch(function(e) { alert('Hiba: ' + e); });
|
}).catch(function(e) { showAlert('Hiba: ' + e); });
|
||||||
}
|
}
|
||||||
// H3: one-click re-enroll a decommissioned/ejected drive — clears the marker, re-attaches under the
|
// H3: one-click re-enroll a decommissioned/ejected drive — clears the marker, re-attaches under the
|
||||||
// parent, restarts the gate-stopped apps (data is intact).
|
// parent, restarts the gate-stopped apps (data is intact).
|
||||||
@@ -612,8 +612,8 @@ function doStorageReEnroll(path) {
|
|||||||
body: JSON.stringify({where: path})
|
body: JSON.stringify({where: path})
|
||||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||||
if (data.ok) { location.reload(); }
|
if (data.ok) { location.reload(); }
|
||||||
else { alert('Hiba: ' + (data.error || 'ismeretlen')); }
|
else { showAlert('Hiba: ' + (data.error || 'ismeretlen')); }
|
||||||
}).catch(function(e) { alert('Hiba: ' + e); });
|
}).catch(function(e) { showAlert('Hiba: ' + e); });
|
||||||
}
|
}
|
||||||
function cancelEditLabel(path, label) {
|
function cancelEditLabel(path, label) {
|
||||||
var wrap = document.getElementById('label-wrap-' + path);
|
var wrap = document.getElementById('label-wrap-' + path);
|
||||||
|
|||||||
@@ -218,8 +218,8 @@ function netStorageRemove(name,label){
|
|||||||
onConfirm:function(){
|
onConfirm:function(){
|
||||||
fetch('/api/storage/netstorage/remove',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({name:name})})
|
fetch('/api/storage/netstorage/remove',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({name:name})})
|
||||||
.then(function(r){return r.json();}).then(function(d){
|
.then(function(r){return r.json();}).then(function(d){
|
||||||
if(d.ok){ location.reload(); } else { alert('Hiba: '+(d.error||'ismeretlen')); }
|
if(d.ok){ location.reload(); } else { showAlert('Hiba: '+(d.error||'ismeretlen')); }
|
||||||
}).catch(function(e){ alert('Hiba: '+e); });
|
}).catch(function(e){ showAlert('Hiba: '+e); });
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
if(document.getElementById('ns-add-form')){ nsToggleSmb(); nsUpdateHostID(); } // form absent on the capability banner
|
if(document.getElementById('ns-add-form')){ nsToggleSmb(); nsUpdateHostID(); } // form absent on the capability banner
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Native-confirm gate (drill F-11) — native confirm()/prompt() dialogs are OS-modals that block
|
"""Native-confirm gate (drill F-11; alert added v0.126.4) — native confirm()/prompt()/alert()
|
||||||
browser automation (CDP freezes) and are banned from the UI; consequential actions use the inline
|
dialogs are OS-modals that block browser automation (CDP freezes) and are banned from the UI;
|
||||||
felhomConfirm helper (layout.html) or the .confirm-overlay dialog.
|
consequential actions use the inline felhomConfirm helper (layout.html) or the .confirm-overlay
|
||||||
|
dialog; error/info surfaces use showAlert (layout.html modal). alert() joined the ban after the
|
||||||
|
0.87.0 wizard leg: a decommission error path alert() froze the automation exactly like F-11
|
||||||
|
predicted (the operator had to click the OS-modal away).
|
||||||
|
|
||||||
Run from controller/: python scripts/native_confirm_gate.py
|
Run from controller/: python scripts/native_confirm_gate.py
|
||||||
Exit 1 if any native confirm(/prompt( call remains in the templates.
|
Exit 1 if any native confirm(/prompt(/alert( call remains in the templates.
|
||||||
"""
|
"""
|
||||||
import io, os, re, sys
|
import io, os, re, sys
|
||||||
|
|
||||||
@@ -13,9 +16,10 @@ ROOTS = [
|
|||||||
os.path.join("internal", "setup", "templates"),
|
os.path.join("internal", "setup", "templates"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# A bare confirm(/prompt( CALL with an argument: not preceded by an identifier character, so felhomConfirm( and
|
# A bare confirm(/prompt(/alert( CALL with an argument: not preceded by an identifier character,
|
||||||
# onRestoreConfirmChange( never match. window.confirm( still matches ('.' is not identifier).
|
# so felhomConfirm(, showAlert( and onRestoreConfirmChange( never match. window.confirm( /
|
||||||
NATIVE = re.compile(r"(?<![A-Za-z0-9_$])(?:confirm|prompt)\(\s*[^)\s]")
|
# window.alert( still match ('.' is not an identifier character).
|
||||||
|
NATIVE = re.compile(r"(?<![A-Za-z0-9_$])(?:confirm|prompt|alert)\(\s*[^)\s]")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -30,9 +34,9 @@ def main():
|
|||||||
total += 1
|
total += 1
|
||||||
print("%s:%d %s" % (fn, lineno, line.strip()[:120].encode('ascii', 'backslashreplace').decode()))
|
print("%s:%d %s" % (fn, lineno, line.strip()[:120].encode('ascii', 'backslashreplace').decode()))
|
||||||
if total:
|
if total:
|
||||||
print("NATIVE CONFIRM GATE FAILED: %d native confirm()/prompt() call(s) remain" % total)
|
print("NATIVE CONFIRM GATE FAILED: %d native confirm()/prompt()/alert() call(s) remain" % total)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
print("native-confirm gate OK — no native confirm()/prompt() in templates")
|
print("native-confirm gate OK — no native confirm()/prompt()/alert() in templates")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user