feat(brahman-handshake): ListMatches endpoint + timeline en broker-explorer

Iter 21. Cierra el loop iniciado en iter 20: ahora se ven sesiones
+ matches actuales + cómo cambian a través del tiempo.

brahman-handshake/messages:
- Frame::ListMatches → Frame::MatchList(Vec<brahman_broker::Match>).

brahman-handshake/server:
- run_post_handshake pasa Option<&SharedBroker> a handle_inbound_frame.
- Sin broker configurado → MatchList vacía (no error).

brahman-handshake/client + brahman-sidecar:
- Client::list_matches() análogo a list_sessions, drena MatchEvents.
- list_matches / list_matches_blocking, mismo patrón.

brahman-broker-explorer:
- Poll-tick agrega list_matches_blocking además de list_sessions.
- last_match_keys: HashSet<MatchKey> para diff entre ticks.
- timeline: VecDeque<TimelineEntry> cap 50.
- diff_matches (free fn): Available para keys nuevas, Lost para
  desaparecidas. Primer tick marca todo Available (boot UX).
- Render: stat_card "Timeline" con HH:MM:SS {+/-} formato compacto.

5 tests broker-explorer (3 nuevos del diff). Stack verde.

Decisión: timeline polled cada POLL_INTERVAL=5s, no push. MatchEvents
del broker son consumer-céntricos (cada session ve sólo SUS matches);
"system-wide timeline" requeriría broker subscribe-all (mucho scope).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-10 15:53:38 +00:00
parent 99cd685dc1
commit a97f6b98f3
9 changed files with 499 additions and 16 deletions
@@ -197,6 +197,52 @@ pub fn list_sessions_blocking(
rt.block_on(list_sessions(label))
}
/// Análogo a `list_sessions` pero pide los matches activos del
/// broker. La Card observer es la misma forma minimalista (sin
/// flow.input/output) — el endpoint no requiere participar en
/// matching.
pub async fn list_matches(
observer_label: impl Into<String>,
) -> Result<brahman_handshake::messages::MatchList, ConsumerError> {
let init_path = transport::default_socket_path();
let card = Card {
payload: Payload::Virtual,
supervision: Supervision::OneShot,
lifecycle: Lifecycle::Oneshot,
priority: Priority::Normal,
kind: CardKind::Ente,
flow: Flows {
input: vec![],
output: vec![],
},
..Card::new(observer_label)
};
let mut client = Client::connect(&init_path, card)
.await
.map_err(|source| ConsumerError::Connect {
socket: init_path.clone(),
source,
})?;
let list = client.list_matches().await?;
let _ = client.farewell().await;
Ok(list)
}
/// Wrapper bloqueante de [`list_matches`].
pub fn list_matches_blocking(
observer_label: impl Into<String>,
) -> Result<brahman_handshake::messages::MatchList, ConsumerError> {
let label = observer_label.into();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.map_err(|e| ConsumerError::Runtime(e.to_string()))?;
rt.block_on(list_matches(label))
}
fn describe_first_input(card: &Card) -> (String, String) {
match card.flow.input.first() {
Some(flow) => {
+2 -2
View File
@@ -18,8 +18,8 @@
pub mod discovery;
pub use discovery::{
await_provider, await_provider_blocking, build_consumer_card, list_sessions,
list_sessions_blocking, ConsumerError,
await_provider, await_provider_blocking, build_consumer_card, list_matches,
list_matches_blocking, list_sessions, list_sessions_blocking, ConsumerError,
};
use std::collections::HashMap;