storage IA: split /storage into Meghajtók + /storage/network (NAS)

User feedback on D1: the NAS-add button and the local-drive enrollment
buttons sat side by side with no separation — two different storage
classes interleaved on one page. Now two subpages under Tárhely:

- /storage — Tárhely — Meghajtók: physical drive registry, migrate,
  wizard entry points (Új meghajtó inicializálása / Meglévő meghajtó
  csatolása), unified agent view, manual add. The enrollment buttons
  now live unambiguously in the local-drive context.
- /storage/network — Tárhely — Hálózati tárhely (NAS): the NAS share
  list (NAS-megosztások) + add form + its JS (moved verbatim, incl.
  its own openDialog copy for the remove overlay).
- layout.html: Tárhely main-nav item gains two always-visible nested
  sub-links (Meghajtók / Hálózati tárhely, .nav-links-nested CSS);
  parent stays highlighted on both.
- handlers.go: NetworkStoragePaths moves out of storagePageData into
  the new networkStoragePageData (page key storage-network) +
  storageNetworkPageHandler; GET /storage/network route.
- Tests: /storage must NOT render the NAS section, /storage/network
  renders it and nothing drive-related; inventory + no-native-confirm
  scans cover the new template. Both template gates green.
This commit is contained in:
2026-07-02 19:34:27 +02:00
parent c427b40b2f
commit d3c97c62fe
7 changed files with 172 additions and 109 deletions
+13 -2
View File
@@ -987,8 +987,14 @@ func (s *Server) storagePageData() map[string]interface{} {
storageViews = append(storageViews, view)
}
data["StoragePaths"] = storageViews
// NAS network storage (Part A2) — separate section with the agent's per-share health (ok/idle/
// unreachable/unknown). Distinct from the physical-drive list above; no drive lifecycle actions.
return data
}
// networkStoragePageData builds the Tárhely → Hálózati tárhely (NAS) subpage: the NAS shares
// with the agent's per-share health (ok/idle/unreachable/unknown). Split from the physical
// drive page — the two storage classes were confusingly interleaved on one page.
func (s *Server) networkStoragePageData() map[string]interface{} {
data := s.settingsBaseData("storage-network", "Hálózati tárhely")
data["NetworkStoragePaths"] = s.networkStorageItems(context.Background())
return data
}
@@ -1061,6 +1067,11 @@ func (s *Server) storagePageHandler(w http.ResponseWriter, r *http.Request) {
s.executeTemplate(w, r, "storage", data)
}
// storageNetworkPageHandler serves the Tárhely → Hálózati tárhely (NAS) subpage.
func (s *Server) storageNetworkPageHandler(w http.ResponseWriter, r *http.Request) {
s.executeTemplate(w, r, "storage_network", s.networkStoragePageData())
}
// settingsNotificationsPageHandler serves GET /settings/notifications (the POST on the same
// path is the save handler — dispatch is split in the router).
func (s *Server) settingsNotificationsPageHandler(w http.ResponseWriter, r *http.Request) {
+2
View File
@@ -264,6 +264,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.settingsHandler(w, r)
case path == "/storage" && r.Method == http.MethodGet:
s.storagePageHandler(w, r)
case path == "/storage/network" && r.Method == http.MethodGet:
s.storageNetworkPageHandler(w, r)
case path == "/settings/notifications" && r.Method == http.MethodGet:
s.settingsNotificationsPageHandler(w, r)
case path == "/settings/security" && r.Method == http.MethodGet:
@@ -70,8 +70,11 @@ func TestSettingsSplitPagesRender(t *testing.T) {
[]string{"Jelszó módosítás", "Földrajzi korlátozás"},
[]string{"Rendszer konfiguráció", "Adattárolók", "Értesítési szünet"}},
{"/storage",
[]string{"Adattárolók", "Hálózati tárhely (NAS)"},
[]string{"Rendszer konfiguráció", "Jelszó módosítás", "Értesítési szünet"}},
[]string{"Adattárolók", "Tárhely — Meghajtók"},
[]string{"Rendszer konfiguráció", "Jelszó módosítás", "Értesítési szünet", "NAS-megosztások"}},
{"/storage/network",
[]string{"Tárhely — Hálózati tárhely (NAS)", "NAS-megosztások"},
[]string{"Adattárolók", "Rendszer konfiguráció", "Jelszó módosítás"}},
}
for _, c := range cases {
rec := getPage(t, s, c.path)
@@ -110,7 +113,7 @@ func TestSettingsSectionInventory(t *testing.T) {
"Kiszolgáló újraindítása",
}
var union strings.Builder
for _, f := range []string{"settings_system.html", "settings_notifications.html", "settings_security.html", "storage.html"} {
for _, f := range []string{"settings_system.html", "settings_notifications.html", "settings_security.html", "storage.html", "storage_network.html"} {
b, err := templateFS.ReadFile("templates/" + f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
@@ -220,7 +223,7 @@ 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"} {
for _, f := range []string{"storage.html", "storage_network.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)
@@ -23,7 +23,12 @@
<ul class="nav-links">
<li><a href="/" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li><a href="/storage" class="{{if eq .Page "storage"}}active{{end}}"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely</a></li>
<li><a href="/storage" class="{{if or (eq .Page "storage") (eq .Page "storage-network")}}active{{end}}"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely</a>
<ul class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="{{if eq .Page "storage"}}active{{end}}">Meghajtók</a></li>
<li><a href="/storage/network" class="{{if eq .Page "storage-network"}}active{{end}}">Hálózati tárhely</a></li>
</ul>
</li>
<li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés</a></li>
<li><a href="/monitoring" class="{{if eq .Page "monitoring"}}active{{end}}"><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
{{if .DebugMode}}<li><a href="/debug" class="{{if eq .Page "debug"}}active{{end}}"><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>{{end}}
+1 -102
View File
@@ -2,7 +2,7 @@
{{template "layout_start" .}}
<div class="page-header">
<h2>Tárhely</h2>
<h2>Tárhely — Meghajtók</h2>
</div>
<!-- Section: Storage Paths -->
@@ -169,107 +169,6 @@
</div>
{{end}}
<!-- NAS network storage (Part A2) — a distinct class from the physical drives above. No
leválasztás/leszerelés/áthelyezés/törlés; the only lifecycle action is Eltávolítás. -->
<div class="storage-section" style="margin-top:1.5rem">
<h3 style="margin-bottom:.25rem">Hálózati tárhely (NAS)</h3>
<p class="form-hint" style="margin-top:0">
Egy NAS-megosztás (NFS vagy SMB) csatlakoztatása nagy méretű médiatartalomhoz (film, fotó, zene).
A megosztás kiválasztható médiaalkalmazás adatkönyvtáraként. A hálózati tárhely nem fizikai
meghajtó — nincs leszerelés/áthelyezés, csak eltávolítás.
</p>
{{if .NetworkStoragePaths}}
<div class="storage-paths-list">
{{range .NetworkStoragePaths}}
<div class="storage-path-item{{if eq .Health "unreachable"}} storage-disconnected{{end}}">
<div class="storage-path-header">
<div class="storage-path-info">
<span class="storage-path-label">{{.Label}}</span>
<span class="storage-path-path mono">{{.Protocol}} · {{.Server}}:{{.Export}} → {{.Path}}</span>
</div>
<div class="storage-path-badges">
{{if eq .Health "ok"}}<span class="badge badge-ok" title="A megosztás elérhető és csatlakoztatva van">Elérhető</span>
{{else if eq .Health "idle"}}<span class="badge badge-neutral" title="Elérhető, jelenleg készenlétben (igény szerint csatlakozik)">Készenlét</span>
{{else if eq .Health "unreachable"}}<span class="badge badge-warn" title="A NAS jelenleg nem érhető el — az érintett alkalmazások átmenetileg nem olvasnak róla">Nem elérhető</span>
{{else}}<span class="badge badge-neutral" title="Az állapot jelenleg nem lekérdezhető">Ismeretlen</span>{{end}}
</div>
</div>
<div class="storage-path-actions">
<button class="btn btn-xs btn-danger-outline" onclick="netStorageRemove('{{.Name}}','{{.Label}}')">Eltávolítás</button>
</div>
</div>
{{end}}
</div>
{{else}}
<p class="form-hint">Nincs hálózati tárhely beállítva.</p>
{{end}}
<details style="margin-top:.75rem">
<summary class="btn btn-xs btn-primary" style="cursor:pointer;display:inline-block">Hálózati tárhely hozzáadása</summary>
<div style="margin-top:.75rem;padding:1rem;border:1px solid var(--border,#ddd);border-radius:6px;max-width:520px">
<div class="form-row"><label>Név (azonosító)</label>
<input id="ns-name" type="text" placeholder="pl. media" class="form-input"></div>
<div class="form-row"><label>Protokoll</label>
<select id="ns-protocol" class="form-input" onchange="nsToggleSmb()">
<option value="nfs">NFS (ajánlott)</option>
<option value="smb">SMB / CIFS</option>
</select></div>
<div class="form-row"><label>Szerver (IP vagy hosztnév)</label>
<input id="ns-server" type="text" placeholder="pl. 192.168.0.10" class="form-input"></div>
<div class="form-row"><label id="ns-export-label">Megosztás (NFS export útvonal)</label>
<input id="ns-export" type="text" placeholder="pl. /volume1/media" class="form-input"></div>
<div class="form-row"><label>Alkalmazás felhasználói azonosító (uid)</label>
<input id="ns-uid" type="number" value="1000" class="form-input">
<span class="form-hint">A legtöbb médiaalkalmazás 1000-es uid-del fut.</span></div>
<div id="ns-smb-creds" style="display:none">
<div class="form-row"><label>SMB felhasználónév</label>
<input id="ns-username" type="text" autocomplete="off" class="form-input"></div>
<div class="form-row"><label>SMB jelszó</label>
<input id="ns-password" type="password" autocomplete="new-password" class="form-input">
<span class="form-hint">A jelszót a gazda ügynök 0600-as fájlba írja; a vezérlő nem tárolja.</span></div>
</div>
<button class="btn btn-sm btn-primary" style="margin-top:.5rem" onclick="netStorageAdd()">Csatlakoztatás</button>
<div id="ns-add-msg" class="form-hint" style="margin-top:.5rem"></div>
</div>
</details>
</div>
<script>
function nsToggleSmb(){
var smb = document.getElementById('ns-protocol').value === 'smb';
document.getElementById('ns-smb-creds').style.display = smb ? 'block' : 'none';
document.getElementById('ns-export-label').textContent = smb ? 'Megosztás (SMB megosztásnév)' : 'Megosztás (NFS export útvonal)';
document.getElementById('ns-export').placeholder = smb ? 'pl. media' : 'pl. /volume1/media';
}
function netStorageAdd(){
var msg = document.getElementById('ns-add-msg');
var body = {
name: (document.getElementById('ns-name').value||'').trim(),
protocol: document.getElementById('ns-protocol').value,
server: (document.getElementById('ns-server').value||'').trim(),
export: (document.getElementById('ns-export').value||'').trim(),
mapped_uid: parseInt(document.getElementById('ns-uid').value||'1000',10),
mapped_gid: parseInt(document.getElementById('ns-uid').value||'1000',10),
username: (document.getElementById('ns-username').value||''),
password: (document.getElementById('ns-password').value||'')
};
msg.textContent = 'Csatlakoztatás folyamatban…';
fetch('/api/storage/netstorage/add',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify(body)})
.then(function(r){return r.json();}).then(function(d){
if(d.ok){ msg.textContent='Sikeres'; setTimeout(function(){location.reload();},900); }
else { msg.textContent='Hiba: '+(d.error||'ismeretlen'); }
}).catch(function(e){ msg.textContent='Hiba: '+e; });
}
function netStorageRemove(name,label){
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})})
.then(function(r){return r.json();}).then(function(d){
if(d.ok){ location.reload(); } else { alert('Hiba: '+(d.error||'ismeretlen')); }
}).catch(function(e){ alert('Hiba: '+e); });
}});
}
</script>
<div id="migrate-progress" style="display:none;margin-top:1rem;padding:1rem;border:1px solid var(--blue);border-radius:6px;background:rgba(0,136,204,0.06)">
<strong>Adatok áthelyezése</strong>
@@ -0,0 +1,130 @@
{{define "storage_network"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>Tárhely — Hálózati tárhely (NAS)</h2>
</div>
<div class="settings-card">
<!-- NAS network storage (Part A2) — a distinct class from the physical drives above. No
leválasztás/leszerelés/áthelyezés/törlés; the only lifecycle action is Eltávolítás. -->
<div class="storage-section">
<h3 style="margin-bottom:.25rem">NAS-megosztások</h3>
<p class="form-hint" style="margin-top:0">
Egy NAS-megosztás (NFS vagy SMB) csatlakoztatása nagy méretű médiatartalomhoz (film, fotó, zene).
A megosztás kiválasztható médiaalkalmazás adatkönyvtáraként. A hálózati tárhely nem fizikai
meghajtó — nincs leszerelés/áthelyezés, csak eltávolítás.
</p>
{{if .NetworkStoragePaths}}
<div class="storage-paths-list">
{{range .NetworkStoragePaths}}
<div class="storage-path-item{{if eq .Health "unreachable"}} storage-disconnected{{end}}">
<div class="storage-path-header">
<div class="storage-path-info">
<span class="storage-path-label">{{.Label}}</span>
<span class="storage-path-path mono">{{.Protocol}} · {{.Server}}:{{.Export}} → {{.Path}}</span>
</div>
<div class="storage-path-badges">
{{if eq .Health "ok"}}<span class="badge badge-ok" title="A megosztás elérhető és csatlakoztatva van">Elérhető</span>
{{else if eq .Health "idle"}}<span class="badge badge-neutral" title="Elérhető, jelenleg készenlétben (igény szerint csatlakozik)">Készenlét</span>
{{else if eq .Health "unreachable"}}<span class="badge badge-warn" title="A NAS jelenleg nem érhető el — az érintett alkalmazások átmenetileg nem olvasnak róla">Nem elérhető</span>
{{else}}<span class="badge badge-neutral" title="Az állapot jelenleg nem lekérdezhető">Ismeretlen</span>{{end}}
</div>
</div>
<div class="storage-path-actions">
<button class="btn btn-xs btn-danger-outline" onclick="netStorageRemove('{{.Name}}','{{.Label}}')">Eltávolítás</button>
</div>
</div>
{{end}}
</div>
{{else}}
<p class="form-hint">Nincs hálózati tárhely beállítva.</p>
{{end}}
<details style="margin-top:.75rem">
<summary class="btn btn-xs btn-primary" style="cursor:pointer;display:inline-block">Hálózati tárhely hozzáadása</summary>
<div style="margin-top:.75rem;padding:1rem;border:1px solid var(--border,#ddd);border-radius:6px;max-width:520px">
<div class="form-row"><label>Név (azonosító)</label>
<input id="ns-name" type="text" placeholder="pl. media" class="form-input"></div>
<div class="form-row"><label>Protokoll</label>
<select id="ns-protocol" class="form-input" onchange="nsToggleSmb()">
<option value="nfs">NFS (ajánlott)</option>
<option value="smb">SMB / CIFS</option>
</select></div>
<div class="form-row"><label>Szerver (IP vagy hosztnév)</label>
<input id="ns-server" type="text" placeholder="pl. 192.168.0.10" class="form-input"></div>
<div class="form-row"><label id="ns-export-label">Megosztás (NFS export útvonal)</label>
<input id="ns-export" type="text" placeholder="pl. /volume1/media" class="form-input"></div>
<div class="form-row"><label>Alkalmazás felhasználói azonosító (uid)</label>
<input id="ns-uid" type="number" value="1000" class="form-input">
<span class="form-hint">A legtöbb médiaalkalmazás 1000-es uid-del fut.</span></div>
<div id="ns-smb-creds" style="display:none">
<div class="form-row"><label>SMB felhasználónév</label>
<input id="ns-username" type="text" autocomplete="off" class="form-input"></div>
<div class="form-row"><label>SMB jelszó</label>
<input id="ns-password" type="password" autocomplete="new-password" class="form-input">
<span class="form-hint">A jelszót a gazda ügynök 0600-as fájlba írja; a vezérlő nem tárolja.</span></div>
</div>
<button class="btn btn-sm btn-primary" style="margin-top:.5rem" onclick="netStorageAdd()">Csatlakoztatás</button>
<div id="ns-add-msg" class="form-hint" style="margin-top:.5rem"></div>
</div>
</details>
</div>
<script>
function nsToggleSmb(){
var smb = document.getElementById('ns-protocol').value === 'smb';
document.getElementById('ns-smb-creds').style.display = smb ? 'block' : 'none';
document.getElementById('ns-export-label').textContent = smb ? 'Megosztás (SMB megosztásnév)' : 'Megosztás (NFS export útvonal)';
document.getElementById('ns-export').placeholder = smb ? 'pl. media' : 'pl. /volume1/media';
}
function netStorageAdd(){
var msg = document.getElementById('ns-add-msg');
var body = {
name: (document.getElementById('ns-name').value||'').trim(),
protocol: document.getElementById('ns-protocol').value,
server: (document.getElementById('ns-server').value||'').trim(),
export: (document.getElementById('ns-export').value||'').trim(),
mapped_uid: parseInt(document.getElementById('ns-uid').value||'1000',10),
mapped_gid: parseInt(document.getElementById('ns-uid').value||'1000',10),
username: (document.getElementById('ns-username').value||''),
password: (document.getElementById('ns-password').value||'')
};
msg.textContent = 'Csatlakoztatás folyamatban…';
fetch('/api/storage/netstorage/add',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify(body)})
.then(function(r){return r.json();}).then(function(d){
if(d.ok){ msg.textContent='Sikeres'; setTimeout(function(){location.reload();},900); }
else { msg.textContent='Hiba: '+(d.error||'ismeretlen'); }
}).catch(function(e){ msg.textContent='Hiba: '+e; });
}
function netStorageRemove(name,label){
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})})
.then(function(r){return r.json();}).then(function(d){
if(d.ok){ location.reload(); } else { alert('Hiba: '+(d.error||'ismeretlen')); }
}).catch(function(e){ alert('Hiba: '+e); });
}});
}
</script>
</div>
<div id="dialog-root"></div>
<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(); };
}
</script>
{{template "layout_end" .}}
{{end}}
@@ -3240,3 +3240,16 @@ span.mono, .mono { font-family: var(--font-data); }
padding: .5rem 1.5rem .5rem 1.75rem;
font-size: .88rem;
}
/* Nested sub-links under a main-nav item (Tárhely → Meghajtók / Hálózati tárhely). */
.nav-links-nested {
padding: 0;
list-style: none;
}
.nav-links-nested a {
padding: .4rem 1.5rem .4rem 3.1rem;
font-size: .85rem;
}
.nav-links-nested a.active {
border-left: 3px solid var(--blue);
}