3 Commits

Author SHA1 Message Date
thejayman77 96d2a1a087 Add photo attachments to parts
A bag falling apart after forty years still has the part number printed on it,
and the picture carries more than any field you could retype it into. Photos
attach from the part form; on a phone the picker opens the camera directly.

Files live beside the database in the same volume, with only metadata in SQLite
— blobs there bloat the database and complicate the VACUUM INTO backup. The
browser downscales to 2000px before uploading, honouring EXIF orientation via
createImageBitmap, which keeps an image library and its native dependencies out
of the server entirely.

Uploads are sniffed by content rather than trusted by their declared type, so a
file that merely claims to be a JPEG cannot be stored and served back from this
origin as something a browser will execute. SVG is refused for the same reason.
Reads are capped rather than trusting Content-Length, at most 12 photos per
part, and a row that fails to insert takes its file with it.

Deleting a photo or a part removes the files, not just the rows, and
`app.admin prune-images` sweeps anything a crash stranded. The README's backup
procedure now covers both halves; capturing only the database would have
silently lost every photo.

python-multipart returns for the upload, pinned at 0.0.32 — the version removed
earlier was 0.0.20, which carried advisories. Audit is clean.

Two frontend bugs surfaced while testing this in the browser: a photo count
changing left the list row stale, and the part form opened from the list's
cached copy rather than fetching current data.

Checks go from 197 to 271.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 17:03:36 -04:00
thejayman77 ab82b5e9a9 Manage the password in the app; close remaining audit findings
Password management moves out of the CLI entirely. The credential is now a
salted scrypt hash in the database (so it survives rebuilds, living in the /data
volume) rather than an environment variable; PARTS_PASSWORD is demoted to a
bootstrap value that stops working the moment a password is set in the UI. Every
token carries a session epoch, so changing the password — or "sign out other
devices" — invalidates outstanding cookies while keeping the browser that made
the change signed in. A banner nags until the handed-over password is replaced.
app/admin.py remains for the one case the UI cannot cover, a forgotten password.

Audit findings:
- Taxonomy update and delete scanned affected parts before taking the write
  lock, so a concurrent rename could leave the search index matching a name the
  UI no longer showed. All four routes now lock first; removing the lock again
  makes the new test fail exactly that way.
- History of a missing part returned 200 with an empty list; now 404.
- Infinity and NaN passed ge=0 and failed at the database. They are rejected as
  422 now, and the validation error handler no longer chokes trying to echo a
  non-finite value back.

Checks go from 134 to 181.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 10:31:06 -04:00
thejayman77 ffe508b7f3 Parts inventory: catalog, search, and stock tracking
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>
2026-08-24 09:19:58 -04:00