Update admin panel to use unified dispensaries table

- Add migration 026 to update dispensary_crawl_status view with new fields
- Update dashboard API to use dispensaries table (not stores)
- Show current inventory counts (products seen in last 7 days)
- Update ScraperSchedule UI to show provider_type correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-01 00:13:41 -07:00
parent 9d8972aa86
commit 9de0d709b2
3 changed files with 105 additions and 35 deletions

View File

@@ -8,58 +8,63 @@ router.use(authMiddleware);
// Get dashboard stats
router.get('/stats', async (req, res) => {
try {
// Store stats
const storesResult = await pool.query(`
SELECT
// Dispensary stats (using unified dispensaries table)
const dispensariesResult = await pool.query(`
SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE active = true) as active,
MIN(last_scraped_at) as oldest_scrape,
MAX(last_scraped_at) as latest_scrape
FROM stores
COUNT(*) FILTER (WHERE scrape_enabled = true) as active,
COUNT(*) FILTER (WHERE menu_url IS NOT NULL) as with_menu_url,
COUNT(*) FILTER (WHERE provider_type IS NOT NULL AND provider_type != 'unknown') as detected,
MIN(last_crawl_at) as oldest_crawl,
MAX(last_crawl_at) as latest_crawl
FROM dispensaries
`);
// Product stats
// Current product stats (active inventory only)
const productsResult = await pool.query(`
SELECT
SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE in_stock = true) as in_stock,
COUNT(*) FILTER (WHERE local_image_path IS NOT NULL) as with_images
COUNT(*) FILTER (WHERE local_image_path IS NOT NULL) as with_images,
COUNT(DISTINCT brand) as unique_brands,
COUNT(DISTINCT dispensary_id) as stores_with_products
FROM products
WHERE last_seen_at >= NOW() - INTERVAL '7 days'
`);
// Campaign stats
const campaignsResult = await pool.query(`
SELECT
SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE active = true) as active
FROM campaigns
`);
// Recent clicks (last 24 hours)
const clicksResult = await pool.query(`
SELECT COUNT(*) as clicks_24h
FROM clicks
WHERE clicked_at >= NOW() - INTERVAL '24 hours'
`);
// Recent products added (last 24 hours)
const recentProductsResult = await pool.query(`
SELECT COUNT(*) as new_products_24h
FROM products
WHERE first_seen_at >= NOW() - INTERVAL '24 hours'
`);
// Proxy stats
const proxiesResult = await pool.query(`
SELECT
SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE active = true) as active,
COUNT(*) FILTER (WHERE is_anonymous = true) as anonymous
FROM proxies
`);
res.json({
stores: storesResult.rows[0],
stores: dispensariesResult.rows[0], // Keep 'stores' key for frontend compat
products: productsResult.rows[0],
campaigns: campaignsResult.rows[0],
clicks: clicksResult.rows[0],
@@ -76,28 +81,28 @@ router.get('/stats', async (req, res) => {
router.get('/activity', async (req, res) => {
try {
const { limit = 20 } = req.query;
// Recent scrapes
// Recent crawls from dispensaries
const scrapesResult = await pool.query(`
SELECT s.name, s.last_scraped_at,
COUNT(p.id) as product_count
FROM stores s
LEFT JOIN products p ON s.id = p.store_id AND p.last_seen_at = s.last_scraped_at
WHERE s.last_scraped_at IS NOT NULL
GROUP BY s.id, s.name, s.last_scraped_at
ORDER BY s.last_scraped_at DESC
SELECT
COALESCE(d.dba_name, d.name) as name,
d.last_crawl_at as last_scraped_at,
(SELECT COUNT(*) FROM products p WHERE p.dispensary_id = d.id AND p.last_seen_at >= NOW() - INTERVAL '7 days') as product_count
FROM dispensaries d
WHERE d.last_crawl_at IS NOT NULL
ORDER BY d.last_crawl_at DESC
LIMIT $1
`, [limit]);
// Recent products
const productsResult = await pool.query(`
SELECT p.name, p.price, s.name as store_name, p.first_seen_at
SELECT p.name, p.price, COALESCE(d.dba_name, d.name) as store_name, p.first_seen_at
FROM products p
JOIN stores s ON p.store_id = s.id
JOIN dispensaries d ON p.dispensary_id = d.id
ORDER BY p.first_seen_at DESC
LIMIT $1
`, [limit]);
res.json({
recent_scrapes: scrapesResult.rows,
recent_products: productsResult.rows