FastAPI + SQLite behind a single-page frontend, deployed as a container on mainserver behind Caddy. One flexible parts table plus key/value specs so components, filament, fasteners and tooling share a schema. Categories and locations are nestable trees whose filters and sidebar counts both roll up through descendants. Category spec templates pre-fill the properties worth recording for each kind of part, which is what makes manual entry tolerable. Every quantity change is logged. Search is FTS5 with prefix matching across names, MPNs, spec values, tags, category and location. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.8 KiB
Parts Inventory
A catalog of what's actually on the shelf — components, filament, fasteners, tooling — so "do I have one of those?" is a five-second search instead of an hour of opening drawers.
Runs as a FastAPI + SQLite container on mainserver (192.168.50.8) behind
Caddy at https://parts.tjm77.com, alongside the other ~/srv services.
The data model
One parts table holds what every kind of stock has in common — name,
quantity, unit, location, cost, min-stock threshold. Everything type-specific
lives in part_specs as key/value rows, so a 0603 resistor, a spool of PLA and
a box of M3 screws share a table without a schema fork.
Categories and locations are both nestable trees. Workshop / Bin A3 is a
real path, and filtering by Workshop returns everything in every bin beneath
it. Sidebar counts roll up the same way, so they always match what clicking the
filter actually returns.
Each category carries a spec template — the properties worth recording for that kind of thing. Pick "Filament" on the add form and it pre-fills Material, Colour, Diameter, Brand, Print Temp, and switches the unit to grams. That's what keeps manual entry from being a blank page. Templates are suggestions only: delete any row, add your own, ignore them entirely.
Every quantity change is written to stock_log, so a part's history answers
"where did those 40 headers go" instead of just showing a smaller number than
you remembered.
Search is SQLite FTS5 over name, description, manufacturer, MPN, spec values,
tags, category and location, with prefix matching so results narrow as you type.
Typing 1.75mm, prusament, 0603 or Bin A3 all find the right things.
Running it locally
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
PARTS_AUTH=off PARTS_DB=$PWD/data/parts.db .venv/bin/uvicorn app.main:app --port 8123
Then open http://127.0.0.1:8123.
Tests
.venv/bin/python -m tests.test_api
Exercises the API end to end against a throwaway database — the auth gate, nested categories and locations, search across every indexed field, filter rollups, stock adjustment and history, patch semantics, and cascade behaviour when a category or location is deleted.
Configuration
| Variable | Meaning |
|---|---|
PARTS_PASSWORD |
The single shared password. Required when auth is on. |
PARTS_SECRET |
Signing key for the session cookie. openssl rand -hex 32. |
PARTS_SESSION_DAYS |
Session lifetime, default 30. |
PARTS_AUTH |
off disables the login gate (LAN-only use). |
PARTS_DB |
SQLite path. /data/parts.db in the container. |
Deploying
Lives at /home/jay/srv/parts/ on .8 and follows the same conventions as the
other services there — build: ., external caddy_web network, named volume
for /data.
ssh jay@192.168.50.8
cd ~/srv/parts && sudo docker compose up -d --build
The database is in the parts_parts_data docker volume, which survives
rebuilds. To back it up:
sudo docker run --rm -v parts_parts_data:/d -v "$PWD":/out alpine \
sh -c 'cp /d/parts.db /out/parts-backup.db'
API
Everything under /api is JSON and cookie-authenticated. GET /healthz is open.
GET /api/parts?q=&category_id=&location_id=&tag=&low_stock=&sort=&limit=&offset=
POST /api/parts
GET /api/parts/{id}
PATCH /api/parts/{id}
DELETE /api/parts/{id}
POST /api/parts/{id}/adjust {delta, reason}
GET /api/parts/{id}/history
GET /api/categories POST /api/categories PATCH|DELETE /api/categories/{id}
GET /api/locations POST /api/locations PATCH|DELETE /api/locations/{id}
GET /api/tags
GET /api/stats
The "what could I build with what I'm holding?" idea is meant to arrive as
another consumer of these endpoints — the arbiter on .8 already has the lane
routing and typed-action machinery for it — rather than as a fork of them.