Files
cannaiq/backend/src/routes/analytics.ts
Kelly 2f483b3084 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>
2025-12-09 00:05:34 -07:00

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;