Apps & Tools

Desktop and web tools, most of them built for my own use first.

Hoard

Complete · 2026

A desktop tracker for physical metals, stocks and crypto, kept one purchase at a time.

Hoard records holdings one purchase at a time. Every coin, share or token is a lot with its own date and cost, so gain and loss are worked out per lot instead of averaged across a position. It started as Silver Stack, a tracker for physical silver, and grew to cover metals, stocks, ETFs and crypto. Silver Stack lives on in the repo as a legacy folder, and Hoard migrates its database on first launch.

Prices need no API keys: metals come from gold-api with Yahoo futures as the fallback, stocks from Yahoo, and crypto from Yahoo with CoinGecko behind it. The last good price is cached so the app opens offline, and any symbol can be overridden by hand. Six importers cover a dealer's emailed receipts and five brokerage CSV formats; only the receipt importer has been run on real files, and the README says so.

Because the receipts used to build that importer were real, the tests include a tripwire that fails if a fixture holds anything shaped like a name, address or account number.

Built with Python, Tkinter, SQLite, openpyxl, pytest, GitHub Actions

Mailhub

Paused · 2026

A local, read-only dashboard that pulls several inboxes into one searchable list.

Mailhub syncs several email accounts over IMAP into one SQLite database on the laptop and shows them in a single triage view. It is read-only by design: it never sends, moves or deletes mail, so a bug in it cannot lose or send anything. Message HTML is sanitised and remote images are stripped, so tracking pixels never load.

Sync is incremental. It tracks message UIDs, commits every 50 messages and caps the first pull at 200; if a server reports a new UIDVALIDITY for a folder, Mailhub throws away its copy of that folder and starts again instead of guessing which messages moved.

Gmail is reached with app passwords rather than OAuth. That was a deliberate trade: Google expires refresh tokens for unverified apps after seven days, which would have meant signing in again every week. Mail is sorted into four lanes, by my own rules first and heuristics second, tuned on a survey of about 51,000 messages. SQLite FTS5 handles search.

Built with Python, Flask, IMAP, SQLite FTS5, bleach, JavaScript, pytest

Lecture Recorder

Active · 2026

One button to record a class and file the audio under the right course.

A small desktop app I use in every class. One button starts recording, and the file lands in a folder for the current semester and course, so there is nothing to sort afterwards.

Most of the code is about not losing a lecture. While it records, it holds a systemd-inhibit lock on sleep and the lid switch, so closing the laptop does not stop it. Audio is written as fragmented MP4 in two-second fragments, so a crash or a dead battery costs about two seconds, and the next launch offers to recover the cut-off file. It warns after five seconds of silence, which catches a muted or wrong microphone early.

The tests run real ffmpeg against a generated tone instead of a microphone, so the recording path is exercised end to end on any machine. The whole app is about 415 lines of Python, and it has filed nine lectures so far this semester.

Built with Python, Tkinter, ffmpeg, PipeWire, systemd-inhibit, pytest

CreditJump

Paused · 2026

Reads a transcript PDF and estimates which courses transfer to another school.

An early prototype for students changing schools. Upload a transcript PDF, pick a destination, and CreditJump sorts each course into transfers, transfers as elective credit, or does not transfer, with a confidence label on every answer.

The parser finds course rows by pattern rather than fixed columns, so it is not tied to one registrar's layout; it was built against a PowerCampus export. Matching follows the order a registrar uses: articulation agreement first, then elective credit, then rejection. Five internal statuses map onto the three buckets in the interface, and each answer is marked verified, estimated or projected so a student can tell a published agreement from a guess.

Adding a school is one JSON file of course equivalencies with no code change. The data so far covers one destination with 13 equivalencies, and five more schools are listed as targets. The API limits each address to 20 evaluations a minute and caps uploads at 10 MB.

Built with Python, FastAPI, pdfplumber, JavaScript, Docker

OmniManager

Retired · 2026

A project manager for content work, built in a day and retired when the logs showed almost nobody used it.

OmniManager tracked content work: workspaces with roles and invites, projects and milestones, a calendar and a board, PDF reports, and a read-only view of the clipper's job queue so nobody had to ask whether a render had finished. It grew out of Cadence, an earlier stand-alone project manager that ran on one Windows PC and served the other machines on the network.

It shared one login with the other OmniChrome apps. Tokens are checked against the identity provider's published keys first, with a shared-secret fallback, and the user id always comes from the verified token, never from the request. A rejected token on the clipper side comes back as a 502, so a problem in one app never signs you out of another. Every fetch is base-relative, so the app runs unchanged under a path prefix.

I retired it in August 2026 when the server logs showed five API calls in the period they covered. The code and tables are kept in case it is needed again.

Built with Node.js, Express, PostgreSQL, pdfkit, JavaScript, Supabase Auth

Other work