← Calvin Skalla
Life Dashboard
Eleven data pipelines, one Postgres, and a phone app that only ever reads from it. Built and run for one user: me.
Personal project Next.js Python ETL Postgres Railway Design system built alongside the app

Every screenshot here is the real app on a made-up dataset. The numbers are fake; the pipelines, layouts and decisions are not. The interesting part was never my resting heart rate. It is how the data gets there, and what the app refuses to do with it.

Why

Recovery in one app, workouts in another, supplements in a note, money in a spreadsheet. None of it in one place at 6am, deciding how hard to train. So I built one screen, then kept adding the rest of the routine to it.

One rule shaped everything: the app never calls an external API. Every source lands in Postgres through a scheduled pipeline, and the phone only reads. That is why it is fast, why it renders on an empty preview database, and why a broken vendor shows up as stale data instead of a spinner.

Today tab: last night's recovery vitals, the day's workouts, nutrition against goals, and a supplements checklist
Tab 1 of 5

Today

The 6am screen. Last night's vitals, today's sessions, macros against target, supplements ticked off automatically, and a chat bar that can answer questions about my own data.

Wearable API, 11 pulls a dayFood logSchedules from TargetsOne fetch builds the screen
The hard part

The wearable's "sleep duration" is time in bed, not time asleep. Five separate readers took the name at face value, so every morning briefing overstated sleep. One fix, in the narrator that turns the night into words: sum the sleep stages, report awake time as its own band.

The call

The briefing is keyed to today's date with no carry-forward. Yesterday's summary is not stale, it is wrong about today, so an empty card is the honest answer until the new one exists.

Progress tab: a health snapshot radar scoring sleep, recovery, HRV, resting heart rate, workouts, supplements and calories against targets
Finance view inside Progress: monthly spend and spending by category, with the Demo toggle on
Tab 2 of 5

Progress

How am I trending, three days to sixty. A wellness radar with one score on top, nutrition adherence, and the full Finance view: accounts, a daily cash forecast, and spending by month, category, card and merchant.

Bank aggregatorCrypto exchangeEvery screen is a Postgres viewNo finance logic in the app
The hard part

Absent is not zero. When the wearable credential expired, two metrics vanished and the chart read them as zero. Two axes invert, so zero is their best value, and the health score went up the day the data died. Missing is now a real state: the spoke draws empty, leaves the score, and is named under the chart.

The call

The Demo toggle scrambles every amount on screen, deterministically, so totals still add up. It is what makes a finance screen shareable at all. It never persists, and it disables every editor so a fake number can never be saved.

Hobbies tab: fantasy football starters to look at this week and swaps worth making, with projected points
Tab 3 of 5

Hobbies

The research pipelines under one slot: fantasy football across three leagues, unusually cheap flights from my home airport, and a fight-prediction model beside two prediction-market prices.

Two fantasy platformsFlight-search API, dailyWeekly fight datasetTwo prediction markets, hourly
The hard part

Two halves on different clocks. Fights arrived weekly on a Tuesday, and prices were captured by the same weekly job, so every fight week's prices landed three days late. Measured thirty hours before one card: twelve of thirteen bouts would have shown no price through fight night. The fix was a second, hourly job whose cadence is the resolution of the odds table.

The call

A green dot marks the cheaper price only where both venues quoted that side for real. A derived price is honest arithmetic but not a price anyone offered, so it is starred and excluded. The caption says "cheaper quote", not "better trade", because the database holds fees for neither venue.

Notes tab: a graph of note collections and the links between them
Tab 4 of 5

Notes

A second brain. Capture from the phone, file into projects, areas and resources, link notes with double brackets, and let an assistant help tidy the pile. It opens on the graph, because you arrive cold and need "what is in here" before "where is that note".

Own schema, created at runtimeNightly embeddings for semantic searchMCP server for the laptopFour write surfaces, all confirmed
The hard part

Search was ranked by recency, not relevance, so a note titled with the exact query came fourth of four. And a note's id never followed a rename, so a retitle silently broke every link to it, and tapping the dead link created a second empty note that stole the backlinks. Ranking is now relevance with a title boost; links resolve by slug or exact title, picking exactly one target.

The call

Machine suggestions never sit among my own links. An outlink is something I asserted; a semantic neighbour is a model's guess, and filing them together lends the guess the assertion's authority.

Targets tab: the weekly workout schedule editor with morning and evening sessions and an effective-from date
Tab 5 of 5

Targets

The one tab that writes. The workout week with an effective-from date so history is never rewritten, macro goals, the supplement catalog, and the keyword map that lets the food log tick things off for me.

Config tables, read everywhere elseReal OAuth for the wearableStatus endpoint returns a token length, never the token
The hard part

A supplement row is a claim that the food diary can see it. Adherence counted every scheduled row whether or not any keyword could match, so a row with no keywords was scored as missed, permanently, and nothing on any screen said so. Two items sat like that for ten weeks as pure denominator.

The call

The Ops card has no status dot. A dot that is green because nothing asked is the exact failure the monitor exists to catch.

Under the hood

The part with no screenshot

The five tabs are the visible tenth. The rest is the system that keeps them honest.

Wearable API Bank aggregator Fantasy platforms Flight search Prediction markets + nutrition, fights, embeddings ETL service 11 pipelines, Python 9 Railway crons writes only Postgres one schema per module views hold the logic read-only role for the UI Phone app Next.js, reads only Monitor asks the data, not the cron load read read
The ETL service

Eleven Python pipelines on nine Railway crons. Two of the market captures are offset by five minutes because they vacuum against the same volume, which has filled once. Scheduling left GitHub Actions after its cron drifted by hours.

The monitor

Asks the data, never the scheduler. A cron can start, exit clean and write nothing; that happened for three hours when a filter rejected all two thousand markets and reported success. Each table's expected cadence is measured from its own loads, so nothing is transcribed and lateness is a multiple of normal.

Read-only by grant, not by guard

The app's queries run on a role that can only read. Preview deployments use it too, so every read route answers with real data and every write fails at the database. A guard in code can be dropped by an edit; a grant survives it.

Six scheduled reviewers

Agent sessions on a timer: design, ETL and database passes that file numbered findings; a doc pass; a build pass that works the queue; a consolidator that merges the day's branches. The design ledger holds over a hundred findings. Eight were wrong about correct code, and each is kept, marked, with the reason.

The design loop

A synthetic database shaped like production, a script that screenshots every surface at two viewports and measures whether any label is clipped, and a smoke test that walks all fifty-two read routes in under a second. Two canary routes must return rows, because a green run over an empty database is the same lie as a cron that wrote nothing.

A template underneath

One tenant, one database, one schema per module, logic in shared packages. A second instance runs on it. The point was not to sell it. It was to find out which parts of a personal tool are actually personal.

Outcome

I open it every morning to decide how to train. It replaced three apps, a spreadsheet and a running note. More usefully, it is where I learned the habits I bring to work now: absent is not zero, ask the data rather than the scheduler, and make a rule a grant or a script rather than something to remember.

↑