Bin check was using count_last_audit (0 for bulk items) as the audit
value for all items. Now looks up product kind and uses
last_audit_weight for bulk items, only updates count_last_audit for
discrete items.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the unified audit system with two purpose-built flows:
- Weigh Ins: rebranded audit flow for bulk products (Flower, Concentrate,
Tincture) with scale weigh, container weigh, and estimate modes
- Bin Checks: new bin-level presence check — select a bin, scan every item,
resolve discrepancies (wrong bin, unknown, missing), auto-records presence
audits on verified items
Adds cadence_days and last_checked to bins table, with per-bin overdue
tracking on the dashboard and bins view.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comprehensive UX audit covering modals, drawers, dashboard, and inventory.
Key changes: confirmation steps before destructive actions, undo via toast
for consume/gone/checkout, back-navigation across entity drawers, optional
ratings, discrete item count field, audit progress bar, and sortable column
affordance.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Record the total weight of a jar (product + container) at acquisition so
audits can be done by simply re-weighing the sealed jar. The tare is
derived (containerWeight − productWeight), and the audit flow offers a
"Weigh container" toggle that auto-calculates remaining product from the
scale reading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Multi-select inventory items via checkboxes (select-all, shift-click
range, group header select) and apply bulk actions through a floating
toolbar: edit fields (shop, bin, price, THC/CBD), consume, checkout,
check in, and mark gone. Backend processes all operations in a single
SQLite transaction for atomicity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Items can now be checked out of their bin into "my custody" and later
checked back in or marked consumed. Adds checkout/checkin API endpoints,
a My Custody sidebar page, CheckoutFlow and CheckinFlow modals, and
updates ProductDetail, Inventory, ConsumeFlow, and MarkGoneFlow to
handle the new checked-out status. Bulk items prompt for remaining
weight on check-in.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Four UX changes after using the rework for a bit:
1. Asset ids are 6-digit numbers from a roll of physical labels — server
no longer generates them. POST /api/inventory requires assetId; the
add-inventory form has a digits-only input that auto-focuses on entry.
2. Strain and product name are the same thing. Drop products.name; the
strain's name supplies the display. Product creation just asks for
"Name (strain)" and matches/creates a strain by that name.
3. Brand moves from inventory_items to products. SKUs are brand-specific,
so all instances of a product share the brand. Brand selector lives
on the product create/edit form, not the per-instance form.
4. Scanning an unknown SKU on the add-inventory step now opens the
create-product subform with the SKU prefilled — one less click.
Migration: detect prior shape (products.name column present) and rename
products/inventory_items/audits to *_v1 archives, recreate empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The products table conflated catalog ("kind of thing you scan") with
instance ("this jar I bought") — splitting it lets us record every
purchase as its own asset and autofill brand/shop/price/THC from the
last instance when scanning a known SKU.
- products: sku + strain + name + type + kind (catalog only)
- inventory_items: physical jars with short-UUID asset ids, per-batch
brand/shop/bin/price/cannabinoids/weight, audits, lifecycle
- audits now key on inventory_id; strains lose brand_id and type
- migration: rename existing products/audits/strains to *_legacy on
first boot so users keep historical reference, fresh start otherwise
- two-step add flow: scan SKU → select/create product → instance
details (autofilled from last instance) → generated asset id shown
- ScanField matches asset id first, falls back to SKU
- inventory list defaults flat, "By product" toggle groups instances
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The client constant TODAY_STR and the bootstrap response's today field
were both hardcoded to 2026-04-25, leaving date inputs in the New
product, Audit, and Mark consumed flows seeded with a stale date and
quietly staling out every "days since" / audit-overdue calculation
across the app.
TODAY_STR now resolves at module load from new Date() in local time,
and bootstrap returns new Date().toISOString().slice(0, 10).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bins follow an A1/A2/B1 naming convention, so the Bins page now parses
the leading letter prefix as a row group and the trailing number as the
within-row order. Each letter starts a fresh grid section; bins whose
names don't match the pattern fall into a trailing "Other" bucket
sorted alphabetically.
Removes the optional location field from bins end to end: the API
client signatures, server POST/PATCH routes, both product-flow inline
creates, the dropdown labels, the ProductDetail bin row, and the
BinsView header line. The bootstrap query explicitly projects only
id/name/capacity so the dead column doesn't leak through.
The location column stays in the bins table on disk to avoid a
migration on existing deployments — it just isn't read or written.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds PATCH /products/:id and an EditProductFlow modal opened from the
product drawer. Editable fields cover name, brand, shop, bin, asset tag,
price, purchase date, size (weight or count + unit weight), and the
cannabinoid profile. SKU, type, kind, and status-derived dates stay
locked because changing them would invalidate audit history math; type
changes are surfaced as "mark gone, add new" in the modal.
The strain row is re-resolved on name or brand change so analytics stay
aligned, and the last-audit mirror (last_audit_weight / count_last_audit)
only syncs with the original size when there are no audits yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds PATCH and DELETE endpoints for brands and shops that mirror the
existing bins pattern: deleting a brand or shop nullifies referencing
products (and strains, for brands) inside a transaction so nothing is
lost. Brand renames return 409 when the new name collides with the
UNIQUE constraint, surfaced inline in the edit modal.
The Brands and Shops views now show inline edit/trash icons on each
card; the trash button confirms with a preview of how many products
will be unparented.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Multi-stage Dockerfile builds server + web into a single node:20-alpine
image; runtime serves API on /api and the SPA from /app/public.
- Express now serves web/dist with an SPA fallback that skips /api so API
misses still 404 cleanly.
- docker-compose.yml is a single-service deploy with a named volume for
the SQLite database at /data/apothecary.db.
- .gitea/workflows/build.yml pushes :latest, :<sha>, and :semver tags to
the Gitea container registry on main and v* tags.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>