feat(event): stream created events over websocket

This commit is contained in:
caoqianming 2026-03-24 12:28:12 +08:00
parent 97d2f6ebf8
commit c50127b9d0
4 changed files with 60 additions and 20 deletions

View File

@ -55,9 +55,11 @@ impl EventManager {
let control_cm = connection_manager.clone(); let control_cm = connection_manager.clone();
let control_pool = pool.clone(); let control_pool = pool.clone();
let control_ws_manager = ws_manager.clone();
tokio::spawn(async move { tokio::spawn(async move {
while let Some(event) = control_receiver.recv().await { while let Some(event) = control_receiver.recv().await {
handle_control_event(event, &control_pool, &control_cm).await; handle_control_event(event, &control_pool, &control_cm, control_ws_manager.as_ref())
.await;
} }
}); });
@ -133,8 +135,9 @@ async fn handle_control_event(
event: AppEvent, event: AppEvent,
pool: &sqlx::PgPool, pool: &sqlx::PgPool,
connection_manager: &std::sync::Arc<crate::connection::ConnectionManager>, connection_manager: &std::sync::Arc<crate::connection::ConnectionManager>,
ws_manager: Option<&std::sync::Arc<crate::websocket::WebSocketManager>>,
) { ) {
persist_event_if_needed(&event, pool).await; persist_event_if_needed(&event, pool, ws_manager).await;
match event { match event {
AppEvent::SourceCreate { source_id } => { AppEvent::SourceCreate { source_id } => {
@ -222,7 +225,11 @@ async fn handle_control_event(
} }
} }
async fn persist_event_if_needed(event: &AppEvent, pool: &sqlx::PgPool) { async fn persist_event_if_needed(
event: &AppEvent,
pool: &sqlx::PgPool,
ws_manager: Option<&std::sync::Arc<crate::websocket::WebSocketManager>>,
) {
let record = match event { let record = match event {
AppEvent::SourceCreate { source_id } => Some(( AppEvent::SourceCreate { source_id } => Some((
"source.created", "source.created",
@ -310,10 +317,11 @@ async fn persist_event_if_needed(event: &AppEvent, pool: &sqlx::PgPool) {
return; return;
}; };
if let Err(err) = sqlx::query( let inserted = sqlx::query_as::<_, crate::model::EventRecord>(
r#" r#"
INSERT INTO event (event_type, level, unit_id, equipment_id, source_id, message, payload) INSERT INTO event (event_type, level, unit_id, equipment_id, source_id, message, payload)
VALUES ($1, $2, $3, $4, $5, $6, $7) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
"#, "#,
) )
.bind(event_type) .bind(event_type)
@ -323,10 +331,21 @@ async fn persist_event_if_needed(event: &AppEvent, pool: &sqlx::PgPool) {
.bind(source_id) .bind(source_id)
.bind(message) .bind(message)
.bind(sqlx::types::Json(payload)) .bind(sqlx::types::Json(payload))
.execute(pool) .fetch_one(pool)
.await .await;
{
tracing::warn!("Failed to persist event: {}", err); match inserted {
Ok(record) => {
if let Some(ws_manager) = ws_manager {
let ws_message = crate::websocket::WsMessage::EventCreated(record);
if let Err(err) = ws_manager.send_to_public(ws_message).await {
tracing::warn!("Failed to broadcast event websocket message: {}", err);
}
}
}
Err(err) => {
tracing::warn!("Failed to persist event: {}", err);
}
} }
} }

View File

@ -17,6 +17,7 @@ use uuid::Uuid;
pub enum WsMessage { pub enum WsMessage {
PointNewValue(crate::telemetry::PointMonitorInfo), PointNewValue(crate::telemetry::PointMonitorInfo),
PointSetValueBatchResult(crate::connection::BatchSetPointValueRes), PointSetValueBatchResult(crate::connection::BatchSetPointValueRes),
EventCreated(crate::model::EventRecord),
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -32,6 +32,22 @@ export function renderEvents() {
}); });
} }
function matchesCurrentFilter(item) {
if (state.selectedUnitId && item.unit_id !== state.selectedUnitId) {
return false;
}
return true;
}
export function prependEvent(item) {
if (!matchesCurrentFilter(item)) {
return;
}
state.events = [item, ...state.events.filter((existing) => existing.id !== item.id)].slice(0, 20);
renderEvents();
}
export async function loadEvents() { export async function loadEvents() {
const params = new URLSearchParams({ const params = new URLSearchParams({
page: "1", page: "1",

View File

@ -1,5 +1,6 @@
import { appendChartPoint } from "./chart.js"; import { appendChartPoint } from "./chart.js";
import { dom } from "./dom.js"; import { dom } from "./dom.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";
@ -74,21 +75,24 @@ export function startPointSocket() {
ws.onmessage = (event) => { ws.onmessage = (event) => {
try { try {
const payload = JSON.parse(event.data); const payload = JSON.parse(event.data);
if (payload.type !== "PointNewValue" && payload.type !== "point_new_value") { if (payload.type === "PointNewValue" || payload.type === "point_new_value") {
const data = payload.data;
const entry = state.pointEls.get(data.point_id);
if (entry) {
entry.value.textContent = formatValue(data);
entry.quality.className = `badge quality-${(data.quality || "unknown").toLowerCase()}`;
entry.quality.textContent = (data.quality || "unknown").toUpperCase();
entry.time.textContent = data.timestamp || "--";
}
if (state.chartPointId === data.point_id) {
appendChartPoint(data);
}
return; return;
} }
const data = payload.data; if (payload.type === "EventCreated" || payload.type === "event_created") {
const entry = state.pointEls.get(data.point_id); prependEvent(payload.data);
if (entry) {
entry.value.textContent = formatValue(data);
entry.quality.className = `badge quality-${(data.quality || "unknown").toLowerCase()}`;
entry.quality.textContent = (data.quality || "unknown").toUpperCase();
entry.time.textContent = data.timestamp || "--";
}
if (state.chartPointId === data.point_id) {
appendChartPoint(data);
} }
} catch { } catch {
// ignore malformed messages // ignore malformed messages