feat: Responsive admin UI, SEO pages, and click analytics

## Responsive Admin UI
- Layout.tsx: Mobile sidebar drawer with hamburger menu
- Dashboard.tsx: 2-col grid on mobile, responsive stats cards
- OrchestratorDashboard.tsx: Responsive table with hidden columns
- PagesTab.tsx: Responsive filters and table

## SEO Pages
- New /admin/seo section with state landing pages
- SEO page generation and management
- State page content with dispensary/product counts

## Click Analytics
- Product click tracking infrastructure
- Click analytics dashboard

## Other Changes
- Consumer features scaffolding (alerts, deals, favorites)
- Health panel component
- Workers dashboard improvements
- Legacy DutchieAZ pages removed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-07 22:48:21 -07:00
parent 38d3ea1408
commit 3bc0effa33
74 changed files with 12295 additions and 807 deletions

View File

@@ -340,6 +340,59 @@ The custom connection module at `src/dutchie-az/db/connection` is **DEPRECATED**
---
## PERFORMANCE REQUIREMENTS
**Database Queries:**
- NEVER write N+1 queries - always batch fetch related data before iterating
- NEVER run queries inside loops - batch them before the loop
- Avoid multiple queries when one JOIN or subquery works
- Dashboard/index pages should use MAX 5-10 queries total, not 50+
- Mentally trace query count - if a page would run 20+ queries, refactor
- Cache expensive aggregations (in-memory or Redis, 5-min TTL) instead of recalculating every request
- Use query logging during development to verify query count
**Before submitting route/controller code, verify:**
1. No queries inside `forEach`/`map`/`for` loops
2. All related data fetched in batches before iteration
3. Aggregations done in SQL (`COUNT`, `SUM`, `AVG`, `GROUP BY`), not in JS
4. **Would this cause a 503 under load? If unsure, simplify.**
**Examples of BAD patterns:**
```typescript
// BAD: N+1 query - runs a query for each store
const stores = await getStores();
for (const store of stores) {
store.products = await getProductsByStoreId(store.id); // N queries!
}
// BAD: Query inside map
const results = await Promise.all(
storeIds.map(id => pool.query('SELECT * FROM products WHERE store_id = $1', [id]))
);
```
**Examples of GOOD patterns:**
```typescript
// GOOD: Batch fetch all products, then group in JS
const stores = await getStores();
const storeIds = stores.map(s => s.id);
const allProducts = await pool.query(
'SELECT * FROM products WHERE store_id = ANY($1)', [storeIds]
);
const productsByStore = groupBy(allProducts.rows, 'store_id');
stores.forEach(s => s.products = productsByStore[s.id] || []);
// GOOD: Single query with JOIN
const result = await pool.query(`
SELECT s.*, COUNT(p.id) as product_count
FROM stores s
LEFT JOIN products p ON p.store_id = s.id
GROUP BY s.id
`);
```
---
## FORBIDDEN ACTIONS
1. **Deleting any data** (products, snapshots, images, logs, traces)