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

@@ -319,12 +319,13 @@ export function createMultiStateRoutes(pool: Pool): Router {
// =========================================================================
/**
* GET /api/analytics/compare/brand/:brandId
* GET /api/analytics/compare/brand/:brandIdOrName
* Compare a brand across multiple states
* Accepts either numeric brand ID or brand name (URL encoded)
*/
router.get('/analytics/compare/brand/:brandId', async (req: Request, res: Response) => {
router.get('/analytics/compare/brand/:brandIdOrName', async (req: Request, res: Response) => {
try {
const brandId = parseInt(req.params.brandId);
const { brandIdOrName } = req.params;
const statesParam = req.query.states as string;
// Parse states - either comma-separated or get all active states
@@ -336,7 +337,22 @@ export function createMultiStateRoutes(pool: Pool): Router {
states = activeStates.map(s => s.code);
}
const comparison = await stateService.compareBrandAcrossStates(brandId, states);
// Check if it's a numeric ID or a brand name
const brandId = parseInt(brandIdOrName);
let comparison;
if (!isNaN(brandId)) {
// Try by ID first
try {
comparison = await stateService.compareBrandAcrossStates(brandId, states);
} catch (idErr: any) {
// If brand ID not found, try as name
comparison = await stateService.compareBrandByNameAcrossStates(brandIdOrName, states);
}
} else {
// Use brand name directly
comparison = await stateService.compareBrandByNameAcrossStates(decodeURIComponent(brandIdOrName), states);
}
res.json({
success: true,