瀏覽代碼

feat: add transition history visibility

tags/v0.9.0
Jan Svabenik 1 月之前
父節點
當前提交
051d5f2de5
共有 2 個文件被更改,包括 106 次插入1 次删除
  1. +2
    -0
      docs/pro-runtime-hardening-workboard.md
  2. +104
    -1
      internal/control/ui.html

+ 2
- 0
docs/pro-runtime-hardening-workboard.md 查看文件

@@ -282,6 +282,7 @@ Einführen eines klaren Betriebsmodells mit Fault-, Recovery- und Muted-Zuständ
- Control-plane UI now also offers a Danger Zone `Reset Fault` button that calls the same endpoint so operators can acknowledge faults from the dashboard.

- Control-plane UI now posts an ops toast/log entry whenever the runtime state shifts so escalations and manual acknowledgements are immediately visible.
- Control-plane UI now keeps a compact Transition History panel beside the Fault History so operators can see recent runtime shifts without scrolling the activity log.


## Zielzustände laut Konzept
@@ -334,6 +335,7 @@ Einführen eines klaren Betriebsmodells mit Fault-, Recovery- und Muted-Zuständ
| 2026-04-05 | Manual fault reset endpoint | Added `POST /runtime/fault/reset` so operators can acknowledge `faulted` before the supervisor re-enters recovery. |
| 2026-04-05 | Fault-reset UI shortcut | Danger Zone now hosts a Reset Fault button wired to `/runtime/fault/reset` so operators get an in-app acknowledgement path without manual HTTP calls. |
| 2026-04-06 | Runtime transition visibility cue | Control UI now posts toast/log entries for runtime state shifts so ops instantly sees escalations and manual reset acknowledgements. |
| 2026-04-06 | Transition history panel | Added a compact Transition History panel next to the Fault History so the last few runtime state shifts stay visible even when the activity log is full. |

## WS-02 Verifikation
| Datum | Fokus | Ergebnis |


+ 104
- 1
internal/control/ui.html 查看文件

@@ -809,6 +809,45 @@ input.input-error {
color: var(--text-muted);
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 {
font-size: 11px;
color: var(--text-dim);
@@ -1174,6 +1213,20 @@ input.input-error {
</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="panel-head" data-panel>
<h2>Fault History</h2>
@@ -1215,6 +1268,7 @@ const configPollMs = 8000;
const mobileMq = window.matchMedia('(max-width: 640px)');
const freqPresetValues = [87.6, 94.5, 99.5, 100.0, 107.9];
const sparkHistoryLimit = 40;
const transitionHistoryLimit = 6;

const state = {
server: {
@@ -1248,6 +1302,7 @@ const state = {
underruns: [],
tx: [],
},
runtimeTransitions: [],
freqPresetIndex: 0,
};

@@ -1425,6 +1480,52 @@ function pushHistory(runtime) {
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) {
arr.push(Number.isFinite(value) ? value : 0);
if (arr.length > sparkHistoryLimit) arr.splice(0, arr.length - sparkHistoryLimit);
@@ -1813,6 +1914,7 @@ function render() {

updateHealth(engine, audioStream);
updateFaultHistory(engine);
updateTransitionHistory();
updateResetHint(engine);
updateMeters(engine, driver, audioStream);
drawSparkline('spark-audio', state.charts.audio, 'good', 1);
@@ -1879,8 +1981,9 @@ function notifyRuntimeTransition(engine) {
const prev = state.lastRuntimeState;
state.lastRuntimeState = next;
if (!prev || prev === next) return;
const message = `Runtime ${prev.toUpperCase()} → ${next.toUpperCase()}`;
const severity = runtimeStateSeverity(next);
pushTransitionHistory(prev, next, severity);
const message = `Runtime ${prev.toUpperCase()} → ${next.toUpperCase()}`;
const logLevel = severity === 'err' ? 'err' : (severity === 'warn' ? 'warn' : 'info');
toast(message, severity);
log(message, logLevel);


Loading…
取消
儲存