## 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>
133 lines
4.1 KiB
TypeScript
Executable File
133 lines
4.1 KiB
TypeScript
Executable File
import { Router } from 'express';
|
|
import { authMiddleware } from '../auth/middleware';
|
|
import { pool } from '../db/pool';
|
|
|
|
const router = Router();
|
|
router.use(authMiddleware);
|
|
|
|
// Get analytics overview
|
|
router.get('/overview', async (req, res) => {
|
|
try {
|
|
const { days = 30 } = req.query;
|
|
|
|
// Total clicks
|
|
const clicksResult = await pool.query(`
|
|
SELECT COUNT(*) as total_clicks
|
|
FROM clicks
|
|
WHERE clicked_at >= NOW() - INTERVAL '${parseInt(days as string)} days'
|
|
`);
|
|
|
|
// Unique products clicked
|
|
const uniqueProductsResult = await pool.query(`
|
|
SELECT COUNT(DISTINCT product_id) as unique_products
|
|
FROM clicks
|
|
WHERE clicked_at >= NOW() - INTERVAL '${parseInt(days as string)} days'
|
|
`);
|
|
|
|
// Clicks by day
|
|
const clicksByDayResult = await pool.query(`
|
|
SELECT DATE(clicked_at) as date, COUNT(*) as clicks
|
|
FROM clicks
|
|
WHERE clicked_at >= NOW() - INTERVAL '${parseInt(days as string)} days'
|
|
GROUP BY DATE(clicked_at)
|
|
ORDER BY date DESC
|
|
`);
|
|
|
|
// Top products
|
|
const topProductsResult = await pool.query(`
|
|
SELECT p.id, p.name_raw as name, p.price_rec as price, COUNT(c.id) as click_count
|
|
FROM clicks c
|
|
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_raw, p.price_rec
|
|
ORDER BY click_count DESC
|
|
LIMIT 10
|
|
`);
|
|
|
|
res.json({
|
|
overview: {
|
|
total_clicks: parseInt(clicksResult.rows[0].total_clicks),
|
|
unique_products: parseInt(uniqueProductsResult.rows[0].unique_products)
|
|
},
|
|
clicks_by_day: clicksByDayResult.rows,
|
|
top_products: topProductsResult.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching analytics:', error);
|
|
res.status(500).json({ error: 'Failed to fetch analytics' });
|
|
}
|
|
});
|
|
|
|
// Get product analytics
|
|
router.get('/products/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { days = 30 } = req.query;
|
|
|
|
// Total clicks for this product
|
|
const totalResult = await pool.query(`
|
|
SELECT COUNT(*) as total_clicks
|
|
FROM clicks
|
|
WHERE product_id = $1
|
|
AND clicked_at >= NOW() - INTERVAL '${parseInt(days as string)} days'
|
|
`, [id]);
|
|
|
|
// Clicks by day
|
|
const byDayResult = await pool.query(`
|
|
SELECT DATE(clicked_at) as date, COUNT(*) as clicks
|
|
FROM clicks
|
|
WHERE product_id = $1
|
|
AND clicked_at >= NOW() - INTERVAL '${parseInt(days as string)} days'
|
|
GROUP BY DATE(clicked_at)
|
|
ORDER BY date DESC
|
|
`, [id]);
|
|
|
|
res.json({
|
|
product_id: parseInt(id),
|
|
total_clicks: parseInt(totalResult.rows[0].total_clicks),
|
|
clicks_by_day: byDayResult.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching product analytics:', error);
|
|
res.status(500).json({ error: 'Failed to fetch product analytics' });
|
|
}
|
|
});
|
|
|
|
// Get campaign analytics
|
|
router.get('/campaigns/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { days = 30 } = req.query;
|
|
|
|
// Total clicks for this campaign
|
|
const totalResult = await pool.query(`
|
|
SELECT COUNT(*) as total_clicks
|
|
FROM clicks
|
|
WHERE campaign_id = $1
|
|
AND clicked_at >= NOW() - INTERVAL '${parseInt(days as string)} days'
|
|
`, [id]);
|
|
|
|
// Clicks by product in this campaign
|
|
const byProductResult = await pool.query(`
|
|
SELECT p.id, p.name_raw as name, COUNT(c.id) as clicks
|
|
FROM clicks c
|
|
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_raw
|
|
ORDER BY clicks DESC
|
|
`, [id]);
|
|
|
|
res.json({
|
|
campaign_id: parseInt(id),
|
|
total_clicks: parseInt(totalResult.rows[0].total_clicks),
|
|
clicks_by_product: byProductResult.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching campaign analytics:', error);
|
|
res.status(500).json({ error: 'Failed to fetch campaign analytics' });
|
|
}
|
|
});
|
|
|
|
export default router;
|