feat: add nav bar button to publish pending config changes

Shows an orange "Publish Config" button to the left of the alert-pie
for admin users when there are staged config changes. Uses localStorage
to persist staged changes across page navigations so the button appears
on any page, not just settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Andreas Wrede
2026-05-12 09:32:32 -04:00
parent 708508157f
commit 59e256a042
3 changed files with 60 additions and 0 deletions
+17
View File
@@ -125,6 +125,23 @@
.nav-links a { margin-right: 0; padding: 6px 0; font-size: 1em; } .nav-links a { margin-right: 0; padding: 6px 0; font-size: 1em; }
} }
/* Pending config publish button */
.nav-publish-btn {
background: #e65100;
color: #fff;
border: none;
border-radius: 4px;
padding: 4px 10px;
font-size: 0.82em;
font-weight: 600;
cursor: pointer;
flex-shrink: 0;
white-space: nowrap;
margin-left: auto;
}
.nav-publish-btn:hover { background: #bf360c; }
.nav-publish-btn:disabled { opacity: 0.7; cursor: default; }
/* Swiss railway clock — nav */ /* Swiss railway clock — nav */
.nav-pie { .nav-pie {
flex-shrink: 0; flex-shrink: 0;
+38
View File
@@ -11,6 +11,9 @@
{% endif %} {% endif %}
<a href="/about"{% if active_page == "about" %} class="active"{% endif %}>About</a> <a href="/about"{% if active_page == "about" %} class="active"{% endif %}>About</a>
</div> </div>
{% if current_user and current_user.admin %}
<button id="nav-publish-btn" class="nav-publish-btn" onclick="navPublishConfig()" style="display:none" title="Publish pending config changes to .hb.yaml">&#9888; Publish Config</button>
{% endif %}
<div class="nav-pie" title="Host alert status"> <div class="nav-pie" title="Host alert status">
<canvas id="alert-pie" width="44" height="44"></canvas> <canvas id="alert-pie" width="44" height="44"></canvas>
</div> </div>
@@ -92,5 +95,40 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
updateAlertPie(); updateAlertPie();
setInterval(updateAlertPie, 30000); setInterval(updateAlertPie, 30000);
navCheckPendingConfig();
window.addEventListener('storage', navCheckPendingConfig);
}); });
function navCheckPendingConfig() {
var btn = document.getElementById('nav-publish-btn');
if (!btn) return;
btn.style.display = localStorage.getItem('hbd_pending_config') ? '' : 'none';
}
async function navPublishConfig() {
var btn = document.getElementById('nav-publish-btn');
var pending = localStorage.getItem('hbd_pending_config');
if (!pending) return;
var staged;
try { staged = JSON.parse(pending); } catch(e) { return; }
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
try {
var resp = await fetch('/api/0/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: pending
});
if (resp.ok) {
localStorage.removeItem('hbd_pending_config');
window.location.reload();
} else {
var err = await resp.json().catch(function() { return {}; });
alert('Error: ' + (err.error || resp.statusText));
if (btn) { btn.disabled = false; btn.textContent = '⚠ Publish Config'; }
}
} catch(e) {
alert('Network error: ' + e.message);
if (btn) { btn.disabled = false; btn.textContent = '⚠ Publish Config'; }
}
}
</script> </script>
+5
View File
@@ -871,9 +871,13 @@
if (count > 0) { if (count > 0) {
document.getElementById('pending-count').textContent = count; document.getElementById('pending-count').textContent = count;
banner.style.display = 'flex'; banner.style.display = 'flex';
localStorage.setItem('hbd_pending_config', JSON.stringify(_staged));
} else { } else {
banner.style.display = 'none'; banner.style.display = 'none';
localStorage.removeItem('hbd_pending_config');
} }
const navBtn = document.getElementById('nav-publish-btn');
if (navBtn) navBtn.style.display = count > 0 ? '' : 'none';
} }
function stageFormSection(sectionId, apiSection) { function stageFormSection(sectionId, apiSection) {
@@ -1050,6 +1054,7 @@
function discardAll() { function discardAll() {
Object.keys(_staged).forEach(k => delete _staged[k]); Object.keys(_staged).forEach(k => delete _staged[k]);
localStorage.removeItem('hbd_pending_config');
updatePendingBanner(); updatePendingBanner();
window.location.reload(); window.location.reload();
} }