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.
- 101 tests for Hoard and 49 for the Silver Stack legacy code, all passing Oct 2026
- 6 importers: emailed dealer receipts plus Robinhood, Fidelity, Coinbase, Schwab and Vanguard CSV Sep 2026
- Prices from three free sources with fallbacks, cached for offline use Sep 2026
- Spreadsheet export keeps live formulas instead of pasted values Sep 2026
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.
- 61 tests Aug 2026
- Never sends, moves or deletes mail; remote images are stripped Aug 2026
- Lane rules tuned on a survey of about 51,000 messages Aug 2026
- First sync capped at 200 messages, with a commit every 50 Aug 2026
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.
- Keeps recording with the lid closed (systemd-inhibit on sleep and lid switch) Sep 2026
- Fragmented MP4 in 2 s fragments: a crash loses about 2 s Sep 2026
- 9 tests, run against real ffmpeg and a generated tone Sep 2026
- 9 lectures recorded and filed in the first weeks of Fall 2026 Oct 2026
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.
- 3 answer buckets backed by 5 internal statuses and 3 confidence levels Jun 2026
- A new school is one JSON file, no code change Jun 2026
- 20 evaluations per minute per address, 10 MB upload cap Jun 2026
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 and deployed in one day from a prototype Jul 2026
- 37 end-to-end checks against a real Postgres database Aug 2026
- Retired after 5 API calls in its final log window; code and tables kept Aug 2026
Built with Node.js, Express, PostgreSQL, pdfkit, JavaScript, Supabase Auth