Add backup shrinking functionality to eMMC provisioning dashboard: implement API for shrinking raw .img backups using PiShrink, update UI to support shrink option after backup, and enhance documentation for backup image handling and storage options on Proxmox host.

This commit is contained in:
nearxos
2026-02-18 18:55:32 +02:00
parent f93d224e8b
commit 5ff46e67d8
9 changed files with 302 additions and 20 deletions

View File

@@ -382,6 +382,9 @@
<section class="section">
<h2 class="section-title">Capture image or deploy</h2>
<p class="backup-deploy-hint">To <strong>capture (backup)</strong> an image from a device: connect it in USB boot mode or register over network. When the device appears below, click <strong>Backup</strong> to save its eMMC to a file. To write an image to a device, click <strong>Deploy</strong> (requires a golden image).</p>
<p id="shrinkOptionWrap" class="backup-deploy-hint" style="display:none; margin-top:0.5rem;">
<label><input type="checkbox" id="shrinkAfterBackup" /> Shrink after backup</label> <span class="backups-mono" style="font-size:0.8rem;">(reduces size; requires PiShrink on host)</span>
</p>
<div id="pendingDevices"></div>
<div id="noPendingPlaceholder" class="placeholder-actions" style="display:none;">
<span>Connect a device to see:</span>
@@ -506,12 +509,16 @@
container.innerHTML = '';
let hasAny = false;
const shrinkWrap = document.getElementById('shrinkOptionWrap');
if (usb) {
hasAny = true;
if (shrinkWrap) shrinkWrap.style.display = 'block';
const el = document.createElement('div');
el.className = 'device-item';
el.innerHTML = '<div class="device-info"><div class="device-type">USB boot</div><div class="device-desc">Device connected — choose Backup or Deploy</div></div><div class="device-actions"><button type="button" class="btn btn-outline" data-source="usb" data-action="backup">Backup</button><button type="button" class="btn btn-primary" data-source="usb" data-action="deploy">Deploy</button></div>';
container.appendChild(el);
} else {
if (shrinkWrap) shrinkWrap.style.display = 'none';
}
(network || []).forEach(function(d) {
hasAny = true;
@@ -532,6 +539,8 @@
const mac = btn.getAttribute('data-mac');
const body = { source: source, action: action };
if (mac) body.mac = mac;
const shrinkCb = document.getElementById('shrinkAfterBackup');
if (action === 'backup' && shrinkCb && shrinkCb.checked) body.shrink = true;
fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
.then(function(r) { return r.json(); })
.then(function(data) {
@@ -557,12 +566,15 @@
tr.dataset.name = b.name;
const displayName = (b.display_name || b.name);
const desc = (b.description || '');
const isRawImg = b.name.endsWith('.img') && !b.name.endsWith('.img.gz') && !b.name.endsWith('.img.xz');
const shrinkBtn = isRawImg ? '<button type="button" class="btn btn-outline btn-sm shrink-btn" data-name="' + escapeHtml(b.name) + '" title="Shrink image (PiShrink)">Shrink</button> ' : '';
tr.innerHTML =
'<td class="backup-name-edit" data-field="name" title="Click to rename">' + escapeHtml(displayName) + '</td>' +
'<td class="backup-desc-edit" data-field="description" title="Click to add description">' + escapeHtml(desc) + '</td>' +
'<td class="backups-mono">' + fmtSize(b.size) + '</td>' +
'<td class="backups-mono">' + fmtDate(b.mtime) + '</td>' +
'<td class="actions-cell">' +
shrinkBtn +
'<button type="button" class="btn btn-primary btn-sm set-golden-btn" data-name="' + escapeHtml(b.name) + '">Set as golden</button> ' +
'<button type="button" class="btn btn-outline btn-sm rename-file-btn" data-name="' + escapeHtml(b.name) + '" title="Rename file">Rename file</button> ' +
'<a href="/api/backups/' + encodeURIComponent(b.name) + '" download class="btn btn-outline btn-sm download-link">Download</a>' +
@@ -572,6 +584,7 @@
bindBackupEdits();
bindSetGolden();
bindRenameFile();
bindShrink();
}
function bindRenameFile() {
@@ -648,6 +661,25 @@
});
}
function bindShrink() {
document.querySelectorAll('.shrink-btn').forEach(function(btn) {
btn.onclick = function() {
const name = btn.getAttribute('data-name');
if (!confirm('Shrink this image with PiShrink? This reduces file size and may take a few minutes.\n\n' + name)) return;
btn.disabled = true;
btn.textContent = 'Shrinking…';
fetch('/api/backups/' + encodeURIComponent(name) + '/shrink', { method: 'POST' })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) { fetchBackups(); }
else alert(data.error || data.detail || 'Failed');
})
.catch(function() { alert('Request failed'); })
.finally(function() { btn.disabled = false; btn.textContent = 'Shrink'; });
};
});
}
function fetchGoldenInfo() {
fetch('/api/golden-info').then(function(r) { return r.json(); }).then(function(d) {
const el = document.getElementById('goldenHint');