Skip to content

Case study

BooxPlanner

A bidirectional handwriting loop between an e-ink tablet and Notion.

Year
2025—
Status
in progress
Role
Architect & Implementer
Stack
Python, WeasyPrint, Jinja2, n8n, Gemini Vision, Notion API, Google Fit

A bidirectional loop between a paper-feel e-ink tablet and a Notion task graph — morning generation live, evening read-back in progress. Pen for input, structured data as the source of truth, an LLM breaking goals into daily detail.

At a glance: Notion goals become a daily PDF, written on an e-ink tablet, then read back into Notion.
At a glance

2026-05-12

first production run

The technical breakdownExpand · ~3 min

The business problem

I've always written by hand — todos, plans, notes — but past a certain load that stopped scaling. Notion held the structure; a phone app held the convenience; the paper notebook held the one thing that actually mattered, the physical act of writing. Each solved part of the problem and broke another.

Pen on screen for input, a structured database as the source of truth, and something in between doing the translation. In the age of AI you don't have to pick — you build both.

The hard constraint is the hierarchy: a quarterly goal that appears identically on the monthly, weekly, and daily pages defeats the reason for printing a planner. So an LLM breaking goals into level-appropriate detail isn't decoration — it's the part that makes the printed page worth printing.

The architecture

A morning pipeline behind a FastAPI service in a Docker container on a home server. A cron POSTs to /generate; the service reads the Notion databases, asks an LLM to break quarterly goals into a monthly focus, weekly goals, and a daily focus (cached weekly so the daily run is mostly free), renders a Jinja2 template, hands the HTML to WeasyPrint, and uploads a weekly PDF to a cloud-drive folder the tablet pulls.

The morning generation path (deployed) reads Notion, breaks goals down with an LLM, renders a PDF and pushes it to the e-ink tablet via one cloud folder. The evening read-back path (drawn dashed — planned/stub) returns annotations through a separate folder, pixel-diffs the changed pages, and OCRs them back into Notion.
The morning generation path (deployed) reads Notion, breaks goals down with an LLM, renders a PDF and pushes it to the e-ink tablet via one cloud folder. The evening read-back path (drawn dashed — planned/stub) returns annotations through a separate folder, pixel-diffs the changed pages, and OCRs them back into Notion.

The evening half — planned, not yet live — reads the annotations back: a drive-change trigger, a pixel-diff to find only the pages I actually wrote on, then vision OCR into Notion. Underneath sits a reverse-engineered .note point-binary parser: 16-byte stroke records behind an 80-byte header, with an inter-stroke separator that shifts alignment and a resync on impossible coordinates.

Decisions & trade-offs

Two folders, not one. Generated pages go down one cloud folder; annotated pages come back up a different one. They physically can't feed each other, so the morning generator can never re-ingest its own output — the simplest possible defense against a sync loop.

Diff before you spend. Before sending anything to a vision model, the system pixel-diffs the clean and annotated PDFs and sends only the changed pages — typically one to three of twelve. Cost tracks edits, not document size.

Most of "AI engineering" on a budget is knowing where not to call the model.

Evening deferred until morning is solid. The /process-annotations endpoint is a 501 stub on purpose. Bringing both halves online at once doubles the surface for half-working dependencies — a flaky OCR step would mask a flaky generation step. One direction has to be solid in production before the other depends on it.

What broke

The health-data path took two attempts: a direct device login prompted for an MFA code, which works at a terminal but not under cron, so the pull moved behind a REST API. The tablet's annotation storage turned out to be hostile to overwrites — the app rewrites its caches from memory unless it's closed — so a weekly filename rotation became the cleanest path out. And the .note parser walks off-by-four whenever its inter-stroke separator happens to look like the start of a record, which is why it resyncs on coordinates that can't be on-screen.

The outcome

The morning generation half is deployed and runs (first production run 2026-05-12). The evening read-back is built in pieces but not wired live/process-annotations is still a stub — and there's no OCR-accuracy measurement yet.

The honest open problem isn't the code — it's the habit. Daily adoption is as much behavioural as technical, and that's the part I haven't solved.