fix: Intelligence stores endpoint and UI consistency

- Fix stores endpoint to only show stores with actual products (INNER JOIN + HAVING)
- Update badge colors to match Workers/Tasks dashboard style
- Use emerald/amber/red/gray color scheme consistently
- Chain badge now uses purple (bg-purple-100)
- Add migration 092 to fix Trulieve store URLs

🤖 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-12 23:37:28 -07:00
parent 5af86edf83
commit 470097eb19
3 changed files with 44 additions and 7 deletions

View File

@@ -0,0 +1,30 @@
-- Fix 3 Trulieve/Harvest stores with incorrect menu URLs
-- These records have NULL or mismatched platform_dispensary_id so store_discovery
-- ON CONFLICT can't update them automatically
UPDATE dispensaries
SET
menu_url = 'https://dutchie.com/dispensary/svaccha-llc-nirvana-center-apache-junction',
updated_at = NOW()
WHERE id = 224;
UPDATE dispensaries
SET
menu_url = 'https://dutchie.com/dispensary/trulieve-of-phoenix-tatum',
updated_at = NOW()
WHERE id = 76;
UPDATE dispensaries
SET
menu_url = 'https://dutchie.com/dispensary/harvest-of-havasu',
updated_at = NOW()
WHERE id = 403;
-- Queue entry_point_discovery tasks to resolve their platform_dispensary_id
-- method='http' ensures only workers that passed http preflight can claim these
INSERT INTO worker_tasks (role, dispensary_id, priority, scheduled_for, method)
VALUES
('entry_point_discovery', 224, 5, NOW(), 'http'),
('entry_point_discovery', 76, 5, NOW(), 'http'),
('entry_point_discovery', 403, 5, NOW(), 'http')
ON CONFLICT DO NOTHING;

View File

@@ -325,11 +325,12 @@ router.get('/stores', async (req: Request, res: Response) => {
(SELECT COUNT(*) FROM store_product_snapshots sps (SELECT COUNT(*) FROM store_product_snapshots sps
WHERE sps.store_product_id IN (SELECT id FROM store_products WHERE dispensary_id = d.id)) as snapshot_count WHERE sps.store_product_id IN (SELECT id FROM store_products WHERE dispensary_id = d.id)) as snapshot_count
FROM dispensaries d FROM dispensaries d
LEFT JOIN store_products sp ON sp.dispensary_id = d.id INNER JOIN store_products sp ON sp.dispensary_id = d.id
LEFT JOIN chains c ON d.chain_id = c.id LEFT JOIN chains c ON d.chain_id = c.id
WHERE d.state IS NOT NULL AND d.crawl_enabled = true WHERE d.state IS NOT NULL AND d.crawl_enabled = true
${stateFilter} ${stateFilter}
GROUP BY d.id, d.name, d.dba_name, d.city, d.state, d.menu_type, d.crawl_enabled, c.name GROUP BY d.id, d.name, d.dba_name, d.city, d.state, d.menu_type, d.crawl_enabled, c.name
HAVING COUNT(sp.id) > 0
ORDER BY sku_count DESC ORDER BY sku_count DESC
LIMIT $1 LIMIT $1
`, params); `, params);

View File

@@ -82,10 +82,16 @@ export function IntelligenceStores() {
}; };
const getCrawlFrequencyBadge = (hours: number | null) => { const getCrawlFrequencyBadge = (hours: number | null) => {
if (hours === null) return <span className="badge badge-sm badge-ghost">Unknown</span>; if (hours === null) {
if (hours <= 4) return <span className="badge badge-sm badge-success">High ({hours}h)</span>; return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600">Unknown</span>;
if (hours <= 12) return <span className="badge badge-sm badge-warning">Medium ({hours}h)</span>; }
return <span className="badge badge-sm badge-error">Low ({hours}h)</span>; if (hours <= 4) {
return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-emerald-100 text-emerald-700">High ({hours}h)</span>;
}
if (hours <= 12) {
return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700">Medium ({hours}h)</span>;
}
return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700">Low ({hours}h)</span>;
}; };
if (loading) { if (loading) {
@@ -263,7 +269,7 @@ export function IntelligenceStores() {
</td> </td>
<td> <td>
{store.chainName ? ( {store.chainName ? (
<span className="badge badge-sm badge-outline">{store.chainName}</span> <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700">{store.chainName}</span>
) : ( ) : (
<span className="text-gray-400">-</span> <span className="text-gray-400">-</span>
)} )}
@@ -275,7 +281,7 @@ export function IntelligenceStores() {
<span className="font-mono">{(store.snapshotCount || 0).toLocaleString()}</span> <span className="font-mono">{(store.snapshotCount || 0).toLocaleString()}</span>
</td> </td>
<td> <td>
<span className={store.lastCrawl ? 'text-green-600' : 'text-gray-400'}> <span className={store.lastCrawl ? 'text-emerald-600' : 'text-gray-400'}>
{formatTimeAgo(store.lastCrawl)} {formatTimeAgo(store.lastCrawl)}
</span> </span>
</td> </td>