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:
parent
d88d8375fd
commit
a405623ec1
|
|
@ -1,18 +1,7 @@
|
||||||
<section class="panel bottom-middle">
|
<section class="panel bottom-middle">
|
||||||
<div class="stack-panel">
|
|
||||||
<div class="stack-section event-section">
|
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h2>系统事件</h2>
|
<h2>系统事件</h2>
|
||||||
<button type="button" class="secondary" id="refreshEventBtn">刷新</button>
|
<button type="button" class="secondary" id="refreshEventBtn">刷新</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="list event-list" id="eventList"></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>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import {
|
||||||
resetEquipmentForm,
|
resetEquipmentForm,
|
||||||
saveEquipment,
|
saveEquipment,
|
||||||
} from "./equipment.js";
|
} from "./equipment.js";
|
||||||
import { startLogs, startPointSocket } from "./logs.js";
|
import { startPointSocket } from "./logs.js";
|
||||||
import {
|
import {
|
||||||
clearBatchBinding,
|
clearBatchBinding,
|
||||||
browseAndLoadTree,
|
browseAndLoadTree,
|
||||||
|
|
@ -136,7 +136,6 @@ async function bootstrap() {
|
||||||
updateSelectedPointSummary();
|
updateSelectedPointSummary();
|
||||||
updatePointFilterSummary();
|
updatePointFilterSummary();
|
||||||
renderChart();
|
renderChart();
|
||||||
startLogs();
|
|
||||||
startPointSocket();
|
startPointSocket();
|
||||||
|
|
||||||
await withStatus(loadUnits());
|
await withStatus(loadUnits());
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ export const dom = {
|
||||||
pointSourceSelect: byId("pointSourceSelect"),
|
pointSourceSelect: byId("pointSourceSelect"),
|
||||||
pointSourceNodeCount: byId("pointSourceNodeCount"),
|
pointSourceNodeCount: byId("pointSourceNodeCount"),
|
||||||
openPointModalBtn: byId("openPointModal"),
|
openPointModalBtn: byId("openPointModal"),
|
||||||
logView: byId("logView"),
|
|
||||||
chartCanvas: byId("chartCanvas"),
|
chartCanvas: byId("chartCanvas"),
|
||||||
chartTitle: byId("chartTitle"),
|
chartTitle: byId("chartTitle"),
|
||||||
chartSummary: byId("chartSummary"),
|
chartSummary: byId("chartSummary"),
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,8 @@ export function renderEvents() {
|
||||||
|
|
||||||
state.events.forEach((item) => {
|
state.events.forEach((item) => {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "list-item event-card";
|
row.className = "event-card";
|
||||||
row.innerHTML = `
|
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>`;
|
||||||
<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>
|
|
||||||
`;
|
|
||||||
dom.eventList.appendChild(row);
|
dom.eventList.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,9 @@
|
||||||
import { appendChartPoint } from "./chart.js";
|
import { appendChartPoint } from "./chart.js";
|
||||||
import { dom } from "./dom.js";
|
|
||||||
import { prependEvent } from "./events.js";
|
import { prependEvent } from "./events.js";
|
||||||
import { formatValue } from "./points.js";
|
import { formatValue } from "./points.js";
|
||||||
import { state } from "./state.js";
|
import { state } from "./state.js";
|
||||||
import { renderUnits } from "./units.js";
|
import { renderUnits } from "./units.js";
|
||||||
|
|
||||||
function escapeHtml(text) {
|
|
||||||
return text
|
|
||||||
.replaceAll("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">");
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
export function startPointSocket() {
|
||||||
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
||||||
const ws = new WebSocket(`${protocol}://${location.host}/ws/public`);
|
const ws = new WebSocket(`${protocol}://${location.host}/ws/public`);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ export const state = {
|
||||||
chartPointId: null,
|
chartPointId: null,
|
||||||
chartPointName: "",
|
chartPointName: "",
|
||||||
chartData: [],
|
chartData: [],
|
||||||
logSource: null,
|
|
||||||
pointSocket: null,
|
pointSocket: null,
|
||||||
apiDocLoaded: false,
|
apiDocLoaded: false,
|
||||||
runtimes: new Map(), // unit_id -> UnitRuntime
|
runtimes: new Map(), // unit_id -> UnitRuntime
|
||||||
|
|
|
||||||
|
|
@ -660,8 +660,7 @@ button.danger:hover { background: var(--danger-hover); }
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit-list,
|
.unit-list {
|
||||||
.event-list {
|
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -670,20 +669,32 @@ button.danger:hover { background: var(--danger-hover); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-card {
|
.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 {
|
.event-card:hover {
|
||||||
background: var(--surface);
|
background: var(--surface-hover, var(--surface));
|
||||||
border-color: var(--border);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-section {
|
.event-type {
|
||||||
flex-basis: 42%;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-section {
|
.event-message {
|
||||||
flex-basis: 58%;
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-select-row {
|
.equipment-select-row {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue