Teaching My Dashboard to Predict, Not Just Report

Eight new intelligence features that shift a personal dashboard from reporting what happened to explaining why and predicting what's next: sleep architecture, goal health, relationship drift prediction, and more.

Teaching My Dashboard to Predict, Not Just Report

Life Hub has been tracking my data for months: mood, sleep, habits, goals, relationships, health metrics, finance, screen time. That's 249 MCP tools, 17 data collectors, and 53 database tables. The system knows a lot about my life.

But until this week, it was mostly a reporter. It could tell me what happened: your mood was 4/10 yesterday, you slept 6.2 hours, your "Learn German" goal hasn't moved in 12 days. Useful, but not intelligent. I wanted it to tell me why things are happening and what's coming.

That's what P23 was about: teaching the dashboard to predict, not just report. Eight new intelligence features, shipped in a single session using parallel AI agents, each building on infrastructure I'd already laid in previous phases.

The Philosophy: Depth Over Breadth

The temptation with any personal dashboard is to keep adding data sources. One more collector, one more API integration, one more chart. But after 17 collectors and 30 UI pages, I'd hit diminishing returns on breadth. The data was there. What was missing was the reasoning layer on top of it.

P23 was guided by a single question: for every metric the dashboard tracks, can it explain why it changed and predict what happens next?

That led to seven new features (the eighth, anomaly explanation, was already built and I just hadn't realized it):

  1. Sleep Architecture Analysis: stage breakdown, efficiency, circadian alignment
  2. Goal Health Composite: "this goal is stalling because Habit X dropped"
  3. Journal Entity Extraction: auto-link journal entries to people, goals, decisions
  4. Decision Outcome Tracking: track decisions, review them later, score quality
  5. Relationship Drift Prediction: detect quality decline before you lose touch
  6. Evidence Portfolio: unified "what I've tested" dashboard
  7. Auto Weekly Podcast: digest → NotebookLM → audio every Sunday

Sleep Architecture: Beyond "You Slept 7 Hours"

Hours of sleep is the least interesting sleep metric. What matters is the architecture: how much time you spend in deep vs. REM vs. light sleep, how efficient your sleep is (time asleep vs. time in bed), and how consistent your schedule is.

I had the data. Garmin sends sleep_deep_pct and sleep_rem_pct. WHOOP adds efficiency, cycle count, and disturbance count. But these numbers were sitting in a HealthMetric table, displayed as individual readings on the health page. Nobody was interpreting them.

The new compute_sleep_architecture() is a pure function: no DB access, no async. It takes the already-fetched metrics_by_type dict from the health page and computes everything:

def compute_sleep_architecture(
    metrics_by_type: dict[str, list],
    sleep_logs: list | None = None,
) -> dict | None:
    deep_vals = [m["value"] for m in metrics_by_type.get("sleep_deep_pct", [])]
    rem_vals = [m["value"] for m in metrics_by_type.get("sleep_rem_pct", [])]
    
    if not deep_vals and not rem_vals:
        return None  # No wearable sleep data
    
    # Derive light sleep (what's left after deep + REM)
    light_vals = [100 - d - r for d, r in zip(deep_vals, rem_vals)]
    
    # Rate each stage against clinical ranges
    deep_avg = sum(deep_vals) / len(deep_vals)
    stage_ratings = {
        "deep": "optimal" if 15 <= deep_avg <= 25 else "low" if deep_avg < 15 else "high",
        "rem": "optimal" if 15 <= rem_avg <= 25 else "low" if rem_avg < 15 else "high",
    }
    
    # Composite score: deep 30%, REM 30%, efficiency 25%, consistency 15%
    architecture_score = _weighted_composite(deep_avg, rem_avg, efficiency, consistency)

The score is 0-10, same scale as life wheel pillars. Stage ratings tell you what's actually wrong: "your deep sleep is 12%, below the 15% threshold for cognitive recovery." The insights list generates context-aware strings: "Low REM percentage may impair memory consolidation. Consider reducing late-night screen time."

It took 17 tests to validate the edge cases: no data, partial data (Garmin only vs. WHOOP), correct rating thresholds, efficiency derivation when WHOOP isn't available.

Goal Health: "Why Is This Goal Stalling?"

This is the feature I'm most excited about. Life Hub already had Granger causality tests: pure-Python implementations of the statistical test that determines if past values of X improve prediction of Y. I'd been using it for mood forecasting (does sleep Granger-cause mood? Yes, with a 2-day lag).

But I'd never applied it to goals. The data was there: GoalHabitLink maps habits to goals, GoalProgressLog tracks daily progress, and Habit records track daily completion. The question was simple: does completing Habit X causally predict progress on Goal Y?

async def compute_goal_health(session, goal_id=None, days=90):
    for goal in active_goals:
        linked_habits = await get_linked_habits(session, goal.id)
        
        for habit in linked_habits:
            # Build daily series
            habit_series = [1 if completed else 0 for day in date_range]
            progress_series = [progress_on(day) for day in date_range]
            
            # Run Granger test: does habit completion predict goal progress?
            granger = _granger_test(habit_series, progress_series, max_lag=3)
            
            if granger:
                habit["causal_strength"] = granger["f_stat"]
                habit["causal_predictor"] = True

The result is a health composite per goal. If your "Learn German" goal is stalling, the dashboard now tells you: "Duolingo practice (linked habit) dropped to 30% completion this month. This habit causally predicts your goal progress." Not just what happened, but why, backed by statistical evidence.

The UI renders this as a card on the goal detail page: health score badge (green/yellow/red), per-habit status bars, and a stall reason in plain English. 8 tests cover the edge cases, including mocked Granger results.

Journal Entity Extraction: Making the Journal Smart

Journal entries are the richest data source in Life Hub: free-text, daily, deeply personal. But until now, only sentiment analysis touched them. The journal knew my mood about what I wrote, but not what I wrote about.

The new extract_journal_entities() mirrors the pattern from the thought classifier (which already extracts people, topics, and action items from captured thoughts). On every journal save, Haiku extracts:

  • People mentioned → linked to relationships
  • Goals referenced → linked to goal tracking
  • Decisions made → flagged for outcome tracking
  • Health concerns → flagged for advisor attention
  • Action items → captured as potential next actions
  • Topics → tagged for trend analysis

These are stored as JournalEntityLink rows and displayed as color-coded pills on the journal sidebar: blue for people, emerald for goals, amber for decisions, red for health concerns. The journal entry becomes a cross-reference hub.

The LLM cost is minimal: ~300 Haiku tokens per entry, cached per day, with graceful fallback if the API is unavailable. The extraction never blocks the journal save: it runs inside a try/except after the entry is committed.

Relationship Drift: Predicting Distance Before It Happens

Life Hub already had relationship health scoring: a composite of interaction frequency, cadence compliance, and trend direction. But health scores are reactive. By the time the score drops, the relationship has already drifted.

The drift predictor uses OLS linear regression (the same _ols_fit function from the causal inference engine) to compute the slope of interaction quality over time. For each relationship with 4+ quality-rated interactions, it fits a line to the quality scores and projects forward 30 days.

# Build quality time-series for this relationship
qualities = [event.quality for event in social_events]  # 1-5 scale

# OLS slope: quality = intercept + slope * interaction_number
X = [[1.0, float(i)] for i in range(len(qualities))]
fit = _ols_fit(X, qualities)
slope = fit[0][1]  # slope per interaction

# Classify
if slope > 0.05:
    direction = "deepening"
elif slope < -0.05:
    direction = "drifting"
    risk = "high" if slope < -0.15 else "medium"
else:
    direction = "stable"

When a relationship hits "high" risk, a new advisor trigger fires: Sister Miriam (the moral compass advisor) generates a contextual comment about the drift and suggests specific reconnection actions. The drift warning card on the relationship detail page shows the slope, projected 30-day change, and data point count.

This is proactive rather than reactive: instead of telling you "you haven't talked to Alex in 3 weeks" (which is just a cadence check), it tells you "your interactions with Alex are getting shorter and lower quality. The trend predicts disengagement within a month."

Decision Outcome Tracking: Closing the Loop

I capture a lot of decisions as thoughts. The thought classifier already labels them as type "decision." But what happened after the decision? Did the expected outcome materialize? Was the decision good?

The new DecisionOutcome model tracks:

  • The original thought (what was decided)
  • Expected outcome (what I thought would happen)
  • Review date (when to check back)
  • Actual outcome (what really happened)
  • Quality score (1-5, in hindsight)

An advisor trigger (decision_review_due) fires when a decision passes its review date without a recorded outcome. Over time, the quality scores aggregate into a personal decision quality index, a feedback loop for better judgment.

The Evidence Portfolio: What I've Tested

Life Hub has an experiments system: hypothesis, intervention, metric, start date, end date, outcome. I've used it to test things like "does cutting caffeine after noon improve sleep quality?" and "does morning sunlight exposure improve mood?". The causal inference engine runs interrupted time-series analysis to classify experiments as helping, hurting, or inconclusive.

But the results were scattered. You had to click into each experiment individually. The new /evidence page consolidates everything into a single view:

  • Summary stats: total experiments, helping, hurting, inconclusive, active
  • Experiments grouped by life pillar (health, intellect, connection, etc.)
  • Each card shows: hypothesis → intervention → metric → verdict badge
  • Bottom section: top Granger causal findings from across the system

It's the "evidence-based life optimization" dashboard I've wanted since I started this project. One page that answers: "based on everything I've measured and tested, what actually works?"

Auto Weekly Podcast: Passive Insight Consumption

The most fun feature to build. Every Sunday at noon, a Kubernetes CronJob fires:

  1. Compute the weekly digest (mood trends, sleep, habits, goals, activity)
  2. Format it as narrative text for NotebookLM
  3. Create a notebook and generate an audio overview ("brief" format, under 5 minutes)
  4. Send a push notification with the link

I wake up Monday morning with a personal podcast reviewing my week. No clicking through pages, no reading charts. Just plug in earbuds during the morning routine and listen to an AI-narrated summary of how the week went.

The orchestration is straightforward. It composes existing functions:

async def generate_weekly_podcast():
    # 1. Compute digest
    digest = await compute_weekly_digest(session, week_start, week_end)
    
    # 2. Format for audio
    text = _format_digest_for_podcast(digest)
    
    # 3. NotebookLM: create notebook + generate audio
    notebook = await create_notebook_from_text(title, text)
    audio = await generate_audio(notebook_id, format="brief",
        instructions="Summarize conversationally. Highlight wins, flag concerns.")
    
    # 4. Notify
    await send_notification("Weekly Podcast Ready", message, tags=["podcast"])

The Engineering Approach: Parallel Agents, Test-Driven

I shipped all eight features in a single session using Claude Code with parallel background agents. The approach:

  1. Explore first: two agents mapped existing code (function signatures, DB models, test patterns) before any implementation started
  2. Plan with constraints: a Plan agent designed implementations that reuse existing infrastructure (causal inference, thought classifier patterns, MCP tool conventions)
  3. Build in parallel: three independent features built simultaneously by background agents, each working on non-overlapping files
  4. Test-driven verification: every feature includes tests (71 total). The full suite ran between each phase to catch conflicts
  5. Fix what you break: the agents introduced 5 test ordering conflicts with existing tests, all diagnosed and fixed

The key architectural decision was reusing existing patterns rather than inventing new ones:

  • Journal extraction mirrors thought_classifier.py exactly (same LLM call pattern, same normalization, same error handling)
  • Sleep architecture is a pure function that takes pre-fetched data (same pattern as compute_sleep_consistency)
  • Goal health uses _granger_test and _ols_fit from causal inference: no new math
  • Drift prediction uses _ols_fit for slope estimation: same function, different context
  • All MCP tools follow the OWNED_TOOL_NAMES + legacy wrapper pattern

The result: 7 new Python modules, 9 new MCP tools, 2 new DB models, 1 new UI page, 2 new advisor triggers, and 71 tests, all building on foundations laid in earlier phases.

By the Numbers

MetricBefore P23After P23
MCP tools240249
Tests2,8993,116
UI pages2930
ORM models5153
Advisor triggers4042
New Python modulesN/A7
New testsN/A71 (P23) + 107 (coverage) + 29 (fixes) = 217

What's Actually Changed

The dashboard used to be a mirror. It reflected what happened today, this week, this month. Now it's more like a conversation partner. It tells me my goal is stalling because a linked habit dropped. It tells me a relationship is drifting before I notice the silence. It predicts tomorrow's energy based on today's calendar and last night's sleep.

The shift from reporting to reasoning is subtle in the UI: an extra card here, a drift warning there, colored pills on journal entries. But the underlying change is significant: every number now comes with context about why it is what it is and where it's heading.

Next up: living with these features for a week and seeing which ones I actually reach for. The best measure of intelligence isn't how much the system knows. It's how often you act on what it tells you.