Covers all work from this session: - Restock history tracking (stock_adjustments table, logging) - Payment tracking (payment_method, amount_paid on orders) - Reports page (5 report sections, date filtering) - Girl Scouts trefoil logo - Mobile network access (Vite host: true) - Bugs found and fixed during development iteration - Full code review fixes (P0-P3): batching bug, ID validation, delete reference checks, audit trail enforcement, global error handler, input validation, client robustness, schema migrations, useEffect deps - README and CLAUDE.md documentation updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
Changelog
All notable changes to the Cookie Tracker are documented in this file.
2026-02-09
Feature: Restock History Tracking
Commit: 1ed2642 — Add restock history tracking, payment tracking, and reports page
Every stock change is now logged to a stock_adjustments table, creating a full audit trail for inventory movements.
Schema (server/db.js):
- Added
stock_adjustmentstable with columns:id,product_id(FK → products),adjustment(integer, positive or negative),reason(text),reference_id(nullable, links to order ID),created_at.
Server changes:
server/routes/products.js—PATCH /:id/stocknow inserts a row intostock_adjustmentswithreason='restock'after updating the product quantity.server/routes/products.js— AddedGET /stock-historyendpoint (placed before/:idto avoid route collision). Accepts optional query params:product_id,start,end,limit. Returns stock adjustment rows joined with product name, ordered by most recent first.server/routes/orders.js— Stock adjustments are logged in all three transaction paths:- POST
/(create order): logsreason='order_created',adjustment=-qty,reference_id=orderIdfor each item. - PUT
/:id(update order): logsreason='order_updated'with positive adjustment when restoring old stock, then negative adjustment when deducting new stock. - DELETE
/:id(delete order): logsreason='order_deleted'with positive adjustment for each restored item.
- POST
Feature: Payment Tracking
Commit: 1ed2642 — Add restock history tracking, payment tracking, and reports page
Orders now track how they were paid and how much was paid, enabling balance-due tracking.
Schema (server/db.js):
- Added
payment_methodcolumn (TEXT, nullable) toorderstable. Valid values:'cash','card','venmo','check','other', ornull. - Added
amount_paidcolumn (REAL, NOT NULL, default 0) toorderstable. - Both columns added via
ALTER TABLEwith try-catch for idempotency on existing databases. (Later migrated to versioned schema migrations — see below.)
Server changes (server/routes/orders.js):
- POST
/: Acceptspayment_methodandamount_paidin request body. Validatespayment_methodagainst allowed values. Includes both in the INSERT statement. - PUT
/:id: Acceptspayment_methodandamount_paid. Includes in UPDATE statement, preserving existing values when not provided. - GET endpoints already use
SELECT o.*, so new columns are returned automatically.
Client changes:
-
client/src/pages/OrderNew.jsx:- Added
paymentMethodstate (dropdown: None/Cash/Card/Venmo/Check/Other) andamountPaidstate (number input). - Payment method and amount paid are on separate rows: Status + Payment method share a
form-row, Amount paid sits below withmaxWidth: 200to prevent overflow. - When a payment method is selected and amount is empty, auto-fills with current order total.
- Both fields included in submit payload.
- Added
-
client/src/pages/OrderDetail.jsx:- Display mode: Shows payment method (capitalized or "Not set"), amount paid, and a red "Balance due" warning when
amount_paid < order total. - Edit mode: Payment method dropdown and amount paid input, initialized from order data in
load(). When switching from "0" to a payment method, auto-fills with total. Both included inhandleUpdatepayload.
- Display mode: Shows payment method (capitalized or "Not set"), amount paid, and a red "Balance due" warning when
Feature: Reports Page
Commit: 1ed2642 — Add restock history tracking, payment tracking, and reports page
A new Reports page provides five aggregated views of sales, customer, and inventory data, with date range filtering.
Server (server/routes/reports.js — new file):
GET /api/reportsaccepts optionalstartandendquery params (YYYY-MM-DD format).- Returns a single JSON object with five sections:
| Section | Description | Key columns |
|---|---|---|
salesByProduct |
LEFT JOIN products → order_items → orders (via subquery for date filtering). All products shown, zero-filled. | product_name, units_sold, revenue |
topCustomers |
JOIN customers → orders → order_items. Sorted by total spent descending. | customer_name, order_count, total_spent |
revenueOverTime |
JOIN orders → order_items, grouped by date(created_at). |
date, revenue, order_count |
orderStatusBreakdown |
GROUP BY orders.status. |
status, count |
inventorySummary |
Products with correlated subqueries on stock_adjustments for restock totals, sold totals, and restock count. |
product_name, current_stock, low_stock_threshold, total_restocked, total_sold, restock_count |
- Date filtering uses positional
?params (not named params, which caused errors with better-sqlite3 when not all params are used in every query). Order-based queries useorderParams, inventory summary usesinvParams(SA params repeated 3x for 3 subqueries). salesByProductuses a subquery in the LEFT JOIN to properly filter order_items by date range (putting date filter directly on the LEFT JOIN ON clause didn't filter correctly).
Server (server/index.js):
- Imported and mounted
reportsRouterat/api/reports(after dashboard mount, behind auth middleware).
Client (client/src/pages/Reports.jsx — new file):
- Date range toolbar with preset dropdown (All time / This week / This month / Custom) and conditional date inputs for custom range.
- Fetches
/api/reportswith start/end params, re-fetches when dates change viauseEffect. - Five sections:
- Sales by Product — table with totals row in
<tfoot>. - Top Customers — table sorted by total spent.
- Revenue Over Time — table with inline bar indicators (
divwith width proportional to max revenue). - Order Status Breakdown — horizontal colored bar segments (yellow=pending, green=paid, blue=delivered) plus a table.
- Inventory Summary — table with low-stock products highlighted (red background, "(low)" label).
- Sales by Product — table with totals row in
Client (client/src/App.jsx):
- Imported
Reportscomponent, added<Route path="/reports" element={<Reports />} />.
Client (client/src/components/Layout.jsx):
- Added
{ path: '/reports', label: 'Reports' }tonavItemsarray.
Enhancement: Girl Scouts Trefoil Logo
Commit: 1ed2642 — Add restock history tracking, payment tracking, and reports page
- Extracted the trefoil icon paths from the full Girl Scouts wordmark SVG and created
client/src/assets/gs-trefoil.svgwith aviewBox="0 0 13 11.7"containing just the two trefoil<path>elements. client/src/components/Layout.jsx: Imported the SVG astrefoilLogoand added an<img>tag next to "Cookie Tracker" in the header link, styled atheight: 1.5emwithverticalAlign: 'middle'.
Enhancement: Mobile Network Access
Commit: 1ed2642 — Add restock history tracking, payment tracking, and reports page
client/vite.config.js: Addedhost: trueto the Vite dev server config so it binds to all network interfaces, allowing access from phones on the same Wi-Fi network.
Bug Fixes and Iteration During Development
These issues were discovered during testing and fixed before the feature commit:
-
Reports named params error: better-sqlite3 threw "Missing named parameter" when using
$start/$saStartnamed params across queries that didn't all use every param. Fixed by switching to positional?params with separateorderParamsandsaParamsarrays. -
salesByProduct date filtering: The initial LEFT JOIN approach with date filter on the orders ON clause didn't properly filter order_items — products still showed all-time sales data. Fixed by restructuring to use a subquery:
LEFT JOIN (SELECT ... FROM order_items JOIN orders WHERE date_filter) oi ON oi.product_id = p.id. -
OrderNew layout overflow: Three
flex: 1form groups (Status, Payment method, Amount paid) in a singleform-rowinside amaxWidth: 640card caused the amount paid input to overflow the card boundary, especially on mobile. Fixed by moving Amount paid to its own row below the Status/Payment method row, withmaxWidth: 200.
2026-02-09 (Code Review Fixes)
Bugfix: OrderDetail Product Change Overwrites product_id
Commit: 7068ea3 — Fix bugs, harden validation, and improve robustness
Priority: P0
Problem: In client/src/pages/OrderDetail.jsx (edit mode), changing the product in a line item called updateLine twice — once for product_id, once for price_at_sale. Under React's state batching, the second setItems call used stale state from before the first call, so it overwrote the product_id back to the old value.
Fix: Replaced the two updateLine calls with a single setItems call that updates both product_id and price_at_sale in one state transition:
setItems((prev) => prev.map((ln, idx) =>
idx === i ? { ...ln, product_id: newId, price_at_sale: p?.price ?? ln.price_at_sale } : ln
));
Bugfix: Validate All :id Route Parameters
Commit: 7068ea3 — Fix bugs, harden validation, and improve robustness
Priority: P0
Problem: req.params.id was passed directly to SQLite queries without validation. Values like "abc", "-1", or "0" could produce 500 errors or unexpected behavior.
Fix:
- Created
server/utils.jswith aparseId(raw)helper that returns a positive integer ornull. - Added
parseIdvalidation at the top of every:idroute handler inproducts.js,customers.js, andorders.js(10 routes total). Invalid IDs return400with a clear error message. - All subsequent DB calls in those routes use the parsed integer
idinstead ofreq.params.id.
Files changed: server/utils.js (new), server/routes/products.js, server/routes/customers.js, server/routes/orders.js.
Bugfix: Product/Customer Delete with Existing References
Commit: 7068ea3 — Fix bugs, harden validation, and improve robustness
Priority: P0
Problem: Deleting a product used in order_items or a customer with existing orders hit the SQLite foreign key constraint, producing an unhandled 500 error with a raw database error message.
Fix:
server/routes/products.js— DELETE/:idnow checksSELECT COUNT(*) FROM order_items WHERE product_id = ?before deleting. If references exist, returns409 Conflictwith message "Cannot delete product that has been used in orders".server/routes/customers.js— DELETE/:idnow checksSELECT COUNT(*) FROM orders WHERE customer_id = ?before deleting. If references exist, returns409 Conflictwith message "Cannot delete customer that has existing orders".- The Inventory page's delete handler (
client/src/pages/Inventory.jsx) already had.catch((e) => setError(e.message)), so the 409 error message is now displayed to the user automatically.
Improvement: Disallow quantity_on_hand in Product PUT
Commit: 7068ea3 — Fix bugs, harden validation, and improve robustness
Priority: P1
Problem: Product quantity_on_hand could be changed via two paths: PUT /products/:id (not logged) and PATCH /products/:id/stock (logged to stock_adjustments). This created an unauditable backdoor for stock changes.
Fix:
server/routes/products.js— PUT/:idno longer accepts or appliesquantity_on_hand. Onlyname,price, andlow_stock_thresholdare updated. The UPDATE query was changed from 4 columns to 3.client/src/pages/Inventory.jsx— Removed the "Qty" input field from the inline edit form. Removedquantity_on_handfrom thehandleUpdatepayload. Stock changes now exclusively go through the "Adjust stock" button (which uses PATCH).
Improvement: Global Error Handler
Commit: 7068ea3 — Fix bugs, harden validation, and improve robustness
Priority: P1
Fix (server/index.js):
- Added Express error middleware (4-arg
(err, req, res, next)) after all routes. Logs the error stack and returns500with either the error message (development) or a generic "Internal server error" (production). - Checks
res.headersSentto avoid double-sending responses. - Added
process.on('unhandledRejection')handler that logs the rejection reason.
Improvement: Validate Report Dates and Stock-History Limit
Commit: 7068ea3 — Fix bugs, harden validation, and improve robustness
Priority: P2
Fix:
server/utils.js— AddedisValidDate(str)(validates YYYY-MM-DD format and that it parses to a real date) andparseLimit(raw, max)(returns a positive integer capped atmax, default 1000, ornull).server/routes/reports.js— Validatesstartandendquery params withisValidDate()before using them in queries. Returns 400 with clear message on invalid format.server/routes/products.js— Stock-history endpoint validatesstart,endwithisValidDate(),product_idwithparseId(), andlimitwithparseLimit().
Improvement: Client-Side Robustness
Commit: 7068ea3 — Fix bugs, harden validation, and improve robustness
Priority: P2
-
api.js 204 handling (
client/src/api.js): Addedres.jsonSafe()method to the response object. Returnsnullfor 204 responses (no body), otherwise callsres.json(). This prevents parse errors if a caller accidentally tries to parse a 204 response body. -
OrderNew setSubmitting (
client/src/pages/OrderNew.jsx): Changed error-onlysetSubmitting(false)in the.catch()to a.finally(() => setSubmitting(false))so the button always resets, even if navigation is slow or fails. -
Login error messages (
client/src/pages/Login.jsx): Now checksres.status === 429and shows "Too many login attempts. Try again later." instead of the generic message. Other auth failures show "Invalid credentials" (generic, doesn't leak information about whether a user exists).
Improvement: Schema Migrations
Commit: 7068ea3 — Fix bugs, harden validation, and improve robustness
Priority: P3
Problem: New columns were added with try { database.exec("ALTER TABLE ..."); } catch (e) {}, which silently swallows all errors including unexpected ones.
Fix (server/db.js):
- Added
schema_versiontable (single row withversioninteger) in the mainCREATE TABLE IF NOT EXISTSblock. - Added
getSchemaVersion(),setSchemaVersion(), andrunMigrations()functions. - Migrations are version-gated:
if (version < 1) { ... setSchemaVersion(database, 1); }. Future migrations increment the version number. - ALTER TABLE errors are now only caught if they contain "duplicate column" (expected on re-run); unexpected errors are re-thrown.
Improvement: OrderDetail useEffect Dependency
Commit: 7068ea3 — Fix bugs, harden validation, and improve robustness
Priority: P3
Fix (client/src/pages/OrderDetail.jsx): Wrapped load function in useCallback with [id] dependency. Changed useEffect dependency from [id] to [load]. This satisfies React's exhaustive-deps lint rule.
Documentation: README Deployment Instructions
Commit: 7068ea3 — Fix bugs, harden validation, and improve robustness
Expanded the "Deployment (self-hosted)" section of README.md from 4 bullet points to comprehensive instructions:
- Prerequisites (Node.js, PM2, reverse proxy)
- Initial setup steps (clone, install, configure .env, PM2 start/save/startup)
- Deploying updates (
git pull,npm run build,pm2 restart) - Nginx reverse proxy example config with HTTPS
- Backup cron job example
Also updated the Features section to include Reports, Stock Audit Trail, and payment/balance tracking.
Documentation: CLAUDE.md Architecture Update
Commit: c00bb90 — Update CLAUDE.md with current architecture and documentation policy
- Added change documentation requirement
- Added
utils.jsto server architecture docs - Updated routes description to include reports endpoint
- Updated pages list to include Reports, Restock
- Updated key data flow to reference
atomicDeductStock()(corrected fromdeductStockForOrder()), stock_adjustments logging, and PUT restriction on quantity_on_hand - Added validation patterns documentation
- Added schema migration approach documentation
- Updated database schema section: 6 tables (was 4), payment fields, stock_adjustments