Enhance eMMC provisioning dashboard: add backup metadata management, implement backup renaming and setting golden image functionality, and improve UI for backup actions and descriptions.
This commit is contained in:
@@ -233,6 +233,50 @@
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.backup-name-edit, .backup-desc-edit {
|
||||
cursor: text;
|
||||
padding: 0.2rem 0.35rem;
|
||||
margin: -0.2rem -0.35rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.backup-name-edit:hover, .backup-desc-edit:hover { background: var(--bg-tertiary); }
|
||||
.backup-name-edit input, .backup-desc-edit input, .backup-desc-edit textarea {
|
||||
width: 100%;
|
||||
min-width: 120px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--accent);
|
||||
color: var(--text);
|
||||
padding: 0.25rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font: inherit;
|
||||
}
|
||||
.backup-desc-edit textarea { min-height: 2em; resize: vertical; }
|
||||
.backups-table .actions-cell { white-space: nowrap; }
|
||||
.backups-table .btn-sm { padding: 0.35rem 0.6rem; font-size: 0.8rem; }
|
||||
.golden-badge { font-size: 0.7rem; color: var(--accent); font-weight: 600; }
|
||||
.backups-table a.download-link { margin-right: 0.5rem; }
|
||||
.backup-deploy-hint {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--radius-sm);
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
.placeholder-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.placeholder-actions .btns { display: flex; gap: 0.5rem; }
|
||||
.placeholder-actions .btn { opacity: 0.5; pointer-events: none; }
|
||||
|
||||
/* ----- Help & Log (collapsible) ----- */
|
||||
details {
|
||||
@@ -330,27 +374,39 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Devices waiting for action -->
|
||||
<!-- 2. Capture (Backup) or Deploy -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">Devices waiting for action</h2>
|
||||
<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>
|
||||
<div id="pendingDevices"></div>
|
||||
<p id="noPending" class="empty-msg" style="display:none;">No devices. Connect reTerminal in USB boot mode or register over network.</p>
|
||||
<div id="noPendingPlaceholder" class="placeholder-actions" style="display:none;">
|
||||
<span>Connect a device to see:</span>
|
||||
<span class="btns">
|
||||
<button type="button" class="btn btn-outline btn-sm" disabled>Backup</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" disabled>Deploy</button>
|
||||
</span>
|
||||
<span>— USB boot mode or network registration</span>
|
||||
</div>
|
||||
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Connect reTerminal in USB boot mode (eMMC disable jumper + USB to host) or register a network-booted device — then the <strong>Backup</strong> and <strong>Deploy</strong> buttons will appear above.</p>
|
||||
</section>
|
||||
|
||||
<!-- 3. Saved backups -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">Saved backups</h2>
|
||||
<p id="goldenHint" class="backups-mono" style="margin-bottom:0.75rem;font-size:0.8rem;"></p>
|
||||
<table class="backups-table" id="backupsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Size</th>
|
||||
<th>Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="backupsBody"></tbody>
|
||||
</table>
|
||||
<p id="backupsEmpty" class="empty-msg" style="display:none;">No backups yet.</p>
|
||||
<p id="backupsEmpty" class="empty-msg" style="display:none;">No backups yet. Capture one from a device (Backup above), then set it as golden for future deploys.</p>
|
||||
</section>
|
||||
|
||||
<!-- 4. How to connect (collapsible) -->
|
||||
@@ -455,7 +511,9 @@
|
||||
container.appendChild(el);
|
||||
});
|
||||
|
||||
const placeholder = document.getElementById('noPendingPlaceholder');
|
||||
noPending.style.display = hasAny ? 'none' : 'block';
|
||||
if (placeholder) placeholder.style.display = hasAny ? 'none' : 'flex';
|
||||
|
||||
container.querySelectorAll('button[data-action]').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
@@ -486,9 +544,105 @@
|
||||
empty.style.display = 'none';
|
||||
backups.forEach(function(b) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td><a href="/api/backups/' + encodeURIComponent(b.name) + '" download>' + escapeHtml(b.name) + '</a></td><td class="backups-mono">' + fmtSize(b.size) + '</td><td class="backups-mono">' + fmtDate(b.mtime) + '</td>';
|
||||
tr.dataset.name = b.name;
|
||||
const displayName = (b.display_name || b.name);
|
||||
const desc = (b.description || '');
|
||||
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">' +
|
||||
'<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>' +
|
||||
'</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
bindBackupEdits();
|
||||
bindSetGolden();
|
||||
bindRenameFile();
|
||||
}
|
||||
|
||||
function bindRenameFile() {
|
||||
document.querySelectorAll('.rename-file-btn').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
const name = btn.getAttribute('data-name');
|
||||
const newName = prompt('New filename (e.g. production-v1.img)', name);
|
||||
if (newName == null || newName.trim() === '') return;
|
||||
const n = newName.trim();
|
||||
if (!/\.(img|img\.gz)$/i.test(n)) { alert('Filename must end with .img or .img.gz'); return; }
|
||||
if (n === name) return;
|
||||
fetch('/api/backups/' + encodeURIComponent(name), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: n }) })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) fetchBackups();
|
||||
else alert(data.error || 'Failed');
|
||||
})
|
||||
.catch(function() { alert('Request failed'); });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function bindBackupEdits() {
|
||||
document.querySelectorAll('.backup-name-edit[data-field], .backup-desc-edit[data-field]').forEach(function(cell) {
|
||||
if (cell._bound) return;
|
||||
cell._bound = true;
|
||||
cell.addEventListener('click', function() {
|
||||
if (cell.querySelector('input, textarea')) return;
|
||||
const field = cell.getAttribute('data-field');
|
||||
const row = cell.closest('tr');
|
||||
const filename = row.dataset.name;
|
||||
const isDesc = field === 'description';
|
||||
const current = cell.textContent.trim();
|
||||
const input = document.createElement(isDesc ? 'textarea' : 'input');
|
||||
input.type = isDesc ? 'text' : 'text';
|
||||
input.value = current;
|
||||
input.placeholder = isDesc ? 'Add a description…' : 'Name';
|
||||
cell.textContent = '';
|
||||
cell.appendChild(input);
|
||||
input.focus();
|
||||
if (isDesc) input.rows = 2;
|
||||
function save() {
|
||||
const val = input.value.trim();
|
||||
const body = isDesc ? { description: val } : { name: val || filename };
|
||||
fetch('/api/backups/' + encodeURIComponent(filename), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) { fetchBackups(); }
|
||||
else { alert(data.error || 'Failed'); cell.innerHTML = escapeHtml(current); }
|
||||
})
|
||||
.catch(function() { cell.innerHTML = escapeHtml(current); });
|
||||
}
|
||||
input.addEventListener('blur', save);
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' && !isDesc) { e.preventDefault(); input.blur(); }
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindSetGolden() {
|
||||
document.querySelectorAll('.set-golden-btn').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
const name = btn.getAttribute('data-name');
|
||||
if (!confirm('Set this backup as the golden image? Future deploys will use it.\n\n' + name)) return;
|
||||
fetch('/api/backups/' + encodeURIComponent(name) + '/set-as-golden', { method: 'POST' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) { fetchBackups(); fetchGoldenInfo(); }
|
||||
else alert(data.error || 'Failed');
|
||||
})
|
||||
.catch(function() { alert('Request failed'); });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function fetchGoldenInfo() {
|
||||
fetch('/api/golden-info').then(function(r) { return r.json(); }).then(function(d) {
|
||||
const el = document.getElementById('goldenHint');
|
||||
el.textContent = d.present ? ('Golden image: ' + fmtSize(d.size) + ', updated ' + fmtDate(d.mtime)) : 'No golden image set. Capture a backup and click "Set as golden" to use it for Deploy.';
|
||||
}).catch(function() {});
|
||||
}
|
||||
|
||||
function fetchStatus() {
|
||||
@@ -520,10 +674,12 @@
|
||||
fetchLog();
|
||||
fetchPending();
|
||||
fetchBackups();
|
||||
fetchGoldenInfo();
|
||||
setInterval(fetchStatus, 2000);
|
||||
setInterval(fetchLog, 4000);
|
||||
setInterval(fetchPending, 2000);
|
||||
setInterval(fetchBackups, 5000);
|
||||
setInterval(fetchGoldenInfo, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user