// ─────────────────────────────────────────────────────────
// Settings page — bridge status, commands, response viewer, log
// ─────────────────────────────────────────────────────────
function Settings({ bridge, bridgeState, bridgeIp, lastResp, log, onClearLog, onClearResponses }) {
const [logFilter, setLogFilter] = React.useState('all');
const [busy, setBusy] = React.useState(null);
const filteredLog = log.filter(l => {
if (logFilter === 'all') return true;
if (logFilter === 'mqtt') return ['pub', 'sub'].includes(l.dir);
if (logFilter === 'modem') return ['tx', 'rx', 'urc'].includes(l.dir);
if (logFilter === 'events') return l.dir === 'evt';
return true;
});
const runCmd = async (name, fn) => {
if (busy) return;
setBusy(name);
try { await fn(); } catch (e) { console.error(e); }
setTimeout(() => setBusy(null), 300);
};
// Newest first, capped to 80 to stay snappy.
const activity = React.useMemo(
() => lastResp.slice(-80).reverse(),
[lastResp]
);
return (
Bridge
EC200U cellular modem over MQTT · mqtt.divyessh.my:8884
{/* Status card */}
Status
client: msg-relay-server
Serial port
COM27 · 115200
{/* Response feed */}
Response feed
modem/rsp · newest first · {activity.length}
{activity.length === 0 && (
No commands sent yet — try connect
)}
{activity.map((r) => )}
{/* Certificates */}
Certificates
mTLS · certs/
{[
['ca.crt', 'Broker CA', 'verifies the server'],
['MSG_Relay_Server.crt', 'Client certificate', 'identity'],
['MSG_Relay_Server.key', 'Client private key', '—'],
].map(([f, label, note]) => (
{label} certs/{f} · {note}
LOADED
))}
{/* Activity log */}
Activity log
{['all','mqtt','modem','events'].map(k => (
))}
{filteredLog.length === 0 && (
no entries
)}
{filteredLog.slice().reverse().map((l, i) =>
)}
);
}
window.Settings = Settings;
// ─────────────────────────────────────────────────────────
// Response block
// ─────────────────────────────────────────────────────────
function ResponseBlock({ r }) {
const tone = r.status === 'success' ? 'success' : r.status === 'event' ? 'event' : 'error';
const isSetup = r.command === 'setup' && Array.isArray(r.results);
// Setup is verbose; default to collapsed so the feed stays scannable.
const [expanded, setExpanded] = React.useState(false);
// One-line summary that shows the *outcome* inline (state for status,
// OK count for setup, etc.) so the feed reads like a timeline.
const summary = oneLineSummary(r);
return (
setExpanded(e => !e) : undefined}
style={isSetup ? { cursor: 'pointer', userSelect: 'none' } : null}>
{tone === 'success' && }
{tone === 'error' && }
{tone === 'event' && }
{r.command || r.event}
{summary && (
<>
·
{summary}
>
)}
{fmtClock(r._ts || Date.now())}
{isSetup && (
)}
{/* setup details — only when expanded */}
{isSetup && expanded && (
{r.results.map((s, i) => (
{s.cmd}
{(s.lines || []).slice(1).join(' · ')}
{s.note ? ' · ' + s.note : ''}
{s.error ? ' · ' + s.error : ''}
{s.ok ? 'OK' : 'FAIL'}
))}
{r.health_check && (
AT (health check)
{(r.health_check.lines || []).join(' · ')}
{r.health_check.ok ? 'OK' : 'FAIL'}
)}
)}
{/* connect / disconnect / reset — show the AT lines */}
{(['connect','disconnect','reset'].includes(r.command)) && r.lines && (
{r.lines.join('\n')}
)}
{/* errors */}
{r.error && (
{r.error}
)}
);
}
function oneLineSummary(r) {
if (r.event === 'usb_connected') return 'port detected';
if (r.event === 'usb_disconnected') return 'port disappeared';
if (r.command === 'status') return r.state + (r.ip ? ` · ${r.ip}` : '');
if (r.command === 'setup' && r.results) {
const total = r.results.length;
const ok = r.results.filter(s => s.ok).length;
const hc = r.health_check ? (r.health_check.ok ? ' · AT ok' : ' · AT fail') : '';
return `${ok}/${total} OK${hc}`;
}
if (r.command === 'connect' && r.lines)
return r.lines.includes('OK') ? 'AT → OK' : 'AT → ' + r.lines.join(' ');
if (r.command === 'disconnect') return 'port closed';
if (r.command === 'reset') return 'AT+CFUN=1,1 → ' + (r.lines || []).join(' ');
return r.status;
}
// ─────────────────────────────────────────────────────────
// Log row
// ─────────────────────────────────────────────────────────
function LogRow({ l }) {
const cls = { pub:'pub', sub:'sub', evt:'evt', tx:'pub', rx:'sub', urc:'urc' }[l.dir] || '';
const labelMap = { pub:'PUB', sub:'SUB', tx:'TX', rx:'RX', urc:'URC', evt:'EVT' };
return (
{fmtClockMs(l.ts)}
{labelMap[l.dir] || l.dir.toUpperCase()}
{l.msg}
);
}
window.ResponseBlock = ResponseBlock;
window.LogRow = LogRow;