D1 Part 3: unified drive view + overlay migration
- storage.html: the registry cards and the separate 'Meghajtók (ügynök
nézet)' merge into ONE view. Each connected registry card gets an
#agent-extra-<path> slot; the enrichment JS joins the agent /api/disks
list on mount path and decorates the matching card in place (role tag
via i-lock, drive class, durable-id mono line, agent-only
register/eject/wipe actions). Two extra groups render below:
'Rendszermeghajtók' (system/backup — read-only, lock tag, NO actions)
and 'Nem regisztrált meghajtók' (unregistered user-data — register
action only). Agent-down: one warn note into #agent-warn-note, all
registry cards still render from server data (graceful degradation).
The agent-view helpers now emit design-system .tag markup instead of
.badge (roleTag/classTag/dataTag/regTag/appBackingTag); the 🔒 lock
emoji is gone (sprite i-lock).
- Overlay migration: every native confirm()/prompt() on the four pages
routes through a light .confirm-overlay dialog (openDialog; texts
verbatim) — storage remove forms, netStorageRemove, storageMigrateAll,
storageDisconnect, storageDecommission (migrate + the type-to-confirm
anyway branch preserved like-for-like), storageReEnroll; and on the
system page triggerUpdate + controller/server restart; on the security
page the two geo Hungary-removal confirms. Scenario F grep: zero
native confirm/prompt in the four templates.
- Deleted the now-orphaned .badge-lock/.lock-ico CSS (grep-zero first).
- Tests: no-native-confirm scan, agent-down warn-note static assertion;
integrity gate stays green.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -212,3 +213,42 @@ func TestPasswordErrorRerendersSecurityPage(t *testing.T) {
|
|||||||
t.Error("re-rendered page lacks the password form section")
|
t.Error("re-rendered page lacks the password form section")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStorageNoNativeConfirm (D1 Scenario F): the four split templates contain no native
|
||||||
|
// confirm()/prompt() — all consequential actions route through the overlay.
|
||||||
|
func TestStorageNoNativeConfirm(t *testing.T) {
|
||||||
|
nativeRe := regexp.MustCompile(`(^|[^A-Za-z_.])(confirm|prompt)\(`)
|
||||||
|
// allow the pre-existing type-to-confirm overlay helper names
|
||||||
|
allow := regexp.MustCompile(`openConfirm|__closeConfirm|confirmEject|confirmWipe|typeToConfirm|confirm-`)
|
||||||
|
for _, f := range []string{"storage.html", "settings_system.html", "settings_notifications.html", "settings_security.html"} {
|
||||||
|
b, err := templateFS.ReadFile("templates/" + f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read %s: %v", f, err)
|
||||||
|
}
|
||||||
|
for i, line := range strings.Split(string(b), "\n") {
|
||||||
|
if nativeRe.MatchString(line) && !allow.MatchString(line) {
|
||||||
|
t.Errorf("%s:%d native confirm/prompt: %s", f, i+1, strings.TrimSpace(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStorageAgentDownNote (D1 Scenario C, static): the enrichment JS has an error path that
|
||||||
|
// renders the exact warn note into #agent-warn-note (graceful degradation when the agent is down).
|
||||||
|
func TestStorageAgentDownNote(t *testing.T) {
|
||||||
|
b, err := templateFS.ReadFile("templates/storage.html")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body := string(b)
|
||||||
|
if !strings.Contains(body, `id="agent-warn-note"`) {
|
||||||
|
t.Error("missing #agent-warn-note element")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "Az ügynök nem elérhető") {
|
||||||
|
t.Error("missing the agent-unreachable warn text")
|
||||||
|
}
|
||||||
|
// the catch block must target the note element
|
||||||
|
if !strings.Contains(body, "warn.innerHTML=") {
|
||||||
|
t.Error("enrichment JS lacks the warn-note error path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -109,7 +109,24 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="dialog-root"></div>
|
||||||
<script>
|
<script>
|
||||||
|
// Light overlay dialog (D1) — replaces the native blocking browser dialogs (same texts).
|
||||||
|
function escText(s){var d=document.createElement('div');d.textContent=String(s==null?'':s);return d.innerHTML;}
|
||||||
|
function closeDialog(){ var r=document.getElementById('dialog-root'); if(r) r.innerHTML=''; }
|
||||||
|
function openDialog(opts){
|
||||||
|
var root=document.getElementById('dialog-root');
|
||||||
|
if(!root) return;
|
||||||
|
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">'
|
||||||
|
+'<h3>'+escText(opts.title||'Megerősítés')+'</h3>'
|
||||||
|
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
|
||||||
|
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
|
||||||
|
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>'
|
||||||
|
+'</div></div>';
|
||||||
|
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); };
|
||||||
|
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
|
||||||
|
}
|
||||||
|
|
||||||
(function(){
|
(function(){
|
||||||
// Geo-restriction UI state
|
// Geo-restriction UI state
|
||||||
var allCountries = [];
|
var allCountries = [];
|
||||||
@@ -171,11 +188,17 @@
|
|||||||
|
|
||||||
window.removeCountry = function(code) {
|
window.removeCountry = function(code) {
|
||||||
if (code === 'HU') {
|
if (code === 'HU') {
|
||||||
if (!confirm('Figyelem: Magyarország eltávolítása azt jelenti, hogy magyar IP-ről sem lesz elérhető a rendszer távolról. Biztosan folytatja?')) return;
|
openDialog({title:'Figyelem', confirmLabel:'Eltávolítás',
|
||||||
|
message:'Figyelem: Magyarország eltávolítása azt jelenti, hogy magyar IP-ről sem lesz elérhető a rendszer távolról. Biztosan folytatja?',
|
||||||
|
onConfirm:function(){ doRemoveCountry(code); }});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
doRemoveCountry(code);
|
||||||
|
};
|
||||||
|
function doRemoveCountry(code) {
|
||||||
selectedCountries = selectedCountries.filter(function(c){return c !== code});
|
selectedCountries = selectedCountries.filter(function(c){return c !== code});
|
||||||
renderTags();
|
renderTags();
|
||||||
};
|
}
|
||||||
|
|
||||||
function renderTags() {
|
function renderTags() {
|
||||||
var el = document.getElementById('geo-selected-tags');
|
var el = document.getElementById('geo-selected-tags');
|
||||||
@@ -223,8 +246,11 @@
|
|||||||
if (!ov) return;
|
if (!ov) return;
|
||||||
var idx = ov.allowed_countries.indexOf(code);
|
var idx = ov.allowed_countries.indexOf(code);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
if (code === 'HU' && !confirm('Magyarország eltávolítása nem ajánlott. Folytatja?')) {
|
if (code === 'HU') {
|
||||||
el.checked = true;
|
openDialog({title:'Figyelem', confirmLabel:'Eltávolítás',
|
||||||
|
message:'Magyarország eltávolítása nem ajánlott. Folytatja?',
|
||||||
|
onConfirm:function(){ ov.allowed_countries.splice(ov.allowed_countries.indexOf(code), 1); },
|
||||||
|
onCancel:function(){ el.checked = true; }});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ov.allowed_countries.splice(idx, 1);
|
ov.allowed_countries.splice(idx, 1);
|
||||||
|
|||||||
@@ -139,7 +139,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="dialog-root"></div>
|
||||||
<script>
|
<script>
|
||||||
|
// Light overlay dialog (D1) — replaces the native blocking browser dialogs (same texts).
|
||||||
|
function escText(s){var d=document.createElement('div');d.textContent=String(s==null?'':s);return d.innerHTML;}
|
||||||
|
function closeDialog(){ var r=document.getElementById('dialog-root'); if(r) r.innerHTML=''; }
|
||||||
|
function openDialog(opts){
|
||||||
|
var root=document.getElementById('dialog-root');
|
||||||
|
if(!root) return;
|
||||||
|
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">'
|
||||||
|
+'<h3>'+escText(opts.title||'Megerősítés')+'</h3>'
|
||||||
|
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
|
||||||
|
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
|
||||||
|
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>'
|
||||||
|
+'</div></div>';
|
||||||
|
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); };
|
||||||
|
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
|
||||||
|
}
|
||||||
|
|
||||||
function checkUpdate() {
|
function checkUpdate() {
|
||||||
var btn = document.getElementById('btn-check-update');
|
var btn = document.getElementById('btn-check-update');
|
||||||
var msg = document.getElementById('update-status-msg');
|
var msg = document.getElementById('update-status-msg');
|
||||||
@@ -167,7 +184,11 @@ function checkUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function triggerUpdate() {
|
function triggerUpdate() {
|
||||||
if (!confirm('Biztosan frissíti a controllert?\n\nA folyamat alatt a vezérlőpult rövid időre elérhetetlenné válik.')) return;
|
openDialog({title:'Vezérlő frissítése', confirmLabel:'Frissítés',
|
||||||
|
message:'Biztosan frissíti a controllert?\n\nA folyamat alatt a vezérlőpult rövid időre elérhetetlenné válik.',
|
||||||
|
onConfirm:doTriggerUpdate});
|
||||||
|
}
|
||||||
|
function doTriggerUpdate() {
|
||||||
var btn = document.getElementById('btn-trigger-update');
|
var btn = document.getElementById('btn-trigger-update');
|
||||||
var checkBtn = document.getElementById('btn-check-update');
|
var checkBtn = document.getElementById('btn-check-update');
|
||||||
var msg = document.getElementById('update-status-msg');
|
var msg = document.getElementById('update-status-msg');
|
||||||
@@ -238,7 +259,11 @@ pollUntilBack();
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function restartController() {
|
function restartController() {
|
||||||
if (!confirm('Biztosan újraindítja a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.')) return;
|
openDialog({title:'Vezérlő újraindítása', confirmLabel:'Újraindítás',
|
||||||
|
message:'Biztosan újraindítja a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.',
|
||||||
|
onConfirm:doRestartController});
|
||||||
|
}
|
||||||
|
function doRestartController() {
|
||||||
var btn = document.getElementById('btn-restart-controller');
|
var btn = document.getElementById('btn-restart-controller');
|
||||||
var status = document.getElementById('restart-status');
|
var status = document.getElementById('restart-status');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -248,7 +273,11 @@ function restartController() {
|
|||||||
.catch(function(){ pollRestart(0); }); // connection may drop as the process exits — poll regardless
|
.catch(function(){ pollRestart(0); }); // connection may drop as the process exits — poll regardless
|
||||||
}
|
}
|
||||||
function restartServer() {
|
function restartServer() {
|
||||||
if (!confirm('Biztosan újraindítja a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.')) return;
|
openDialog({title:'Kiszolgáló újraindítása', confirmLabel:'Újraindítás',
|
||||||
|
message:'Biztosan újraindítja a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.',
|
||||||
|
onConfirm:doRestartServer});
|
||||||
|
}
|
||||||
|
function doRestartServer() {
|
||||||
var btn = document.getElementById('btn-restart-server');
|
var btn = document.getElementById('btn-restart-server');
|
||||||
var status = document.getElementById('server-restart-status');
|
var status = document.getElementById('server-restart-status');
|
||||||
if (btn) btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
<div class="storage-path-actions">
|
<div class="storage-path-actions">
|
||||||
<button class="btn btn-xs btn-primary" onclick="storageReEnroll('{{.Path}}','{{.Label}}')">Visszacsatlakoztatás</button>
|
<button class="btn btn-xs btn-primary" onclick="storageReEnroll('{{.Path}}','{{.Label}}')">Visszacsatlakoztatás</button>
|
||||||
<form method="POST" action="/settings/storage/remove" style="display:inline"
|
<form method="POST" action="/settings/storage/remove" style="display:inline"
|
||||||
onsubmit="return confirm('Biztosan eltávolítja a(z) {{.Label}} ({{.Path}}) meghajtót a rendszerből?\n\nA meghajtó adatai NEM törlődnek.')">
|
onsubmit="return storageRemoveDialog(event, this, 'Biztosan eltávolítja a(z) {{.Label}} ({{.Path}}) meghajtót a rendszerből?\n\nA meghajtó adatai NEM törlődnek.')">
|
||||||
{{$.CSRFField}}
|
{{$.CSRFField}}
|
||||||
<input type="hidden" name="storage_path" value="{{.Path}}">
|
<input type="hidden" name="storage_path" value="{{.Path}}">
|
||||||
<button type="submit" class="btn btn-xs btn-outline">Eltávolítás a rendszerből</button>
|
<button type="submit" class="btn btn-xs btn-outline">Eltávolítás a rendszerből</button>
|
||||||
@@ -111,6 +111,7 @@
|
|||||||
<span class="form-hint">Nincs alkalmazás ezen a tárolón</span>
|
<span class="form-hint">Nincs alkalmazás ezen a tárolón</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="drive-agent-extra" id="agent-extra-{{.Path}}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="storage-path-actions">
|
<div class="storage-path-actions">
|
||||||
{{if not .IsDefault}}
|
{{if not .IsDefault}}
|
||||||
@@ -140,7 +141,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{if and (not .IsDefault) (eq .AppCount 0)}}
|
{{if and (not .IsDefault) (eq .AppCount 0)}}
|
||||||
<form method="POST" action="/settings/storage/remove" style="display:inline"
|
<form method="POST" action="/settings/storage/remove" style="display:inline"
|
||||||
onsubmit="return confirm('Biztosan eltávolítja a(z) {{.Path}} adattárolót?')">
|
onsubmit="return storageRemoveDialog(event, this, 'Biztosan eltávolítja a(z) {{.Path}} adattárolót?')">
|
||||||
{{$.CSRFField}}
|
{{$.CSRFField}}
|
||||||
<input type="hidden" name="storage_path" value="{{.Path}}">
|
<input type="hidden" name="storage_path" value="{{.Path}}">
|
||||||
<button type="submit" class="btn btn-xs btn-danger-outline">Eltávolítás</button>
|
<button type="submit" class="btn btn-xs btn-danger-outline">Eltávolítás</button>
|
||||||
@@ -259,11 +260,14 @@
|
|||||||
}).catch(function(e){ msg.textContent='Hiba: '+e; });
|
}).catch(function(e){ msg.textContent='Hiba: '+e; });
|
||||||
}
|
}
|
||||||
function netStorageRemove(name,label){
|
function netStorageRemove(name,label){
|
||||||
if(!confirm('Biztosan eltávolítja a(z) '+label+' hálózati tárhelyet?\n\nA megosztás leválasztásra kerül; a NAS-on lévő adatok érintetlenek maradnak.')) return;
|
openDialog({title:'Hálózati tárhely eltávolítása', confirmLabel:'Eltávolítás',
|
||||||
|
message:'Biztosan eltávolítja a(z) '+label+' hálózati tárhelyet?\n\nA megosztás leválasztásra kerül; a NAS-on lévő adatok érintetlenek maradnak.',
|
||||||
|
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 { alert('Hiba: '+(d.error||'ismeretlen')); }
|
||||||
}).catch(function(e){ alert('Hiba: '+e); });
|
}).catch(function(e){ alert('Hiba: '+e); });
|
||||||
|
}});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -301,10 +305,13 @@
|
|||||||
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){ alert('Válassz céltárolót a legördülő menüből.'); return; }
|
||||||
if(!confirm('Á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.')) return;
|
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.',
|
||||||
|
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 { alert('Hiba: '+(d.error||'ismeretlen')); } })
|
||||||
.catch(function(e){ alert('Hiba: '+e); });
|
.catch(function(e){ alert('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.
|
||||||
// Must NOT re-watch a terminal (done/aborted) job: the journal keeps returning the last completed
|
// Must NOT re-watch a terminal (done/aborted) job: the journal keeps returning the last completed
|
||||||
@@ -319,45 +326,53 @@
|
|||||||
<a href="/storage/attach" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-link"/></svg> Meglévő meghajtó csatolása</a>
|
<a href="/storage/attach" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-link"/></svg> Meglévő meghajtó csatolása</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:1.5rem">
|
<div id="agent-warn-note"></div>
|
||||||
<h4 style="margin-bottom:.25rem">Meghajtók (ügynök nézet)</h4>
|
|
||||||
<p class="form-hint" style="margin-bottom:.75rem">A host-ügynök által észlelt meghajtók élő nézete. A meghajtó <strong>szerepkörét</strong> az ügynök saját vizsgálattal állapítja meg: a rendszer- és biztonsági-mentés meghajtók védettek (csak operátori aláírással módosíthatók), a felhasználói adatmeghajtókat Ön kezeli.</p>
|
<div style="margin-top:1.5rem" id="agent-unregistered-wrap" hidden>
|
||||||
<div id="agent-disks">Betöltés…</div>
|
<div class="section-h"><h3>Nem regisztrált meghajtók</h3><span class="hint">az ügynök által észlelt, még nem regisztrált adatmeghajtók</span></div>
|
||||||
|
<div id="agent-unregistered" class="drive-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:1.5rem" id="agent-system-wrap" hidden>
|
||||||
|
<div class="section-h"><h3>Rendszermeghajtók</h3><span class="hint">védett — csak operátori aláírással módosíthatók</span></div>
|
||||||
|
<div id="agent-system" class="drive-list"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="confirm-root"></div>
|
<div id="confirm-root"></div>
|
||||||
|
<div id="dialog-root"></div>
|
||||||
<script>
|
<script>
|
||||||
window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}{{end}}];
|
window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}{{end}}];
|
||||||
(function(){
|
(function(){
|
||||||
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];}); }
|
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];}); }
|
||||||
function hum(b){ if(!b||b<=0) return ''; var u=['B','KB','MB','GB','TB'],i=0,v=b; while(v>=1024&&i<u.length-1){v/=1024;i++;} return (v>=10||i===0?Math.round(v):v.toFixed(1))+' '+u[i]; }
|
function hum(b){ if(!b||b<=0) return ''; var u=['B','KB','MB','GB','TB'],i=0,v=b; while(v>=1024&&i<u.length-1){v/=1024;i++;} return (v>=10||i===0?Math.round(v):v.toFixed(1))+' '+u[i]; }
|
||||||
function usageColorClass(p){ if(p>=85) return 'crit'; if(p>=70) return 'warn'; return 'nominal'; }
|
function usageColorClass(p){ if(p>=85) return 'crit'; if(p>=70) return 'warn'; return 'nominal'; }
|
||||||
function classBadge(d){
|
var LOCK_ICO='<svg class="ico ico-sm"><use href="#i-lock"/></svg>';
|
||||||
if(d.class==='fast') return '<span class="badge badge-ok">gyors</span>';
|
function classTag(d){
|
||||||
if(d.class==='slow') return '<span class="badge badge-muted">lassú</span>';
|
if(d.class==='fast') return '<span class="tag">gyors</span>';
|
||||||
|
if(d.class==='slow') return '<span class="tag tag-off">lassú</span>';
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
function roleBadge(role){
|
function roleTag(role){
|
||||||
if(role==='system') return '<span class="badge badge-lock"><span class="lock-ico">🔒</span>Rendszer</span>';
|
if(role==='system') return '<span class="tag">'+LOCK_ICO+'Rendszer — védett</span>';
|
||||||
if(role==='backup') return '<span class="badge badge-lock"><span class="lock-ico">🔒</span>Biztonsági mentés — védett</span>';
|
if(role==='backup') return '<span class="tag">'+LOCK_ICO+'Biztonsági mentés — védett</span>';
|
||||||
if(role==='user-data') return '<span class="badge badge-ok">Felhasználói adat</span>';
|
if(role==='user-data') return '<span class="tag tag-run"><span class="dot"></span>Felhasználói adat</span>';
|
||||||
return '<span class="badge badge-lock"><span class="lock-ico">🔒</span>Védett</span>';
|
return '<span class="tag">'+LOCK_ICO+'Védett</span>';
|
||||||
}
|
}
|
||||||
function dataBadge(d){ return d.data_bearing ? '<span class="badge badge-error" title="'+esc(d.data_reason)+'">Adatot tartalmaz</span>' : ''; }
|
function dataTag(d){ return d.data_bearing ? '<span class="tag tag-warn" title="'+esc(d.data_reason)+'">Adatot tartalmaz</span>' : ''; }
|
||||||
// regKey is the path a drive is REGISTERED under: the STABLE intermediary path (guest_path,
|
// regKey is the path a drive is REGISTERED under: the STABLE intermediary path (guest_path,
|
||||||
// /mnt/felhom-drives/<name>) the agent reports, else the raw mount_path (legacy). The registry
|
// /mnt/felhom-drives/<name>) the agent reports, else the raw mount_path (legacy). The registry
|
||||||
// stores the STABLE path (handleStorageRegister/runStorageInit), so checking the raw mount_path
|
// stores the STABLE path (handleStorageRegister/runStorageInit), so checking the raw mount_path
|
||||||
// alone made an enrolled drive read as "Nem regisztrált" + show a spurious Regisztrálás button.
|
// alone made an enrolled drive read as "Nem regisztrált" + show a spurious Regisztrálás button.
|
||||||
function regKey(d){ return d.guest_path || d.mount_path; }
|
function regKey(d){ return d.guest_path || d.mount_path; }
|
||||||
function regBadge(d, registered){
|
function regTag(d, registered){
|
||||||
if(!d.mount_path) return '';
|
if(!d.mount_path) return '';
|
||||||
return registered[regKey(d)] ? '<span class="badge badge-ok">Regisztrálva</span>' : '<span class="badge badge-muted">Nem regisztrált</span>';
|
return registered[regKey(d)] ? '<span class="tag tag-run"><span class="dot"></span>Regisztrálva</span>' : '<span class="tag tag-off"><span class="dot"></span>Nem regisztrált</span>';
|
||||||
}
|
}
|
||||||
// appBackingTag marks the storages that actually hold deployed apps: the internal SSD (app
|
// appBackingTag marks the storages that actually hold deployed apps: the internal SSD (app
|
||||||
// databases + Docker) and the external user-data drives (large app files). Keyed on the agent's
|
// databases + Docker) and the external user-data drives (large app files). Keyed on the agent's
|
||||||
// authoritative role/type — pure presentation, no agent contract change.
|
// authoritative role/type — pure presentation, no agent contract change.
|
||||||
function appBackingTag(d){
|
function appBackingTag(d){
|
||||||
if(d.type==='lvmthin') return '<span class="badge badge-info">Alkalmazás-rendszer</span>';
|
if(d.type==='lvmthin') return '<span class="tag">Alkalmazás-rendszer</span>';
|
||||||
if(d.role==='user-data') return '<span class="badge badge-info">Alkalmazás-adatok</span>';
|
if(d.role==='user-data') return '<span class="tag">Alkalmazás-adatok</span>';
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
// purposeDesc explains, in plain Hungarian, what each storage is for — so the "which one do the
|
// purposeDesc explains, in plain Hungarian, what each storage is for — so the "which one do the
|
||||||
@@ -394,36 +409,58 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
|||||||
if(d.backing_device){ btns += ' <button class="btn btn-xs btn-danger-outline" onclick="confirmWipe(\''+dev+'\',\''+reg+'\')">Törlés…</button>'; }
|
if(d.backing_device){ btns += ' <button class="btn btn-xs btn-danger-outline" onclick="confirmWipe(\''+dev+'\',\''+reg+'\')">Törlés…</button>'; }
|
||||||
return '<div class="drive-actions">'+btns+'</div>';
|
return '<div class="drive-actions">'+btns+'</div>';
|
||||||
}
|
}
|
||||||
|
// Unified drive view (D1): the agent disk list ENRICHES the registry cards in place
|
||||||
|
// (role tag + durable-id + agent-only actions), and two extra groups render below:
|
||||||
|
// protected system/backup drives (read-only) and unregistered user-data drives.
|
||||||
|
function driveRow(d, registered, withActions){
|
||||||
|
var sub = esc(d.type)+' · '+esc(d.backing_device||'—')+(d.mount_path?' · '+esc(regKey(d)):'');
|
||||||
|
var badges = roleTag(d.role)+appBackingTag(d)+classTag(d)+dataTag(d);
|
||||||
|
if(withActions) badges += regTag(d, registered);
|
||||||
|
var purpose = purposeDesc(d);
|
||||||
|
return '<div class="drive-card role-'+esc(d.role||'system')+'">'
|
||||||
|
+'<div class="drive-card-top"><div class="drive-id"><span class="drive-name">'+esc(d.name)+'</span><span class="drive-sub">'+sub+'</span></div>'
|
||||||
|
+'<div class="drive-badges">'+badges+'</div></div>'
|
||||||
|
+(purpose?'<div class="drive-purpose">'+esc(purpose)+'</div>':'')
|
||||||
|
+(d.durable_id?'<div class="drive-sub mono">'+esc(d.durable_id)+'</div>':'')
|
||||||
|
+capBar(d)
|
||||||
|
+(withActions?actions(d,registered):'')
|
||||||
|
+'</div>';
|
||||||
|
}
|
||||||
|
function enrichCard(d, registered){
|
||||||
|
var slot=document.getElementById('agent-extra-'+regKey(d));
|
||||||
|
if(!slot) return false;
|
||||||
|
var extra='<div class="metarows" style="margin:.4rem 0 0">'
|
||||||
|
+'<span class="metarow">'+roleTag(d.role)+'</span>'
|
||||||
|
+(d.class?'<span class="metarow">'+classTag(d)+'</span>':'')
|
||||||
|
+(d.durable_id?'<span class="metarow mono" title="Tartós azonosító">'+esc(d.durable_id)+'</span>':'')
|
||||||
|
+'</div>'
|
||||||
|
+actions(d,registered);
|
||||||
|
slot.innerHTML=extra;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
async function load(){
|
async function load(){
|
||||||
var box=document.getElementById('agent-disks'); if(!box) return;
|
var warn=document.getElementById('agent-warn-note');
|
||||||
try{
|
try{
|
||||||
var r=await fetch('/api/disks'); var j=await r.json();
|
var r=await fetch('/api/disks'); var j=await r.json();
|
||||||
if(!j.ok){ box.innerHTML='<p class="form-hint">'+esc(j.error||'Nem elérhető')+'</p>'; return; }
|
if(!j.ok) throw new Error(j.error||'Nem elérhető');
|
||||||
var disks=(j.data&&j.data.disks)||[];
|
var disks=(j.data&&j.data.disks)||[];
|
||||||
if(disks.length===0){ box.innerHTML='<p class="form-hint">Nincs észlelt meghajtó.</p>'; return; }
|
|
||||||
var registered={}; (window.__registeredPaths||[]).forEach(function(p){registered[p]=true;});
|
var registered={}; (window.__registeredPaths||[]).forEach(function(p){registered[p]=true;});
|
||||||
// One-line tiering explanation so "which storage do the apps use?" is answered at a glance.
|
var sysHtml='', unregHtml='';
|
||||||
var html='<p class="drive-tiering-note">Az alkalmazások rendszere és adatbázisai a belső SSD-n (local-lvm), a nagy méretű fájljaik a külső adattárolókon tárolódnak.</p>';
|
|
||||||
html+='<div class="drive-list">';
|
|
||||||
disks.forEach(function(d){
|
disks.forEach(function(d){
|
||||||
// Show the path the customer's system actually uses: the STABLE in-guest path
|
if(d.role==='user-data'){
|
||||||
// (/mnt/felhom-drives/<name>, = guest_path) for an external drive, not the raw host PVE mount
|
if(registered[regKey(d)] && enrichCard(d, registered)) return; // one card, enriched in place
|
||||||
// (/mnt/<name>) which doesn't exist inside the guest. regKey falls back to mount_path for
|
unregHtml+=driveRow(d, registered, true);
|
||||||
// non-intermediary drives (system/local).
|
}else{
|
||||||
var sub = esc(d.type)+' · '+esc(d.backing_device||'—')+(d.mount_path?' · '+esc(regKey(d)):'');
|
sysHtml+=driveRow(d, registered, false); // protected: read-only, lock tag, NO actions
|
||||||
var badges = roleBadge(d.role)+appBackingTag(d)+classBadge(d)+dataBadge(d)+regBadge(d,registered);
|
}
|
||||||
var purpose = purposeDesc(d);
|
|
||||||
html+='<div class="drive-card role-'+esc(d.role||'system')+'">'
|
|
||||||
+'<div class="drive-card-top"><div class="drive-id"><span class="drive-name">'+esc(d.name)+'</span><span class="drive-sub">'+sub+'</span></div>'
|
|
||||||
+'<div class="drive-badges">'+badges+'</div></div>'
|
|
||||||
+(purpose?'<div class="drive-purpose">'+esc(purpose)+'</div>':'')
|
|
||||||
+capBar(d)
|
|
||||||
+actions(d,registered)
|
|
||||||
+'</div>';
|
|
||||||
});
|
});
|
||||||
html+='</div>';
|
var sysWrap=document.getElementById('agent-system-wrap');
|
||||||
box.innerHTML=html;
|
var unregWrap=document.getElementById('agent-unregistered-wrap');
|
||||||
}catch(e){ box.innerHTML='<p class="form-hint">Hiba: '+esc(e.message)+'</p>'; }
|
if(sysHtml){ document.getElementById('agent-system').innerHTML=sysHtml; sysWrap.hidden=false; }
|
||||||
|
if(unregHtml){ document.getElementById('agent-unregistered').innerHTML=unregHtml; unregWrap.hidden=false; }
|
||||||
|
}catch(e){
|
||||||
|
if(warn) warn.innerHTML='<div class="alert alert-warning" style="margin-top:1rem">Az ügynök nem elérhető — élő meghajtóadatok nélkül.</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- type-to-confirm modal (destructive user-data actions) ----
|
// ---- type-to-confirm modal (destructive user-data actions) ----
|
||||||
@@ -524,6 +561,40 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Light overlay dialog (D1) — replaces the native blocking browser dialogs: same texts,
|
||||||
|
// reuses the .confirm-overlay/.confirm-box pattern. typeToConfirm adds the typed-name gate
|
||||||
|
// ONLY where the old flow already had one.
|
||||||
|
function escText(s){var d=document.createElement('div');d.textContent=String(s==null?'':s);return d.innerHTML;}
|
||||||
|
function closeDialog(){ var r=document.getElementById('dialog-root'); if(r) r.innerHTML=''; }
|
||||||
|
function openDialog(opts){
|
||||||
|
var root=document.getElementById('dialog-root');
|
||||||
|
if(!root) return;
|
||||||
|
var typeGate=opts.typeToConfirm||'';
|
||||||
|
var html='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">'
|
||||||
|
+'<h3>'+escText(opts.title||'Megerősítés')+'</h3>'
|
||||||
|
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>';
|
||||||
|
if(typeGate){
|
||||||
|
html+='<div class="confirm-input"><label>Megerősítéshez írja be: <strong class="mono">'+escText(typeGate)+'</strong></label>'
|
||||||
|
+'<input type="text" id="dialog-type" class="form-control" autocomplete="off" placeholder="'+escText(typeGate)+'"></div>';
|
||||||
|
}
|
||||||
|
html+='<div class="form-actions"><button id="dialog-go" class="btn '+(typeGate?'btn-danger-outline':'btn-primary')+'"'+(typeGate?' disabled':'')+'>'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
|
||||||
|
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>'
|
||||||
|
+'</div></div>';
|
||||||
|
root.innerHTML=html;
|
||||||
|
var go=document.getElementById('dialog-go');
|
||||||
|
if(typeGate){
|
||||||
|
document.getElementById('dialog-type').oninput=function(){ go.disabled=(this.value.trim()!==typeGate); };
|
||||||
|
}
|
||||||
|
go.onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); };
|
||||||
|
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
|
||||||
|
}
|
||||||
|
// Overlay-confirmed POST form submit (registry drive-remove forms).
|
||||||
|
function storageRemoveDialog(ev, form, msg){
|
||||||
|
ev.preventDefault();
|
||||||
|
openDialog({title:'Meghajtó eltávolítása', message:msg, confirmLabel:'Eltávolítás', onConfirm:function(){ form.submit(); }});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function editStorageLabel(path, currentLabel) {
|
function editStorageLabel(path, currentLabel) {
|
||||||
var wrap = document.getElementById('label-wrap-' + path);
|
var wrap = document.getElementById('label-wrap-' + path);
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
@@ -541,7 +612,9 @@ function storageDisconnect(path, label, appCount) {
|
|||||||
var msg = 'Biztos leválasztja a meghajtót: ' + label + '?';
|
var msg = 'Biztos leválasztja a meghajtót: ' + label + '?';
|
||||||
if (appCount > 0) msg += '\n\nA rajta futó ' + appCount + ' alkalmazás le fog állni.';
|
if (appCount > 0) msg += '\n\nA rajta futó ' + appCount + ' alkalmazás le fog állni.';
|
||||||
msg += '\n\nA meghajtó ezután biztonságosan eltávolítható.';
|
msg += '\n\nA meghajtó ezután biztonságosan eltávolítható.';
|
||||||
if (!confirm(msg)) return;
|
openDialog({title:'Meghajtó leválasztása', message:msg, confirmLabel:'Leválasztás', onConfirm:function(){ doStorageDisconnect(path); }});
|
||||||
|
}
|
||||||
|
function doStorageDisconnect(path) {
|
||||||
fetch('/api/storage/disconnect', {
|
fetch('/api/storage/disconnect', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||||
@@ -594,18 +667,19 @@ function storageRestartApps(path) {
|
|||||||
function storageDecommission(path, label, appCount) {
|
function storageDecommission(path, label, appCount) {
|
||||||
var sel = document.getElementById('migrate-target-' + path);
|
var sel = document.getElementById('migrate-target-' + path);
|
||||||
var target = sel ? sel.value : '';
|
var target = sel ? sel.value : '';
|
||||||
var body;
|
|
||||||
if (target) {
|
if (target) {
|
||||||
if (!confirm('Leszerelés áthelyezéssel: minden adat átmásolása ide: ' + target + ', majd a(z) ' + label + ' leszerelése?\n\nAz adatok NEM törlődnek.')) return;
|
openDialog({title:'Leszerelés áthelyezéssel', confirmLabel:'Leszerelés',
|
||||||
body = {where: path, mode: 'migrate', target: target};
|
message:'Leszerelés áthelyezéssel: minden adat átmásolása ide: ' + target + ', majd a(z) ' + label + ' leszerelése?\n\nAz adatok NEM törlődnek.',
|
||||||
|
onConfirm:function(){ doStorageDecommission({where: path, mode: 'migrate', target: target}); }});
|
||||||
} else {
|
} else {
|
||||||
var name = path.split('/').pop();
|
var name = path.split('/').pop();
|
||||||
var typed = prompt('A(z) ' + label + ' leszereléséhez (áthelyezés nélkül) írja be a meghajtó nevét megerősítésként:\n\n' + name +
|
openDialog({title:'Meghajtó leszerelése', confirmLabel:'Leszerelés', typeToConfirm:name,
|
||||||
(appCount > 0 ? '\n\nFIGYELEM: ' + appCount + ' alkalmazás leáll (az adatok megmaradnak).' : ''));
|
message:'A(z) ' + label + ' leszereléséhez (áthelyezés nélkül) írja be a meghajtó nevét megerősítésként.' +
|
||||||
if (typed === null) return;
|
(appCount > 0 ? '\n\nFIGYELEM: ' + appCount + ' alkalmazás leáll (az adatok megmaradnak).' : ''),
|
||||||
if (typed.trim() !== name) { alert('A név nem egyezik — megszakítva.'); return; }
|
onConfirm:function(){ doStorageDecommission({where: path, mode: 'anyway', mount_name: name}); }});
|
||||||
body = {where: path, mode: 'anyway', mount_name: name};
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
function doStorageDecommission(body) {
|
||||||
fetch('/api/storage/decommission', {
|
fetch('/api/storage/decommission', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||||
@@ -618,7 +692,11 @@ function storageDecommission(path, label, appCount) {
|
|||||||
// 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).
|
||||||
function storageReEnroll(path, label) {
|
function storageReEnroll(path, label) {
|
||||||
if (!confirm('Visszacsatlakoztatja a(z) ' + label + ' meghajtót?\n\nAz adatok érintetlenek; a leállított alkalmazások újraindulnak.')) return;
|
openDialog({title:'Visszacsatlakoztatás', confirmLabel:'Visszacsatlakoztatás',
|
||||||
|
message:'Visszacsatlakoztatja a(z) ' + label + ' meghajtót?\n\nAz adatok érintetlenek; a leállított alkalmazások újraindulnak.',
|
||||||
|
onConfirm:function(){ doStorageReEnroll(path); }});
|
||||||
|
}
|
||||||
|
function doStorageReEnroll(path) {
|
||||||
fetch('/api/storage/reconnect', {
|
fetch('/api/storage/reconnect', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||||
|
|||||||
@@ -3123,7 +3123,6 @@ a.stat-card:hover {
|
|||||||
/* badges missing from the global sheet */
|
/* badges missing from the global sheet */
|
||||||
.badge-ok { background: var(--blue-dim); color: var(--blue); }
|
.badge-ok { background: var(--blue-dim); color: var(--blue); }
|
||||||
.badge-neutral { background: var(--bg-2); color: var(--text-2); }
|
.badge-neutral { background: var(--bg-2); color: var(--text-2); }
|
||||||
.badge-lock { background: var(--warn-dim); color: var(--warn); }
|
|
||||||
.badge-muted { background: var(--bg-2); color: var(--text-3); }
|
.badge-muted { background: var(--bg-2); color: var(--text-3); }
|
||||||
.badge-info { background: var(--blue-dim); color: var(--blue-bright); }
|
.badge-info { background: var(--blue-dim); color: var(--blue-bright); }
|
||||||
/* Backup-page tier divider between the whole-guest section and the per-app section. */
|
/* Backup-page tier divider between the whole-guest section and the per-app section. */
|
||||||
@@ -3138,7 +3137,6 @@ a.stat-card:hover {
|
|||||||
margin: 0 0 .75rem; padding: .5rem .75rem;
|
margin: 0 0 .75rem; padding: .5rem .75rem;
|
||||||
background: var(--blue-dim); border-left: 3px solid var(--blue); border-radius: var(--radius);
|
background: var(--blue-dim); border-left: 3px solid var(--blue); border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
.badge .lock-ico { margin-right: .25rem; }
|
|
||||||
span.mono, .mono { font-family: var(--font-data); }
|
span.mono, .mono { font-family: var(--font-data); }
|
||||||
|
|
||||||
/* Type-to-confirm modal (destructive user-data eject/wipe) */
|
/* Type-to-confirm modal (destructive user-data eject/wipe) */
|
||||||
|
|||||||
Reference in New Issue
Block a user