tawasuyu

tawasuyu/brahman

Rust 483TOML 191Markdown 50JSON 176777 Definitions

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>

a539fab15ca0d46a1b6263c7af91303907802575 · by sergio · (5 months ago) · parent 295c9ba55 · browse the tree at this commit

Semantic delta · 5

crates/apps/tahuantinsuyu/src/shell.rs

3 files · +78 −0

diff --git a/Cargo.lock b/Cargo.lock
index 3b83ae6..e370d4c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10904,6 +10904,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
 name = "tahuantinsuyu"
 version = "0.1.0"
 dependencies = [
+ "brahman-sidecar",
  "directories",
  "gpui",
  "serde_json",
diff --git a/crates/apps/tahuantinsuyu/Cargo.toml b/crates/apps/tahuantinsuyu/Cargo.toml
index 3732241..f6e2994 100644
--- a/crates/apps/tahuantinsuyu/Cargo.toml
+++ b/crates/apps/tahuantinsuyu/Cargo.toml
@@ -15,6 +15,7 @@ tahuantinsuyu-panel = { path = "../../modules/tahuantinsuyu/tahuantinsuyu-panel"
 tahuantinsuyu-store = { path = "../../modules/tahuantinsuyu/tahuantinsuyu-store" }
 tahuantinsuyu-theme = { path = "../../modules/tahuantinsuyu/tahuantinsuyu-theme" }
 tahuantinsuyu-tree = { path = "../../modules/tahuantinsuyu/tahuantinsuyu-tree" }
+brahman-sidecar = { path = "../../shared/brahman-sidecar" }
 
 yahweh-bus = { workspace = true }
 yahweh-core = { workspace = true }
diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs
index c6cdea4..2a680c0 100644
--- a/crates/apps/tahuantinsuyu/src/shell.rs
+++ b/crates/apps/tahuantinsuyu/src/shell.rs
@@ -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()