feat(tahuantinsuyu): fase 24 — observabilidad del broker brahman

Primera pieza concreta de integración con el fractal brahman. La app
deja de ser standalone visible: ahora muestra el estado del broker
en el header con un badge actualizado cada 30s.

- Shell gana enum BrahmanStatus { Pending, Connected { count },
  Offline { reason } } + field brahman_status.
- spawn_brahman_status_loop arma un task cx.spawn que cada 30s
  invoca brahman_sidecar::list_sessions_blocking sobre el
  background_executor (no UI thread — list_sessions_blocking abre su
  propio tokio runtime, hacerlo en el UI panicearía con "nested
  runtime"). Update via this.update + cx.notify dispara repintado del
  badge.
- header agrega pill "Brahman ✓ N sessions" (color accent cuando
  conectado), "Brahman · offline" (fg_disabled) o "Brahman · …"
  (fg_muted) según el último ping. Entre el separador flex_grow y
  el theme_switcher.
- apps Cargo agrega brahman-sidecar como dep directa.

La Card de tahuantinsuyu (fase 1) sigue declarando los flows
`chart-request` (input) y `chart-result` (output), pero ESTOS NO
ESTÁN CABLEADOS A UN DATA PLANE — solo aparecen en el broker como
declaración. Para que tahuantinsuyu PUBLIQUE/CONSUMA datos reales
(otra app del fractal recibiendo una carta serializada, o pidiendo
un cómputo) hay que:
1) Abrir un service_socket Unix server en el sidecar
2) Implementar protocolo postcard sobre ese socket
3) Otro módulo descubre el socket via broker → conecta y envía/recibe

Eso es una fase separada (25+). Esta fase 24 cubre la observabilidad
mínima: la app sabe que el fractal está vivo y muestra el head count.
Cubre el espíritu del brief inicial ("integrar con yahweh que maneja
gpui para intercomunicar widgets") al nivel de visibility — el data
plane real es un proyecto en sí mismo.

cargo check verde. Sin tests nuevos (la lógica nueva es interacción
UI + background task — los tests serían smoke tests del Shell que
no tenemos).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-17 23:51:22 +00:00
parent 295c9ba554
commit a539fab15c
3 changed files with 78 additions and 0 deletions
+76
View File
@@ -49,6 +49,18 @@ use yahweh_widget_theme_switcher::theme_switcher;
const TREE_WIDTH: f32 = 280.0;
const PANEL_HEIGHT: f32 = 180.0;
/// Status del broker brahman tal como lo vimos en el último ping.
/// Se refresca cada 30 segundos desde un background task.
#[derive(Clone, Debug)]
pub enum BrahmanStatus {
/// Aún no probamos (boot, primer ciclo).
Pending,
/// Connect OK al broker, devolvió la lista de sessions activas.
Connected { session_count: usize },
/// Connect falló — broker no escucha en el socket o tomó timeout.
Offline { reason: String },
}
pub struct Shell {
store: Store,
#[allow(dead_code)]
@@ -63,6 +75,9 @@ pub struct Shell {
/// Splitter vertical entre el main_row (arriba) y el panel de
/// control (abajo).
outer_split: Entity<SplitContainer>,
/// Último estado conocido del broker brahman — refrescado cada
/// 30s desde el background task.
brahman_status: BrahmanStatus,
current_chart: Option<Chart>,
current_offset_minutes: i64,
/// Estado de los módulos overlay (transit, progression, …) por
@@ -162,15 +177,52 @@ impl Shell {
panel,
main_split,
outer_split,
brahman_status: BrahmanStatus::Pending,
current_chart: None,
current_offset_minutes: 0,
module_configs: HashMap::new(),
render_seq: 0,
};
shell.refresh_chart_options(cx);
shell.spawn_brahman_status_loop(cx);
shell
}
/// Loop que cada 30s pregunta al broker la lista de sessions
/// activas y actualiza `brahman_status`. El cómputo bloqueante
/// (list_sessions_blocking abre su propio tokio runtime) corre en
/// el background_executor — no bloquea el UI thread. Cuando llega
/// el resultado, el `this.update` dispara cx.notify para repintar
/// el badge del header.
fn spawn_brahman_status_loop(&self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| {
loop {
let result = cx
.background_executor()
.spawn(async {
brahman_sidecar::list_sessions_blocking("tahuantinsuyu-observer")
})
.await;
let _ = this.update(cx, |this, cx| {
this.brahman_status = match result {
Ok(list) => BrahmanStatus::Connected {
session_count: list.entries.len(),
},
Err(e) => BrahmanStatus::Offline {
reason: format!("{:?}", e),
},
};
cx.notify();
});
let timer = cx
.background_executor()
.timer(std::time::Duration::from_secs(30));
timer.await;
}
})
.detach();
}
/// Recarga la lista de opciones para los `Control::ChartPicker` y
/// la pushea al panel. Llamado al boot + tras cada
/// `TreeEvent::HierarchyChanged`.
@@ -844,6 +896,29 @@ impl Render for Shell {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = Theme::global(cx).clone();
// Badge del estado del broker brahman — pequeña pill con
// color según el estado actual del ping cada-30s.
let (badge_text, badge_color) = match &self.brahman_status {
BrahmanStatus::Pending => ("Brahman · …".to_string(), theme.fg_muted),
BrahmanStatus::Connected { session_count } => (
format!("Brahman ✓ {} sessions", session_count),
theme.accent,
),
BrahmanStatus::Offline { .. } => {
("Brahman · offline".to_string(), theme.fg_disabled)
}
};
let brahman_badge = div()
.px(px(8.0))
.py(px(2.0))
.rounded(px(8.0))
.bg(theme.bg_panel_alt.clone())
.border_1()
.border_color(theme.border)
.text_size(px(10.0))
.text_color(badge_color)
.child(SharedString::from(badge_text));
let header = div()
.h(px(34.0))
.px(px(12.0))
@@ -866,6 +941,7 @@ impl Render for Shell {
.child("estudio de astrología profesional"),
)
.child(div().flex_grow())
.child(brahman_badge)
.child(theme_switcher(cx));
let body = div()