cookie-tracker/CHANGELOG.md
adamp f083e2888c Update CHANGELOG with security hardening entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 22:07:37 -06:00

20 KiB
Raw Blame History

Changelog

All notable changes to the Cookie Tracker are documented in this file.


2026-02-09 (Security Hardening)

Enhancement: Security Headers and Rate Limiting

Commit: a4ef21dAdd 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: b1d3e69Fix 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: de71565Fix 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 <thead> and showing data-label attributes on each <td>, but the Reports tables were missing those attributes entirely. This meant cells rendered without labels and the 45 column tables were crammed into the narrow viewport.

Fix:

  • client/src/pages/Reports.jsx — Added data-label attributes to every <td> across all 5 report tables:
    • Sales by Product: Product, Units sold, Revenue (including <tfoot> 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 <tfoot> 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: 1ed2642Add 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.jsPATCH /: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: 1ed2642Add 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: 1ed2642Add 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 <tfoot>.
    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 <Route path="/reports" element={<Reports />} />.

Client (client/src/components/Layout.jsx):

  • Added { path: '/reports', label: 'Reports' } to navItems array.

Commit: 1ed2642Add 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 <path> elements.
  • client/src/components/Layout.jsx: Imported the SVG as trefoilLogo and added an <img> tag next to "Cookie Tracker" in the header link, styled at height: 1.5em with verticalAlign: 'middle'.

Enhancement: Mobile Network Access

Commit: 1ed2642Add 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: 7068ea3Fix 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: 7068ea3Fix 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: 7068ea3Fix 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: 7068ea3Fix 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: 7068ea3Fix 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: 7068ea3Fix 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: 7068ea3Fix 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: 7068ea3Fix 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: 7068ea3Fix 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: 7068ea3Fix 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: c00bb90Update 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