Projects
Open source and anything else I've put out publicly. Most of what I build is for work and lives in private repositories, so this is the smaller half of what I do — but it's the half you can actually read. All of it was written outside work, mostly to use libraries I don't touch day to day.
-
mtg-life-tracker Kotlin Multiplatform Compose Bluetooth LE GitHub Actions
A life tracker for Magic: The Gathering sharing one codebase across Android, iOS, Windows, macOS and Linux. Five formats — free-for-all, Star, Two-Headed Giant, Archenemy and Emperor — are all arrangements of teams, so the engine has one win rule rather than five. The board rearranges itself for the format and turns each panel to face the person it belongs to. On Android and iOS it will also read rolls from a Pixels Bluetooth die: to decide who goes first the die is passed round the table in the order people are really sitting in, lighting up in the colour of whoever it is waiting on. The rules live in a pure-Kotlin module with no UI dependency, so they test on any JDK with no simulator or Android SDK involved — and how a tie is broken is a rule, so the die is settled by the same code the app's own dice go through.
Download Android, Windows, macOS, Linux, iOS — self-signed
-
ktor-task-api Kotlin Ktor Exposed PostgreSQL
A small REST API for tasks. Ktor on Netty with kotlinx.serialization, Exposed over a HikariCP pool, and a consistent response envelope so successes and failures come back the same shape. Written to get familiar with Ktor and Exposed.
-
vapor-webhook-relay Swift Vapor Fluent PostgreSQL
A webhook relay, and the Swift counterpart to the Ktor service above. Verifies inbound signatures in constant time over the raw body, stores each event once behind a unique index rather than a check-then-insert, and delivers onwards with exponential backoff, jitter and a dead-letter queue. The work queue is a Postgres table drained with
FOR UPDATE SKIP LOCKED, so an event and its deliveries are enqueued in one transaction. -
discord-bot Java JDA Bungie API OAuth2
A Destiny 2 assistant on JDA, over a hand-rolled Bungie API client. Gear sets are stored as item instance ids rather than the game's twenty loadout slots, so a set names the exact roll and brings back the subclass, mods, perks and shaders with it. The game only accepts gear changes in orbit, so asking for one mid-raid queues it and puts it on when you land. Names come from a manifest cache that streams the 190 MB definition table at startup and keeps only the fields it shows — 1.4 seconds and about 10 MB for 36,000 items. It also does Xûr, clear counts, post-game reports, the postmaster, and an LFG board with a private voice channel per fireteam.
-
ncartmell.github.io Python Static site GitHub Actions
This site. The pages are hand-written HTML with no build step, but the parts that are derived rather than written — the Atom feed, the sitemap, each post's head block and its previous/next links, the counts in the index — belong to
sync.py, which edits only the regions it marks and leaves everything around them alone. CI runs it and fails on a diff, so a stale feed is caught on the push that caused it rather than noticed months later.check.pycovers the rest of what breaks silently on a static site: a link pointing at nothing, a post on disk that was never listed, a topic outside the controlled vocabulary, an og:image referenced but missing, a “see also” still using a title that has since changed. The printed CV is the same idea from another angle —cvstamp.pyfingerprints only the part of the page the print stylesheet keeps, so it objects when the PDF is genuinely out of date and stays quiet for edits that would never reach it.