// ───────────────────────────────────────────────────────── // 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
Bridge state
Modem IP
{bridgeIp || }
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;