A 7-day forward lens for the things I already track
I have a morning briefing that summarizes today (weather, todos, calendar, bills, sobriety counter). Useful. But it's only today, and it's spoken once on an Echo, then gone. I wanted a visual companion that showed the next seven days the same way — and that I could glance at any time.
What was happening
Everything the briefing needs is already in DynamoDB or behind a small API: weather from NWS, calendar commitments, todos with due-by dates, recurring bills predicted from Plaid transaction history, predicted paydays, sobriety milestones, moon phase. The data was there. The aggregation wasn't.
So I built one: seven columns, one per day, each with weather + commitments + todos + bills + paycheck + a milestone line. Dark navy background, cyan accent, dense layout, today's column gets a cyan top-rule so it's obvious where "now" is.
What I found
The hard parts weren't the data — they were the design constraints I'd already locked in months earlier.
The main backend's main.py is chattr +i'd to defend
against an unrelated sync bug (separate post). I couldn't just
import a new module and add a route. The deploy pattern I'd
already established for new surfaces is "edit a sibling file
that main.py imports at startup," which lets me add routes
without touching the locked file.
So this new view registers itself via a sibling module that the main app already calls at startup:
# crystal_routes.py
from fastapi import FastAPI
from crystal import render_html, render_json
def register(app: FastAPI, require_auth):
@app.get("/crystal", response_class=HTMLResponse)
async def crystal(user=Depends(require_auth)):
return await render_html(user)
@app.get("/crystal.json")
async def crystal_json(user=Depends(require_auth)):
return await render_json(user)
# todo_routes.py (already imported by main.py at startup)
def register_todo_routes(app, require_auth):
# ...existing todos stuff...
try:
import crystal_routes
crystal_routes.register(app, require_auth)
except Exception:
log.exception("crystal registration failed")
The try/except is deliberate: the existing routes have to keep working even if the new module is broken. New features should never take down old ones, especially when the deploy mechanism already has constraints around the main file.
The fix
The aggregator pulls from each source in parallel and renders one big HTML payload. The expensive part is the bills lookup, which scans the Plaid-derived transactions table to identify recurring charges by category. Each load runs that full scan, ~2-3 seconds. That's fine for personal use — if hit volume ever rises, an in-memory cache with a ten-minute TTL would knock it to single- digit milliseconds.
async def render_json(user):
days = next_7_days(user.timezone)
results = await asyncio.gather(
weather(user.location, days),
commitments.list_upcoming(user.id, days),
todos.due_within(user.id, days),
finance.predict_bills(user.id, days),
finance.predict_paydays(user.id, days),
sobriety.milestones_in_window(user.id, days),
moon_phase(days),
)
return zip_by_day(days, results)
The visual design ended up mattering more than I expected. Seven columns of mixed-type data become unreadable fast without hierarchy. I color-coded by source (cyan = commitments, amber = todos, red = bills, green = paychecks), kept everything on a single line per item, and let the day column be the only thing that scrolls. On mobile it collapses to two columns.
What I'd do differently
The "edit a sibling module" trick works, but it's load-bearing
on me remembering that the main file is locked. The next time I
start a project where the main module is hard to touch, I'd
build the route registration as a directory scan from day one —
"any *_routes.py in this folder gets registered" — so adding a
new surface really is just dropping a file. The try/except
wrapper is the right pattern, but it shouldn't be reinvented for
every feature.
The other note: a horizon of seven days isn't enough for monthly recurring bills that fall just past day seven. Extending to fourteen days catches the start-of-next-month items. That's a trivial change to the aggregator and a less trivial change to the layout. Probably worth doing.