diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..12a3d92
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,287 @@
+# 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_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