Remove obsolete audio and buzzer control documentation files, including detailed guides and HTML interfaces, to streamline the repository and eliminate redundancy. This cleanup enhances maintainability and focuses on essential resources for the reTerminal DM4 audio and buzzer functionalities.
This commit is contained in:
224
emmc-provisioning/dashboard/templates/home.html
Normal file
224
emmc-provisioning/dashboard/templates/home.html
Normal file
@@ -0,0 +1,224 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Deploy · CM4 Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #0a0e14;
|
||||
--bg-secondary: #11151c;
|
||||
--bg-card: #151a24;
|
||||
--accent: #00d4aa;
|
||||
--accent-dim: #00b894;
|
||||
--text: #e6e8eb;
|
||||
--text-dim: #8b949e;
|
||||
--text-muted: #5c6370;
|
||||
--border: #2d333b;
|
||||
--danger: #f87171;
|
||||
--warn: #fbbf24;
|
||||
--success: #00d4aa;
|
||||
--radius: 10px;
|
||||
--radius-sm: 6px;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Outfit', sans-serif; background: var(--bg-primary); color: var(--text); min-height: 100vh; font-size: 14px; line-height: 1.45; }
|
||||
.wrap { max-width: 1100px; margin: 0 auto; padding: 1rem; }
|
||||
.header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.75rem; margin-bottom: 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid var(--border); }
|
||||
.header h1 { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.02em; }
|
||||
.header a { color: var(--text-dim); text-decoration: none; font-size: 0.9rem; }
|
||||
.header a:hover { color: var(--accent); }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
@media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem;
|
||||
}
|
||||
.card-title { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); margin-bottom: 0.6rem; }
|
||||
.status-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
|
||||
.status-pill { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; padding: 0.25rem 0.6rem; border-radius: 999px; }
|
||||
.status-pill.idle { background: rgba(0,212,170,0.15); color: var(--accent); }
|
||||
.status-pill.waiting_choice, .status-pill.rpiboot, .status-pill.flashing, .status-pill.backup { background: rgba(251,191,36,0.15); color: var(--warn); }
|
||||
.status-pill.done { background: rgba(0,212,170,0.15); color: var(--success); }
|
||||
.status-pill.error { background: rgba(248,113,113,0.15); color: var(--danger); }
|
||||
.status-msg { flex: 1; min-width: 0; font-size: 0.9rem; }
|
||||
.status-err { width: 100%; margin-top: 0.4rem; font-size: 0.8rem; color: var(--danger); }
|
||||
.progress-track { margin-top: 0.5rem; height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: var(--accent); transition: width 0.3s; }
|
||||
.progress-fill.indeterminate { width: 35%; animation: slide 1.2s ease-in-out infinite; }
|
||||
@keyframes slide { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
|
||||
.device-item {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
|
||||
padding: 0.6rem 0.75rem; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.device-desc { font-size: 0.85rem; color: var(--text-dim); }
|
||||
.btn { padding: 0.4rem 0.8rem; font-size: 0.8rem; font-weight: 500; font-family: inherit; border-radius: var(--radius-sm); cursor: pointer; border: none; transition: background 0.15s, color 0.15s; }
|
||||
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
||||
.btn-outline:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.btn-primary { background: var(--accent); color: var(--bg-primary); }
|
||||
.btn-primary:hover { background: var(--accent-dim); }
|
||||
.btn-sm { padding: 0.3rem 0.5rem; font-size: 0.75rem; }
|
||||
.golden-info { font-size: 0.85rem; color: var(--text-dim); }
|
||||
.golden-info strong { color: var(--text); }
|
||||
.empty-msg { text-align: center; padding: 1rem; font-size: 0.85rem; color: var(--text-muted); }
|
||||
details { margin-top: 0.5rem; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
|
||||
summary { padding: 0.5rem 0.75rem; font-size: 0.85rem; font-weight: 500; color: var(--text-dim); cursor: pointer; list-style: none; }
|
||||
summary::-webkit-details-marker { display: none; }
|
||||
summary::before { content: '▶'; display: inline-block; margin-right: 0.4rem; font-size: 0.6rem; transition: transform 0.2s; }
|
||||
details[open] summary::before { transform: rotate(90deg); }
|
||||
details .inner { padding: 0.75rem; padding-top: 0; font-size: 0.8rem; color: var(--text-dim); border-top: 1px solid var(--border); }
|
||||
.log-pre { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; background: var(--bg-secondary); border-radius: var(--radius-sm); padding: 0.5rem 0.75rem; max-height: 160px; overflow: auto; white-space: pre-wrap; word-break: break-all; color: var(--text-muted); }
|
||||
.steps-list { list-style: none; }
|
||||
.steps-list li { display: flex; gap: 0.5rem; padding: 0.3rem 0; }
|
||||
.steps-list .num { flex-shrink: 0; width: 1.2rem; height: 1.2rem; background: var(--bg-secondary); color: var(--text-dim); border-radius: 50%; font-size: 0.7rem; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.steps-list strong { color: var(--text); }
|
||||
.help-sub { font-weight: 600; color: var(--text); margin: 0.5rem 0 0.2rem 0; font-size: 0.85rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="header">
|
||||
<h1>CM4 eMMC Provisioning</h1>
|
||||
<a href="/admin">Admin</a>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Left: Status + Deploy -->
|
||||
<div>
|
||||
<div class="card" style="margin-bottom: 1rem;">
|
||||
<h2 class="card-title">Status</h2>
|
||||
<div id="status" class="status-row">
|
||||
<span id="statusPill" class="status-pill idle">Idle</span>
|
||||
<span id="statusMsg" class="status-msg">Waiting for device</span>
|
||||
</div>
|
||||
<div id="statusErr" class="status-err" style="display:none;"></div>
|
||||
<div id="statusClearWrap" style="display:none; margin-top:0.5rem;"><button type="button" id="statusClearBtn" class="btn btn-outline btn-sm">Clear message</button></div>
|
||||
<div id="progressWrap" class="progress-track" style="display:none;"><div id="progressFill" class="progress-fill"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Capture or deploy</h2>
|
||||
<p id="shrinkOptionWrap" style="display:none; margin-bottom: 0.5rem; font-size: 0.8rem;"><label><input type="checkbox" id="shrinkAfterBackup" /> Shrink after backup</label></p>
|
||||
<div id="pendingDevices"></div>
|
||||
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Use USB boot mode or register over network.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Golden + Log + Connect -->
|
||||
<div>
|
||||
<div class="card" style="margin-bottom: 1rem;">
|
||||
<h2 class="card-title">Current deploy image</h2>
|
||||
<p id="goldenInfo" class="golden-info">Loading…</p>
|
||||
</div>
|
||||
|
||||
<details class="card" style="padding: 0;">
|
||||
<summary>Recent log</summary>
|
||||
<div class="inner"><pre id="log" class="log-pre"></pre></div>
|
||||
</details>
|
||||
|
||||
<details class="card" style="padding: 0; margin-top: 1rem;">
|
||||
<summary>How to connect</summary>
|
||||
<div class="inner">
|
||||
<p class="help-sub">USB boot</p>
|
||||
<ol class="steps-list">
|
||||
<li><span class="num">1</span> Set reTerminal to <strong>boot mode</strong> (eMMC disable jumper).</li>
|
||||
<li><span class="num">2</span> Connect USB to host; choose <strong>Backup</strong> or <strong>Deploy</strong> above.</li>
|
||||
<li><span class="num">3</span> Remove jumper and power cycle when done.</li>
|
||||
</ol>
|
||||
<p class="help-sub">Network</p>
|
||||
<ol class="steps-list">
|
||||
<li><span class="num">1</span> Enable network boot; device must reach this server.</li>
|
||||
<li><span class="num">2</span> Boot with provisioning client; choose <strong>Backup</strong> or <strong>Deploy</strong>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const phaseLabels = { idle: 'Idle', rpiboot: 'Connecting', waiting_choice: 'Choose action', flashing: 'Flashing', backup: 'Backing up', done: 'Done', error: 'Error' };
|
||||
function renderStatus(data) {
|
||||
const phase = data.phase || 'idle';
|
||||
document.getElementById('statusPill').className = 'status-pill ' + phase;
|
||||
document.getElementById('statusPill').textContent = phaseLabels[phase] || phase;
|
||||
document.getElementById('statusMsg').textContent = data.message || '';
|
||||
const err = document.getElementById('statusErr');
|
||||
if (data.error) { err.textContent = data.error; err.style.display = 'block'; } else { err.style.display = 'none'; }
|
||||
var clearWrap = document.getElementById('statusClearWrap');
|
||||
if (clearWrap) clearWrap.style.display = (phase === 'error' ? 'block' : 'none');
|
||||
const progress = data.progress;
|
||||
const inProgress = ['rpiboot', 'flashing', 'backup'].includes(phase);
|
||||
const wrap = document.getElementById('progressWrap');
|
||||
const fill = document.getElementById('progressFill');
|
||||
if (inProgress || (phase === 'done' && progress != null)) {
|
||||
wrap.style.display = 'block';
|
||||
fill.classList.remove('indeterminate');
|
||||
fill.style.width = (progress != null ? progress + '%' : '35%');
|
||||
if (progress == null) fill.classList.add('indeterminate');
|
||||
} else wrap.style.display = 'none';
|
||||
}
|
||||
function renderPending(usb, network) {
|
||||
const container = document.getElementById('pendingDevices');
|
||||
const noPending = document.getElementById('noPending');
|
||||
container.innerHTML = '';
|
||||
let hasAny = false;
|
||||
const shrinkWrap = document.getElementById('shrinkOptionWrap');
|
||||
if (usb) {
|
||||
hasAny = true;
|
||||
shrinkWrap.style.display = 'block';
|
||||
const el = document.createElement('div');
|
||||
el.className = 'device-item';
|
||||
el.innerHTML = '<div class="device-desc">USB device — choose action</div><div><button type="button" class="btn btn-outline btn-sm" data-source="usb" data-action="backup">Backup</button> <button type="button" class="btn btn-primary btn-sm" data-source="usb" data-action="deploy">Deploy</button></div>';
|
||||
container.appendChild(el);
|
||||
} else shrinkWrap.style.display = 'none';
|
||||
(network || []).forEach(function(d) {
|
||||
hasAny = true;
|
||||
const el = document.createElement('div');
|
||||
el.className = 'device-item';
|
||||
el.innerHTML = '<div class="device-desc">' + escapeHtml(d.ip || '') + ' · ' + escapeHtml(d.mac || '') + '</div><div><button type="button" class="btn btn-outline btn-sm" data-source="network" data-mac="' + escapeHtml(d.mac || '') + '" data-action="backup">Backup</button> <button type="button" class="btn btn-primary btn-sm" data-source="network" data-mac="' + escapeHtml(d.mac || '') + '" data-action="deploy">Deploy</button></div>';
|
||||
container.appendChild(el);
|
||||
});
|
||||
noPending.style.display = hasAny ? 'none' : 'block';
|
||||
container.querySelectorAll('button[data-action]').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
const body = { source: btn.getAttribute('data-source'), action: btn.getAttribute('data-action') };
|
||||
if (body.source === 'network') body.mac = btn.getAttribute('data-mac');
|
||||
if (body.action === 'backup' && document.getElementById('shrinkAfterBackup').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) { if (data.ok) { fetchPending(); fetchStatus(); } else alert(data.error || 'Failed'); })
|
||||
.catch(function() { alert('Request failed'); });
|
||||
};
|
||||
});
|
||||
}
|
||||
function escapeHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
function fmtSize(n) { if (n >= 1e9) return (n/1e9).toFixed(1)+' GB'; if (n >= 1e6) return (n/1e6).toFixed(1)+' MB'; return (n/1e3).toFixed(0)+' KB'; }
|
||||
function fmtDate(ts) { return new Date(ts*1000).toLocaleString(); }
|
||||
function fetchStatus() { fetch('/api/status').then(function(r){ return r.json(); }).then(renderStatus).catch(function(){ renderStatus({ phase: 'error', message: 'Could not load status.' }); }); }
|
||||
function fetchPending() { fetch('/api/pending-devices').then(function(r){ return r.json(); }).then(function(d){ renderPending(d.usb || null, d.network || []); }).catch(function(){ renderPending(null, []); }); }
|
||||
function fetchLog() { fetch('/api/log').then(function(r){ return r.json(); }).then(function(d){ document.getElementById('log').textContent = d.log || ''; }).catch(function(){}); }
|
||||
function fetchGolden() {
|
||||
fetch('/api/golden-info').then(function(r){ return r.json(); }).then(function(d){
|
||||
const el = document.getElementById('goldenInfo');
|
||||
if (!d.present) { el.innerHTML = 'No image set. An admin must set a backup or cloud-init image as golden for Deploy.'; return; }
|
||||
let t = 'Ready for deploy: ' + fmtSize(d.size) + ', updated ' + fmtDate(d.mtime);
|
||||
if (d.name) t += ' <br><strong>' + (d.source === 'cloudinit' ? 'Cloud-init' : 'Backup') + ':</strong> ' + escapeHtml(d.name);
|
||||
el.innerHTML = t;
|
||||
}).catch(function(){ document.getElementById('goldenInfo').textContent = 'Could not load.'; });
|
||||
}
|
||||
document.getElementById('statusClearBtn').addEventListener('click', function(){ fetch('/api/status-clear', { method: 'POST' }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) fetchStatus(); }); });
|
||||
fetchStatus(); fetchLog(); fetchPending(); fetchGolden();
|
||||
setInterval(fetchStatus, 2000);
|
||||
setInterval(fetchLog, 4000);
|
||||
setInterval(fetchPending, 2000);
|
||||
setInterval(fetchGolden, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user