controller v0.68.0: storage lifecycle on intermediary model (H2/H3/M1/M3 + boot-id)

H2 decommission UI button (migrate / anyway); H3 one-click re-enroll of a
decommissioned drive; M1 default reassignment (auto-promote + block-if-none);
M3 migrate re-asserts 2775 setgid on userdata dirs; deterministic guest-reboot
recreate via agent boot_id (replaces the timed sample). Fixes the {path}/{where}
H1 JS bug. Non-hollow tests + companions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 19:26:02 +02:00
parent bc41acf4da
commit 42f69dadda
10 changed files with 293 additions and 46 deletions
@@ -247,6 +247,7 @@ function pollUntilBack() {
</div>
</div>
<div class="storage-path-actions">
<button class="btn btn-xs btn-primary" onclick="storageReEnroll('{{.Path}}','{{.Label}}')">Visszacsatlakoztatás</button>
<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.')">
{{$.CSRFField}}
@@ -343,6 +344,7 @@ function pollUntilBack() {
<button class="btn btn-xs btn-outline" onclick="storageMigrateAll('{{.Path}}','{{.Label}}')">Összes adat áthelyezése</button>
</span>
{{end}}
<button class="btn btn-xs btn-danger-outline" onclick="storageDecommission('{{.Path}}','{{.Label}}',{{.AppCount}})">Leszerelés</button>
</div>
{{end}}
</div>
@@ -1130,7 +1132,7 @@ function storageDisconnect(path, label, appCount) {
fetch('/api/storage/disconnect', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({path: path})
body: JSON.stringify({where: path})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.ok) {
alert('A meghajtó biztonságosan eltávolítható.');
@@ -1146,7 +1148,7 @@ function storageReconnect(path) {
fetch('/api/storage/reconnect', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({path: path})
body: JSON.stringify({where: path})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.ok) {
location.reload();
@@ -1163,19 +1165,56 @@ function storageRestartApps(path) {
fetch('/api/storage/restart-apps', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({path: path})
body: JSON.stringify({where: path})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.ok) {
var msg = '';
if (data.started && data.started.length) msg += 'Elindítva: ' + data.started.join(', ');
if (data.failed && data.failed.length) msg += (msg ? '\n' : '') + 'Sikertelen: ' + data.failed.join(', ');
if (msg) alert(msg);
var r2 = data.restarted || [];
if (r2.length) alert('Elindítva: ' + r2.join(', '));
location.reload();
} else {
alert('Hiba: ' + (data.error || 'ismeretlen'));
}
}).catch(function(e) { alert('Hiba: ' + e); });
}
// 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.
function storageDecommission(path, label, appCount) {
var sel = document.getElementById('migrate-target-' + path);
var target = sel ? sel.value : '';
var body;
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;
body = {where: path, mode: 'migrate', target: target};
} else {
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 +
(appCount > 0 ? '\n\nFIGYELEM: ' + appCount + ' alkalmazás leáll (az adatok megmaradnak).' : ''));
if (typed === null) return;
if (typed.trim() !== name) { alert('A név nem egyezik — megszakítva.'); return; }
body = {where: path, mode: 'anyway', mount_name: name};
}
fetch('/api/storage/decommission', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify(body)
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.ok) { alert('Leszerelés elindítva.'); location.reload(); }
else { alert('Hiba: ' + (data.error || 'ismeretlen')); }
}).catch(function(e) { alert('Hiba: ' + e); });
}
// 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).
function storageReEnroll(path, label) {
if (!confirm('Visszacsatlakoztatja a(z) ' + label + ' meghajtót?\n\nAz adatok érintetlenek; a leállított alkalmazások újraindulnak.')) return;
fetch('/api/storage/reconnect', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({where: path})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.ok) { location.reload(); }
else { alert('Hiba: ' + (data.error || 'ismeretlen')); }
}).catch(function(e) { alert('Hiba: ' + e); });
}
function cancelEditLabel(path, label) {
var wrap = document.getElementById('label-wrap-' + path);
if (!wrap) return;