Enhance cloud-init scripts and dashboard for improved USB boot functionality</message>

<message>Update the bootstrap script to ensure hostname resolution by adding entries to /etc/hosts, preventing "sudo: unable to resolve host" errors. Modify user-data.bootstrap to include the same hostname resolution logic. Revise dashboard templates to reflect the new project name "GNSS Guard Provisioning" and improve user interface elements related to USB boot operations, including clearer instructions and status messages. These changes enhance the overall user experience and streamline the provisioning process.
This commit is contained in:
nearxos
2026-02-24 08:50:32 +02:00
parent 59f8ebe61d
commit 1501c17ab9
11 changed files with 111 additions and 40 deletions

View File

@@ -1,4 +1,9 @@
#!/bin/bash #!/bin/bash
# Minimal bootstrap script for cloud-init first boot (test). # Minimal bootstrap script for cloud-init first boot (test).
set -e set -e
# Ensure hostname resolves (avoids "sudo: unable to resolve host")
H="$(hostname)"
grep -q "127.0.1.1.*$H" /etc/hosts || echo "127.0.1.1 $H" >> /etc/hosts
echo "[$(date -Iseconds)] test completed" | tee -a /var/log/cloud-init-bootstrap.log echo "[$(date -Iseconds)] test completed" | tee -a /var/log/cloud-init-bootstrap.log

View File

@@ -20,6 +20,10 @@ write_files:
PermitRootLogin no PermitRootLogin no
runcmd: runcmd:
# Ensure hostname resolves (avoids "sudo: unable to resolve host" when meta-data sets hostname)
- |
H="$(hostname)"
grep -q "127.0.1.1.*$H" /etc/hosts || echo "127.0.1.1 $H" >> /etc/hosts
- systemctl enable ssh - systemctl enable ssh
- systemctl start ssh - systemctl start ssh
# Download and run bootstrap script (edit URL to match your file server) # Download and run bootstrap script (edit URL to match your file server)

View File

@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin · CM4 Provisioning</title> <title>Admin · GNSS Guard Provisioning</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -76,6 +76,23 @@
<p id="goldenInfo" class="mono" style="font-size:0.9rem;">Loading…</p> <p id="goldenInfo" class="mono" style="font-size:0.9rem;">Loading…</p>
</div> </div>
<!-- Update boot (EEPROM) -->
<div class="section" id="adminUpdateBootSection">
<h2 class="section-title">Update boot (EEPROM)</h2>
<p id="adminUpdateBootHint" class="mono" style="font-size:0.85rem; color:var(--text-muted);">When a device is connected in USB boot mode, it appears here. Use this to write EEPROM boot order (e.g. eMMC only) to the device.</p>
<div id="adminUpdateBootDevice" style="display:none; margin-top:0.5rem;">
<p class="mono" style="font-size:0.9rem; margin-bottom:0.5rem;">Device in USB boot mode</p>
<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap;">
<label>Boot order:</label>
<select id="adminEepromPreset" class="eeprom-preset" title="Boot order">
<option value="0x1">eMMC only</option>
</select>
<button type="button" class="btn btn-outline btn-sm" id="adminUpdateEepromBtn">Update EEPROM</button>
</div>
</div>
<p id="adminUpdateBootNone" class="mono" style="font-size:0.85rem; color:var(--text-muted); display:none;">No device in USB boot mode. Connect a device with eMMC disable jumper and USB to host.</p>
</div>
<!-- Backups --> <!-- Backups -->
<div class="section"> <div class="section">
<h2 class="section-title"> <h2 class="section-title">
@@ -247,6 +264,47 @@
authFetch('/api/admin/users', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({username: username, password: password}) }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) { document.getElementById('addUserForm').style.display = 'none'; document.getElementById('newUsername').value = ''; document.getElementById('newPassword').value = ''; fetchUsers(); } else alert(d.error); }); authFetch('/api/admin/users', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({username: username, password: password}) }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) { document.getElementById('addUserForm').style.display = 'none'; document.getElementById('newUsername').value = ''; document.getElementById('newPassword').value = ''; fetchUsers(); } else alert(d.error); });
}; };
function fetchPendingDevices() {
authFetch('/api/pending-devices').then(function(r){ return r.json(); }).then(function(d){
var devEl = document.getElementById('adminUpdateBootDevice');
var noneEl = document.getElementById('adminUpdateBootNone');
if (d.usb) {
if (devEl) devEl.style.display = 'block';
if (noneEl) noneEl.style.display = 'none';
} else {
if (devEl) devEl.style.display = 'none';
if (noneEl) noneEl.style.display = 'block';
}
}).catch(function(){});
}
function fetchEepromPresets() {
authFetch('/api/eeprom-presets').then(function(r){ return r.json(); }).then(function(d){
var sel = document.getElementById('adminEepromPreset');
if (!sel) return;
sel.innerHTML = '';
(d.presets || []).forEach(function(p){
var opt = document.createElement('option');
opt.value = p.id || p.value;
opt.textContent = p.label || p.id || p.value;
sel.appendChild(opt);
});
}).catch(function(){});
}
fetchPendingDevices();
fetchEepromPresets();
var adminUpdateEepromBtn = document.getElementById('adminUpdateEepromBtn');
if (adminUpdateEepromBtn) {
adminUpdateEepromBtn.onclick = function(){
var presetEl = document.getElementById('adminEepromPreset');
var bootOrder = (presetEl && presetEl.value) ? presetEl.value : '0x1';
authFetch('/api/device-action', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ source: 'usb', action: 'eeprom_update', boot_order: bootOrder }) })
.then(function(r){ return r.json(); })
.then(function(d){ if (d.ok) { fetchPendingDevices(); alert('Update EEPROM sent. The host will write to the device.'); } else alert(d.error || 'Failed'); })
.catch(function(){ alert('Request failed'); });
};
}
setInterval(fetchPendingDevices, 3000);
fetchBackups(); fetchCloudinit(); fetchUsers(); fetchLogs(); fetchGolden(); fetchBackups(); fetchCloudinit(); fetchUsers(); fetchLogs(); fetchGolden();
setInterval(fetchLogs, 30000); setInterval(fetchLogs, 30000);
</script> </script>

View File

@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cloud-init build · Admin · CM4 Provisioning</title> <title>Cloud-init build · Admin · GNSS Guard Provisioning</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">

View File

@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Deploy · CM4 Provisioning</title> <title>Deploy · GNSS Guard Provisioning</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -96,7 +96,7 @@
<body> <body>
<div class="wrap"> <div class="wrap">
<header class="header"> <header class="header">
<h1>CM4 eMMC Provisioning</h1> <h1>GNSS Guard Provisioning</h1>
<a href="/admin">Admin</a> <a href="/admin">Admin</a>
</header> </header>
@@ -107,7 +107,7 @@
<h2 class="card-title">Status</h2> <h2 class="card-title">Status</h2>
<div id="status" class="status-row"> <div id="status" class="status-row">
<span id="statusPill" class="status-pill idle">Idle</span> <span id="statusPill" class="status-pill idle">Idle</span>
<span id="statusMsg" class="status-msg">Waiting for device</span> <span id="statusMsg" class="status-msg">Waiting for Device in USB boot mode.</span>
<button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button> <button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button>
</div> </div>
<div id="statusErr" class="status-err" style="display:none;"></div> <div id="statusErr" class="status-err" style="display:none;"></div>
@@ -128,7 +128,7 @@
</div> </div>
<div class="card"> <div class="card">
<h2 class="card-title">Capture or deploy</h2> <h2 class="card-title">Deploy and Backup</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> <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> <div id="pendingDevices"></div>
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Use USB boot mode.</p> <p id="noPending" class="empty-msg" style="display:none;">No device connected. Use USB boot mode.</p>
@@ -142,12 +142,12 @@
<p id="goldenInfo" class="golden-info">Loading…</p> <p id="goldenInfo" class="golden-info">Loading…</p>
</div> </div>
<details class="card" style="padding: 0;"> <details class="card" style="padding: 0;" open>
<summary>Recent log</summary> <summary>Recent log</summary>
<div class="inner"><pre id="log" class="log-pre"></pre></div> <div class="inner"><pre id="log" class="log-pre"></pre></div>
</details> </details>
<details class="card" style="padding: 0; margin-top: 1rem;"> <details class="card" style="padding: 0; margin-top: 1rem;" open>
<summary>DHCP leases</summary> <summary>DHCP leases</summary>
<div class="inner"> <div class="inner">
<p id="leasesNote" class="help-sub" style="margin-top:0;">Provisioning LAN (dnsmasq) — when dashboard runs on LXC.</p> <p id="leasesNote" class="help-sub" style="margin-top:0;">Provisioning LAN (dnsmasq) — when dashboard runs on LXC.</p>
@@ -162,7 +162,7 @@
<div class="inner"> <div class="inner">
<p class="help-sub">USB boot</p> <p class="help-sub">USB boot</p>
<ol class="steps-list"> <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">1</span> Set device 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">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> <li><span class="num">3</span> Remove jumper and power cycle when done.</li>
</ol> </ol>
@@ -178,7 +178,7 @@
const phase = data.phase || 'idle'; const phase = data.phase || 'idle';
document.getElementById('statusPill').className = 'status-pill ' + phase; document.getElementById('statusPill').className = 'status-pill ' + phase;
document.getElementById('statusPill').textContent = phaseLabels[phase] || phase; document.getElementById('statusPill').textContent = phaseLabels[phase] || phase;
document.getElementById('statusMsg').textContent = data.message || ''; document.getElementById('statusMsg').textContent = data.message || (phase === 'idle' ? 'Waiting for Device in USB boot mode.' : '');
const err = document.getElementById('statusErr'); const err = document.getElementById('statusErr');
if (data.error) { err.textContent = data.error; err.style.display = 'block'; } else { err.style.display = 'none'; } if (data.error) { err.textContent = data.error; err.style.display = 'block'; } else { err.style.display = 'none'; }
if (phase === 'done') scheduleDoneClear(); if (phase === 'done') scheduleDoneClear();
@@ -204,7 +204,7 @@
shrinkWrap.style.display = 'block'; shrinkWrap.style.display = 'block';
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'device-item'; el.className = 'device-item';
el.innerHTML = '<div class="device-desc">USB device — choose Backup, Deploy, or Update EEPROM</div><div class="device-actions-row"><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> <select class="eeprom-preset" title="Boot order"><option value="0x1">eMMC only</option></select> <button type="button" class="btn btn-outline btn-sm" data-source="usb" data-action="eeprom_update">Update EEPROM</button></div>'; el.innerHTML = '<div class="device-desc">USB device — choose Backup or Deploy</div><div class="device-actions-row"><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); container.appendChild(el);
} else shrinkWrap.style.display = 'none'; } else shrinkWrap.style.display = 'none';
noPending.style.display = hasAny ? 'none' : 'block'; noPending.style.display = hasAny ? 'none' : 'block';
@@ -212,10 +212,6 @@
btn.onclick = function() { btn.onclick = function() {
const body = { source: btn.getAttribute('data-source'), action: btn.getAttribute('data-action') }; const body = { source: btn.getAttribute('data-source'), action: btn.getAttribute('data-action') };
if (body.source === 'network') body.mac = btn.getAttribute('data-mac'); if (body.source === 'network') body.mac = btn.getAttribute('data-mac');
if (body.action === 'eeprom_update' && body.source === 'usb') {
const presetEl = btn.closest('.device-item') && btn.closest('.device-item').querySelector('.eeprom-preset');
body.boot_order = (presetEl && presetEl.value) ? presetEl.value : '0x1';
}
if (body.action === 'backup' && document.getElementById('shrinkAfterBackup').checked) body.shrink = true; 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) }) fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
@@ -267,7 +263,14 @@
fetch('/api/first-boot-status').then(function(r){ return r.json(); }).then(renderFirstBootStatus).catch(function(){}); fetch('/api/first-boot-status').then(function(r){ return r.json(); }).then(renderFirstBootStatus).catch(function(){});
} }
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 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 fetchLog() {
fetch('/api/log').then(function(r){ return r.json(); }).then(function(d){
var logEl = document.getElementById('log');
if (!logEl) return;
logEl.textContent = d.log || '';
logEl.scrollTop = logEl.scrollHeight;
}).catch(function(){});
}
function fetchGolden() { function fetchGolden() {
fetch('/api/golden-info').then(function(r){ return r.json(); }).then(function(d){ fetch('/api/golden-info').then(function(r){ return r.json(); }).then(function(d){
const el = document.getElementById('goldenInfo'); const el = document.getElementById('goldenInfo');

View File

@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>CM4 eMMC Provisioning</title> <title>GNSS Guard Provisioning</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -359,8 +359,8 @@
<body> <body>
<div class="wrap"> <div class="wrap">
<header class="header"> <header class="header">
<h1>CM4 eMMC Provisioning</h1> <h1>GNSS Guard Provisioning</h1>
<p>Deploy or backup reTerminal via USB boot mode</p> <p>Deploy or backup device via USB boot mode</p>
</header> </header>
<!-- 1. Current status --> <!-- 1. Current status -->
@@ -368,12 +368,12 @@
<h2 class="section-title">Current status</h2> <h2 class="section-title">Current status</h2>
<div id="status" class="status-row"> <div id="status" class="status-row">
<span id="statusPill" class="status-pill idle">Idle</span> <span id="statusPill" class="status-pill idle">Idle</span>
<span id="statusMsg" class="status-msg">Waiting for device</span> <span id="statusMsg" class="status-msg">Waiting for Device in USB boot mode.</span>
<button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button> <button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button>
</div> </div>
<div id="statusErr" class="status-err" style="display:none;"></div> <div id="statusErr" class="status-err" style="display:none;"></div>
<div id="statusGoldenHint" class="backup-deploy-hint" style="display:none; margin-top:0.75rem;"> <div id="statusGoldenHint" class="backup-deploy-hint" style="display:none; margin-top:0.75rem;">
No golden image is required to <strong>capture</strong>. Connect a device in USB boot mode; when it appears under “Capture image or deploy”, click <strong>Backup</strong> to save its image. Then set that backup as golden in the list below. No golden image is required to <strong>backup</strong>. Connect a device in USB boot mode; when it appears under “Deploy and Backup”, click <strong>Backup</strong> to save its image. Then set that backup as golden in the list below.
<button type="button" id="statusClearHintBtn" class="btn btn-outline btn-sm" style="margin-left:0.5rem;">Clear message</button> <button type="button" id="statusClearHintBtn" class="btn btn-outline btn-sm" style="margin-left:0.5rem;">Clear message</button>
</div> </div>
<div id="statusMeta" class="status-meta" style="display:none;"></div> <div id="statusMeta" class="status-meta" style="display:none;"></div>
@@ -384,8 +384,8 @@
<!-- 2. Capture (Backup) or Deploy --> <!-- 2. Capture (Backup) or Deploy -->
<section class="section"> <section class="section">
<h2 class="section-title">Capture image or deploy</h2> <h2 class="section-title">Deploy and Backup</h2>
<p class="backup-deploy-hint">To <strong>capture (backup)</strong> an image from a device: connect it in USB boot mode. 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 class="backup-deploy-hint">Connect a device in USB boot mode. When it appears below, click <strong>Backup</strong> to save its eMMC to a file, or <strong>Deploy</strong> to write the golden image to the device (requires a golden image set in Admin).</p>
<p id="shrinkOptionWrap" class="backup-deploy-hint" style="display:none; margin-top:0.5rem;"> <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> <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> </p>
@@ -398,7 +398,7 @@
</span> </span>
<span>— USB boot mode</span> <span>— USB boot mode</span>
</div> </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) — then the <strong>Backup</strong> and <strong>Deploy</strong> buttons will appear above.</p> <p id="noPending" class="empty-msg" style="display:none;">No device connected. Connect device in USB boot mode (eMMC disable jumper + USB to host) — then the <strong>Backup</strong> and <strong>Deploy</strong> buttons will appear above.</p>
</section> </section>
<!-- 3. Saved backups --> <!-- 3. Saved backups -->
@@ -477,7 +477,7 @@
<div class="inner"> <div class="inner">
<p class="help-sub">USB boot mode</p> <p class="help-sub">USB boot mode</p>
<ol class="steps-list"> <ol class="steps-list">
<li><span class="num">1</span> Set reTerminal to <strong>boot mode</strong> (eMMC disable jumper, e.g. J2 / nRPIBOOT).</li> <li><span class="num">1</span> Set device to <strong>boot mode</strong> (eMMC disable jumper, e.g. J2 / nRPIBOOT).</li>
<li><span class="num">2</span> Connect <strong>USB slave</strong> to the host and power on. The device appears above; choose <strong>Backup</strong> or <strong>Deploy</strong>.</li> <li><span class="num">2</span> Connect <strong>USB slave</strong> to the host and power on. The device appears above; choose <strong>Backup</strong> or <strong>Deploy</strong>.</li>
<li><span class="num">3</span> When done, remove the jumper and power cycle to boot from eMMC.</li> <li><span class="num">3</span> When done, remove the jumper and power cycle to boot from eMMC.</li>
</ol> </ol>
@@ -485,7 +485,7 @@
</details> </details>
<!-- 5. Recent log (collapsible) --> <!-- 5. Recent log (collapsible) -->
<details class="section" style="padding:0;"> <details class="section" style="padding:0;" open>
<summary>Recent log</summary> <summary>Recent log</summary>
<div class="inner"> <div class="inner">
<pre id="log" class="log-pre"></pre> <pre id="log" class="log-pre"></pre>
@@ -516,7 +516,7 @@
const phase = data.phase || 'idle'; const phase = data.phase || 'idle';
statusPill.className = 'status-pill ' + phase; statusPill.className = 'status-pill ' + phase;
statusPill.textContent = phaseLabels[phase] || phase; statusPill.textContent = phaseLabels[phase] || phase;
statusMsg.textContent = data.message || ''; statusMsg.textContent = data.message || (phase === 'idle' ? 'Waiting for Device in USB boot mode.' : '');
if (data.error) { if (data.error) {
statusErr.textContent = data.error; statusErr.textContent = data.error;
@@ -566,7 +566,7 @@
if (shrinkWrap) shrinkWrap.style.display = 'block'; if (shrinkWrap) shrinkWrap.style.display = 'block';
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'device-item'; 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, Deploy, or Update EEPROM</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><select class="eeprom-preset" title="Boot order"><option value="0x1">eMMC only</option></select><button type="button" class="btn btn-outline" data-source="usb" data-action="eeprom_update">Update EEPROM</button></div>'; 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); container.appendChild(el);
} else { } else {
if (shrinkWrap) shrinkWrap.style.display = 'none'; if (shrinkWrap) shrinkWrap.style.display = 'none';
@@ -583,10 +583,6 @@
const mac = btn.getAttribute('data-mac'); const mac = btn.getAttribute('data-mac');
const body = { source: source, action: action }; const body = { source: source, action: action };
if (mac) body.mac = mac; if (mac) body.mac = mac;
if (action === 'eeprom_update' && source === 'usb') {
const presetEl = btn.closest('.device-item') && btn.closest('.device-item').querySelector('.eeprom-preset');
body.boot_order = (presetEl && presetEl.value) ? presetEl.value : '0x1';
}
const shrinkCb = document.getElementById('shrinkAfterBackup'); const shrinkCb = document.getElementById('shrinkAfterBackup');
if (action === 'backup' && shrinkCb && shrinkCb.checked) body.shrink = true; if (action === 'backup' && shrinkCb && shrinkCb.checked) body.shrink = true;
fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
@@ -785,7 +781,12 @@
fetch('/api/pending-devices').then(function(r) { return r.json(); }).then(function(d) { renderPending(d.usb || null, d.network || []); }).catch(function() { renderPending(null, []); }); 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() { function fetchLog() {
fetch('/api/log').then(function(r) { return r.json(); }).then(function(d) { document.getElementById('log').textContent = d.log || ''; }).catch(function() {}); fetch('/api/log').then(function(r) { return r.json(); }).then(function(d) {
var logEl = document.getElementById('log');
if (!logEl) return;
logEl.textContent = d.log || '';
logEl.scrollTop = logEl.scrollHeight;
}).catch(function() {});
} }
function fetchBackups() { function fetchBackups() {
fetch('/api/backups').then(function(r) { return r.json(); }).then(function(d) { fetch('/api/backups').then(function(r) { return r.json(); }).then(function(d) {

View File

@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin login · CM4 Provisioning</title> <title>Admin login · GNSS Guard Provisioning</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -28,7 +28,7 @@
<body> <body>
<div class="box"> <div class="box">
<h1>Admin login</h1> <h1>Admin login</h1>
<p class="sub">CM4 eMMC provisioning dashboard</p> <p class="sub">GNSS Guard Provisioning admin</p>
{% if error %}<p class="err">{{ error }}</p>{% endif %} {% if error %}<p class="err">{{ error }}</p>{% endif %}
<form method="post" action="{{ url_for('login') }}"> <form method="post" action="{{ url_for('login') }}">
<label for="username">Username</label> <label for="username">Username</label>

View File

@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Portal files · Admin · CM4 Provisioning</title> <title>Portal files · Admin · GNSS Guard Provisioning</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">

View File

@@ -210,7 +210,7 @@ Or copy `scripts/monitor-from-host.sh` to the host and run `./monitor-from-host.
4. **"No 'bootcode' files found in mass-storage-gadget64"** Usually because `bootfiles.bin` is a **broken symlink** (e.g. `-> ../firmware/bootfiles.bin`) and that target doesnt exist. **Fix on host:** run `scripts/fix-gadget-bootcode-on-host.sh` on the host (it removes the symlink and extracts `bootcode4.bin` from the installed rpiboot binary). From your machine: `ssh root@10.130.60.224 'bash -s' < scripts/fix-gadget-bootcode-on-host.sh`. **Alternative:** repopulate the gadget dir with `./scripts/populate-gadget-on-host.sh root@10.130.60.224`, or full reinstall with `./scripts/build-and-deploy-usbboot-to-host.sh root@10.130.60.224`. Then verify: `ls -la /opt/usbboot/mass-storage-gadget64/` (should list a real `bootcode4.bin` or `bootfiles.bin`, plus `boot.img`, `config.txt`). 4. **"No 'bootcode' files found in mass-storage-gadget64"** Usually because `bootfiles.bin` is a **broken symlink** (e.g. `-> ../firmware/bootfiles.bin`) and that target doesnt exist. **Fix on host:** run `scripts/fix-gadget-bootcode-on-host.sh` on the host (it removes the symlink and extracts `bootcode4.bin` from the installed rpiboot binary). From your machine: `ssh root@10.130.60.224 'bash -s' < scripts/fix-gadget-bootcode-on-host.sh`. **Alternative:** repopulate the gadget dir with `./scripts/populate-gadget-on-host.sh root@10.130.60.224`, or full reinstall with `./scripts/build-and-deploy-usbboot-to-host.sh root@10.130.60.224`. Then verify: `ls -la /opt/usbboot/mass-storage-gadget64/` (should list a real `bootcode4.bin` or `bootfiles.bin`, plus `boot.img`, `config.txt`).
4. **Clear stuck error in portal** If the portal shows an old error (e.g. "Golden image not found" or "rpiboot failed"), click **Clear message** in the dashboard, or: `ssh root@10.130.60.224 "echo '{\"phase\":\"idle\",\"message\":\"Waiting for reTerminal in boot mode or network.\",\"progress\":null}' > /var/lib/cm4-provisioning/status.json"`. Then unplug/replug the device. 4. **Clear stuck error in portal** If the portal shows an old error (e.g. "Golden image not found" or "rpiboot failed"), click **Clear message** in the dashboard, or: `ssh root@10.130.60.224 "echo '{\"phase\":\"idle\",\"message\":\"Waiting for Device in USB boot mode.\",\"progress\":null}' > /var/lib/cm4-provisioning/status.json"`. Then unplug/replug the device.
5. **"PiShrink not installed" when clicking Shrink/Compress** Shrink and Compress run **on the host**, not in the LXC. Install PiShrink on the host: `ssh root@HOST 'bash -s' < emmc-provisioning/scripts/install-pishrink-on-host.sh`. Ensure deploy has been run so the host has `run-shrink-on-host.sh` and `cm4-shrink.path` enabled (`systemctl status cm4-shrink.path`). 5. **"PiShrink not installed" when clicking Shrink/Compress** Shrink and Compress run **on the host**, not in the LXC. Install PiShrink on the host: `ssh root@HOST 'bash -s' < emmc-provisioning/scripts/install-pishrink-on-host.sh`. Ensure deploy has been run so the host has `run-shrink-on-host.sh` and `cm4-shrink.path` enabled (`systemctl status cm4-shrink.path`).

View File

@@ -206,7 +206,7 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
fi fi
log "Backup complete: $BACKUPS_DIR/$final_name" log "Backup complete: $BACKUPS_DIR/$final_name"
write_status "done" "Backup complete: $final_name" "100" write_status "done" "Backup complete: $final_name" "100"
( sleep 90 && write_status "idle" "Waiting for reTerminal in boot mode or network." "null" ) & ( sleep 90 && write_status "idle" "Waiting for Device in USB boot mode." "null" ) &
else else
write_status "error" "Backup failed" "null" "dd failed" write_status "error" "Backup failed" "null" "dd failed"
fi fi
@@ -230,7 +230,7 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
log "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal." log "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal."
write_status "done" "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal." "100" write_status "done" "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal." "100"
# Auto-reset status to idle after 90s so dashboard does not stay on this message (dashboard also auto-clears after 60s when open) # Auto-reset status to idle after 90s so dashboard does not stay on this message (dashboard also auto-clears after 60s when open)