Add MQTT diagnostics, broker connection probe, and switch to Debian base
The MQTT publisher now tracks connection state, publish counters, discovery and availability timestamps, and the last error, exposed at /api/mqtt and on the dashboard as an MQTT card and a Home Assistant section. Connection failures, disconnects, and dropped publishes are logged; paho's on_connect_fail callback was not registered before, so failed attempts were silent. MQTT connect, disconnect, and unreachable events go to the timeline. When a connection attempt fails the service runs a probe from inside the container: system resolver, each nameserver from resolv.conf queried directly, /etc/hosts, and a TCP connect to every address found. The result is shown as a summary and raw JSON on the dashboard and can be rerun via POST /api/mqtt/probe. The image base moves from Alpine to Debian slim. musl queries all nameservers in parallel and accepts the first reply, so a fast public NXDOMAIN beats a slower local server that knows the name. glibc asks the nameservers in order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
d80c40065e
commit
62b57a7e56
8 files changed
+737
-14
No files matched your search
+114
-1
@@ -53,6 +53,14 @@
|
||||
.kind.up, .kind.wifi_connected, .kind.public_ip { background: var(--ok); }
|
||||
.kind.down, .kind.wifi_disconnected, .kind.reboot { background: var(--bad); }
|
||||
.kind.public_ip_changed, .kind.wifi_roamed, .kind.service_started { background: var(--warn); }
|
||||
.kind.mqtt_connected { background: var(--ok); }
|
||||
.kind.mqtt_disconnected, .kind.mqtt_connect_failed { background: var(--bad); }
|
||||
.btn { background: var(--panel); border: 1px solid var(--border); color: var(--text); padding: 4px 12px; border-radius: 6px; cursor: pointer; }
|
||||
.btn:disabled { opacity: .5; cursor: default; }
|
||||
ul.probe { list-style: none; margin: 0 0 8px; padding: 0; font-size: 13px; }
|
||||
ul.probe li { padding: 4px 0; border-bottom: 1px solid var(--border); }
|
||||
ul.probe li.bad { color: var(--bad); } ul.probe li.ok { color: var(--ok); }
|
||||
pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; font-size: 12px; overflow-x: auto; margin: 8px 0 0; }
|
||||
@media (max-width: 600px) { ul.timeline li { grid-template-columns: 1fr; gap: 2px; } }
|
||||
</style>
|
||||
</head>
|
||||
@@ -69,6 +77,7 @@
|
||||
<div class="card" id="card-dns"><div class="label">DNS</div><div class="value">…</div><div class="sub"></div></div>
|
||||
<div class="card" id="card-ip"><div class="label">Public IP</div><div class="value">…</div><div class="sub"></div></div>
|
||||
<div class="card" id="card-wifi"><div class="label">Wi-Fi</div><div class="value">…</div><div class="sub"></div></div>
|
||||
<div class="card" id="card-mqtt"><div class="label">MQTT</div><div class="value">…</div><div class="sub"></div></div>
|
||||
</section>
|
||||
|
||||
<h2>History</h2>
|
||||
@@ -106,6 +115,30 @@
|
||||
<div class="panel chart"><h3>Wi-Fi retries / failed / beacon loss per interval</h3><canvas id="ch-errors"></canvas><div class="legend" id="lg-errors"></div></div>
|
||||
</div>
|
||||
|
||||
<h2>Home Assistant (MQTT)</h2>
|
||||
<div class="panel">
|
||||
<div id="mqtt-notice"></div>
|
||||
<div id="mqtt-details"></div>
|
||||
<details id="mqtt-payload-wrap" style="margin-top:12px">
|
||||
<summary style="cursor:pointer;color:var(--muted)">Last state payload</summary>
|
||||
<pre id="mqtt-payload"></pre>
|
||||
</details>
|
||||
</div>
|
||||
<div class="panel" id="probe-panel" style="margin-top:12px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:8px">
|
||||
<strong>Broker connection probe</strong>
|
||||
<span style="display:flex;align-items:center;gap:10px">
|
||||
<span class="meta" id="probe-meta" style="color:var(--muted);font-size:12px"></span>
|
||||
<button id="probe-run" class="btn">Run probe now</button>
|
||||
</span>
|
||||
</div>
|
||||
<ul id="probe-summary" class="probe"></ul>
|
||||
<details id="probe-json-wrap">
|
||||
<summary style="cursor:pointer;color:var(--muted)">Raw JSON</summary>
|
||||
<pre id="probe-json"></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
@@ -198,6 +231,86 @@ async function loadWifi() {
|
||||
'</tbody></table>';
|
||||
}
|
||||
|
||||
const MQTT_STATUS = {
|
||||
connected: ['ok', 'connected'], connecting: ['warn', 'connecting'], failed: ['bad', 'unreachable'],
|
||||
refused: ['bad', 'refused'], disconnected: ['bad', 'disconnected'], not_started: ['', 'not started'],
|
||||
};
|
||||
|
||||
async function loadMqtt() {
|
||||
const m = await (await fetch('/api/mqtt')).json();
|
||||
if (!m.enabled) {
|
||||
card('card-mqtt', 'warn', 'not configured', 'set MQTT_HOST to publish to Home Assistant');
|
||||
$('mqtt-notice').innerHTML = '';
|
||||
$('mqtt-details').innerHTML = '<div class="empty">MQTT is disabled. Set <code>MQTT_HOST</code> to enable Home Assistant publishing.</div>';
|
||||
$('mqtt-payload-wrap').hidden = true;
|
||||
$('probe-panel').hidden = true;
|
||||
return;
|
||||
}
|
||||
const [cls, label] = MQTT_STATUS[m.status] || ['', m.status];
|
||||
const since = m.connected ? m.connected_at : (m.disconnected_at || m.last_attempt_at);
|
||||
card('card-mqtt', cls, label,
|
||||
`${esc(m.host)}:${m.port}` + (since ? ` · ${m.connected ? 'since' : 'for'} ${ago(since, m.now).replace(' ago', '')}` : '') +
|
||||
(m.last_publish_at ? ` · published ${ago(m.last_publish_at, m.now)}` : ''),
|
||||
m.connected ? m.last_publish_error : m.last_error);
|
||||
|
||||
let notice = '';
|
||||
if (!m.connected) notice = `Not connected to the broker (${esc(m.last_error || m.status)}). Home Assistant shows the device as unavailable because the broker sent the retained "offline" will message and nothing has replaced it.`;
|
||||
else if (m.publish_dropped && m.last_publish_error) notice = `Connected, but the last publish failed: ${esc(m.last_publish_error)}.`;
|
||||
else if (!m.availability_at) notice = 'Connected, but availability has not been published yet.';
|
||||
else if (!m.last_payload) notice = 'Connected and discovery sent, waiting for the first monitor result to publish state.';
|
||||
$('mqtt-notice').innerHTML = notice ? `<div class="notice">${notice}</div>` : '';
|
||||
|
||||
const when = (ts) => ts ? `${fmtTime(ts)} (${ago(ts, m.now)})` : 'never';
|
||||
const rows = [
|
||||
['Broker', `${m.host}:${m.port}` + (m.username ? ` as ${m.username}` : ' (no auth)')],
|
||||
['Client id', m.client_id],
|
||||
['Status', label + (m.last_error ? ` · ${m.last_error}` : '')],
|
||||
['Publisher started', when(m.started_at)],
|
||||
['Connected', when(m.connected_at)],
|
||||
['Disconnected', when(m.disconnected_at) + (m.disconnects ? ` · ${m.disconnects} total` : '')],
|
||||
['Connect failures', String(m.connect_failures) + (m.connect_failures ? ` · last attempt ${when(m.last_attempt_at)}` : '')],
|
||||
['Discovery sent', when(m.discovery_at) + (m.discovery_count ? ` · ${m.discovery_count} times` : '')],
|
||||
['Availability "online" sent', when(m.availability_at)],
|
||||
['Home Assistant status', m.ha_status ? `${m.ha_status} at ${when(m.ha_status_at)}` : 'nothing received on ' + m.discovery_prefix + '/status'],
|
||||
['Messages published', `${m.publish_count} ok · ${m.publish_dropped} dropped` + (m.last_publish_error ? ` · last error: ${m.last_publish_error}` : '')],
|
||||
['Last publish', when(m.last_publish_at)],
|
||||
['State topic', m.state_topic],
|
||||
['Availability topic', m.availability_topic],
|
||||
['Discovery prefix', m.discovery_prefix],
|
||||
];
|
||||
$('mqtt-details').innerHTML = '<dl class="kv">' + rows.map(([k, v]) => `<dt>${k}</dt><dd>${esc(v)}</dd>`).join('') + '</dl>';
|
||||
$('mqtt-payload-wrap').hidden = !m.last_payload;
|
||||
$('mqtt-payload').textContent = m.last_payload ? JSON.stringify(m.last_payload, null, 2) : '';
|
||||
$('probe-panel').hidden = false;
|
||||
renderProbe(m.probe, m.now, m.probe_running);
|
||||
}
|
||||
|
||||
function renderProbe(p, now, running) {
|
||||
$('probe-run').disabled = !!running;
|
||||
$('probe-run').textContent = running ? 'Running…' : 'Run probe now';
|
||||
if (!p) {
|
||||
$('probe-meta').textContent = 'not run yet; runs automatically when a connection attempt fails';
|
||||
$('probe-summary').innerHTML = ''; $('probe-json-wrap').hidden = true; return;
|
||||
}
|
||||
$('probe-meta').textContent = `ran ${fmtTime(p.ts)} (${ago(p.ts, now)})` + (p.ms != null ? ` in ${p.ms} ms` : '');
|
||||
$('probe-summary').innerHTML = p.summary.map(line => {
|
||||
const cls = /fail|no reply|NXDOMAIN|SERVFAIL|REFUSED|disagree|No address|No nameservers|crashed/.test(line) ? 'bad' : /connected in/.test(line) ? 'ok' : '';
|
||||
return `<li class="${cls}">${esc(line)}</li>`;
|
||||
}).join('');
|
||||
$('probe-json-wrap').hidden = false;
|
||||
$('probe-json').textContent = JSON.stringify(p, null, 2);
|
||||
}
|
||||
|
||||
$('probe-run').addEventListener('click', async () => {
|
||||
renderProbe(null, null, true);
|
||||
try {
|
||||
const r = await fetch('/api/mqtt/probe', { method: 'POST' });
|
||||
const p = await r.json();
|
||||
if (!r.ok) { $('probe-meta').textContent = p.error || 'probe failed'; $('probe-run').disabled = false; $('probe-run').textContent = 'Run probe now'; return; }
|
||||
renderProbe(p, Date.now() / 1000, false);
|
||||
} catch (e) { console.error(e); $('probe-run').disabled = false; $('probe-run').textContent = 'Run probe now'; }
|
||||
});
|
||||
|
||||
async function loadEvents() {
|
||||
const events = await (await fetch('/api/events?limit=300')).json();
|
||||
$('timeline').innerHTML = events.length ? events.map(e =>
|
||||
@@ -270,7 +383,7 @@ $('range').addEventListener('click', (e) => {
|
||||
});
|
||||
window.addEventListener('resize', () => loadCharts());
|
||||
|
||||
async function refreshFast() { try { await Promise.all([loadStatus(), loadWifi(), loadEvents()]); } catch (e) { console.error(e); } }
|
||||
async function refreshFast() { try { await Promise.all([loadStatus(), loadWifi(), loadMqtt(), loadEvents()]); } catch (e) { console.error(e); } }
|
||||
refreshFast(); loadCharts();
|
||||
setInterval(refreshFast, 10000);
|
||||
setInterval(loadCharts, 60000);
|
||||
|
||||
Reference in new issue
Block a user