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

@@ -92,9 +92,9 @@ router.get('/brands', async (req: Request, res: Response) => {
if (brandIds.length > 0) {
const brandNamesResult = await pool.query(`
SELECT DISTINCT brand_name
FROM dutchie_products
WHERE brand_name = ANY($1)
SELECT DISTINCT brand_name_raw as brand_name
FROM store_products
WHERE brand_name_raw = ANY($1)
`, [brandIds]);
brandNamesResult.rows.forEach(r => {
@@ -201,14 +201,14 @@ router.get('/products', async (req: Request, res: Response) => {
// Try to match by external_id or id
const productDetailsResult = await pool.query(`
SELECT
external_id,
provider_product_id as external_id,
id::text as product_id,
name,
brand_name,
type,
subcategory
FROM dutchie_products
WHERE external_id = ANY($1) OR id::text = ANY($1)
name_raw as name,
brand_name_raw as brand_name,
category_raw as type,
subcategory_raw as subcategory
FROM store_products
WHERE provider_product_id = ANY($1) OR id::text = ANY($1)
`, [productIds]);
productDetailsResult.rows.forEach(r => {