Catching up

Drag the slider to roughly when we last spoke, and this page will tell you what has happened since.

1w2w1m2m3m4m5m6m

Showing the last two weeks

No new projects in the last week, but continued work on 8 existing ones — mostly CopyPasteDataBase (cpdb), Serpentine, MapMyFriends (iOS / Mac). That is roughly 142 hours at the keyboard.

1 new project since then: Property Tasks — A shared app for keeping track of jobs that need doing around a house or other property. Alongside that, continued work on 8 existing projects — mostly CopyPasteDataBase (cpdb), Serpentine, MapMyFriends (iOS / Mac). That is roughly 145 hours at the keyboard.

3 new projects since then: Property Tasks — A shared app for keeping track of jobs that need doing around a house or other property; and Serpentine for Android — An Android version of Serpentine, the app that plans motorbike rides along curvy, interesting roads instead of the fastest route, plus 1 other new project. Alongside that, continued work on 10 existing projects — mostly CopyPasteDataBase (cpdb), Serpentine, MapMyFriends (iOS / Mac). That is roughly 155 hours at the keyboard.

5 new projects since then: Serpentine — A ride planner for motorbikes that deliberately avoids the fastest route, picking curvy, quiet roads instead, and planning charging stops for electric bikes along the way; and Autodidact — Quietly saves the text of web pages you spend real time reading, in whichever browser you use, so you can find one again months later from a hazy memory of what it said, plus 3 other new projects. Alongside that, continued work on 13 existing projects — mostly CopyPasteDataBase (cpdb), Image Wizard, MapMyFriends (iOS / Mac). That is roughly 196 hours at the keyboard.

7 new projects since then: Serpentine — A ride planner for motorbikes that deliberately avoids the fastest route, picking curvy, quiet roads instead, and planning charging stops for electric bikes along the way; and AI Project Portfolio Generator — Gathers AI coding session logs and repository data from several machines, then compiles honest statistics on work done into a single self-contained HTML page that can be viewed without internet or JavaScript, plus 5 other new projects. Alongside that, continued work on 14 existing projects — mostly CopyPasteDataBase (cpdb), Image Wizard, MapMyFriends (iOS / Mac). That is roughly 230 hours at the keyboard.

8 new projects since then: Serpentine — A ride planner for motorbikes that deliberately avoids the fastest route, picking curvy, quiet roads instead, and planning charging stops for electric bikes along the way; and AI Project Portfolio Generator — Gathers AI coding session logs and repository data from several machines, then compiles honest statistics on work done into a single self-contained HTML page that can be viewed without internet or JavaScript, plus 6 other new projects. Alongside that, continued work on 14 existing projects — mostly CopyPasteDataBase (cpdb), Image Wizard, MapMyFriends (iOS / Mac). That is roughly 230 hours at the keyboard.

10 new projects since then: Serpentine — A ride planner for motorbikes that deliberately avoids the fastest route, picking curvy, quiet roads instead, and planning charging stops for electric bikes along the way; and AI Project Portfolio Generator — Gathers AI coding session logs and repository data from several machines, then compiles honest statistics on work done into a single self-contained HTML page that can be viewed without internet or JavaScript, plus 8 other new projects. Alongside that, continued work on 14 existing projects — mostly CopyPasteDataBase (cpdb), Image Wizard, MapMyFriends (iOS / Mac). That is roughly 235 hours at the keyboard.

13 new projects since then: Serpentine — A ride planner for motorbikes that deliberately avoids the fastest route, picking curvy, quiet roads instead, and planning charging stops for electric bikes along the way; and MapMyFriends (iOS / Mac) — Shows the people in your address book on a map so you can see who lives nearby, find a convenient meeting spot between friends, and notice when contacts have moved, plus 11 other new projects. Alongside that, continued work on 11 existing projects — mostly CopyPasteDataBase (cpdb), Image Wizard, Marlowe. That is roughly 235 hours at the keyboard.

0
new projects
0
ongoing
0
hours

CopyPasteDataBase (cpdb)new

CopyPasteDataBase stores a history of everything you copy on macOS, iOS and Windows, keeping the list synchronised across your devices so you can retrieve past clips anywhere. It enriches items automatically - reading text from images, adding page titles and thumbnails to URLs, detecting dates or phone numbers, and allowing smart pasting such as inserting only a URL into a text field. The tool lets you search by meaning rather than just keywords, making it easy to find the right snippet even if you don't remember its exact wording.

SwiftCloudKitWindowscross-platform60 h
The longer version

cpdb is a cross-platform clipboard history manager spanning macOS, iOS, and Windows (x64 and arm64), with synchronized history across devices via CloudKit and a Cloudflare-based relay under design to bridge the Windows side. Clips are full-fledged records: OCR runs over image clips (with a self-healing re-sweep pass), captured URLs grow their real page titles and preview thumbnails in the background, entries can be pinned, reversibly deleted, and pasted intelligently — including URL-only entries into text fields — and everything lands in a plain SQLite file any tool can open. Search is no longer only keyword: every text and link clip is embedded on-device with NLContextualEmbedding, and results fuse the FTS5 bm25 ranking with cosine similarity by reciprocal rank fusion, so "that docker thing" finds the clip that never said docker.

The Apple side is Swift/SwiftUI shipped as a universal .dmg; Windows is a separate C#/.NET 8/WinUI 3 port shipped via Velopack installers, running the same schema and the same search semantics. Cross-platform content identity is handled by a canonical semantic hashing scheme (v2) — image, then file, then URL, then normalized text — so volatile sidecar flavors can't fork an entry's identity and the same clip is recognized on every OS. CI runs macOS, iOS, and Windows test suites on GitHub Actions. Recent work leans hard on what the OS now offers on-device: clips are scanned at capture for dates, addresses, phone numbers, tracking numbers, flights and money amounts and rendered as tappable action chips, with QR codes in screenshots picked up in the same Vision pass as OCR; image cards gain "Copy as Table", which runs Vision's document recognizer over a screenshot and pastes real table structure out as CSV or Markdown; long text clips get an auto-title and summary generated by Foundation Models; and a Shortcuts, Spotlight and cpdb:// deep-link surface exposes search, paste and pin as system intents.

The semantic index is deliberately unfashionable — no vector database, just every stored vector in one contiguous Float buffer (~20MB at 10k entries) scanned with a vDSP dot product. Vectors are L2-normalized at write time so the dot product *is* cosine similarity with no per-query division, and staleness is a generation counter rather than a live observation, because nothing renders the index directly and there is no UI to keep ticking while it goes unused. A relevance floor exists because top-K always returns something: a garbage query with no real signal would otherwise surface its best noise wearing the same "similar" badge as a genuine paraphrase. A thermal-gated drain loop embeds an existing library in minutes instead of days, and model_id/revision columns make a future model upgrade a background re-embed rather than a migration. The same release also treats Apple's coming pasteboard-permission enforcement as a scheduled event rather than a surprise — the capture loop watches NSPasteboard.accessBehavior, pre-classifies with the alert-free detect APIs, skips capture while secure input is active, and pauses with a banner if access is ever denied.

This is the deepest Claude Code collaboration in the portfolio — roughly 54 focused hours and 369 commits, with the AI pairing on everything from CloudKit zone migration and sync gating to release engineering. The v3.3 work was built as six parallel feature streams in separate worktrees, each adversarially reviewed before merge; the review-findings commits sitting behind each merge are where the race conditions, push storms and TOCTOU bugs died. A permanent popup-perf log line, added during earlier optimization work, has since earned its keep twice: it caught a layout regression the day it appeared, and then caught the popup's recency ordering falling off a cliff once per-row enrichment writes fattened the pages behind an always-unindexed sort. Now at v3.3.0 with public releases for every platform, versioned changelogs, and design docs (parity.md, schema.md, the canonical-hash and relay-protocol write-ups) kept current as part of the workflow rather than after it.

CopyPasteDataBase stores a history of everything you copy on macOS, iOS and Windows, keeping the list synchronised across your devices so you can retrieve past clips anywhere. It enriches items automatically - reading text from images, adding page titles and thumbnails to URLs, detecting dates or phone numbers, and allowing smart pasting such as inserting only a URL into a text field. The tool lets you search by meaning rather than just keywords, making it easy to find the right snippet even if you don't remember its exact wording.

Serpentinenew

A ride planner for motorbikes that deliberately avoids the fastest route, picking curvy, quiet roads instead, and planning charging stops for electric bikes along the way. You can ask for a route by distance, by how much time you have, or as a loop that brings you home. It runs on iPhone, iPad and Mac.

SwiftGoiOSMac Catalyst28 h
The longer version

A motorcycle ride planner that optimises for the wrong thing on purpose. Every other routing app wants to get you there quickly; Serpentine favours curves and quiet roads over minimum time, because the point of the ride is the ride. Plan by distance, by the time you actually have — "two hours on Saturday" — or A to B with a detour budget, and it will tell you the quick way alongside however much longer the good roads will take. For the trips that genuinely aren't about riding, the helmet shop or the dealer, "just get me there" takes the freeway. It runs on iPhone, iPad and Mac via Catalyst, with no subscription, and hands off to Apple Maps when it is time for voice navigation.

It plans for electric bikes as a first-class case rather than an afterthought, which turns out to reshape the whole problem. Charge stops are part of the route, with backups in case the one you wanted is occupied or broken, and the key question is reach rather than proximity: not which charger is nearest, but which ones you can still get to on what you have left. A ride can be paused mid-way to detour for charge and then carried on from where it left off. Beyond point-to-point it generates scored loops and out-and-backs, so "somewhere good for a couple of hours, ending back here" is a real query.

The app is Swift with a Go backend — roughly 7,400 lines of Go to 3,300 of Swift, since the road-scoring work lives on the server. It ships through TestFlight as "Serpentine EV" with its own product, support and privacy pages. The commit history is unusually candid about operational lessons: a Caddy log filter that was written but never actually deployed, log records deleted once that landed, monitoring moved onto a machine that isn't the one being monitored, and a careful note that an empty keychain proves nothing about a signing certificate's expiry.

A ride planner for motorbikes that deliberately avoids the fastest route, picking curvy, quiet roads instead, and planning charging stops for electric bikes along the way. You can ask for a route by distance, by how much time you have, or as a loop that brings you home. It runs on iPhone, iPad and Mac.

Image Wizardnew

Image Wizard creates a private, searchable catalogue of all the photos and videos on a Mac, organising them by date, camera type and location while recognising objects, faces and text within the files. It lets you find images with natural-language queries or locate who appears in a video at specific times, all without sending anything to the cloud. The tool also exports selected groups back into Apple Photos.

PythonMLApple Siliconcomputer vision25 h
The longer version

Image Wizard is a local, on-device photo indexer for macOS on Apple Silicon: it walks a photo library, indexes by date, camera, and geolocation, then runs a battery of ML models with no cloud calls — YOLO 11 for object detection, InsightFace ArcFace embeddings clustered with HDBSCAN for iOS-style "People" albums, OpenCLIP for natural-language image search ("dog on a beach"), and Apple Vision OCR making text in photos full-text searchable. Videos get the same treatment frame-by-frame, so "who appears in this video, and when?" is answerable with timestamps.

It's Python end to end with a built-in web browser UI, using AVFoundation/ VideoToolbox for hardware HEVC decoding and correct rotation handling. About 32 hours of Claude Code sessions across 89 commits shaped it, including a memory-engineering pass that took indexing RSS from 53 GB to 5.8 GB via batched ML and a vector cascade, near-duplicate detection with perceptual hashing, an ML model that learns per-year orientation fixes, and durable incremental ingestion that survives dropped network mounts.

The result: a private, searchable index of a lifetime photo collection, with export tooling to feed curated subsets back into Apple Photos.

Image Wizard creates a private, searchable catalogue of all the photos and videos on a Mac, organising them by date, camera type and location while recognising objects, faces and text within the files. It lets you find images with natural-language queries or locate who appears in a video at specific times, all without sending anything to the cloud. The tool also exports selected groups back into Apple Photos.

MapMyFriends (iOS / Mac)new

MapMyFriends shows the people in your address book on a map so you can see who lives nearby, find a convenient meeting spot between friends, and notice when contacts have moved. It lets you pick a friend and an activity, suggests venues halfway, and sends calendar-ready invitations by text or email without sharing any data with third parties. Filters let you quickly spot contacts missing phone numbers or emails, helping you tidy your address book while planning meetups.

SwiftMapKitTestFlight23 h
The longer version

MapMyFriends puts your Contacts on a map to answer questions Apple makes hard: who do I know in Austin next week? Where's a coffee shop roughly between Sridhar and me? Which friends moved recently? It reads Contacts and Calendar on-device, geocodes through Apple's APIs, and shares nothing with third parties — no accounts, no servers.

It's Swift with MapKit, shipping for iPhone, iPad, and Mac (Catalyst). Beyond the map — profile-picture pins, real clustering, Home/Work/Other coloring — it has grown a person-first meetup planner: pick a friend and an activity, get venue suggestions between you, tap the preview map to pin the exact spot you actually meant when the search only found the labelled one nearby (the trailhead, not the trail), and send calendar-ready invites by text, email, or calendar, with a share-sheet fallback for friends whose mail app isn't Apple Mail. A filter button layers five combinable lenses over the map: overdue to see, computed from the app's own meetup history; within a distance band of you; has a phone or has an email, worth checking before you try to invite someone; and missing a phone number, which pairs with an in-app contact editor so the gap the map just exposed can be closed on the spot. A combination you keep reaching for saves under a name and reapplies in one tap. Presence is all the filter stores — whether a contact has a phone or an email, never the values themselves.

Development runs through a disciplined PR workflow — feature branches, code-review passes, an automated TestFlight pipeline that now attaches each build to its beta groups and submits external ones for review, so testers stop being added to every release by hand — with Claude Code as the collaborator across 83 commits. Currently v0.6.1 in invite-only TestFlight beta across iOS and macOS, with the Android port (see MapMyFriends Android) tracking feature parity.

MapMyFriends shows the people in your address book on a map so you can see who lives nearby, find a convenient meeting spot between friends, and notice when contacts have moved. It lets you pick a friend and an activity, suggests venues halfway, and sends calendar-ready invitations by text or email without sharing any data with third parties. Filters let you quickly spot contacts missing phone numbers or emails, helping you tidy your address book while planning meetups.

HappyJumpnew

HappyJump is an endless-runner where the player's smile makes the character jump, using the webcam feed processed entirely in the browser so no image or face data ever leaves the device. It can be installed and played offline, with an optional feature to share scores and view aggregate statistics via a tiny backend service. The game was created to offer a fun, hands-free experience while keeping user privacy intact.

TypeScriptMediaPipePWA22 h
The longer version

HappyJump is a Chrome-dino-style endless runner where smiling replaces the spacebar: the browser's webcam feed runs through MediaPipe's Face Landmarker at 30–60 fps, entirely client-side, and a smile's rising edge triggers the jump. No images, video, or face data ever leave the device — the page makes zero API calls from the game itself, no telemetry, no analytics.

The stack is Vite + TypeScript with no framework, MediaPipe Tasks Vision via WASM/WebGL with models vendored locally, and vite-plugin-pwa for a fully offline-capable installable app with silent updates. A small edge API handles opt-in score sharing and aggregate stats, with the share card rendered as a frozen scene screenshot. Roughly 26 hours of Claude Code sessions across two working trees built and polished it — including the production-hardening details like health probes, defensive stats SQL, and edge caching for the social-preview image.

Live and playable at happyjump.day.

HappyJump is an endless-runner where the player's smile makes the character jump, using the webcam feed processed entirely in the browser so no image or face data ever leaves the device. It can be installed and played offline, with an optional feature to share scores and view aggregate statistics via a tiny backend service. The game was created to offer a fun, hands-free experience while keeping user privacy intact.

AI Project Portfolio Generatornew

It gathers AI coding session logs and repository data from several machines, then compiles honest statistics on work done into a single self-contained HTML page that can be viewed without internet or JavaScript. The page updates automatically each morning, flagging any offline hosts or missing project descriptions, so the author can see what has actually been built across their fleet.

Pythonstatic sitedata viz9 h
The longer version

The tool that generates this page. It answers a question that turned out to be harder than it sounds: across seven machines and five different AI coding tools, what have I actually built, and how much work went into it?

A stdlib-only collector runs on each host — shipped over SSH on stdin, so nothing is installed remotely and it runs unmodified on old Raspberry Pis. It reads Claude Code session logs, Cowork desktop sessions, Codex CLI rollouts, Cursor's workspace databases and aider's in-repo history files, then walks the git repos it finds for commit counts, language mix and lines of code. A build step merges one JSON per host with hand-written descriptions into a single self-contained HTML page — light and dark, no external requests, no JavaScript required to read it.

Most of the engineering is in getting the numbers honest. Sessions are sittings rather than log files, since a resumed conversation appends to the same file for weeks. Idle gaps are capped at 15 minutes so a terminal left open overnight isn't counted as work. A repo cloned on several machines sums its sessions but never its commits or lines. Embedded credentials are stripped from remotes before anything is written to disk. Each of those rules exists because the first version got it wrong and reported something implausible.

It now maintains itself: a launchd job sweeps every host each morning, rebuilds the page and commits only when something changed, flagging hosts that were down or projects still waiting on a description. The source is public under the MIT license at phubbard/portfolio — the host list and the descriptions are configuration, so it will run against someone else's fleet unmodified.

It gathers AI coding session logs and repository data from several machines, then compiles honest statistics on work done into a single self-contained HTML page that can be viewed without internet or JavaScript. The page updates automatically each morning, flagging any offline hosts or missing project descriptions, so the author can see what has actually been built across their fleet.

iMessage Family Searchnew

iMessage Family Search lets a household search all their iMessage chats and DMs on a local web page, offering fast full-text results that can be filtered by sender or date and show highlighted snippets. A later version adds a natural-language query feature that answers questions from the messages, streaming replies with citations that link back to the original threads, while keeping every piece of data inside the home network.

SwiftRAGsqlite-veclocal LLM9 h
The longer version

A LAN web app that gives a family better search over their entire iMessage history — every group chat and DM — than the native Messages app. V1 is fast, ranked, snippet-highlighted full-text search filterable by sender and date; v2 adds "Ask": natural-language questions answered from the actual messages via RAG, with streamed responses and citations that deep-link back into the thread.

The indexer is Swift and uses Apple's NSUnarchiver to decode Messages' attributedBody blobs from chat.db into an index.db combining SQLite FTS5 and sqlite-vec chunk embeddings. A Hummingbird server fronts it for the LAN; inference is fully local through LM Studio (gpt-oss-120b for answers, bge-m3 for embeddings). Everything runs on one always-on Mac and no family data ever leaves the house. Built in an intense ~9-hour Claude Code collaboration producing ~15K lines, then hardened with Developer ID signing for stable Full Disk Access grants, event-driven live indexing with SSE push, and config hot-reload.

Messages identifies people by raw handle — a phone number or email — so conversations read as digits until something maps them to names. Reading macOS Contacts directly would have meant taking on extra TCC permission scope for a cosmetic feature, so the mapping is instead built offline: phone numbers are resolved against Fastmail contacts through its MCP server, and the results land in the handles config that the indexer applies to chat names. The lookup is a build-time step, not a runtime dependency — the running app still talks only to itself, which keeps the no-data-leaves-the-house property intact.

iMessage Family Search lets a household search all their iMessage chats and DMs on a local web page, offering fast full-text results that can be filtered by sender or date and show highlighted snippets. A later version adds a natural-language query feature that answers questions from the messages, streaming replies with citations that link back to the original threads, while keeping every piece of data inside the home network.

Marlowenew

Marlowe is an iOS Reddit client that avoids any analytics, tracking, ads or third-party data collection, communicating only with Reddit's own authentication and API services. It was created by forking an existing open-source app and stripping out telemetry, fixing stability issues and adding dark mode and a simpler visual system. The changes are limited to a small set of commits that remove phone-home code and improve reliability.

SwiftiOSReddit APIfork8 h
The longer version

Marlowe is a privacy-focused, open-source Reddit client for iOS — no analytics, no tracking, no ads, no third-party data collection, with the app talking only to Reddit's own OAuth and API endpoints. It is an actively maintained fork of lo-cafe/winston, rebranded to Marlowe and carrying a noir app icon to match.

The app is Swift on iOS 17+, roughly 38,000 lines of it. The privacy work meant removing the upstream ThemeStore and WinstonAPI phone-home code outright, and reworking authentication to redirect OAuth through a custom URL scheme so the app no longer depends on winston.cafe, with support for installed-app credentials that need no client secret. Alongside that came a run of stability fixes the upstream had left open: an unbounded AVPlayer leak that froze feed videos over long sessions, recurring video decode-pipeline exhaustion, scroll position jumping during pagination, tab bar hit detection in Split View on iPad, duplicate subreddits in the CoreData sync, post text truncation, and a startup blur race condition. CI was replaced with simple build and release workflows. This fork work was done across eight Claude Code sessions in spring 2026.

Releases ran from v1.2.0 (fork setup) through v1.6.0, which brought dark mode, a simplified appearance system, and consistent post tap behaviour. Worth reading the numbers on this card with care: the repository carries the whole upstream project's history, so the commit and line counts are overwhelmingly other people's work. The fork's own contribution is 20 commits touching 116 files, +814/−1777 lines — a deliberately small, surgical diff against a large existing codebase.

Marlowe is an iOS Reddit client that avoids any analytics, tracking, ads or third-party data collection, communicating only with Reddit's own authentication and API services. It was created by forking an existing open-source app and stripping out telemetry, fixing stability issues and adding dark mode and a simpler visual system. The changes are limited to a small set of commits that remove phone-home code and improve reliability.

Autodidactnew

Quietly saves the text of web pages you spend real time reading, in whichever browser you use, so you can find one again months later from a hazy memory of what it said. Everything is stored on your own machines rather than anyone else's service.

PythonFlaskSQLitebrowser extension8 h
The longer version

A personal, private answer to "I read something about this a few months ago and I cannot find it again". Autodidact records the text of web pages you actually dwell on — not everything you glance at — across every browser you use, so that a vague recollection is enough to find the page weeks later. Search accepts keywords with date and site filters, which is usually how a half-memory is shaped: roughly when, roughly where, a word or two.

Everything stays on the home network. A browser extension does the capturing, a small Flask server stores and indexes the text in SQLite with full-text search, and a search page queries it; nothing is sent anywhere else. FreshRSS was added as a second source, so things read through the feed reader are findable the same way.

Most of the work has gone into the unglamorous reality of browser extensions: a Safari queue that stalled on a storage quota, one combined iOS and macOS Safari project replacing a Mac-only one, and documented installation paths for Safari, iOS and unsigned Firefox forks. The server grew a debug page listing recent requests, and captures are tagged by device, because the first question when something goes missing is which machine failed to send it. It also deliberately skips its own pages, having otherwise indexed its own search results. This is milestone one — capture plus keyword search — with the milestone plan in the repository.

Quietly saves the text of web pages you spend real time reading, in whichever browser you use, so you can find one again months later from a hazy memory of what it said. Everything is stored on your own machines rather than anyone else's service.

GI Logger (BMLogger)new

GI Logger records gut-health symptoms locally and can optionally copy them into the phone's health app as clinical entries, keeping all data machine-readable and never locked away. It adds a monthly heatmap, range plots, CSV/PDF export and a watch companion that lets users log from the wrist and shows daily counts and a weekly consistency chart. Recent work fixed the watch-phone sync so updates always reach the watch and added visible status messages for any stalls.

SwiftHealthKitwatchOSFoundation Models6 h
The longer version

A privacy-first SwiftUI app for logging GI health data, with optional mirroring into Apple Health as clinical GI symptoms (Bristol scale mapped to constipation/diarrhea entries with severity). All data is local-first in SwiftData; Health mirroring is opt-in and carries rich metadata so the samples remain machine-readable. It ships to TestFlight as GI Logger — the rename covered display names, the iCloud Drive folder, the PDF report title and share-sheet filenames, but stopped at the bundle id, the export filenames and the store on disk, all of which still say BMLogger so existing installs migrate cleanly and the laptop-side CSV workflow keeps working. It is the same distinction MapMyFriends drew: what users read changes, what the system keys permissions and data off does not.

Beyond the fast-entry form it grew a monthly heatmap calendar, a range plot against Cleveland Clinic's published normal range, PDF and CSV export, and a standing guarantee that the data is never trapped — a CSV of every entry is kept current in iCloud Drive and the app's Documents folder, with the SwiftData store itself exposed through the Files app and Finder file sharing for direct inspection. A watchOS companion logs an entry from the wrist (Bristol type plus a 5–60 minute duration stepper matching the phone form) and puts two complications on the watch face: a circular today-count that taps through to logging, and a full-width seven-day bar chart colored by consistency, rendered in the accented mode so they pick up the dial's own tint. The Insights view splits the work between arithmetic and language: an InsightsEngine computes a least-squares weekly frequency trend, IQR outlier weeks, and blood/strain/duration flags over the last 90 days on every device, and Foundation Models then turns those pre-computed numbers into a two-or-three sentence on-device summary — availability-gated with graceful fallbacks, cached by data hash, and instructed never to diagnose or invent a number. The math decides the status; the model only narrates it.

Most of the recent effort went into making the watch link honest. The complications sat at zero for phone-logged entries because the push was gated on a remaining-transfers budget that reads zero when the complications are WidgetKit-only, so the wake transfer never went out; then diagnostics showed a second failure after every TestFlight reinstall, where the phone's cached isWatchAppInstalled stayed false until some unrelated watch-state change and the pusher silently skipped every summary. Pushes are now unconditional and re-sent on session activation and watch-state change, the watch handles the background connectivity task so a background launch survives long enough to take delivery, complication storage moved to an atomic JSON file in the app group, and both apps now show a sync status line so a stall is visible instead of silent. An App Store Connect rejection along the way traced back to deleting a mirrored Health sample requiring a read API, and therefore a Health *share* purpose string it never previously needed. About six hours of Claude Code collaboration across two dozen commits, notably reusing the TestFlight release pipeline built for MapMyFriends — infrastructure paying for itself across projects. Shipped to TestFlight at v1.4.

GI Logger records gut-health symptoms locally and can optionally copy them into the phone's health app as clinical entries, keeping all data machine-readable and never locked away. It adds a monthly heatmap, range plots, CSV/PDF export and a watch companion that lets users log from the wrist and shows daily counts and a weekly consistency chart. Recent work fixed the watch-phone sync so updates always reach the watch and added visible status messages for any stalls.

Skylight Calendar Proxynew

A lightweight proxy sits between the calendar feeds and the two household displays, recording each request's timestamp, source, response hash and any changes to the data. It was added because the displays stopped showing new events while other devices worked, and there was no way to see whether the fault lay in the feed, the vendor's servers or the panels themselves.

JavaScriptCloudflare WorkersiCaldebugging6 h
The longer version

Two Skylight calendar displays run the household schedule, and both quietly stopped showing newly created events — while the vendor's phone app, reading the same account, showed every one of them. Nothing in the product exposes what a display last fetched or when, so there was no way to tell whether the calendar feeds, the vendor's servers, or the panels themselves were at fault. This proxy inserts a logging observer into the middle of that path: the displays subscribe to it instead of to the calendars directly, and every request they make is recorded.

It is a single Cloudflare Worker — about 480 lines of dependency-free JavaScript, one KV namespace, no build step. Each subscribed calendar is served under a secret token prefix and passed through byte-for-byte; the Worker records every fetch (user agent, source address, conditional headers, upstream latency, HTTP status and a SHA-256 of the body) and writes a full snapshot only when the content actually changes, both on a 30-day TTL. Debug endpoints then answer the questions that matter: when did the display last poll, what did it get, what changed between any two snapshots — diffed per event by UID and RECURRENCE-ID rather than as text — and does the live upstream feed still agree with the last thing served. Conditional GETs are deliberately ignored so every poll is visible, and an upstream failure returns an error rather than a valid but empty calendar, which a display would happily render as "no events."

The logs settled the question in two rounds. Across both incidents the vendor's sync service fetched the feeds a dozen or more times a day, every response HTTP 200 with an identical hash, and the missing events were present in every single one — so the feed and the fetch path were exonerated, and a power cycle recovering everything pointed at the cloud-to-display sync inside the product. A follow-up experiment trimmed every feed down to upcoming events only, roughly one percent of the original size, using an optional past-event filter in the Worker that drops whole expired VEVENT blocks and leaves the rest byte-identical; the stall recurred anyway, which killed the feed-size hypothesis. That evidence — timestamps, hashes, and the exact bytes served — became a reproducible bug report to the vendor, which is a better outcome than a guess. The Worker stays deployed as standing instrumentation.

A lightweight proxy sits between the calendar feeds and the two household displays, recording each request's timestamp, source, response hash and any changes to the data. It was added because the displays stopped showing new events while other devices worked, and there was no way to see whether the fault lay in the feed, the vendor's servers or the panels themselves.

Family Tree in LaTeXnew

Family Tree in LaTeX produces a clean vector diagram of six generations of a single family line, fitting the whole chart on one printable page with careful spacing and alignment. It was created because existing automatic genealogy tools handle long narrow descents poorly and the author needed precise control while confirming names and dates from archival research.

LaTeXTikZgenealogy6 h
The longer version

A typeset genealogical chart tracing six generations of one family line, built in LaTeX with TikZ so the result is a proper vector diagram rather than a screenshot from a genealogy website. The descent is mostly linear, which sounds simpler than it is: the interesting work is in spacing, alignment and keeping the whole thing legible on a single printable page as generations accumulate.

TikZ was the right tool precisely because the layout needed hand control — automatic family-tree packages make reasonable choices for large branching trees and poor ones for a long narrow line of descent. Supporting research went into the archives to confirm names and dates, including full-text searches of digitised county histories on archive.org for one of the surnames.

Seven sittings and close to six hours, including a handoff step that wrote the entire working state out to a markdown file so the project could move between machines mid-flight.

Family Tree in LaTeX produces a clean vector diagram of six generations of a single family line, fitting the whole chart on one printable page with careful spacing and alignment. It was created because existing automatic genealogy tools handle long narrow descents poorly and the author needed precise control while confirming names and dates from archival research.

LAN Ollama Inference Hostnew

LAN Ollama Inference Host runs a local large-language-model service on a Mac Studio for the whole network, providing embeddings, a chatbot, a web UI and an API used by several applications. The work fixed two reliability problems - response times that worsened under continuous load until the server stopped answering, and automatic upgrades that reset the service to listen only on localhost, cutting off other machines. It also identified the correct model files from public repositories to ensure the right versions were used.

OllamaLLM opsmacOShome lab5 h
The longer version

Axiom, a Mac Studio, serves local LLM inference to the rest of the network — RAG embedding, a chatbot, a web UI, and a handful of applications calling the chat API directly. This is the work of making that host actually dependable, which mostly meant chasing two failure modes.

The first was a slow degradation under sustained load: response times creeping up until the server stopped answering entirely, sometimes needing a full restart. The second was quieter and more annoying — every brew upgrade silently reset Ollama's configuration to bind to localhost only, cutting off every other machine on the LAN until someone noticed. Alongside the debugging went model selection work, tracking down the exact Hugging Face repositories carrying GGUF builds of specific embedding and instruct models rather than trusting a name to resolve to the right quantisation.

Roughly five and a half hours across several sittings. Unglamorous infrastructure work, but everything else on this network that talks to a local model depends on it staying up.

LAN Ollama Inference Host runs a local large-language-model service on a Mac Studio for the whole network, providing embeddings, a chatbot, a web UI and an API used by several applications. The work fixed two reliability problems - response times that worsened under continuous load until the server stopped answering, and automatic upgrades that reset the service to listen only on localhost, cutting off other machines. It also identified the correct model files from public repositories to ensure the right versions were used.

Internet Wizardnew

A tool that runs through the checks you would do by hand when the internet stops working - do I have an address, can I reach the router, can I look up names, can I reach the outside world - and uses the pattern of what passed and failed to say which part is actually broken.

PythonnetworkingiPad5 h
The longer version

An automation of the mental checklist every home-network owner runs when the internet "feels broken": do I have an IP? Can I ping the router by IP? By name? Can I reach public DNS by IP? Resolve hostnames? Each layer's pass/fail feeds an inference about what actually broke — DHCP, DNS, wifi, or the upstream link — and what to reboot.

The self-imposed constraint shapes the design: it must run on an iPad under Pyto or a-Shell, so it's pure Python 3 standard library plus Python-only packages — no C extensions, which meant swapping the DNS library for Julia Evans' pure-Python implement-dns code. Developed across Claude Code and Cursor sessions ("vibe coded improvements" is a literal commit message), with VPN-aware configs so the same tool works from inside or outside the LAN.

A tool that runs through the checks you would do by hand when the internet stops working - do I have an address, can I reach the router, can I look up names, can I reach the outside world - and uses the pattern of what passed and failed to say which part is actually broken.

MapMyFriends (Android)new

MapMyFriends lets users see their contacts on a map, plan meet-ups, send text invitations, drop pins for locations, and save favourite places with notes and ratings, while filtering contacts in custom groups. It mirrors the iOS version so friends can use the same features across both phones. The app is being prepared for beta release alongside its iPhone counterpart.

KotlinAndroid4 h
The longer version

The Android port of MapMyFriends, written in Kotlin, tracking the iOS app's feature set release by release: contacts-to-map with geocoding, the person-first meetup planner, text invites through the system SMS composer, drop-a-pin-to-plan, favorite places with ratings and notes, and contact filters with saved combinations.

The interesting engineering is in the parity discipline — a shared cross-platform feature matrix drives both codebases, and commits land as "iOS PR #7 parity" pairs, with platform divergences (like smsto: intents not carrying .ics attachments) documented as they're found. Built with Claude Code assistance across 35 commits, including staged database migrations (v4→v6) and Play Store release signing, ready for beta distribution alongside the iOS TestFlight.

MapMyFriends lets users see their contacts on a map, plan meet-ups, send text invitations, drop pins for locations, and save favourite places with notes and ratings, while filtering contacts in custom groups. It mirrors the iOS version so friends can use the same features across both phones. The app is being prepared for beta release alongside its iPhone counterpart.

Claude Footswitchnew

Claude Footswitch is a tiny macOS menu-bar app that lets you tap a USB foot pedal to confirm Claude Desktop's "Allow once" prompts, keeping your hands on the keyboard. It translates each pedal press into the shortcut that approves the prompt and handles the necessary system permissions so the app continues to work after updates.

SwiftmacOSUSB HID4 h
The longer version

A tiny macOS menu-bar app that maps a USB foot pedal to Claude Desktop's "Allow once" action — stomp to approve whatever Claude is asking to do, hands never leaving the keyboard. A hardware accelerant for an AI-heavy workflow, built with the AI it accelerates.

The engineering is deeper than it sounds, mostly because every obvious approach turned out to be blocked. The pedal enumerates as a USB HID keyboard, so the app matches it by vendor/product ID with IOHIDManager — but it has to read it non-exclusively, because seizing the device made it re-enumerate in a loop, deliver nothing to either callback, and still leak its keystroke to the focused app. Approving was originally a press of Claude's "Allow once" button through the accessibility tree; that failed because the permission prompt is web content exposing no tappable AX button, so the app injects ⌘↩ instead, which is the shortcut the prompt already binds. And because the pedal isn't seized, its own plain Return would still reach whatever is focused and could submit a line in your terminal — so a CGEventTap drops that Return inside a short window armed by each confirmed press. The real Enter key is outside the window and the app's own ⌘↩ carries the Command flag, so neither is touched; timing plus that flag are the discriminator because the pedal reports an inconsistent keyboard type from press to press. Focus is restored to the previous app afterward, so Claude can sit off to the side.

The other half of the work is permissions, which on macOS are keyed to a code signature: ad-hoc signing produces a new hash every build, so every rebuild silently revoked the Accessibility and Input Monitoring grants — the cause of a long "I granted this and it says I didn't" hunt. The build now picks the most stable identity it can find, preferring a Developer ID Application certificate whose Team-ID-based designated requirement lets grants survive not just rebuilds but certificate renewal, and falling back through a stable local self-signed identity to ad-hoc. Input Monitoring only takes effect for a process that held it at launch, so granting it to the running app did nothing until the app learned to relaunch itself the moment the permission flips on. Nine commits took it from initial commit to signed, notarized, universal DMGs built by a tagged-release CI workflow — the last of them fixing a release failure that only appeared on GitHub's runners, where bash 3.2 in a UTF-8 locale swallows a trailing multibyte character into the variable name of an unbraced expansion.

Claude Footswitch is a tiny macOS menu-bar app that lets you tap a USB foot pedal to confirm Claude Desktop's "Allow once" prompts, keeping your hands on the keyboard. It translates each pedal press into the shortcut that approves the prompt and handles the necessary system permissions so the app continues to work after updates.

TGN Whisperernew

TGN Whisperer turns three watch podcasts into searchable static websites that include speaker-identified transcripts and episode notes, so listeners can read the full archive instead of only hearing it. It was built because a decade's worth of podcast audio from friends deserved to be accessible as text. The system automatically updates each site when new episodes are released.

PythonPrefectASRstatic sites4 h
The longer version

TGN Whisperer transcribes and publishes the full archives of three watch podcasts — The Grey NATO, 40 & 20, and Hodinkee Radio — as searchable static websites with speaker-attributed transcripts and per-episode shownotes. It began with whisper.cpp and the idea that a decade of friends' podcast audio ought to be text.

It's a distributed pipeline: a Raspberry Pi 4 runs the Prefect orchestration and serves the sites through Caddy, while speech-to-text and diarization run on a Mac Studio via the FluidAudio backend (see FluidAudio Web Service), called through an async submit-and-poll API. The Anthropic API handles speaker attribution, zensical builds the sites, and Pagefind supplies the search index. Two details absorb most of the fiddliness. Episode numbering is per-podcast, because TGN's feed carries sequential itunes:episode tags that are simply wrong — the real numbers are parsed out of episode titles instead, with unnumbered specials getting fractional ones — and a vocabulary-correction layer fixes the terms the recognizer reliably mangles ("Graynado" for Grey NATO, "Hodinky" for Hodinkee, a handful of watch brands), scoped carefully enough that it never touches a real domain name or the gray NATO strap that shares the name. The raw recognizer output stays on disk untouched as the authentic source; corrections are applied on the way to markdown.

The project spans 282 commits since early 2023, with CI and a pull-request workflow, and picked up Claude Code and Cursor assistance along the way for reprocessing tooling, format migrations, and site rebuilds. Recent work has gone into making the system legible from outside: docs/ now carries an interactive architecture map generated with archify from a typed JSON spec, self-contained and pinned to the revision it was built from, so its source links can't quietly drift from the code. An experiment script puts Claude and a locally hosted Gemma 4 31B through the same attribution and synopsis prompts and emits a side-by-side HTML report — kept in the repo against the day open-weight models are good enough to take the one paid API call out of the loop — and a companion utility renders traffic dashboards straight from Caddy's JSON access logs. All three sites are live and update automatically as new episodes drop, each home page stamped with when it last rebuilt, and the transcript corpus feeds the TGN Chat RAG bot.

TGN Whisperer turns three watch podcasts into searchable static websites that include speaker-identified transcripts and episode notes, so listeners can read the full archive instead of only hearing it. It was built because a decade's worth of podcast audio from friends deserved to be accessible as text. The system automatically updates each site when new episodes are released.

Property Tasksnew

A shared app for keeping track of jobs that need doing around a house or other property. You photograph a problem, file it under the place it belongs — building, then room, then task — and everyone sharing the list sees it, with a permanent record of who changed what and when. It runs in a web browser and as an app for iPhone, iPad and Mac, and the camera works even with no signal, uploading once a connection comes back.

TypeScriptSwiftCloudflare WorkersDurable Objects3 h
The longer version

A shared, hierarchical tracker for property upkeep: notice that the bathroom AC has failed, take a few photos of the state it is in, and file it under location → building → room → task, where anyone else in the household sees it and every change carries a name and a timestamp. The prior-art survey that opened the project is the reason it exists at all — professional CMMS tools have the hierarchy but fixed two or three levels and per-seat pricing, consumer home-maintenance apps have the photos but no audit log and no real sharing, and nothing found combined arbitrary depth, heterogeneous content, attributed history, a handful of shared users and household pricing.

The model deliberately knows nothing about buildings. It knows Spaces, Nodes, Kinds, Labels, Attachments and Events; "Location", "Building", "Room", "Repair" and "Inspection" are just labels on top. That keeps the same engine useful for a boat, a garden, a club's equipment or an estate clear-out, and it is the kind of decision that is cheap on day one and impossible on day two hundred. State is an append-only event log with a projection over it, so history is not a feature bolted onto the side — it *is* the data, and the current view is derived from it.

The stack is Cloudflare: one Durable Object per Space holding that log in SQLite, R2 for the photos and documents, D1 for identity, and a single Worker serving the API and rendering the web pages itself from template literals — no client framework, with a service worker making recently viewed pages readable offline. Filters live entirely in query parameters, so every view is a URL you can send to someone. The Apple side is one SwiftUI target covering iPhone, iPad and Mac, explicitly not Catalyst, with a camera-first capture flow and an offline upload queue so a photo taken in a basement with no signal still lands. The Swift core is a port of the TypeScript projection, and swift test replays the *same* fixture files the TypeScript tests use — one set of recorded events proving both implementations agree, which is the only honest way to keep two clients of an event-sourced system from quietly diverging.

Four days old at the time of writing: the scaffold, the design and research notes, the server-rendered web app and the first iOS capture flow.

A shared app for keeping track of jobs that need doing around a house or other property. You photograph a problem, file it under the place it belongs — building, then room, then task — and everyone sharing the list sees it, with a permanent record of who changed what and when. It runs in a web browser and as an app for iPhone, iPad and Mac, and the camera works even with no signal, uploading once a connection comes back.

Serpentine for Androidnew

An Android version of Serpentine, the app that plans motorbike rides along curvy, interesting roads instead of the fastest route. The hard work of choosing roads happens on a server that the iPhone app already uses, so this is the Android screens talking to the same service. It is at an early stage: it can plan a loop from where you are and show the route, but the map and charging features are still to come.

KotlinJetpack ComposeAndroidmaps3 h
The longer version

The Android client for Serpentine, the curvy-road motorcycle ride planner. The framing that shapes the whole project is that this is a *second client, not a port of the app*: routing, loop scoring, the energy model, charge stops and GPX export already live in the Go server that the iOS app talks to, so the Android work is the screens, the wire types and the thin layer between them. Route logic written here would be route logic in two places — the rule in the repo's conventions is to stop and move it to the server, where it serves both clients at once.

Kotlin and Jetpack Compose, about 2,100 lines of Kotlin so far. The pure model layer is deliberately free of Android imports — the planner, the reach calculation, bike definitions, ride progress, formatting and geometry — so its unit tests run on the JVM in a second, against real server responses copied over from the iOS test fixtures. Wire.kt is field-for- field with the Swift models, and three of its JSON settings are load-bearing enough to each have their own test, which is the kind of thing you only learn by having the server reject your request. The same privacy constraints carry over from iOS: the app talks to one host and nothing else, with no analytics, no crash reporting SDK, no ad network and no background location.

It is early and honest about it. One session so far produced the wire types, the ride logic and working plan and ride screens: it will plan a loop or an out-and-back from where you are and show the route's shape, distance, time, climb and the roads it uses. A real map, place search, navigation hand-off, saved rides and charge planning are all still ahead, and feature parity with iOS is tracked row by row in a shared sync.md where a row only counts as done once it is in a build a tester can install. The map is the one genuinely open question, since every obvious option is an SDK that would break the single-host rule.

An Android version of Serpentine, the app that plans motorbike rides along curvy, interesting roads instead of the fastest route. The hard work of choosing roads happens on a server that the iPhone app already uses, so this is the Android screens talking to the same service. It is at an early stage: it can plan a loop from where you are and show the route, but the map and charging features are still to come.

TGN Chatnew

TGN Chat lets you ask questions about ten years of The Grey NATO podcast and get streamed answers that cite the exact episode they come from. It works by searching a single SQLite file that stores both semantic vectors and keyword indexes, then letting your browser generate the reply using a locally-run language model, so no cloud service or external database is required. This setup was created to provide a cheap, self-hosted way of retrieving information from a private transcript collection.

PythonRAGsqlite-vecLM Studio2 h
The longer version

A self-hosted RAG chatbot over 380 episodes — ten years — of The Grey NATO podcast, built on the transcript corpus from TGN Whisperer. It answers questions with streamed responses and citations pointing to real episodes, using hybrid retrieval: sqlite-vec KNN over bge-m3 embeddings combined with SQLite FTS5 keyword search, across transcripts, synopses, and 7,663 curated episode links. The two halves cover for each other — dense vectors find the semantic match, BM25 catches the exact proper noun, a reference number or a person or a place, that embeddings blur.

The serving stack is deliberately small: one Python file on the standard library's HTTP server does search and logging, a vanilla-JS front end with no build step does the chat, Caddy fans requests between them, and LM Studio provides OpenAI-compatible local inference — no cloud LLM, no vector database, no per-query cost. The division of labour is the interesting part: the server orchestrates retrieval and the browser orchestrates generation, posting to LM Studio through the same proxy, so streamed tokens never round-trip through Python. Everything else lives in a single SQLite file — episode text, 1024-dimension vectors in a vec0 virtual table, and the keyword index side by side. It runs on the Mac Studio as a pair of launchd agents rather than daemons, because LM Studio is a GUI app in the login session; a wrapper script starts LM Studio's server and blocks until it answers, so the search API can never come up before embeddings are available. Around that sit a real-time monitoring dashboard, session logging, thumbs up/down feedback, topic extraction wired into search, and shareable chat URLs.

Rebuilds used to punish every sync: the file-hash manifest treated a cosmetically regenerated episode file as changed, so a routine refresh re-embedded the whole corpus in about eleven minutes. Replacing the manifest with a cache keyed on the SHA-256 of each chunk's text made the pipeline genuinely incremental — an embedding is a pure function of text and model, so unchanged text is simply reused, and a no-op rebuild now reuses all ~9,000 cached vectors in six seconds while a shownotes edit re-embeds one chunk per episode instead of all of them. The repo also carries a long-form design document and four interactive archify diagrams — runtime architecture, ingest and rebuild, query lifecycle, data lineage — written for people who want to port the pattern, local hybrid RAG over a private corpus on commodity hardware, to their own data. Live at tgnchat.phfactor.net.

TGN Chat lets you ask questions about ten years of The Grey NATO podcast and get streamed answers that cite the exact episode they come from. It works by searching a single SQLite file that stores both semantic vectors and keyword indexes, then letting your browser generate the reply using a locally-run language model, so no cloud service or external database is required. This setup was created to provide a cheap, self-hosted way of retrieving information from a private transcript collection.

Network Canarynew

A small screen on a shelf that shows at a glance whether the home network is healthy. A tiny computer behind it keeps testing each layer of the connection, from the local wiring out to the wider internet, and colours each result so a problem is obvious without checking anything manually.

PythonRaspberry Pie-paper2 h
The longer version

A glanceable network status monitor: a Raspberry Pi 4 driving a Waveshare 3.97-inch color e-paper display, continuously testing the layers of home connectivity — ethernet IP, DNS server, local resolution, gateway, raw internet reachability, and remote DNS — and showing color-coded results on an 800×480 panel.

It's Python with PIL for rendering, installed as a systemd service, with a twist: the same rendered image is served over HTTP, so any device on the LAN can check status without walking to the display. Updates are intelligent — the e-ink only refreshes when status actually changes, preserving panel lifespan. Built in about 2.6 hours of Claude Code sessions in October 2025, from initial commit to boot-at-startup service with an HTTPS health check added.

It sits on the network shelf doing exactly one job: telling you which layer broke before you start debugging the wrong one.

A small screen on a shelf that shows at a glance whether the home network is healthy. A tiny computer behind it keeps testing each layer of the connection, from the local wiring out to the wider internet, and colours each result so a problem is obvious without checking anything manually.

Claude Session Renderernew

Claude Session Renderer turns Claude Code session transcripts into self-contained HTML pages that can be browsed, searched by text and ordered by date behind a simple login. It gathers transcripts from every machine on the network, deduplicates them and presents the results as an in-house alternative to the official team-plan sharing feature, avoiding any external requirements.

Pythonself-hostedtooling2 h
The longer version

Renders Claude Code session transcripts into styled, self-contained HTML pages, aggregates them from every machine on the network over SSH, and serves the result behind basic auth — a self-hosted stand-in for Claude Code's team-plan session sharing, with none of the requirements.

It's Python: one script renders a JSONL transcript to a zero-external- request HTML page (CSS/JS inlined, works offline and under strict CSP), another SSHes to N machines, renders everything, deduplicates cross-host sessions, builds a pagefind full-text search index and a date-ordered browse index, and deploys. Credential redaction runs by default in both tools. It also knows how to collect Cowork desktop-app sessions via a reserved target. Runs nightly under launchd with incremental rsync — the observability layer for the same fleet-wide AI work this portfolio documents.

Claude Session Renderer turns Claude Code session transcripts into self-contained HTML pages that can be browsed, searched by text and ordered by date behind a simple login. It gathers transcripts from every machine on the network, deduplicates them and presents the results as an in-house alternative to the official team-plan sharing feature, avoiding any external requirements.

Chat with Historical Figures (RAG)new

It lets you chat with people from the 1800s, answering questions in their own words by drawing on public-domain writings. The aim was to create a fully local service that avoids any external API costs while keeping a record of each interaction for later analysis of how well the answers match the source material. Marie Curie was the first figure added, with plans to expand to other authors whose works are out of copyright.

RAGSQLiteOllamapublic domain1 h
The longer version

A retrieval-augmented chat site that lets you interview figures from the 1800s in their own words — the copyright cutoff being the whole point, since it means the corpus can be built entirely from public-domain writing. Marie Curie was the first subject: the opening work was simply finding as much of her surviving written output as possible, then widening the search to other authors whose work has fallen out of copyright.

The stack is deliberately lightweight and entirely local — gpt-oss:120b served by the LAN Ollama host, with SQLite plus its vector extension handling embeddings and retrieval, so there is no external API in the loop and no per-query cost. By the beta stage the focus had moved to observability: logging every query and response in structured markdown with local timestamps, token rate, generation duration and total duration, so that retrieval quality and performance could be judged from real usage rather than impressions.

It lets you chat with people from the 1800s, answering questions in their own words by drawing on public-domain writings. The aim was to create a fully local service that avoids any external API costs while keeping a record of each interaction for later analysis of how well the answers match the source material. Marie Curie was the first figure added, with plans to expand to other authors whose works are out of copyright.

Women-directed Movie Browsernew

A small website for browsing films directed by women, built deliberately without any JavaScript, so it loads fast and works everywhere. It keeps its own local copy of the film database and refreshes it once a week overnight, and you can tick films into a list and share that list as a link. It began as an attempt to show what was watchable across the streaming services at once, and became something more specific instead. It is online at movies.phfactor.net.

PythonFlaskSQLiteTMDb1 h
The longer version

A small server-rendered web app for browsing films directed by women, built around a deliberate constraint: no client-side JavaScript at all. Every filter, every page of results, the selection checkboxes and the shareable shortlists are plain HTML forms and query parameters, which means every view is a bookmarkable URL and the whole thing works in any browser, on any connection, with no framework underneath it. Water.css and a few dozen lines of custom CSS do the styling.

It started as something else. The original question was whether anything existed that showed what was watchable tonight across Netflix, Apple TV and Prime at once, without ads or affiliate links — and when the answer was no, this was going to be it. Full streaming availability is now listed in the project's own notes as an explicit non-goal, intentionally dropped; what survives is a JustWatch search link per film, which hands that question off to a site that already answers it well. What replaced it is a narrower and more interesting catalogue: TMDb data, filtered to films whose director's gender is actually known to be female, which turns out to be the hard part. TMDb's person records carry a gender field, but it is unpopulated for a great many people, so the women-directed filter can only include films whose credits have been hydrated — the coverage problem, not the filter, is the real work.

Flask 3 over SQLite in WAL mode, with the browse pages served entirely from the local cache so no TMDb call happens while a visitor waits. A separate ingest script fetches TMDb's daily export files, hydrates directors and details, and prefetches posters, at a rate limit it sets itself. It is newly wired to a systemd timer — Sunday at 03:00, with Persistent=true so a machine that was off still catches up, a randomised delay to avoid hammering TMDb on the hour, and Nice=10 with idle I/O scheduling so a weekly bulk ingest never makes the browsing feel slow. A session-based basket survives pagination without any login, and can be shared as a link that someone else can fork into a basket of their own.

It is live at movies.phfactor.net, served from the Raspberry Pi 5 that runs the rest of the home lab's public sites.

A small website for browsing films directed by women, built deliberately without any JavaScript, so it loads fast and works everywhere. It keeps its own local copy of the film database and refreshes it once a week overnight, and you can tick films into a list and share that list as a link. It began as an attempt to show what was watchable across the streaming services at once, and became something more specific instead. It is online at movies.phfactor.net.

Fanless 2.5G Switch Selectionnew

A search for a silent, fanless network switch after a cheap one dropped traffic when a fast link fed a slower device. The requirement was six to eight fast ports, no cooling fan, and correct handling of the signal that tells a sender to slow down. The machines in this portfolio sit behind whatever it picked.

Networkinghardwareresearch1 h
The longer version

A hardware selection exercise for the home lab's network core, driven by a specific and easily-missed failure: a cheap desktop switch whose flow control didn't actually work, dropping packets whenever traffic from the 10G SFP ports overran a 1G link. The requirement set was narrow — at least six but no more than eight 2.5GbE ports, completely silent so no cooling fans, unmanaged being acceptable, but correct TCP flow control being non-negotiable.

Most of the work was in evaluating candidates against that last point, which manufacturer datasheets tend to gloss over: whether a given switch implements pause frames properly, how it behaves when a fast uplink feeds a slower client, and which fanless designs stay silent without thermally throttling. Four sittings across February 2026, roughly 1.3 hours of active research.

It is documented here because buying decisions for the fleet's plumbing are as much a part of running this network as the software is — the hosts in this portfolio all sit behind whatever switch this picked.

A search for a silent, fanless network switch after a cheap one dropped traffic when a fast link fed a slower device. The requirement was six to eight fast ports, no cooling fan, and correct handling of the signal that tells a sender to slow down. The machines in this portfolio sit behind whatever it picked.

Hazard (Risk variant)new

Hazard lets you play a web version of the classic board game where the map behaves like the original, with each region represented as an interactive shape that tracks who owns it and which neighbours it touches. It started as a joke for a friend who wanted Greenland to always win, then expanded because the idea proved enjoyable enough to become a proper playable game.

JavaScriptSVGgame1 h
The longer version

This began as a joke for a friend — a playable web version of Risk in which Greenland always wins, no matter what the dice say. It made the friend happy enough that it grew into a real thing: renamed to Hazard, given a git repository, and built out with gameplay closer to the original board game rather than a single gag.

The interesting problem was the map. A recognisable Risk board needs accurate territory shapes with correct adjacency, and the territory outlines had to be sourced as open SVG path data from existing open-source implementations rather than traced by hand. Everything runs client-side in the browser as a single page, with the territories as addressable SVG paths so that adjacency, ownership and highlighting are all just attributes on the map.

Four sittings, a little over an hour. A reminder that a throwaway joke with a good hook is often a better starting point for a project than a serious plan.

Hazard lets you play a web version of the classic board game where the map behaves like the original, with each region represented as an interactive shape that tracks who owns it and which neighbours it touches. It started as a joke for a friend who wanted Greenland to always win, then expanded because the idea proved enjoyable enough to become a proper playable game.

Vandenberg Launch Calendarnew

Vandenberg Launch Calendar supplies a subscribable iCal feed of upcoming rocket launches from Vandenberg Space Force Base, including provider, vehicle, pad and mission details in each event. It lets any calendar app display those launches automatically, without installing extra software or manually checking schedules, by generating the feed on request and relying on the client's own polling rather than a separate scheduler.

Cloudflare WorkersiCalserverlessJavaScript1 h
The longer version

A subscribable calendar feed of upcoming rocket launches from Vandenberg Space Force Base. Point any calendar app at the URL and launches appear alongside everything else, with provider, vehicle, pad and mission details in the event body — no app to install and nothing to check manually.

It started as a search for an existing feed. The obvious candidate was empty, but rocketlaunch.live publishes the schedule as JSON, so the project became a thin translation layer: fetch JSON, keep the Vandenberg launches, emit RFC 5545 iCal. That layer is a single Cloudflare Worker of about 140 lines with no dependencies, no build step and no storage.

The design choice worth keeping is that there is no scheduler. The obvious architecture — a cron job that regenerates a static .ics — turns out to be unnecessary, because calendar clients already poll their subscriptions: the Worker generates the feed on request and Cache-Control tells the client how long to hold it, so the client's own refresh *is* the schedule. It also degrades rather than breaking, running unauthenticated against the next five launches worldwide and filtering locally, or using an API key to have the service filter by location server-side.

Writing it up afterwards turned up two things worth knowing. Cloudflare's edge cache does not front *.workers.dev, so the s-maxage and stale-while-revalidate headers are inert until a custom hostname is bound — the intended protection against an upstream outage isn't actually in place. And the unauthenticated mode can empty the calendar silently, since a busy launch period elsewhere pushes Vandenberg out of the global top five and an empty feed is still valid iCal. The source is now public at phubbard/vandenberg-launches; until then its only copy was sandbox output, with Cloudflare running the sole deployed instance.

Vandenberg Launch Calendar supplies a subscribable iCal feed of upcoming rocket launches from Vandenberg Space Force Base, including provider, vehicle, pad and mission details in each event. It lets any calendar app display those launches automatically, without installing extra software or manually checking schedules, by generating the feed on request and relying on the client's own polling rather than a separate scheduler.

Dad Bod Levo Squadnew

A t-shirt graphic for a weekend electric-mountain-bike group, showing two middle-aged riders under the caption 'Dad Bod Levo Squad'. It was made by generating and refining images over an evening, with most of the effort going into getting readable lettering into the picture.

Graphic designimage generation
The longer version

A t-shirt graphic for a weekend eMTB riding group: two middle-aged men on electric mountain bikes, under the legend "Dad Bod Levo Squad". The brief was exactly that self-aware — the joke is the point, and the design had to land it without tipping into either genuine athletic-brand seriousness or outright mockery.

The work was iterative image generation rather than code: establishing the two riders and their bikes, then refining pose, framing and lettering across nineteen exchanges in two sittings on a single evening in January 2026. Getting legible type integrated into a generated illustration took most of the back-and-forth, which is the usual sticking point with this kind of prompt-driven design.

It's the one piece here that isn't software, and it earns its place for that reason: the same AI-assisted iteration loop used on the rest of these projects, pointed at a design problem instead of a codebase.

A t-shirt graphic for a weekend electric-mountain-bike group, showing two middle-aged riders under the caption 'Dad Bod Levo Squad'. It was made by generating and refining images over an evening, with most of the effort going into getting readable lettering into the picture.

Lanternnew

Lantern provides a single self-hosted service that hands out IP addresses and resolves domain names for a home network, while also caching lookups, blocking ads via host lists and identifying devices passively. It was created to replace the existing router-based DNS/DHCP setup with something lightweight that can run on modest hardware and be switched on or off in one step.

GoDNSDHCPself-hosted
The longer version

Lantern is a self-hosted DNS and DHCP server for home networks: a single stateless Go binary combining dynamic host configuration, a recursive DNS resolver with caching and DoH upstream fallback, hosts-format ad blocking, and passive p0f-style device fingerprinting. Inspired by Pi-hole, it targets minimal hardware — a Raspberry Pi, NUC, or VM.

It started as a Claude Cowork prototype called homedns, built around one requirement: tell it your subnet and every address it leases gets valid forward *and* reverse DNS, with the lease table as the single source of truth for both. That prototype settled the architecture — Go 1.22, miekg/dns and insomniacslk/dhcp, CGo-free SQLite, JSON config with validation and hot reload — before the work moved into this repository under its current name. On top of it sit a Pi-hole-style web dashboard driven by server-sent events, a full REST API, latency percentiles via hdrhistogram, hot config reload on SIGHUP, and drop-in systemd/launchd/Docker deployment. Development was heavily Claude Code-assisted: the commit history shows AI-paired work on the GitHub Actions CI pipeline, Docker-based DNS/DHCP integration tests, a release workflow with cross-platform binaries, and a test-coverage push from 29.5% to 47.5%.

Features landed to date include blocklist subscriptions with pause/resume, a two-tier cache, UniFi config import, a DNS-only deployment mode, and lease detail pages that pull together the lease itself, the device fingerprint, a paginated DHCP/DNS event history, per-host query stats, and a button that releases a dynamic lease outright. The live-data pages have taken most of the recent attention. The DNS query log now actually renders its event stream — the SSE handler had been logging to the browser console and never touching the DOM — as a capped, auto-scrolling table badged by where each answer came from: blocked, local, cached, or upstream. Chart.js moved out of the base layout onto the two pages that need it, after its CDN fetch turned out to be blocking every page render. And DHCP option 61 client IDs are hex-encoded rather than cast to a string, because the option carries a hardware-type byte plus a MAC in binary and printing it as text produced unreadable garbage.

Testing happens on a deliberately isolated segment: Lantern binds to a USB ethernet adapter, NAT scripts give test clients a route out, and a development runner pulls, rebuilds, and brings the interface up on every launch so there is never a stale binary on the bench. Going live is scripted as a reversible cutover rather than a migration — one script starts the service and prints the single gateway setting to change, its counterpart stops it and prints the revert — because the household's DNS and DHCP are still held by a UniFi gateway with Pi-hole resolving in front of it, and taking over name resolution for a working home is a change worth being able to undo in one command. Shipped as tagged releases with pre-built Linux and macOS binaries.

Lantern provides a single self-hosted service that hands out IP addresses and resolves domain names for a home network, while also caching lookups, blocking ads via host lists and identifying devices passively. It was created to replace the existing router-based DNS/DHCP setup with something lightweight that can run on modest hardware and be switched on or off in one step.

Can Jev Detect PII?new

An experiment to find out whether a particular AI service can reliably spot personal information — names, card numbers, addresses — hidden in large volumes of records. It was tested against made-up data where the right answers were already known, so its accuracy could be measured rather than guessed. It did well, around 96–99% depending on the approach, but two things stood out: asking about several records at once roughly halves accuracy, and some of its apparent mistakes were really disagreements about what counts as personal in the first place.

PythonPIIevaluationcalibration
The longer version

A proof of concept built to answer one question with numbers instead of opinions: can TypeSafe's Jev reliably find personal data in bulk records? Jev is a "System One" model — it does not generate text. You hand it some state and typed questions (yes/no, one-of-N, ordered) and it returns calibrated probabilities. That constraint is the whole design problem, because a model that cannot emit a span cannot extract PII directly; code has to propose candidates for it to judge, or ask questions about a whole record and let it answer. So the POC measures three framings — flag the record, confirm regex-proposed spans, classify a tabular column — against a synthetic corpus with exact ground truth, built with Faker so it contains no real person's data and regenerates from a seed. The obvious public datasets were rejected on licence grounds: academic-only terms forbidding redistribution and derivative works.

The live run says yes, with a caveat that turned out to be the most useful finding. At one record per request: 96.5% micro-F1 flagging records, 98.9% confirming spans, 95.4% accuracy classifying columns, with expected calibration error of 0.02–0.05 — which matters, because calibration is what makes a threshold portable rather than a magic number tuned per deployment. The whole run cost nine cents and ten seconds. The span result is the interesting one in practice: cheap regex alone scored 63% F1 and produced 1,230 false positives, and Jev removed essentially all of them while keeping recall, which is exactly the precision-layer-over-high-recall-patterns shape TypeSafe recommends. Then the caveat: packing records into one request halves accuracy and saves almost nothing. Ten records per request drops micro-F1 from 96.5% to 63.7%, twenty-five to 49.9%, while input tokens barely move because the record text dominates. Throughput for bulk data has to come from concurrency, not from stuffing state.

The error analysis is where it stopped being a benchmark and became useful. Most of the record-level false positives were not model errors at all: 52 of 65 were PERSON_NAME, and nearly all were surname-shaped tokens the taxonomy had declined to label — [email protected], Brown Group, 494 Jennifer Grove, [email protected]. Asked literally whether the text contains a person's name, Jev was right and the ground truth was the thing making a policy choice. That is a criteria problem, not a model problem, and knowing which one you have is the difference between rewriting a prompt and holding a meeting. The genuinely concerning failures were elsewhere and quieter: with headers obfuscated, SSN-shaped case numbers were classified as SSNs at 0.90–0.99 confidence, and version strings as IP addresses — confident errors, which confidence gating by definition cannot catch. The repo is honest about the rest of the ceiling too: names have no regex so the span strategy's recall is bounded by a capitalised-words heuristic, it is English only, and synthetic data is cleaner than the real thing, so every number should be read as an upper bound.

An experiment to find out whether a particular AI service can reliably spot personal information — names, card numbers, addresses — hidden in large volumes of records. It was tested against made-up data where the right answers were already known, so its accuracy could be measured rather than guessed. It did well, around 96–99% depending on the approach, but two things stood out: asking about several records at once roughly halves accuracy, and some of its apparent mistakes were really disagreements about what counts as personal in the first place.

Raven Power Monitornew

It continuously reads the home's electric-meter data, stores it locally and presents three simple visualisations - a live gauge refreshed every few seconds, a reduced-resolution chart of today's usage and a side-by-side view of today versus yesterday. The system was built to give a long-term, reliable picture of household power consumption for personal monitoring.

PythonFlasktime series
The longer version

A two-part system that has been capturing and displaying this house's electrical power consumption for a decade: a USB/serial listener reads an SDG&E smart meter through a Raven Zigbee radio into SQLite, and a Flask front end serves three visualizations — a live gauge updating every five seconds, today's usage decimated to 100 points, and a today-vs-yesterday comparison.

The codebase is a longevity story: started in 2015, migrated through Python 3.7 byte/string changes, a Chart.js v3 rewrite with a live-updating gauge, and — in its 2025 chapter — a refactor done with Cursor that replaced the parser with a state machine and moved logging to Rich, plus log rotation and cron-aware console handling. Self-hosted, reliable, and still running; a personal project maintained across ten years of platform churn, now with AI assistance in the loop.

It continuously reads the home's electric-meter data, stores it locally and presents three simple visualisations - a live gauge refreshed every few seconds, a reduced-resolution chart of today's usage and a side-by-side view of today versus yesterday. The system was built to give a long-term, reliable picture of household power consumption for personal monitoring.

FluidAudio Web Servicenew

A web service transcribes podcast recordings and separates the spoken parts by speaker, producing output in the same JSON format used by earlier tools. It was created to replace an older system with a faster Apple-native solution that runs efficiently on Apple Silicon while remaining compatible with existing downstream workflows. The service automatically processes each new episode as it becomes available.

SwiftVaporASRdiarization
The longer version

A Swift web service for podcast transcription with speaker diarization, replacing an earlier Flask/WhisperX setup with Apple-native FluidAudio for both ASR and diarization — faster on Apple Silicon and output-compatible with the WhisperX JSON format downstream tools expect.

Built on Vapor 4 with Fluent/SQLite job tracking, Leaf-templated monitoring UI, and an async two-endpoint submit/poll API behind a reverse proxy. The commit history tracks real production tuning: Sortformer-only diarization to fix mid-word speaker splits, speaker-boundary smoothing to prevent sentence fragmentation, and SentencePiece detokenization fixes. It runs on a Mac Studio as the transcription backend for the TGN Whisperer pipeline, processing every new episode of both podcasts.

A web service transcribes podcast recordings and separates the spoken parts by speaker, producing output in the same JSON format used by earlier tools. It was created to replace an older system with a faster Apple-native solution that runs efficiently on Apple Silicon while remaining compatible with existing downstream workflows. The service automatically processes each new episode as it becomes available.

SNMP Gaugesnew

Two analogue needle meters in a wooden box show live upload and download speeds read from the router. They were built as a physical, steampunk-style display of network traffic, and run continuously on a small dedicated computer. The first version drove the needles through a separate microcontroller; it now drives them directly.

Raspberry PiPythonSNMPGPIO
The longer version

A steampunk network monitor: two 5-volt analog needle gauges in a wooden box, showing live upload and download throughput read from the router over SNMP. When a big download runs, old-school needles dance. It's extra, and that's the point — but it has also become a real fixture of the network, running as a systemd service on netmon, the small Raspberry Pi dedicated to it.

A Python daemon polls the router's 64-bit interface counters over SNMPv2c, converts them to a rate, and drives each gauge from a GPIO pin — BCM 13 for upload, BCM 18 for download — using software PWM at 100 Hz, where the duty cycle *is* the needle position. Scaling is logarithmic against known link maxima, which matters because the connection is deeply asymmetric: 40 Mbps up against roughly 1.2 Gbps down, so a linear scale would leave the upload needle asleep and the download needle pinned. Smoothing, min/max clamping and counter-wrap handling sit on top, and 2-second polling turned out to be the router's accurate floor.

It did not start there. The first version drove the needles through an Arduino Duemilanove with an Ethernet shield, setting PWM by HTTP request to the RESTduino sketch — literally curl-ing a value at a microcontroller on the LAN. Moving to direct GPIO removed the Arduino, the shield, the network hop and a whole class of failure from the design; the old HTTP-driving script is still in the repo as the historical version. The original SNMP polling and scaling code was vibe-coded with ChatGPT 4.5 in a few prompt iterations, and the port to the Pi was done almost entirely with aider, which signs 15 of the repository's 17 commits.

Two analogue needle meters in a wooden box show live upload and download speeds read from the router. They were built as a physical, steampunk-style display of network traffic, and run continuously on a small dedicated computer. The first version drove the needles through a separate microcontroller; it now drives them directly.

RookRadarnew

An iPhone app that records where you were working - home office, a client's office, or the car - by noticing small radio beacons placed in each spot. It was built for a friend who needed timestamped location records for billing, where satellite positioning was not precise enough.

SwiftiBeaconiOS
The longer version

An iBeacon-based context tracker built for a consulting friend who needed timestamped work-location records — home office, client office, or car — for billing and expense purposes. Beacons beat GPS here: what's being tracked is context, not coordinates, so it keeps working when the car moves or the office relocates, and discloses nothing sensitive about where that office is.

The iOS app (Swift) monitors beacon regions and logs entries and exits to a small web backend — verified end-to-end with the home beacon pushing events correctly. Historically notable in this portfolio: it was built in 2023 through claude.ai chat sessions, before Claude Code existed, and the commit log literally documents the workflow — commit messages link the chat URLs, record failed attempts, and celebrate "Working code - home beacon tested!" It's a snapshot of AI-assisted development one tool generation ago.

An iPhone app that records where you were working - home office, a client's office, or the car - by noticing small radio beacons placed in each spot. It was built for a friend who needed timestamped location records for billing, where satellite positioning was not precise enough.

Retirement Financial Plannernew

The retirement planner runs wholly in the browser, using Monte Carlo simulation to show thousands of possible financial outcomes and letting users tweak inflation rates, account types, property-tax rules, stock vesting, disability scenarios and relocation options. It keeps all data locally so nothing is sent to a server, and it displays live totals, version information and updated charts as inputs change.

HTMLJavaScriptMonte Carlopersonal finance
The longer version

An interactive retirement planning tool that runs entirely in the browser and uses Monte Carlo simulation to model thousands of possible financial futures. It was built to allow absurd levels of detail and control: CPI and PCE inflation series, multiple retirement account types, California Prop 13 property tax treatment, unvested stock modelled as vesting linearly over three years, disability-to-retirement scenarios, and relocation analysis for comparing where you might live.

The whole thing is a self-contained HTML and JavaScript page — no backend, no build step, no accounts. That's a privacy decision as much as an architectural one: financial data never leaves the browser, which the About tab documents alongside the source link. Later work added semantic versioning displayed on the About tab and in printed output, live expense and asset totals per section, a Prop 13 chart that updates as housing inputs change, and a fix for imported configs being overwritten by stale localStorage.

It began as a Claude Cowork desktop session — about 3 hours of modelling and iteration — and was then moved into a git repository under ~/code, where it has continued through 14 commits to v1.1.0. Because that later work happened outside AI sessions, the repository carries no local AI evidence of its own; this card is joined to its originating Cowork sessions explicitly.

The retirement planner runs wholly in the browser, using Monte Carlo simulation to show thousands of possible financial outcomes and letting users tweak inflation rates, account types, property-tax rules, stock vesting, disability scenarios and relocation options. It keeps all data locally so nothing is sent to a server, and it displays live totals, version information and updated charts as inputs change.