Commit Graph

49 Commits

Author SHA1 Message Date
josh fc7b3d5de2 Remove estimated remaining decay, use audit values directly
Build and push image / build (push) Successful in 1m6s
Replace burn-rate estimation (linear decay between audits) with actual
last audit values. Remaining is now always the last weigh-in value or
original weight if no audits exist — no more speculative daily decay.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 19:12:09 -04:00
josh 538e5079ab Fix 18 UX issues: confirmations, undo, drawer nav, empty states, and polish
Build and push image / build (push) Successful in 54s
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>
2026-05-08 16:25:41 -04:00
josh 9e31a6ad00 Overhaul shops tab with table view, search/sort, and detail drawer
Build and push image / build (push) Successful in 48s
Replaces the card grid with a table matching the brands/SKU view patterns:
sortable columns (name, items, spend, rating, last purchase), search by
name or location, and a right-side detail drawer showing aggregate stats,
lifecycle pills, unique brands list, and recent inventory items with
cross-navigation to the brand and inventory detail drawers. Location
appears as a subtitle under the shop name in table rows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 15:50:25 -04:00
josh 8f09504f26 Overhaul brands tab with table view, search/sort, and detail drawer
Build and push image / build (push) Successful in 52s
Replaces the simple card grid with a table matching the inventory and SKU
view patterns: sortable columns (name, SKUs, items, spend, rating, last
purchase), search by brand name, and a right-side detail drawer showing
aggregate stats, SKU list, and recent inventory items with cross-navigation
to the SKU and inventory detail drawers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 15:41:37 -04:00
josh 6c8bed9679 Prefill asset ID from last added item instead of global max
Build and push image / build (push) Successful in 43s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 15:26:07 -04:00
josh 82a72805cf Allow editing SKU value from the Edit SKU modal
Build and push image / build (push) Successful in 54s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 15:19:43 -04:00
josh 00a76a10d7 Fix 15 UX friction points across modals, navigation, and accessibility
Build and push image / build (push) Successful in 49s
Addresses bulk operation safety (confirmation for consume/gone), drawer
action hierarchy, exit animations, sidebar action distinction, toast
dismissibility and ARIA, retry affordance, sort direction toggle,
sequential audit queue from dashboard, mobile nav separation, price label
disambiguation, grouped-view sort consistency, footer context hints, bin
deletion feedback, segmented control ARIA, and drawer focus trapping.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 10:44:19 -04:00
josh 3bdf857099 Add SKUs section with list view, detail drawer, and CRUD modals
Build and push image / build (push) Successful in 51s
New catalog-level view for browsing, searching, and managing product SKUs
with per-SKU insights (spend, lifecycle, ratings) and full inventory linking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 10:12:35 -04:00
josh 564cae253a Only estimate remaining after first audit, not from purchase date
Build and push image / build (push) Successful in 53s
Before any audit, bulk items show their full original weight with no
decay applied. The linear decay model only kicks in after the first
audit provides an actual data point. This prevents freshly added,
unopened items from showing misleading consumption estimates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 09:36:29 -04:00
josh b088953133 Auto-derive total cannabinoids from THC + CBD when adding inventory
Build and push image / build (push) Successful in 50s
Total cannabinoids % now defaults to THC + CBD and stays in sync as
either field changes. Once the user manually edits the total field it
stops auto-updating, since some labels list an explicit total that
differs from the sum. When autofilling from a previous instance the
field is treated as manual (preserving the stored value).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 09:33:40 -04:00
josh c91fa1a192 Round 'Was' value in audit modal to 2 decimal places
Build and push image / build (push) Successful in 48s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 01:31:19 -04:00
josh 69ffc5ed26 Remove direct entry option from concentrate audits
Build and push image / build (push) Successful in 52s
Concentrates with a container weight now always use the weigh-container
input — no toggle is shown. Other bulk types (Flower) still get the
toggle between weigh container and direct entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 01:18:27 -04:00
josh ffc05ca526 Fix floating point display in audit history and estimated remaining
Build and push image / build (push) Successful in 56s
Round bulk audit values to 2 decimal places in the audit history list
and the linear-decay subtitle. Remove container weight / tare from the
detail rows — the info is internal to the audit flow and showing the
stale initial value after audits is confusing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 01:12:33 -04:00
josh a1be29ab6e Add container weight tracking for weigh-based concentrate audits
Build and push image / build (push) Successful in 1m6s
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>
2026-05-07 23:11:39 -04:00
josh e9e66ab1cb Add purchase date to bulk edit modal
Build and push image / build (push) Successful in 1m2s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 22:34:12 -04:00
josh 4044de7bfc Add timezone preference and fix all date handling to be timezone-aware
Build and push image / build (push) Successful in 56s
Dates were computed using browser/server local time with no explicit timezone,
causing inconsistencies when server runs in UTC. Now all "today" computations
and date formatting use the user's chosen IANA timezone, persisted in
localStorage and selectable from Settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 22:30:01 -04:00
josh 946e96c3ea Add bulk editing to inventory tab with atomic batch API
Build and push image / build (push) Successful in 1m3s
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>
2026-05-07 22:14:01 -04:00
josh d44c23ef6d Autofill next asset ID and pre-select bin from existing inventory
Build and push image / build (push) Successful in 55s
Asset tag field now defaults to the highest existing asset ID + 1.
Bin selector pre-selects the bin where other instances of the same
product are stored, falling back to the first bin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 21:38:10 -04:00
josh 5c67f1e2e0 Match custody view layout to inventory view pattern
Build and push image / build (push) Successful in 1m0s
Use responsive clamped padding, full-width maxWidth, and even column
proportions so the page fills available space consistently with other
tabs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 21:31:35 -04:00
josh fdfaa4503d Fix inventory list: rename THC% column to THC, show mg for edibles, fix ct on hand
Build and push image / build (push) Successful in 1m1s
Column now shows "75.4%" for flower/concentrate and "200 mg" for edibles.
Discrete "on hand" sums raw counts instead of count × unitWeight, fixing
the inflated value (was showing 1000 instead of 5 for edibles).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 21:29:51 -04:00
josh 9aea9535e6 Tailor edible ingestion flow: use mg units and hide cannabinoid % fields
Build and push image / build (push) Successful in 59s
Edibles are dosed in milligrams, not grams, and percentage-based cannabinoid
profiles don't apply. Adds weightUnit and showCannabinoidPct to TypeConfig so
the add/edit/detail views adapt per product type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 21:12:28 -04:00
josh a3559062db Fix custody view column alignment by giving action buttons a fixed width
Build and push image / build (push) Successful in 1m2s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 20:55:14 -04:00
josh e7fd9af62c Add checkout/custody feature for tracking items in personal possession
Build and push image / build (push) Successful in 1m8s
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>
2026-05-07 20:49:58 -04:00
josh 04bf009a83 Scope ScanField by mode: asset ID only for audit/consume, SKU only for add inventory
Build and push image / build (push) Successful in 57s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 20:32:52 -04:00
josh c031058d1d Remove weed-tracker design handoff bundle
Build and push image / build (push) Successful in 50s
Early prototype directory is no longer needed. Clean up
.dockerignore reference as well.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:40:20 -04:00
josh bc81cc8d18 Require asset ID scan in audit and consume modals
Build and push image / build (push) Successful in 47s
No default item selection — modals open with a scan prompt.
Form fields appear only after scanning an asset ID.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:34:31 -04:00
josh e50e8ef1fe Remove catalog edit notice from edit inventory modal
Build and push image / build (push) Successful in 57s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:27:45 -04:00
josh b5141f139d Simplify audit modal: asset ID scan only
Build and push image / build (push) Successful in 46s
Remove eyebrow header, dropdown picker, and SKU matching.
Require scanning the physical asset ID to audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:25:15 -04:00
josh 50d61a78d5 Simplify consume modal: asset ID scan only
Build and push image / build (push) Successful in 44s
Remove eyebrow header, dropdown picker, and SKU matching.
Require scanning the physical asset ID to mark consumed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:22:10 -04:00
josh cb26a8e634 Remove stale hint text from scan field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:19:55 -04:00
josh bae0386766 Simplify add inventory modal: SKU scan only
Build and push image / build (push) Successful in 50s
Remove product dropdown picker and step headers. Entry is now
scan-a-SKU or create-a-new-product only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:16:47 -04:00
josh c88b79b414 Delete unused EditProductFlow.tsx
Build and push image / build (push) Successful in 49s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:14:56 -04:00
josh 925a57aa03 Remove edit product flow
Build and push image / build (push) Successful in 46s
Product catalog entries are set at creation time. Per-instance
details (price, THC, weight) are edited from the inventory drawer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:14:29 -04:00
josh db2af4b79d Fix dashboard date showing yesterday due to UTC parsing
Build and push image / build (push) Successful in 48s
Date-only ISO strings are parsed as UTC midnight, which displays
as the previous day in US timezones. Append T00:00:00 to force
local time interpretation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:07:35 -04:00
josh 670d56ba4c Remove quantity option from add/edit forms
Build and push image / build (push) Successful in 43s
Each discrete item (pre-roll, edible, etc.) is now always one
physical unit with its own asset ID. The quantity field is gone
from both add and edit flows, countOriginal is hardcoded to 1,
and price is just "Price" instead of "Price per unit."

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:05:11 -04:00
josh 839dbf0430 Remove underline from sidebar nav links
Build and push image / build (push) Successful in 43s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 18:58:22 -04:00
josh a82045d1bd UX overhaul: routing, accessibility, feedback, and polish
Build and push image / build (push) Successful in 50s
Add react-router-dom for URL-based navigation with browser
back/forward, deep links, and bookmarks. Replace window.confirm()
with styled ConfirmDialog. Add toast notifications and success
feedback on consume/audit/gone flows. Add escape-to-close and
focus trapping on modals. Add entrance animations for drawers,
modals, and toasts. Make grids responsive, add sortable inventory
headers, working CSV/JSON export, time-aware greeting, focus-visible
outlines, search clear button, and hover chevrons on inventory rows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 18:54:49 -04:00
josh 80034b47c5 User-supplied asset ids; brand on product; strain is the name
Build and push image / build (push) Successful in 48s
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>
2026-05-04 18:17:12 -04:00
josh 02dc6e523f Track inventory at the instance level, not by product
Build and push image / build (push) Successful in 46s
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>
2026-05-04 05:59:46 -04:00
josh 1abfda7989 Use the real today everywhere
Build and push image / build (push) Successful in 53s
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>
2026-05-03 22:20:24 -04:00
josh c7f3bf25d1 Bins: fixed N-column grid per letter group
Build and push image / build (push) Successful in 52s
The bins-by-letter grid used auto-fill with a 380px minimum, so a row
of A1-A6 wrapped once the viewport got tight, and rows with fewer
bins than the auto-fill column count left empty tracks on the right.

Switch to repeat(N, minmax(0, 1fr)) where N is the number of bins in
the group: cards smush to fit so a letter never wraps to a second
visual row, and minmax(0, 1fr) lets columns share whatever width is
available so a sparse row (e.g. C with 2 bins) widens to fill the
line just as a dense row does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:14:45 -04:00
josh d335525073 Group bins by letter, sort by number, drop location
Build and push image / build (push) Successful in 46s
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>
2026-05-03 22:07:12 -04:00
josh cd7aeb9d09 Per-unit pricing for discrete products
Build and push image / build (push) Successful in 51s
Pre-rolls, edibles, and vaporizers are sold and reasoned about per unit
("$10 each") rather than as a bag total. The Add and Edit forms now ask
for "Price per unit" when the kind is discrete, and the product drawer
displays the per-unit number with the total as a small subline. Bulk
products (flower, concentrate, tincture) still take and show a total.

The stored price column remains the total, so existing data, spend
totals, sort-by-price, and bin value calculations all keep working
unchanged. Conversion (pricePerUnit × countOriginal) happens at the
form boundary on save and the inverse on edit-modal load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:59:35 -04:00
josh edb8e2ac92 Bin slot count: discrete products fill by unit, not by row
Build and push image / build (push) Successful in 54s
A 3ct pre-roll was filling one slot of its bin instead of three. Bins
visualise physical capacity, so each unit of a discrete product (rolls,
edibles, vapes) should take its own slot; bulk jars still take one slot
each. The slot tally and fill bar both use the current count
(countLastAudit ?? countOriginal) so the bin frees up as units are
audited away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:42:39 -04:00
josh 592bb28740 Edit existing products
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>
2026-05-03 21:42:33 -04:00
josh 8ef8859c7d Edit and delete brands and shops
Build and push image / build (push) Successful in 49s
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>
2026-05-03 21:33:42 -04:00
josh d00eb4c12b ci: drop GHA cache backend
Build and push image / build (push) Successful in 1m1s
act_runner advertises a GHA cache endpoint that isn't reachable from
inside the build container, causing the workflow to fail after a
successful image push. The build itself was fine; only the cache export
timed out. Remove cache-from/cache-to until a real cache backend is
configured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:36:29 -04:00
josh 2a623e0b9c Add Docker image, compose, and Gitea CI
Build and push image / build (push) Failing after 2m5s
- 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>
2026-05-03 20:29:18 -04:00
josh 027cf032be Initial commit: Apothecary v0.4.0 2026-05-03 20:19:26 -04:00