ffe508b7f3
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>
106 lines
3.8 KiB
Markdown
106 lines
3.8 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
.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`.
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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.
|