feat: SEO template library, discovery pipeline, and orchestrator enhancements

## SEO Template Library
- Add complete template library with 7 page types (state, city, category, brand, product, search, regeneration)
- Add Template Library tab in SEO Orchestrator with accordion-based editors
- Add template preview, validation, and variable injection engine
- Add API endpoints: /api/seo/templates, preview, validate, generate, regenerate

## Discovery Pipeline
- Add promotion.ts for discovery location validation and promotion
- Add discover-all-states.ts script for multi-state discovery
- Add promotion log migration (067)
- Enhance discovery routes and types

## Orchestrator & Admin
- Add crawl_enabled filter to stores page
- Add API permissions page
- Add job queue management
- Add price analytics routes
- Add markets and intelligence routes
- Enhance dashboard and worker monitoring

## Infrastructure
- Add migrations for worker definitions, SEO settings, field alignment
- Add canonical pipeline for scraper v2
- Update hydration and sync orchestrator
- Enhance multi-state query service

🤖 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-09 00:05:34 -07:00
parent 9711d594db
commit 2f483b3084
83 changed files with 16700 additions and 1277 deletions

View File

@@ -35,11 +35,11 @@ router.get('/overview', async (req, res) => {
// Top products
const topProductsResult = await pool.query(`
SELECT p.id, p.name, p.price, COUNT(c.id) as click_count
SELECT p.id, p.name_raw as name, p.price_rec as price, COUNT(c.id) as click_count
FROM clicks c
JOIN products p ON c.product_id = p.id
JOIN store_products p ON c.product_id = p.id
WHERE c.clicked_at >= NOW() - INTERVAL '${parseInt(days as string)} days'
GROUP BY p.id, p.name, p.price
GROUP BY p.id, p.name_raw, p.price_rec
ORDER BY click_count DESC
LIMIT 10
`);
@@ -109,12 +109,12 @@ router.get('/campaigns/:id', async (req, res) => {
// Clicks by product in this campaign
const byProductResult = await pool.query(`
SELECT p.id, p.name, COUNT(c.id) as clicks
SELECT p.id, p.name_raw as name, COUNT(c.id) as clicks
FROM clicks c
JOIN products p ON c.product_id = p.id
JOIN store_products p ON c.product_id = p.id
WHERE c.campaign_id = $1
AND c.clicked_at >= NOW() - INTERVAL '${parseInt(days as string)} days'
GROUP BY p.id, p.name
GROUP BY p.id, p.name_raw
ORDER BY clicks DESC
`, [id]);