tawasuyu

tawasuyu/brahman

Rust 492TOML 194Markdown 53JSON 176939 Definiciones

feat(tahuantinsuyu): fase 11 — persistencia de module_configs por carta

Los toggles, sliders y partner pickers de cada overlay (transit,
progression, solar_arc, synastry) ahora persisten por carta en la
tabla SQLite `module_state` (que estaba creada desde fase 1 pero
sin cablear). Cambiar de carta y volver mantiene exactamente el
estado que el usuario dejó.

- shell:
  - apply_selection(Chart): tras setear defaults (target_age_years =
    edad actual), llama load_persisted_module_states(chart.id) que
    mergea sobre los defaults los valores guardados. Luego
    sync_panel_from_configs empuja todos los toggles/sliders al
    panel para reflejar el estado restaurado. Render al final.
  - load_persisted_module_states: lee list_module_states(chart_id),
    reconstruye el JSON combinado (mergea `enabled` de la columna
    SQL en el config), y lo mergea sobre lo que ya hay en
    module_configs. Vacant entries se insertan tal cual; occupied
    se patchean field-a-field para no perder defaults no guardados.
  - sync_panel_from_configs: itera module_configs, push toggle/slider
    al panel por cada key Bool/f64.
  - persist_module(module_id): extrae enabled del JSON, deja resto en
    config_json, llama upsert_module_state. Invocada desde
    on_panel_event "else" tras cada update + desde on_canvas_event
    para [T] + tras auto-disable del conflicting module en mutual
    exclusion.
- store: nuevo test module_state_roundtrip que cubre upsert/list +
  cambio de enabled vía upsert (UPSERT clause de fase 1 vuelve a
  validarse).

Flujo de usuario: ajustás el slider de progresión a 42.5 años,
activás synastry, cambiás de carta, volvés — todo está como lo
dejaste. La DB persiste por chart_id, así que distintos sujetos
mantienen estados independientes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

22e6ed6a71e9020477fe871c094467644a086409 · por sergio · (hace 5 meses) · padre 97a6aab88 · ver el árbol en este commit

Delta semántico · 7

crates/apps/tahuantinsuyu/src/shell.rs

crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs

2 archivos · +184 −8

diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs
index 5b5aad4..fcdc296 100644
--- a/crates/apps/tahuantinsuyu/src/shell.rs
+++ b/crates/apps/tahuantinsuyu/src/shell.rs
@@ -32,7 +32,7 @@ use tahuantinsuyu_canvas::{
     AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
 };
 use tahuantinsuyu_engine::{LayerKind, PipelineRequest, compose};
-use tahuantinsuyu_model::{Chart, TreeSelection};
+use tahuantinsuyu_model::{Chart, ChartId, ModuleState, TreeSelection};
 use tahuantinsuyu_panel::{ControlPanel, PanelEvent};
 use tahuantinsuyu_store::Store;
 use tahuantinsuyu_tree::{TahuantinsuyuTree, TreeEvent};
@@ -118,9 +118,10 @@ impl Shell {
                 let age = current_age_years(&chart.birth_data);
                 self.current_chart = Some(chart.clone());
                 self.current_offset_minutes = 0;
-                // Inicializar la edad objetivo de los módulos basados
-                // en edad con la edad actual del sujeto, así sus
-                // sliders arrancan donde corresponde al activarse.
+                // 1) Defaults frescos para esta carta: edad objetivo =
+                //    edad actual. Estos quedan en module_configs como
+                //    valor base si el usuario nunca tocó el slider.
+                self.module_configs.clear();
                 for module_id in ["progression", "solar_arc"] {
                     let entry = self
                         .module_configs
@@ -130,12 +131,15 @@ impl Shell {
                         map.insert("target_age_years".into(), serde_json::json!(age));
                     }
                 }
-                self.render_current(cx);
+                // 2) Sobreescribir con lo que el usuario persistió la
+                //    última vez para esta carta (SQLite `module_state`).
+                self.load_persisted_module_states(chart.id);
+                // 3) Sincronizar panel: active_kind + toggles/sliders.
                 self.panel.update(cx, |p, cx| {
                     p.set_active_kind(Some(chart.kind), cx);
-                    p.set_slider("progression", "target_age_years", age, cx);
-                    p.set_slider("solar_arc", "target_age_years", age, cx);
                 });
+                self.sync_panel_from_configs(cx);
+                self.render_current(cx);
             }
             TreeSelection::Contact(id) => {
                 self.current_chart = None;
@@ -225,6 +229,101 @@ impl Shell {
         siblings.into_iter().find(|c| c.id != current.id)
     }
 
+    /// Lee `module_state` desde SQLite para la carta dada y los mergea
+    /// con los defaults ya cargados en `module_configs`. Los valores
+    /// persistidos ganan sobre los defaults.
+    fn load_persisted_module_states(&mut self, chart_id: ChartId) {
+        let states = match self.store.list_module_states(chart_id) {
+            Ok(s) => s,
+            Err(e) => {
+                eprintln!("[shell] list_module_states {}: {}", chart_id, e);
+                return;
+            }
+        };
+        for st in states {
+            // Re-mergeamos `enabled` (columna separada en SQL) dentro
+            // del JSON config, así el resto del shell sigue leyendo
+            // todo desde una única estructura.
+            let mut combined = match st.config {
+                serde_json::Value::Object(m) => serde_json::Value::Object(m),
+                _ => serde_json::json!({}),
+            };
+            if let serde_json::Value::Object(map) = &mut combined {
+                map.insert("enabled".into(), serde_json::Value::Bool(st.enabled));
+            }
+            // Mergear sobre defaults previos (no sobreescribir si la
+            // entrada nueva no trae un campo).
+            match self.module_configs.entry(st.module_id) {
+                std::collections::hash_map::Entry::Vacant(v) => {
+                    v.insert(combined);
+                }
+                std::collections::hash_map::Entry::Occupied(mut o) => {
+                    if let (serde_json::Value::Object(dst), serde_json::Value::Object(src)) =
+                        (o.get_mut(), &combined)
+                    {
+                        for (k, v) in src {
+                            dst.insert(k.clone(), v.clone());
+                        }
+                    } else {
+                        o.insert(combined);
+                    }
+                }
+            }
+        }
+    }
+
+    /// Pushea cada toggle/slider del `module_configs` al panel para que
+    /// la UI refleje el estado persistido al cargar una carta.
+    fn sync_panel_from_configs(&mut self, cx: &mut Context<Self>) {
+        let snapshot: Vec<(String, serde_json::Value)> = self
+            .module_configs
+            .iter()
+            .map(|(k, v)| (k.clone(), v.clone()))
+            .collect();
+        self.panel.update(cx, |p, cx| {
+            for (module_id, config) in &snapshot {
+                if let serde_json::Value::Object(map) = config {
+                    for (key, value) in map {
+                        if let Some(b) = value.as_bool() {
+                            p.set_toggle(module_id, key, b, cx);
+                        } else if let Some(f) = value.as_f64() {
+                            p.set_slider(module_id, key, f, cx);
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+    /// Persiste el estado actual de un módulo a SQLite. Extrae
+    /// `enabled` del JSON y lo guarda en la columna dedicada; el resto
+    /// va al `config_json`.
+    fn persist_module(&self, module_id: &str) {
+        let Some(chart) = self.current_chart.as_ref() else {
+            return;
+        };
+        let Some(config) = self.module_configs.get(module_id) else {
+            return;
+        };
+        let enabled = config
+            .get("enabled")
+            .and_then(|v| v.as_bool())
+            .unwrap_or(false);
+        let mut clean = config.clone();
+        if let serde_json::Value::Object(map) = &mut clean {
+            map.remove("enabled");
+        }
+        let state = ModuleState {
+            chart_id: chart.id,
+            module_id: module_id.to_string(),
+            enabled,
+            config: clean,
+        };
+        if let Err(e) = self.store.upsert_module_state(&state) {
+            eprintln!("[shell] upsert_module_state {}: {}", module_id, e);
+        }
+    }
+
     /// Lee `target_age_years` del módulo o cae a la edad actual del
     /// sujeto (calculada desde la fecha de nacimiento y el reloj).
     fn module_age_or_current(&self, module_id: &str) -> f64 {
@@ -282,6 +381,7 @@ impl Shell {
                 // module_configs["transit"]["enabled"] + re-render.
                 if matches!(kind, LayerKind::Outer) {
                     set_module_enabled(&mut self.module_configs, "transit", *visible);
+                    self.persist_module("transit");
                     self.panel.update(cx, |p, cx| {
                         p.set_toggle("transit", "enabled", *visible, cx)
                     });
@@ -353,6 +453,7 @@ impl Shell {
                                 self.panel.update(cx, |p, cx| {
                                     p.set_toggle(&other_str, "enabled", false, cx)
                                 });
+                                self.persist_module(&other_str);
                             }
                         }
                     }
@@ -364,6 +465,7 @@ impl Shell {
                             c.set_layer_visible(LayerKind::Outer, bool_val, cx)
                         });
                     }
+                    self.persist_module(module_id);
                     self.render_current(cx);
                 }
             }
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs
index bb345c4..439c1d7 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs
@@ -550,7 +550,7 @@ fn now_ms() -> i64 {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use tahuantinsuyu_model::{StoredBirthData, StoredChartConfig};
+    use tahuantinsuyu_model::{ModuleState, StoredBirthData, StoredChartConfig};
 
     #[test]
     fn open_and_migrate() {
@@ -559,6 +559,80 @@ mod tests {
         assert!(groups.is_empty());
     }
 
+    #[test]
+    fn module_state_roundtrip() {
+        let s = Store::in_memory().unwrap();
+        let g = s.create_group(None, "Familia", None).unwrap();
+        let c = s.create_contact(Some(g.id), "Sergio", None).unwrap();
+        let chart = s
+            .create_chart(
+                c.id,
+                ChartKind::Natal,
+                "Natal",
+                &StoredBirthData {
+                    year: 1987,
+                    month: 3,
+                    day: 14,
+                    hour: 5,
+                    minute: 22,
+                    second: 0.0,
+                    tz_offset_minutes: -240,
+                    latitude_deg: 10.4806,
+                    longitude_deg: -66.9036,
+                    altitude_m: 900.0,
+                    time_certainty: Default::default(),
+                    subject_name: None,
+                    birthplace_label: None,
+                },
+                &StoredChartConfig::default(),
+                None,
+            )
+            .unwrap();
+
+        // Persistir dos módulos con configs distintos.
+        let state1 = ModuleState {
+            chart_id: chart.id,
+            module_id: "transit".into(),
+            enabled: true,
+            config: serde_json::json!({}),
+        };
+        let state2 = ModuleState {
+            chart_id: chart.id,
+            module_id: "progression".into(),
+            enabled: false,
+            config: serde_json::json!({ "target_age_years": 42.5 }),
+        };
+        s.upsert_module_state(&state1).unwrap();
+        s.upsert_module_state(&state2).unwrap();
+
+        let loaded = s.list_module_states(chart.id).unwrap();
+        assert_eq!(loaded.len(), 2);
+        let by_id: std::collections::HashMap<_, _> =
+            loaded.into_iter().map(|m| (m.module_id.clone(), m)).collect();
+        assert_eq!(by_id["transit"].enabled, true);
+        assert_eq!(by_id["progression"].enabled, false);
+        assert_eq!(
+            by_id["progression"]
+                .config
+                .get("target_age_years")
+                .and_then(|v| v.as_f64()),
+            Some(42.5)
+        );
+
+        // Upsert: cambiar enabled de transit a false.
+        let state1_off = ModuleState {
+            chart_id: chart.id,
+            module_id: "transit".into(),
+            enabled: false,
+            config: serde_json::json!({}),
+        };
+        s.upsert_module_state(&state1_off).unwrap();
+        let loaded = s.list_module_states(chart.id).unwrap();
+        let by_id: std::collections::HashMap<_, _> =
+            loaded.into_iter().map(|m| (m.module_id.clone(), m)).collect();
+        assert_eq!(by_id["transit"].enabled, false);
+    }
+
     #[test]
     fn full_hierarchy_roundtrip() {
         let s = Store::in_memory().unwrap();