refactor(web): remove realtime log stream, compact event list to single line

- Remove SSE log stream (EventSource /api/logs/stream) and logView panel
- System events panel now occupies the full bottom-middle panel
- Each event renders as a single flex row: level badge, type, message, timestamp
- Remove logSource from state, logView from dom, startLogs from app bootstrap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-03-24 16:39:01 +08:00
parent d88d8375fd
commit a405623ec1
7 changed files with 27 additions and 101 deletions

View File

@ -1,18 +1,7 @@
<section class="panel bottom-middle">
<div class="stack-panel">
<div class="stack-section event-section">
<div class="panel-head">
<h2>系统事件</h2>
<button type="button" class="secondary" id="refreshEventBtn">刷新</button>
</div>
<div class="list event-list" id="eventList"></div>
</div>
<div class="stack-section stack-section-bordered log-section">
<div class="panel-head">
<h2>实时日志</h2>
</div>
<div class="log" id="logView"></div>
</div>
</div>
</section>

View File

@ -14,7 +14,7 @@ import {
resetEquipmentForm,
saveEquipment,
} from "./equipment.js";
import { startLogs, startPointSocket } from "./logs.js";
import { startPointSocket } from "./logs.js";
import {
clearBatchBinding,
browseAndLoadTree,
@ -136,7 +136,6 @@ async function bootstrap() {
updateSelectedPointSummary();
updatePointFilterSummary();
renderChart();
startLogs();
startPointSocket();
await withStatus(loadUnits());

View File

@ -15,7 +15,6 @@ export const dom = {
pointSourceSelect: byId("pointSourceSelect"),
pointSourceNodeCount: byId("pointSourceNodeCount"),
openPointModalBtn: byId("openPointModal"),
logView: byId("logView"),
chartCanvas: byId("chartCanvas"),
chartTitle: byId("chartTitle"),
chartSummary: byId("chartSummary"),

View File

@ -19,15 +19,8 @@ export function renderEvents() {
state.events.forEach((item) => {
const row = document.createElement("div");
row.className = "list-item event-card";
row.innerHTML = `
<div class="row">
<strong>${item.event_type}</strong>
<span class="badge">${(item.level || "info").toUpperCase()}</span>
</div>
<div>${item.message}</div>
<div class="muted">${formatTime(item.created_at)}</div>
`;
row.className = "event-card";
row.innerHTML = `<span class="badge">${(item.level || "info").toUpperCase()}</span><strong class="event-type">${item.event_type}</strong><span class="event-message">${item.message}</span><span class="muted event-time">${formatTime(item.created_at)}</span>`;
dom.eventList.appendChild(row);
});
}

View File

@ -1,73 +1,9 @@
import { appendChartPoint } from "./chart.js";
import { dom } from "./dom.js";
import { prependEvent } from "./events.js";
import { formatValue } from "./points.js";
import { state } from "./state.js";
import { renderUnits } from "./units.js";
function escapeHtml(text) {
return text
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
function parseLogLine(line) {
const trimmed = line.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
return null;
}
try {
return JSON.parse(trimmed);
} catch {
return null;
}
}
export function appendLog(line) {
const atBottom = dom.logView.scrollTop + dom.logView.clientHeight >= dom.logView.scrollHeight - 10;
const div = document.createElement("div");
const parsed = parseLogLine(line);
if (!parsed) {
div.className = "log-line";
div.textContent = line;
} else {
const levelRaw = (parsed.level || "").toString();
const level = levelRaw.toLowerCase();
div.className = `log-line${level ? ` level-${level}` : ""}`;
div.innerHTML = [
`<span class="level">${escapeHtml(levelRaw || "LOG")}</span>`,
parsed.timestamp ? `<span class="muted"> ${escapeHtml(parsed.timestamp)}</span>` : "",
parsed.target ? `<span class="muted"> ${escapeHtml(parsed.target)}</span>` : "",
`<span class="message">${escapeHtml(
parsed.fields?.message || parsed.message || parsed.msg || line,
)}</span>`,
].join("");
}
dom.logView.appendChild(div);
if (atBottom) {
dom.logView.scrollTop = dom.logView.scrollHeight;
}
}
export function startLogs() {
if (state.logSource) {
state.logSource.close();
}
state.logSource = new EventSource("/api/logs/stream");
state.logSource.addEventListener("log", (event) => {
const data = JSON.parse(event.data);
(data.lines || []).forEach(appendLog);
});
state.logSource.addEventListener("error", () => {
appendLog("[log stream error]");
});
}
export function startPointSocket() {
const protocol = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${protocol}://${location.host}/ws/public`);

View File

@ -18,7 +18,6 @@ export const state = {
chartPointId: null,
chartPointName: "",
chartData: [],
logSource: null,
pointSocket: null,
apiDocLoaded: false,
runtimes: new Map(), // unit_id -> UnitRuntime

View File

@ -660,8 +660,7 @@ button.danger:hover { background: var(--danger-hover); }
max-height: 50vh;
}
.unit-list,
.event-list {
.unit-list {
padding-top: 6px;
}
@ -670,20 +669,32 @@ button.danger:hover { background: var(--danger-hover); }
}
.event-card {
cursor: default;
display: flex;
align-items: baseline;
gap: 8px;
padding: 3px 8px;
font-size: 12px;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
.event-card:hover {
background: var(--surface);
border-color: var(--border);
background: var(--surface-hover, var(--surface));
}
.event-section {
flex-basis: 42%;
.event-type {
flex-shrink: 0;
}
.log-section {
flex-basis: 58%;
.event-message {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.event-time {
flex-shrink: 0;
font-size: 11px;
}
.equipment-select-row {