# Changelog All notable changes to the Cookie Tracker are documented in this file. --- ## 2026-02-09 (404 Fix) ### Bugfix: Return Proper 404s for Non-Existent Routes **Commit:** `5074f3d` — *Return proper 404s for unmatched API routes and static files* **Problem**: Nessus flagged that requesting a non-existent page returned a 200 with `index.html` instead of a proper 404. The SPA catch-all (`app.get('*')`) was serving `index.html` for everything — including unmatched `/api/*` routes and non-existent static files like `/foo.js`. **Fix** (`server/index.js`): - Added a `/api` catch-all after all API route mounts that returns `404 JSON` (`{"error":"Not found"}`) for any unmatched API route. When auth is enabled, unauthenticated requests to non-existent API routes return 401 (the auth middleware intercepts before the 404 handler, avoiding route enumeration). - Replaced the file-extension check with a regex whitelist of known client routes (`/`, `/inventory`, `/customers`, `/orders`, `/orders/new`, `/orders/:id`, `/restock`, `/reports`). Only these paths serve `index.html` for React Router. All other paths — extensionless (`/foo`), with special characters (`/it's`), or non-existent files (`/missing.js`) — return a real 404. --- ## 2026-02-09 (Security Hardening) ### Enhancement: Security Headers and Rate Limiting **Commit:** `a4ef21d` — *Add security headers via helmet and improve rate limiting* Added `helmet` and `express-rate-limit` packages to harden the application against common web vulnerabilities flagged by security scanners (e.g. Nessus). **Server** (`server/index.js`): - Added `helmet()` middleware before all routes. This sets security headers: `Content-Security-Policy`, `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `Strict-Transport-Security`, `Referrer-Policy: no-referrer`, and removes `X-Powered-By`. - Added a global API rate limiter on `/api` — 100 requests per minute per IP — using `express-rate-limit`. Returns standard `RateLimit-*` headers. **Server** (`server/routes/auth.js`): - Removed the hand-rolled in-memory rate limiter (~25 lines): `loginAttempts` Map, `isRateLimited()`, `recordAttempt()` functions, and per-request IP tracking/recording. - Replaced with a dedicated `express-rate-limit` instance applied as middleware on the `POST /login` route: 5 attempts per minute per IP. Same behavior, cleaner implementation. **Already solid (no changes needed):** - Session cookies: httpOnly, SameSite=lax, Secure in production - HMAC signing with timing-safe comparison - CSRF: SameSite=lax (the `csurf` package is deprecated) - SQL injection: parameterized queries throughout - XSS: React escapes output --- ## 2026-02-09 (README Fix) ### Fix: Clone URL in Deployment Instructions **Commit:** `b1d3e69` — *Fix clone URL in README deployment instructions* - `README.md` — Updated the placeholder `git clone https://your-git-host/your-repo/cookie-tracker.git` to the actual repository URL `https://git.raylos.net/adamp/cookie-tracker.git` in the deployment setup instructions. --- ## 2026-02-09 (Mobile Reports Fix) ### Bugfix: Reports Page Mobile Layout **Commit:** `de71565` — *Fix Reports page mobile layout* **Problem**: On mobile (iPhone 13 Pro Max portrait, and any screen < 640px), the Reports page tables were squished and unreadable. The existing CSS converts `.table-wrap` tables to a stacked card layout on small screens by hiding `` and showing `data-label` attributes on each ``, but the Reports tables were missing those attributes entirely. This meant cells rendered without labels and the 4–5 column tables were crammed into the narrow viewport. **Fix**: - `client/src/pages/Reports.jsx` — Added `data-label` attributes to every `` across all 5 report tables: - **Sales by Product**: Product, Units sold, Revenue (including `` total row) - **Top Customers**: Customer, Orders, Total spent - **Revenue Over Time**: Date, Orders, Revenue - **Order Status Breakdown**: Status, Count - **Inventory Summary**: Product, Current stock, Total restocked, Total sold, Restock count - `client/src/pages/Reports.jsx` — Added `className="bar-cell"` to the Revenue Over Time inline bar chart column. - `client/src/index.css` — Added `display: block` for `tbody` and `tfoot` inside `.table-wrap` at the `max-width: 639px` breakpoint so `` rows also get the mobile card treatment. - `client/src/index.css` — Added `.table-wrap td.bar-cell { display: none }` at the mobile breakpoint to hide the bar chart column (doesn't translate well to stacked layout). --- ## 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_adjustments` table 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/stock` now inserts a row into `stock_adjustments` with `reason='restock'` after updating the product quantity. - `server/routes/products.js` — Added `GET /stock-history` endpoint (placed before `/:id` to 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): logs `reason='order_created'`, `adjustment=-qty`, `reference_id=orderId` for each item. - **PUT `/:id`** (update order): logs `reason='order_updated'` with positive adjustment when restoring old stock, then negative adjustment when deducting new stock. - **DELETE `/:id`** (delete order): logs `reason='order_deleted'` with positive adjustment for each restored item. --- ### 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_method` column (TEXT, nullable) to `orders` table. Valid values: `'cash'`, `'card'`, `'venmo'`, `'check'`, `'other'`, or `null`. - Added `amount_paid` column (REAL, NOT NULL, default 0) to `orders` table. - Both columns added via `ALTER TABLE` with try-catch for idempotency on existing databases. (Later migrated to versioned schema migrations — see below.) **Server changes** (`server/routes/orders.js`): - **POST `/`**: Accepts `payment_method` and `amount_paid` in request body. Validates `payment_method` against allowed values. Includes both in the INSERT statement. - **PUT `/:id`**: Accepts `payment_method` and `amount_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 `paymentMethod` state (dropdown: None/Cash/Card/Venmo/Check/Other) and `amountPaid` state (number input). - Payment method and amount paid are on separate rows: Status + Payment method share a `form-row`, Amount paid sits below with `maxWidth: 200` to prevent overflow. - When a payment method is selected and amount is empty, auto-fills with current order total. - Both fields included in submit payload. - `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 in `handleUpdate` payload. --- ### 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/reports` accepts optional `start` and `end` query 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 use `orderParams`, inventory summary uses `invParams` (SA params repeated 3x for 3 subqueries). - `salesByProduct` uses 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 `reportsRouter` at `/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/reports` with start/end params, re-fetches when dates change via `useEffect`. - Five sections: 1. **Sales by Product** — table with totals row in ``. 2. **Top Customers** — table sorted by total spent. 3. **Revenue Over Time** — table with inline bar indicators (`div` with width proportional to max revenue). 4. **Order Status Breakdown** — horizontal colored bar segments (yellow=pending, green=paid, blue=delivered) plus a table. 5. **Inventory Summary** — table with low-stock products highlighted (red background, "(low)" label). **Client** (`client/src/App.jsx`): - Imported `Reports` component, added `} />`. **Client** (`client/src/components/Layout.jsx`): - Added `{ path: '/reports', label: 'Reports' }` to `navItems` array. --- ### 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.svg` with a `viewBox="0 0 13 11.7"` containing just the two trefoil `` elements. - `client/src/components/Layout.jsx`: Imported the SVG as `trefoilLogo` and added an `` tag next to "Cookie Tracker" in the header link, styled at `height: 1.5em` with `verticalAlign: 'middle'`. --- ### Enhancement: Mobile Network Access **Commit:** `1ed2642` — *Add restock history tracking, payment tracking, and reports page* - `client/vite.config.js`: Added `host: true` to 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: 1. **Reports named params error**: better-sqlite3 threw "Missing named parameter" when using `$start`/`$saStart` named params across queries that didn't all use every param. Fixed by switching to positional `?` params with separate `orderParams` and `saParams` arrays. 2. **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`. 3. **OrderNew layout overflow**: Three `flex: 1` form groups (Status, Payment method, Amount paid) in a single `form-row` inside a `maxWidth: 640` card 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, with `maxWidth: 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: ```jsx 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.js` with a `parseId(raw)` helper that returns a positive integer or `null`. - Added `parseId` validation at the top of every `:id` route handler in `products.js`, `customers.js`, and `orders.js` (10 routes total). Invalid IDs return `400` with a clear error message. - All subsequent DB calls in those routes use the parsed integer `id` instead of `req.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 `/:id` now checks `SELECT COUNT(*) FROM order_items WHERE product_id = ?` before deleting. If references exist, returns `409 Conflict` with message "Cannot delete product that has been used in orders". - `server/routes/customers.js` — DELETE `/:id` now checks `SELECT COUNT(*) FROM orders WHERE customer_id = ?` before deleting. If references exist, returns `409 Conflict` with 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 `/:id` no longer accepts or applies `quantity_on_hand`. Only `name`, `price`, and `low_stock_threshold` are 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. Removed `quantity_on_hand` from the `handleUpdate` payload. 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 returns `500` with either the error message (development) or a generic "Internal server error" (production). - Checks `res.headersSent` to 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` — Added `isValidDate(str)` (validates YYYY-MM-DD format and that it parses to a real date) and `parseLimit(raw, max)` (returns a positive integer capped at `max`, default 1000, or `null`). - `server/routes/reports.js` — Validates `start` and `end` query params with `isValidDate()` before using them in queries. Returns 400 with clear message on invalid format. - `server/routes/products.js` — Stock-history endpoint validates `start`, `end` with `isValidDate()`, `product_id` with `parseId()`, and `limit` with `parseLimit()`. --- ### Improvement: Client-Side Robustness **Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness* **Priority:** P2 1. **api.js 204 handling** (`client/src/api.js`): Added `res.jsonSafe()` method to the response object. Returns `null` for 204 responses (no body), otherwise calls `res.json()`. This prevents parse errors if a caller accidentally tries to parse a 204 response body. 2. **OrderNew setSubmitting** (`client/src/pages/OrderNew.jsx`): Changed error-only `setSubmitting(false)` in the `.catch()` to a `.finally(() => setSubmitting(false))` so the button always resets, even if navigation is slow or fails. 3. **Login error messages** (`client/src/pages/Login.jsx`): Now checks `res.status === 429` and 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_version` table (single row with `version` integer) in the main `CREATE TABLE IF NOT EXISTS` block. - Added `getSchemaVersion()`, `setSchemaVersion()`, and `runMigrations()` 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.js` to 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 from `deductStockForOrder()`), 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