Skip to content

05 / Personal project · 2026

Two weeks, one new stack, 243 tests

A Python desktop app with 243 tests and 4-platform CI, built in two weeks in a stack I had never shipped.

Role
Solo Developer
Timeline
~2 weeks
Type
Cross-platform desktop activity tracker
Stack
Python 3.12 · FastAPI · SQLAlchemy 2.0 · PySide6 · PostgreSQL · Vue 3 · Tailwind CSS 4

At a glance

  • Cross-platform desktop activity tracker: foreground app usage, screenshots, idle time. Local-first, no cloud required.
  • Built in ~2 weeks in a stack I had never shipped: Python 3.12, FastAPI, SQLAlchemy 2.0, PySide6.
  • 243+ tests, strict mypy, ruff, vue-tsc. More automated verification than most client budgets ever allow.
  • AI-assisted on purpose, engineering-led on principle. The architecture, reviews, and every accepted line were mine.

01 / Why

A tool I wanted, and a claim I wanted to prove

I wanted an activity tracker that kept its data on my machine: what apps I use, when I'm idle, screenshots I can search. Without shipping my work life to someone's cloud. None of the existing tools fit, which is the oldest reason in software to build one.

The second reason was professional. My resume says I learn fast; a resume would say that. screen.io is the demonstration: pick a stack I had never shipped. Python desktop apps are about as far from Laravel SaaS as it gets. And produce something with production discipline in two weeks. In public, with the test suite as the receipt.

02 / Architecture

Layered like it plans to live

Speed came from constraints, not shortcuts. The backend is layered: services hold logic, repositories own persistence, models stay dumb. OS-specific behavior (active-window tracking, screenshots) lives behind Protocol interfaces. macOS gets a PyObjC adapter; Windows and Linux get stubs that satisfy the same contract. A small hand-rolled DI container wires it: no framework, no magic, everything traceable.

worker threadsscreenshotmss · interval captureactivityforeground app watchidleinput gap detectioncleanupretention pruninginput listenerpynput daemonEventBusthread-safe pub/subPySide6 UIQt main thread · tray · 10 pagessubscribeDB writer threadsingle writer, no lock fightsPostgreSQLFastAPI · uvicorn daemon127.0.0.1:8111 · 13 routerseventsreads via SQLAlchemyVue 3 SPAdashboard · reports · searchREST/api/ws/eventsOS adapters behind Protocols,macOS: PyObjC · Win/Linux: stubs,swappable without touching services
Fig. 01: the runtime. Workers publish to an event bus, one thread owns database writes, FastAPI serves the SPA.

The concurrency design is the part I'd defend in a systems interview: capture workers (screenshots, activity, idle detection, cleanup) never touch the database. They publish to a thread-safe event bus, and a single writer thread owns all persistence. No lock contention, no interleaved write bugs, and the Qt main thread stays free to keep the UI at 60fps. A FastAPI daemon on localhost serves 13 routers and a WebSocket feed to the Vue dashboard.

03 / Discipline

Fast is only fast if it's still standing in week three

The test suite runs 243+ cases: repositories tested against a real temporary database (because mocked SQL tests prove nothing about SQL), services tested with fakes injected through those Protocol seams, and the Vue frontend under vitest. Strict mypy across the codebase caught a class of bug the tests never even saw: None-leaks across layer boundaries, mostly.

None of this rigor was required for a personal tool. That's the point. Client work rarely budgets for the testing culture I want to bring to it; screen.io is where I practice that culture at full strength, so it's a habit rather than an aspiration.

04 / The AI part

AI-assisted, with the emphasis on assisted

I built screen.io with an AI pair (opencode with a free model), and I'll say so plainly rather than let the two-week number imply something it shouldn't. The AI accelerated the parts that deserve acceleration: unfamiliar API surfaces, boilerplate, the hundredth similar test case.

What it could not do is the actual work. Choosing the layering. Insisting on a single-writer thread. Deciding what a Protocol boundary owes its callers. Rejecting generated code that compiled but lied. Review standards stayed high, and that is why two weeks was possible. Senior engineering with these tools means the speed changes but the responsibility stays the same.

05 / Platform work

Shipping to four platforms from one repo

GitHub Actions builds a matrix with macOS ARM64, Intel, Linux, and Windows targets. PyInstaller produces both onedir and onefile bundles; a version tag becomes a GitHub Release with artifacts for each platform. macOS needed entitlements and permission flows for Screen Recording and Accessibility, the kind of platform paperwork that turns "runs on my machine" into "installable by a stranger."

tests, pytest + vitest
CI build platforms
API routers + WebSocket
weeks from empty repo

06 / Next

Designed but not yet built

Cloud sync exists as a design (an outbox pattern queuing local changes for eventual upload), deliberately unimplemented until the local-first core earned it. Windows and Linux adapters are the other obvious frontier; the Protocol seams mean implementing them touches no service code.

07 / Lessons

What two weeks in a foreign stack taught me

  • Learning speed is mostly architecture. With clean seams, ignorance stays local. You can be new to PyObjC without being new to software.
  • A single-writer thread is the cheapest concurrency correctness you can buy. Boring beats clever, again.
  • Strict typing pays fastest in unfamiliar territory. Mypy was my senior reviewer in a stack where I had none.
  • AI assistance raises the ceiling on speed and lowers the floor on quality. Which one you get is a review-discipline question.