The thing that clicked today: most of a sprawling, week's-worth-of-fronts day turned out to be about one throughline, making Loupe runnable by someone who isn't me, with the rest spent making it look like one coherent product while I was in there anyway.
Built / shipped
The portable Apple-enrichment path, end to end. This is the big one. Loupe's enrichment database (aesthetic scores, scene labels, people seeds) is derived entirely from a macOS photo library, which until now meant my macOS photo library. The whole chain now exists so a second person can produce theirs:
- A standalone Mac-side helper that extracts the records and the search index, packages everything into a single archive.
- A generalized builder that consumes that archive and reconstructs the database. I proved the key the Apple label index uses to identify a photo is the photo's own UUID (99.6% match), not the other identifiers I'd assumed might work.
- A new upload endpoint and a "Read the negatives" card in the setup console: drop in the archive, it builds to a scratch database, sanity-checks the row counts, backs up the live one, swaps it in, and reloads. No command line anywhere in the flow.
I validated it against my own 81,000-photo library: the new build went from ~73,000 matched records to ~76,800, zero regressions, and it actually corrected 700-odd previously mis-stamped photos. Matching on file size as well as name recovered files that the old, stricter logic had refused.
A setup console that can run the pipeline. The console used to be a read-only status page. Now it can start the hand-run stages (ingest, contact-sheet generation, face detection) as background jobs that run one at a time, survive a page refresh, and can resume, each with its own trigger card. You click "Develop," it kicks off ingestion in the background and streams progress with a live rate and ETA; the same machinery generalizes across all three stages.
Frontend out of the monolith. The app's frontend was 2,000+ lines of HTML/CSS/JS embedded inside a 4,400-line Python server file. It came out into its own files in two stages (first the whole page as one block, then the CSS and JS split into real static files), and I verified each step by a byte-for-byte diff of the served page.
A unified brand, and real email. I rebuilt the marketing site's wordmark from the app's exact mark so the site and the app now render identically, synced the site's mockup navigation to the app's real nav, added a People feature section, folded in a founder origin beat and a "how it decides" section, and swapped the closer to the brand tagline. On the design side, I produced a canonical design-system spec and a self-demonstrating visual-system teaching guide, and confirmed the design tooling is now at parity with both the app and the site. I also stood up real email on the marketing domain: diagnosed that the early-access form had no backend and the address couldn't even receive mail, then set up a proper custom-domain mailbox with the full DNS record set verified live.
Problems & fixes
- A bug had been silently breaking the face pipeline. Earlier, a set of shared values (which file extensions count as video, among others) had been moved into one common module so every script read the same list. Two scripts were pasting that list straight into a SQL query as text, and the new form of the list is not valid SQL, so the database rejected it with an error about an unrecognized token. Every full face-detection run since that change had failed; the live app only looked fine because its face data came from an older run. The fix was to pass the values into the query as parameters instead of text. Once I understood the pattern, the rest of the code got checked for the same mistake, and there was one more in the pipeline repo.
- The setup console's "Done" message kept vanishing. The console rebuilds its cards from a static template every few seconds, so any finished state got wiped on the next refresh. Fix was to make the card's state replayable: store it, re-apply it after every rebuild.
- The marketing site's mockups rendered broken: full-width images, bare "?" rows. It read like a CSS bug, but the live CSS was correct. The real cause: I'd reused the same cache-busting version tag across several CSS edits, so returning browsers kept serving a stale stylesheet. Bumping the version fixed it. Lesson logged: bump the version on every CSS change, ideally automatically in the deploy script.
- The site kept deploying to the wrong place. My local branch was named one thing while the host's production branch was another, so every plain deploy silently landed as a preview and the live site stayed stale. Two generations behind, it turned out. Fixed by pinning the production branch explicitly and writing a deploy script that hard-codes it, refuses to run with uncommitted changes, and verifies the live site after.
Decisions
- Background pipeline jobs run in their own process group, not as plain background processes. The service kills everything it started when it restarts, so a naively-detached child gets taken down with it, including by the restart the import flow itself triggers. Putting each run in its own group lets it survive, which matters for a multi-hour ingest.
- Cold-start by guarding the readers, not by bootstrapping a database. The app couldn't boot with a truly empty library. Rather than have the server write a starter database, the three readers that crashed on the empty path got guards: the server never writes the metadata store; the "Develop" click creates it, because the pipeline owns it.
- The design system is the canonical source of truth. The live app's colors are a tracked drift that reconciles up to canonical, not the other way around.
- Backed off a downloadable Mac app for the test user: code-signing and bundling is a whole second product to maintain, and a script does the job for now.
Learned
- Putting a list of values straight into a SQL query as text is a trap I now know about: the values get passed as parameters instead, and when one of these shows up I look for its siblings.
- Reusing a cache-busting version while editing the file it points at ships stale assets to returning browsers; the symptom masquerades as a content bug.
- For a direct-upload static host, a deploy lands on production only when the branch matches the host's production branch: keep the local branch name aligned and pin it.
Still open / next
- The marketing early-access form is still a leaky mailto capture; a real backend is deferred until it's worth building.
- The in-process reload of one pipeline stage still falls back to a full service restart. A cleaner version waits for a focused refactor window.
- Some app views still need a design reconciliation pass; the People view was the priority gap and it's done.