Teaching My Dashboard to Think Across Modules

Life Hub had 239 MCP tools and 34 UI pages. Each module was smart in isolation, but they didn't talk to each other. Here's how a cross-module intelligence layer changed everything.

Teaching My Dashboard to Think Across Modules

Last month I wrote about teaching Life Hub to think across categories: adding cross-category correlations, co-anomaly detection, and person-centric context. That work connected data domains that had been running independently: sleep affecting mood, spending correlating with stress, social energy predicting the next day's productivity.

But I'd been ignoring a bigger problem. The intelligence modules themselves were siloed.

The Problem: Smart Modules That Don't Talk

After a systematic product evaluation, Life Hub had:

  • 239 MCP tools across 5 domains
  • 34 UI pages with 134 HTMX interactive endpoints
  • 21 data collectors pulling from external services
  • A pillar scoring engine with 60+ sub-scores across 8 life pillars
  • Granger causality tests running on 13 metric pairs
  • Co-anomaly detection, mood forecasting, energy prediction, advisor triggers

Each module was genuinely intelligent in its own right. The Granger causality engine uses proper OLS regression with Gaussian elimination: no numpy, pure Python. The next-action system considers 12 different factors including recovery state, weather, and calendar load. The baselines system tracks rolling 30-day statistics with z-score utilities.

But when an advisor trigger fired "mood low for 3 consecutive days," the advisor had no idea why. The Granger tests knew that sleep causally affects mood with a 2-day lag. The co-anomaly detector knew that mood, sleep, and habits had all dropped simultaneously. An active experiment in the health pillar might explain the change entirely.

None of this context reached the trigger. The advisor gave generic advice because the intelligence was locked in separate modules.

CrossModuleContext: One Object, All Signals

The fix was a unified context assembly layer: not a cache, not a new database, just a Python dataclass that pulls from existing modules on demand:

@dataclass
class CrossModuleContext:
    baselines: dict[str, Any]           # Personal rolling stats
    causal_findings: list[dict]          # Granger test results
    top_causal_finding: dict | None      # Strongest relationship
    co_anomalies: list[dict]             # Multi-metric drops
    mood_forecast: dict | None           # 7-day prediction
    energy_forecast: dict | None         # Tomorrow's energy
    pillar_scores: dict[str, float]      # Current 0-10 scores
    weakest_pillars: list[str]           # Below 6.0
    active_experiments: list[dict]       # May affect interpretation

The key method is enrich_trigger(): it takes an advisor trigger dict and adds causal context:

def enrich_trigger(self, trigger: dict) -> dict:
    # Map trigger types to metrics
    metric = {"mood_drop": "mood_score", "sleep_decline": "sleep_hours", ...}
    
    explanation = self.causal_explanation_for(metric)
    if explanation:
        trigger["causal_context"] = explanation
    # Also check co-anomalies and experiments
    return trigger

Now when the mood trigger fires, the advisor commentary can reference actual causal data: "Your mood has been low for 3 days. Strong causal link: higher sleep_hours tends to increase mood_score with ~1-day lag." The LLM generating the commentary gets this context and produces specific, actionable advice instead of generic coaching.

Wiring It Into Everything

The context layer connects to 7 systems:

SystemWhat It Gets
Advisor triggersCausal explanations in commentary + webhooks
Next actionsReason enrichment ("habit_rate causally affects mood")
Mood forecastDiscovers its own causal predictors and reports them
Daily briefTop causal finding + energy outlook for LLM context
Life Wheel pagePer-pillar causal drivers + experiment badges
Trends pageGranger pairs table + co-anomaly alert banner
Today page3-day energy outlook card

The mood forecast integration was particularly satisfying. Previously, forecast_mood() used three signals: day-of-week seasonality, sleep correlation, and meeting correlation. Now it queries the cross-module context for significant Granger findings where mood is the effect variable, and reports them as named causal predictors:

for finding in xctx.causal_findings:
    if finding["y"] == "mood_score" and finding["strength"] in ("strong", "moderate"):
        causal_adjustments.append({
            "predictor": finding["x"],
            "lag": finding["best_lag"],
            "direction": finding["direction"],
        })

The forecast output now includes a causal_predictors list and active_experiment_warnings: if you're running a health experiment, the forecast's confidence downgrades from "high" to "medium" because the experimental intervention may confound the prediction.

Performance: Cache Once, Use Everywhere

The context assembly queries baselines (30-day rolling stats), Granger causality (60-day scan), co-anomalies (14-day scan), and active experiments. That's expensive, and 6 different call sites were running it independently.

The fix: one cached wrapper with a 120-second TTL:

@ttl_cache(ttl=120, key_prefix="cross_module_context")
async def cached_cross_module_context():
    async with get_session() as session:
        return await build_cross_module_context(
            session, include_forecasts=False, include_pillars=False,
        )

All 6 call sites (advisor triggers, next actions, predictions, daily brief, Life Wheel, Trends) now go through this single cached entry point. In practice, the context computes once when the first page loads, and every subsequent page within 2 minutes gets the cached result for free.

The TTL of 120 seconds works because the underlying data (causal findings, baselines) changes on a daily scale, not a per-request scale. Granger tests over 60-day windows don't shift meaningfully in 2 minutes.

Beyond Wiring: What Else Shipped

The cross-module work was the architectural centerpiece, but it enabled a cascade of smaller features that would have been awkward without it:

Thought → Action Pipeline. Every thought captured in the Open Brain now has two conversion buttons: "→ Action" creates an ActionItem with one click, "→ Goal" opens a dropdown of active goals and creates a milestone. Previously, thoughts were captured but never became anything.

Forward Energy Planner. A 3-day outlook card on the Today page that combines calendar workload (CalDAV meeting density), mood forecast, and energy prediction into a per-day strategy: push (high energy, low meetings: do hard things), protect (heavy meeting day: guard your focus), balanced, or recovery (low energy: scale back). The same outlook appears on the Calendar page.

Project Activity Feeds. The project detail page now shows a keyword-matched timeline of GitHub commits, time blocks, and journal entries related to the project. No manual tagging: project names are tokenized and matched against activity metadata.

Learning Pattern Analysis. Reading (Miniflux) and audiobook (Audiobookshelf) activity is analyzed for patterns: top feeds, top authors, reading streaks, time-of-day preferences, feed diversity. Available via MCP tool and eventually the Intellect pillar page.

Experiment-Aware Forecasts. The mood forecast now checks for active experiments targeting health, happiness, or connection pillars. If one exists, the forecast's confidence is downgraded and the experiment is flagged in the output. This prevents the system from treating an intentional intervention as anomalous behavior.

The Shift: From Data-Rich to Insight-Connected

The data didn't change. The collectors are the same 21 sources. The ORM models are the same 51 tables. The intelligence modules are the same Granger tests, baselines, anomaly detectors.

What changed is that every surface that shows intelligence now has access to context from every other module. The advisor doesn't just know your mood is low: it knows why (causal), how unusual (baselines), what else dropped (co-anomalies), and whether you're running an experiment (experiments). The forecast doesn't just extrapolate: it discovers its own predictors and flags when its assumptions might be wrong.

The implementation is 250 lines of Python. One dataclass, one assembly function, one cache wrapper. No ML frameworks, no vector databases, no external services. Just a layer that asks "what do the other modules know?" and makes the answer available everywhere.

2,791 tests pass. The total code footprint for P22 was about 3,500 lines across 20 files and 4 new modules. Same data, dramatically better answers.