hub v0.3.1: Config diff display + pull config
Replace broken SHA256 hash comparison with value-based YAML comparison.
Add "Show Diff" button showing per-key differences in a color-coded table.
Add "Pull Config" to import controller's current config into the Hub.
New endpoints: GET /customers/{id}/config-diff, POST /customers/{id}/pull-config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -375,11 +375,13 @@
|
||||
<span class="label">Config Sync</span>
|
||||
<span class="value">
|
||||
{{if eq .ConfigSyncStatus "in_sync"}}<span style="color: #22c55e;">✓ In sync</span>
|
||||
{{else if eq .ConfigSyncStatus "mismatch"}}<span style="color: #f59e0b;">⚠ Config mismatch — Hub config differs from controller</span>
|
||||
{{else}}<span style="color: #94a3b8;">Unknown — controller not reporting config hash (update controller)</span>
|
||||
{{else if eq .ConfigSyncStatus "mismatch"}}<span style="color: #f59e0b;">⚠ Config mismatch — {{.ConfigDiffCount}} difference{{if gt .ConfigDiffCount 1}}s{{end}}</span>
|
||||
<button class="btn btn-outline btn-sm" style="margin-left: 0.5em; font-size: 0.8em;" onclick="showConfigDiff('{{.CustomerID}}')">Show Diff</button>
|
||||
{{else}}<span style="color: #94a3b8;">Unknown — no infra backup available yet</span>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
<div id="config-diff-container" style="display: none; margin-top: 0.5rem;"></div>
|
||||
{{end}}
|
||||
<div style="margin-top: 0.75em; display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
{{if and .ControllerURL .UpdateAvailable}}
|
||||
@@ -395,6 +397,9 @@
|
||||
<button class="btn btn-outline btn-sm" id="btn-push-config" onclick="pushConfig('{{.CustomerID}}')">
|
||||
Push Config
|
||||
</button>
|
||||
<button class="btn btn-outline btn-sm" id="btn-pull-config" onclick="pullConfig('{{.CustomerID}}')">
|
||||
Pull Config
|
||||
</button>
|
||||
{{end}}
|
||||
<span id="action-msg" style="margin-left: 0.5em; display: none;"></span>
|
||||
</div>
|
||||
@@ -610,6 +615,83 @@
|
||||
});
|
||||
}
|
||||
|
||||
function pullConfig(customerID) {
|
||||
if (!confirm('Import the controller\'s current config into the Hub?\n\nThis updates the Hub\'s stored configuration to match the controller.')) return;
|
||||
var btn = document.getElementById('btn-pull-config');
|
||||
var msg = document.getElementById('action-msg');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Pulling...';
|
||||
msg.style.display = 'none';
|
||||
fetch('/customers/' + customerID + '/pull-config', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
msg.textContent = 'Config imported successfully';
|
||||
msg.style.display = 'inline';
|
||||
msg.style.color = '#4ade80';
|
||||
setTimeout(function() { location.reload(); }, 1500);
|
||||
} else {
|
||||
msg.textContent = data.error || 'Failed';
|
||||
msg.style.display = 'inline';
|
||||
msg.style.color = '#f87171';
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Pull Config';
|
||||
})
|
||||
.catch(function() {
|
||||
msg.textContent = 'Connection error';
|
||||
msg.style.display = 'inline';
|
||||
msg.style.color = '#f87171';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Pull Config';
|
||||
});
|
||||
}
|
||||
|
||||
function showConfigDiff(customerID) {
|
||||
var container = document.getElementById('config-diff-container');
|
||||
if (container.style.display !== 'none') {
|
||||
container.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = '<p class="text-muted">Loading diff...</p>';
|
||||
container.style.display = 'block';
|
||||
fetch('/customers/' + customerID + '/config-diff')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (!data.ok) {
|
||||
container.innerHTML = '<p style="color: #f87171;">' + (data.error || 'Failed to load diff') + '</p>';
|
||||
return;
|
||||
}
|
||||
if (data.in_sync) {
|
||||
container.innerHTML = '<p style="color: #22c55e;">Configs are in sync (no differences found).</p>';
|
||||
return;
|
||||
}
|
||||
var html = '<table class="data-table" style="font-size: 0.85em;">';
|
||||
html += '<thead><tr><th>Key</th><th>Hub Value</th><th>Controller Value</th><th>Status</th></tr></thead><tbody>';
|
||||
data.diffs.forEach(function(d) {
|
||||
var cls = 'diff-' + d.status;
|
||||
var statusLabel = d.status === 'changed' ? 'Changed' : d.status === 'hub_only' ? 'Hub only' : 'Controller only';
|
||||
html += '<tr class="' + cls + '">';
|
||||
html += '<td style="font-family: monospace; white-space: nowrap;">' + escHtml(d.key) + '</td>';
|
||||
html += '<td style="word-break: break-all;">' + escHtml(d.hub) + '</td>';
|
||||
html += '<td style="word-break: break-all;">' + escHtml(d.controller) + '</td>';
|
||||
html += '<td>' + statusLabel + '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
})
|
||||
.catch(function() {
|
||||
container.innerHTML = '<p style="color: #f87171;">Failed to fetch diff from controller.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(s));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
{{if .HasConfig}}
|
||||
// Load YAML preview
|
||||
fetch('/configs/{{.CustomerID}}/preview')
|
||||
|
||||
Reference in New Issue
Block a user