Skip to main content

kaptein_viewmodel/
render.rs

1//! Layer 1 — the data plane.
2//!
3//! A virtualized, queryable source that emits deltas. It never materializes the world:
4//! the frontend asks for a `Page` and receives `RowPatch` deltas keyed by stable `RowId`.
5//!
6//! The trait is deliberately **object-safe, async, fallible, and streaming** so it can
7//! cross the `serve`/gRPC-Web boundary (ADR-0002): the time machine replays historical
8//! state, test fixtures feed recordings, `serve` proxies over the network, and the fleet
9//! layer aggregates several clusters — four runtime-swappable implementations.
10
11use std::ops::Range;
12use std::pin::Pin;
13
14use futures_util::Stream;
15use serde::{Deserialize, Serialize};
16
17use crate::error::Error;
18
19/// A monotonically increasing revision of the underlying store.
20///
21/// Consumers use it to detect staleness: if a held revision is older than the latest,
22/// they re-query rather than assuming their snapshot is current.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
24pub struct Revision(pub u64);
25
26/// A stable row identity — a Kubernetes `uid`, or a `group/kind/namespace/name` tuple
27/// when a `uid` is unavailable. Never a positional index.
28#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct RowId(pub String);
30
31/// A lazy, virtualized query against the data plane.
32///
33/// `start`/`end`, `sort`, and `filter` describe what the frontend wants *now* — the
34/// store returns a bounded page, never a full materialization of every object. The
35/// window is a plain `start`/`end` pair (not `std::ops::Range<usize>`) so it serializes
36/// stably over the wire.
37#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
38pub struct Query {
39    /// Inclusive window start (e.g. 400 for a virtualized table window).
40    pub start: usize,
41    /// Exclusive window end (e.g. 460).
42    pub end: usize,
43    /// Sort key and direction (column id + descending flag).
44    pub sort: Option<SortSpec>,
45    /// Filter predicate in a stable, serializable form (not a closure — it must cross
46    /// the `serve`/gRPC-Web boundary).
47    pub filter: Option<Filter>,
48}
49
50impl Query {
51    pub fn range(&self) -> Range<usize> {
52        self.start..self.end
53    }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct SortSpec {
58    pub column: String,
59    pub descending: bool,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct Filter {
64    /// A string in a documented query language (e.g. `metadata.labels.app = "foo"`).
65    pub expression: String,
66}
67
68/// A single cell value. Typed and redaction-aware (secrets never reach a cell as a
69/// plaintext value — the semantic layer already replaced them with a marker).
70///
71/// Variants use `#[serde(tag = "type")]` for a stable wire representation.
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73#[serde(tag = "type", rename_all = "snake_case")]
74pub enum Cell {
75    Text {
76        value: String,
77    },
78    Number {
79        value: i64,
80    },
81    /// A typed instant (unix epoch millis) — the frontend formats and localizes, so
82    /// sorting and localization stay possible.
83    Timestamp {
84        millis: i64,
85    },
86    /// A redacted value — the frontend renders a mask, never the secret.
87    Redacted,
88    /// A typed status chip; `level` drives color, `label_key` is localized by the
89    /// frontend. Status inference lives in the semantic layer, not in string matching.
90    Status {
91        level: StatusLevel,
92        label_key: String,
93    },
94}
95
96/// The severity of a status cell.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum StatusLevel {
100    Ok,
101    Info,
102    Warning,
103    Error,
104    Pending,
105}
106
107/// One row of the virtualized table, keyed by a stable identity.
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109pub struct Row {
110    pub id: RowId,
111    pub cells: Vec<Cell>,
112}
113
114/// A page of rows plus enough metadata to render it correctly.
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116pub struct Page {
117    pub rows: Vec<Row>,
118    /// Total number of matching rows (for scrollbar sizing), not just this page.
119    pub total: usize,
120    /// The revision this page reflects.
121    pub revision: Revision,
122}
123
124/// A delta to a specific row, keyed by `RowId` — never by position. Position is
125/// geometry, which the frontend owns; keying by identity keeps the patch stream
126/// idempotent and reorder-safe (a reconnect can re-apply the same patch without harm).
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub enum RowPatch {
129    Upsert { id: RowId, row: Row },
130    Remove { id: RowId },
131}
132
133/// The data plane exposed to every frontend.
134///
135/// Object-safe (`Box<dyn DataPlane>`), async, fallible, and streaming — matching ADR-0002
136/// (browser → `serve` → `kaptein-core`) and the unified error enum.
137#[async_trait::async_trait]
138pub trait DataPlane: Send + Sync {
139    /// Execute a lazy query and return a bounded page.
140    async fn query(&self, query: &Query) -> Result<Page, Error>;
141
142    /// Subscribe to deltas from the given revision onward.
143    fn subscribe(&self, from: Revision) -> Pin<Box<dyn Stream<Item = RowPatch> + Send>>;
144}