Building Intelligence Into a Personal Life Dashboard

How I turned a personal CRUD dashboard into a system that finds patterns across 15 data sources, with embeddings, timeline analysis, and no vector database.

Building Intelligence Into a Personal Life Dashboard

Life Hub started as a personal dashboard: a place to see habits, mood, sleep, and journal entries in one view. Over the past few weeks I've pushed it into territory that feels less like a CRUD app and more like a system that actually understands patterns in my data. Here's what changed and why.

The Thought System

I added a capture layer called Open Brain: quick thoughts, ideas, and observations that don't fit neatly into a journal entry. The interesting part isn't the capture itself, it's what happens after.

Every thought gets embedded via Claude's embedding API. On save, the system finds semantically similar past thoughts and surfaces them. Capture "I should write more" and it pulls up the three times you said something similar in the last month. It also cross-references thoughts against active goals: if a thought's embedding is ≥0.4 similar to a goal's text, that connection gets surfaced in the goal detail view.

Duplicate detection runs at capture time too. If a new thought is ≥0.85 similar to an existing one, the UI warns you before saving. This sounds simple but it's surprisingly useful when you realize how often you have the same idea without acting on it.

Council Enrichment

Life Hub has an advisory council: five AI personas that comment on your data from different perspectives (a stoic philosopher, a fitness coach, a creative director, etc.). The council was already functional, but it was operating on structured data only: pillar scores, habit streaks, sleep averages.

Now the council briefing includes recent thoughts from the past 14 days plus semantically related thoughts. When Dr. Voss (the psychologist persona) notices you've captured 3+ thoughts about the same topic in a week, a dedicated trigger fires and she comments on the pattern. This turns the council from a dashboard narrator into something that actually catches recurring themes you might miss.

Performance

With 157 MCP tools and 34 UI pages all hitting the same SQLite database, performance mattered. A few targeted changes:

Consolidated queries. The daily digest was making 9 separate database queries that could be batched. I combined them into 3 queries using SQLAlchemy's .in_() operator. Same data, fewer round trips.

TTL cache. A lightweight in-memory cache (result_cache.py) with 2-minute TTL and per-key expiry. Pillar scores and digest computations get cached here. The cache self-evicts stale entries when it exceeds 50 keys. I considered APScheduler for periodic refresh but a TTL cache is simpler and sufficient for a single-user app.

Pooled Anthropic client. Instead of creating a new HTTP client for every LLM call, ai_engine.py now uses a singleton _get_client() that reuses the connection pool. Small change, noticeable improvement when the council runs 5 advisor calls in sequence.

Batch embeddings. Journal backfill was embedding entries one at a time. generate_embeddings_batch() sends up to 20 texts per API call. Backfilling 200 journal entries went from minutes to seconds.

Timeline Intelligence

Life Hub ingests activity data from 15 sources: GitHub commits, Spotify plays, RSS reads (Miniflux), blog posts (Ghost), Garmin workouts, calendar events, bookmarks, audiobooks, and more. All of this was visible on a timeline page, but the analysis was limited to an hourly heatmap.

I built a dedicated analysis module (timeline_intelligence.py) with six functions:

Activity streaks. Per-source consecutive-day tracking. GitHub has a 23-day streak. Miniflux reading streak broke 4 days ago. The digest surfaces sources with 7+ day active streaks and warns about 3+ day gaps in sources you normally use daily.

Cross-source correlations. Daily co-occurrence analysis between sources, plus correlation with mood and sleep scores. Turns out my GitHub activity and Spotify listening correlate strongly: I code with music. Days with high Miniflux reading correlate with slightly higher mood scores the next day.

Weekly rhythm. Compares this week's activity per source against a 30-day rolling baseline. Shows percentage shifts: GitHub up 40% this week, RSS reading down 25%. Big shifts (>30%) get flagged in the daily digest.

Source diversity. A Shannon entropy-inspired score (0-10) measuring how evenly your activity is spread across sources. High diversity means you're engaging with many different activities. Low diversity with a dominant source gets flagged: if 80% of your activity is GitHub, the system notices.

Timeline digest. A comprehensive N-day summary: total events, peak day, per-source breakdown, and notable events (commits, blog posts, workouts get priority over passive events like RSS reads).

Life context detection. A sliding-window classifier that auto-tags periods as "Deep coding", "Reading & learning", "Writing & reflection", "Music immersion", "Active & fitness", or "Balanced & varied" based on source distribution percentages. This feeds into the daily digest as a context label.

Transaction Categorization

The finance module gained automatic categorization. A rule-based categorizer (transaction_categorizer.py) maps transaction descriptions to categories using keyword matching: "Whole Foods" → groceries, "Netflix" → subscriptions, "Shell" → transportation. It handles about 150 merchant patterns and falls back to "other" for unknowns.

The savings rate calculation feeds into the Wealth pillar score as a sub-component, weighted alongside spending trends and transaction volume. This closes the loop on financial data actually affecting the life dashboard's assessment of that pillar.

Architecture Decisions Worth Noting

No vector database. Embeddings are stored as JSON arrays in SQLite columns. With ~2,000 journal entries and ~500 thoughts, brute-force cosine similarity over the full corpus takes under 100ms. A dedicated vector DB would add operational complexity for a problem that doesn't exist yet at this scale.

Upper-bound query filters. Every time-range query in the codebase now bounds both ends: >= start AND <= end. I learned this the hard way when timeline intelligence tests started failing due to cross-test data leakage. Events seeded for one date range were appearing in queries for earlier ranges because the queries only had a lower bound. This is the kind of bug that works fine in production (where data grows forward in time) but breaks immediately in tests (where data is seeded across arbitrary date ranges).

Rule-based over ML for classification. The life context classifier and transaction categorizer both use simple threshold rules, not trained models. The context classifier checks if GitHub activity is ≥40% of total events in a window. If so, it's "Deep coding". This is transparent, debuggable, and doesn't need training data. If the rules get wrong, I change a number. If a model gets it wrong, I need more labeled data and a retraining pipeline.

Cache TTL over scheduled refresh. I briefly considered APScheduler for periodic cache warming. But a 2-minute TTL with lazy evaluation means the first request after expiry takes the hit, and every subsequent request in that window is instant. For a single-user app where I'm the only one hitting it, this is the right tradeoff: no background scheduler process, no startup ordering concerns, just a dictionary with timestamps.

What's Next

The roadmap is technically complete: every planned feature is implemented and tested (1,840 tests across 118 files). But "complete" just means the backlog is empty, not that the system is done. The timeline intelligence data is already suggesting features I hadn't planned: anomaly detection on source patterns, predictive context tagging ("you usually enter a deep coding phase on Mondays"), and cross-day momentum scoring.

The source code lives on GitHub. The whole thing runs on a single SQLite database in a Kubernetes pod on my homelab cluster, deployed via Flux GitOps. No managed services, no cloud bills, no vendor lock-in. Just a FastAPI app that knows more about my patterns than I do.