|
|
@@ -809,6 +809,45 @@ input.input-error { |
|
|
color: var(--text-muted); |
|
|
color: var(--text-muted); |
|
|
font-size: 11px; |
|
|
font-size: 11px; |
|
|
} |
|
|
} |
|
|
|
|
|
.transition-history { |
|
|
|
|
|
margin-top: 12px; |
|
|
|
|
|
padding: 10px; |
|
|
|
|
|
border: 1px solid var(--border); |
|
|
|
|
|
border-radius: 6px; |
|
|
|
|
|
background: var(--surface); |
|
|
|
|
|
font-size: 11px; |
|
|
|
|
|
max-height: 180px; |
|
|
|
|
|
overflow-y: auto; |
|
|
|
|
|
line-height: 1.3; |
|
|
|
|
|
} |
|
|
|
|
|
.transition-history-entry { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
justify-content: space-between; |
|
|
|
|
|
gap: 10px; |
|
|
|
|
|
padding: 4px 0; |
|
|
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08); |
|
|
|
|
|
} |
|
|
|
|
|
.transition-history-entry:last-child { |
|
|
|
|
|
border-bottom: none; |
|
|
|
|
|
} |
|
|
|
|
|
.transition-history-entry .transition-history-time { |
|
|
|
|
|
color: var(--text-dim); |
|
|
|
|
|
} |
|
|
|
|
|
.transition-history-entry.good { color: var(--green); } |
|
|
|
|
|
.transition-history-entry.warn { color: var(--amber); } |
|
|
|
|
|
.transition-history-entry.err { color: var(--accent); } |
|
|
|
|
|
.transition-history-entry.info { color: var(--text); } |
|
|
|
|
|
.transition-history-desc { |
|
|
|
|
|
font-size: 10px; |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
text-transform: uppercase; |
|
|
|
|
|
letter-spacing: 0.5px; |
|
|
|
|
|
} |
|
|
|
|
|
.transition-history-empty { |
|
|
|
|
|
padding: 6px 0; |
|
|
|
|
|
color: var(--text-muted); |
|
|
|
|
|
font-size: 11px; |
|
|
|
|
|
} |
|
|
.section-note.reset-hint { |
|
|
.section-note.reset-hint { |
|
|
font-size: 11px; |
|
|
font-size: 11px; |
|
|
color: var(--text-dim); |
|
|
color: var(--text-dim); |
|
|
@@ -1174,6 +1213,20 @@ input.input-error { |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div class="card panel" data-panel-key="transition-history"> |
|
|
|
|
|
<div class="panel-head" data-panel> |
|
|
|
|
|
<h2>Transition History</h2> |
|
|
|
|
|
<div class="meta">recent state shifts</div> |
|
|
|
|
|
<span class="chevron"></span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="panel-body"> |
|
|
|
|
|
<div class="section-note">Keeps runtime escalations visible without scrolling the activity log.</div> |
|
|
|
|
|
<div class="transition-history" id="transition-history"> |
|
|
|
|
|
<div class="transition-history-empty">No transitions yet.</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="card panel" data-panel-key="fault-history"> |
|
|
<div class="card panel" data-panel-key="fault-history"> |
|
|
<div class="panel-head" data-panel> |
|
|
<div class="panel-head" data-panel> |
|
|
<h2>Fault History</h2> |
|
|
<h2>Fault History</h2> |
|
|
@@ -1215,6 +1268,7 @@ const configPollMs = 8000; |
|
|
const mobileMq = window.matchMedia('(max-width: 640px)'); |
|
|
const mobileMq = window.matchMedia('(max-width: 640px)'); |
|
|
const freqPresetValues = [87.6, 94.5, 99.5, 100.0, 107.9]; |
|
|
const freqPresetValues = [87.6, 94.5, 99.5, 100.0, 107.9]; |
|
|
const sparkHistoryLimit = 40; |
|
|
const sparkHistoryLimit = 40; |
|
|
|
|
|
const transitionHistoryLimit = 6; |
|
|
|
|
|
|
|
|
const state = { |
|
|
const state = { |
|
|
server: { |
|
|
server: { |
|
|
@@ -1248,6 +1302,7 @@ const state = { |
|
|
underruns: [], |
|
|
underruns: [], |
|
|
tx: [], |
|
|
tx: [], |
|
|
}, |
|
|
}, |
|
|
|
|
|
runtimeTransitions: [], |
|
|
freqPresetIndex: 0, |
|
|
freqPresetIndex: 0, |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
@@ -1425,6 +1480,52 @@ function pushHistory(runtime) { |
|
|
pushChart(state.charts.tx, txState === 'running' ? 1 : state.txBusy ? 0.55 : 0.05); |
|
|
pushChart(state.charts.tx, txState === 'running' ? 1 : state.txBusy ? 0.55 : 0.05); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function pushTransitionHistory(from, to, severity) { |
|
|
|
|
|
if (!from || !to) return; |
|
|
|
|
|
const entry = { |
|
|
|
|
|
from: normalizeRuntimeState(from), |
|
|
|
|
|
to: normalizeRuntimeState(to), |
|
|
|
|
|
severity: severity || 'info', |
|
|
|
|
|
time: nowTs(), |
|
|
|
|
|
}; |
|
|
|
|
|
state.runtimeTransitions.unshift(entry); |
|
|
|
|
|
if (state.runtimeTransitions.length > transitionHistoryLimit) { |
|
|
|
|
|
state.runtimeTransitions.splice(transitionHistoryLimit); |
|
|
|
|
|
} |
|
|
|
|
|
updateTransitionHistory(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function transitionSeverityClass(severity) { |
|
|
|
|
|
switch (String(severity || '').toLowerCase()) { |
|
|
|
|
|
case 'err': |
|
|
|
|
|
return 'err'; |
|
|
|
|
|
case 'warn': |
|
|
|
|
|
return 'warn'; |
|
|
|
|
|
case 'ok': |
|
|
|
|
|
case 'good': |
|
|
|
|
|
return 'good'; |
|
|
|
|
|
default: |
|
|
|
|
|
return 'info'; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function updateTransitionHistory() { |
|
|
|
|
|
const container = $('transition-history'); |
|
|
|
|
|
if (!container) return; |
|
|
|
|
|
if (!state.runtimeTransitions.length) { |
|
|
|
|
|
container.innerHTML = '<div class="transition-history-empty">No transitions yet.</div>'; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const rows = state.runtimeTransitions.map((entry) => { |
|
|
|
|
|
const when = entry?.time ? new Date(entry.time) : null; |
|
|
|
|
|
const timeLabel = when && !Number.isNaN(when.getTime()) ? when.toLocaleTimeString() : '--:--'; |
|
|
|
|
|
const desc = `${entry.from.toUpperCase()} → ${entry.to.toUpperCase()}`; |
|
|
|
|
|
const severityClass = transitionSeverityClass(entry.severity); |
|
|
|
|
|
return `<div class="transition-history-entry ${severityClass}"><span class="transition-history-time">${timeLabel}</span><span class="transition-history-desc">${desc}</span></div>`; |
|
|
|
|
|
}); |
|
|
|
|
|
container.innerHTML = rows.join(''); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function pushChart(arr, value) { |
|
|
function pushChart(arr, value) { |
|
|
arr.push(Number.isFinite(value) ? value : 0); |
|
|
arr.push(Number.isFinite(value) ? value : 0); |
|
|
if (arr.length > sparkHistoryLimit) arr.splice(0, arr.length - sparkHistoryLimit); |
|
|
if (arr.length > sparkHistoryLimit) arr.splice(0, arr.length - sparkHistoryLimit); |
|
|
@@ -1813,6 +1914,7 @@ function render() { |
|
|
|
|
|
|
|
|
updateHealth(engine, audioStream); |
|
|
updateHealth(engine, audioStream); |
|
|
updateFaultHistory(engine); |
|
|
updateFaultHistory(engine); |
|
|
|
|
|
updateTransitionHistory(); |
|
|
updateResetHint(engine); |
|
|
updateResetHint(engine); |
|
|
updateMeters(engine, driver, audioStream); |
|
|
updateMeters(engine, driver, audioStream); |
|
|
drawSparkline('spark-audio', state.charts.audio, 'good', 1); |
|
|
drawSparkline('spark-audio', state.charts.audio, 'good', 1); |
|
|
@@ -1879,8 +1981,9 @@ function notifyRuntimeTransition(engine) { |
|
|
const prev = state.lastRuntimeState; |
|
|
const prev = state.lastRuntimeState; |
|
|
state.lastRuntimeState = next; |
|
|
state.lastRuntimeState = next; |
|
|
if (!prev || prev === next) return; |
|
|
if (!prev || prev === next) return; |
|
|
const message = `Runtime ${prev.toUpperCase()} → ${next.toUpperCase()}`; |
|
|
|
|
|
const severity = runtimeStateSeverity(next); |
|
|
const severity = runtimeStateSeverity(next); |
|
|
|
|
|
pushTransitionHistory(prev, next, severity); |
|
|
|
|
|
const message = `Runtime ${prev.toUpperCase()} → ${next.toUpperCase()}`; |
|
|
const logLevel = severity === 'err' ? 'err' : (severity === 'warn' ? 'warn' : 'info'); |
|
|
const logLevel = severity === 'err' ? 'err' : (severity === 'warn' ? 'warn' : 'info'); |
|
|
toast(message, severity); |
|
|
toast(message, severity); |
|
|
log(message, logLevel); |
|
|
log(message, logLevel); |
|
|
|