use std::sync::Arc; use datastream::frame::{Frame, StreamId}; use parking_lot::RwLock; use serde::Serialize; use serde_json::Value; use crate::FrameEvent; /// Read-only interpretation of one or more datastream channels. /// /// Views are observation-only: they fold incoming frames into local state and /// expose JSON/HTML. They do not send control messages back to the runtime. pub trait DashboardView: Send + Sync { fn id(&self) -> &'static str; fn title(&self) -> &'static str; fn path(&self) -> &'static str { self.id() } fn channels(&self) -> &'static [&'static str]; fn ingest(&self, stream: &StreamId, frame: &Frame, event: &FrameEvent); fn snapshot_json(&self) -> Value; fn html(&self) -> Option<&'static str> { None } } #[derive(Debug, Clone, Serialize)] pub struct ViewDescriptor { pub id: &'static str, pub title: &'static str, pub path: &'static str, pub page: String, pub api: String, pub channels: &'static [&'static str], } #[derive(Default)] pub(crate) struct ViewRegistry { views: RwLock>>, } impl ViewRegistry { pub fn new() -> Self { Self::default() } pub fn register(&self, view: Arc) { let mut views = self.views.write(); views.retain(|existing| existing.id() != view.id() && existing.path() != view.path()); views.push(view); } pub fn dispatch(&self, stream: &StreamId, frame: &Frame, event: &FrameEvent) { let channel = event.channel.as_str(); for view in self.views.read().iter() { let channels = view.channels(); if channels.is_empty() || channels.contains(&channel) { view.ingest(stream, frame, event); } } } pub fn descriptors(&self) -> Vec { self.views .read() .iter() .map(|view| ViewDescriptor { id: view.id(), title: view.title(), path: view.path(), page: format!("/view/{}", view.path()), api: format!("/api/view/{}", view.path()), channels: view.channels(), }) .collect() } pub fn snapshot(&self, path: &str) -> Option { self.views .read() .iter() .find(|view| view.path() == path || view.id() == path) .map(|view| view.snapshot_json()) } pub fn html(&self, path: &str) -> Option<&'static str> { self.views .read() .iter() .find(|view| view.path() == path || view.id() == path) .and_then(|view| view.html()) } }