From e8862b8a8b402e7417612718e24c69fbfa84644c Mon Sep 17 00:00:00 2001 From: Kelly Date: Wed, 10 Dec 2025 23:27:01 -0700 Subject: [PATCH 1/6] fix(national): Remove Refresh Metrics button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed unused refresh button and related state/handlers from National Dashboard. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cannaiq/dist/index.html | 2 +- cannaiq/src/pages/NationalDashboard.tsx | 36 ++++--------------------- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/cannaiq/dist/index.html b/cannaiq/dist/index.html index a407b190..4cbefcb9 100644 --- a/cannaiq/dist/index.html +++ b/cannaiq/dist/index.html @@ -7,7 +7,7 @@ CannaIQ - Cannabis Menu Intelligence Platform - + diff --git a/cannaiq/src/pages/NationalDashboard.tsx b/cannaiq/src/pages/NationalDashboard.tsx index 598ad708..4bccbdc7 100644 --- a/cannaiq/src/pages/NationalDashboard.tsx +++ b/cannaiq/src/pages/NationalDashboard.tsx @@ -20,7 +20,6 @@ import { DollarSign, MapPin, ArrowRight, - RefreshCw, AlertCircle } from 'lucide-react'; @@ -204,7 +203,6 @@ export default function NationalDashboard() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [summary, setSummary] = useState(null); - const [refreshing, setRefreshing] = useState(false); const fetchData = async () => { setLoading(true); @@ -229,18 +227,6 @@ export default function NationalDashboard() { fetchData(); }, []); - const handleRefreshMetrics = async () => { - setRefreshing(true); - try { - await api.post('/api/admin/states/refresh-metrics'); - await fetchData(); - } catch (err) { - console.error('Failed to refresh metrics:', err); - } finally { - setRefreshing(false); - } - }; - const handleStateClick = (stateCode: string) => { setSelectedState(stateCode); navigate(`/national/state/${stateCode}`); @@ -277,23 +263,11 @@ export default function NationalDashboard() {
{/* Header */} -
-
-

National Dashboard

-

- Multi-state cannabis market intelligence -

-
-
- -
+
+

National Dashboard

+

+ Multi-state cannabis market intelligence +

{/* Summary Cards */} From c091d2316b860472ecd9ee2abee4fd078bb528bb Mon Sep 17 00:00:00 2001 From: Kelly Date: Wed, 10 Dec 2025 23:28:18 -0700 Subject: [PATCH 2/6] fix(dashboard): Remove refresh button and HealthPanel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed refresh button and refreshing state from Dashboard - Removed HealthPanel component (deploy status auto-refresh) - Simplified header layout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cannaiq/dist/index.html | 4 ++-- cannaiq/src/pages/Dashboard.tsx | 27 ++++----------------------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/cannaiq/dist/index.html b/cannaiq/dist/index.html index 4cbefcb9..f760e6ab 100644 --- a/cannaiq/dist/index.html +++ b/cannaiq/dist/index.html @@ -7,8 +7,8 @@ CannaIQ - Cannabis Menu Intelligence Platform - - + +
diff --git a/cannaiq/src/pages/Dashboard.tsx b/cannaiq/src/pages/Dashboard.tsx index 5a4802e8..501433c1 100755 --- a/cannaiq/src/pages/Dashboard.tsx +++ b/cannaiq/src/pages/Dashboard.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import { Layout } from '../components/Layout'; -import { HealthPanel } from '../components/HealthPanel'; import { api } from '../lib/api'; import { useNavigate } from 'react-router-dom'; import { @@ -42,7 +41,6 @@ export function Dashboard() { const [activity, setActivity] = useState(null); const [nationalStats, setNationalStats] = useState(null); const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); const [pendingChangesCount, setPendingChangesCount] = useState(0); const [showNotification, setShowNotification] = useState(false); const [taskCounts, setTaskCounts] = useState | null>(null); @@ -93,10 +91,7 @@ export function Dashboard() { } }; - const loadData = async (isRefresh = false) => { - if (isRefresh) { - setRefreshing(true); - } + const loadData = async () => { try { // Fetch dashboard data (primary data source) const dashboard = await api.getMarketDashboard(); @@ -158,7 +153,6 @@ export function Dashboard() { console.error('Failed to load dashboard:', error); } finally { setLoading(false); - setRefreshing(false); } }; @@ -271,24 +265,11 @@ export function Dashboard() {
{/* Header */} -
-
-

Dashboard

-

Monitor your dispensary data aggregation

-
- +
+

Dashboard

+

Monitor your dispensary data aggregation

- {/* System Health */} - - {/* Stats Grid */}
{/* Products */} From 5b34b5a78cca8d67c8c6a12b9cdbf803a3deb980 Mon Sep 17 00:00:00 2001 From: Kelly Date: Wed, 10 Dec 2025 23:32:57 -0700 Subject: [PATCH 3/6] fix(admin): Consistent navigation across Intelligence pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add state selector dropdown to all three Intelligence pages (Brands, Stores, Pricing) - Use consistent emerald-styled page navigation badges with current page highlighted - Remove Refresh buttons from all Intelligence pages - Update chart styling to use emerald gradient bars (matching Pricing page) - Load all available states from orchestrator API instead of extracting from local data - Fix z-index and styling on state dropdown for better visibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cannaiq/src/pages/IntelligenceBrands.tsx | 139 +++++++++++----------- cannaiq/src/pages/IntelligencePricing.tsx | 61 +++++----- cannaiq/src/pages/IntelligenceStores.tsx | 88 +++++++++----- 3 files changed, 161 insertions(+), 127 deletions(-) diff --git a/cannaiq/src/pages/IntelligenceBrands.tsx b/cannaiq/src/pages/IntelligenceBrands.tsx index ef6f2a25..5b2b54c0 100644 --- a/cannaiq/src/pages/IntelligenceBrands.tsx +++ b/cannaiq/src/pages/IntelligenceBrands.tsx @@ -9,7 +9,6 @@ import { MapPin, Package, DollarSign, - RefreshCw, Search, TrendingUp, BarChart3, @@ -100,35 +99,60 @@ export function IntelligenceBrands() {
{/* Header */} -
+

Brands Intelligence

Brand penetration and pricing analytics across markets

-
- - - +
+ {/* State Selector */} + + + {/* Page Navigation */} +
+ + + +
@@ -180,51 +204,32 @@ export function IntelligenceBrands() { {/* Top Brands Chart */}
-
-

- - Top 10 Brands by Store Count -

- -
+

+ + Top 10 Brands by Store Count +

- {topBrands.map((brand, idx) => ( -
- {idx + 1}. - - {brand.brandName} - -
-
+ {topBrands.map((brand) => { + const barWidth = Math.min((brand.storeCount / maxStoreCount) * 100, 100); + return ( +
+ + {brand.brandName} + +
+
+
+
+
+ + {brand.storeCount} +
- - {brand.storeCount} stores - -
- ))} + ); + })}
diff --git a/cannaiq/src/pages/IntelligencePricing.tsx b/cannaiq/src/pages/IntelligencePricing.tsx index ffcaaf57..6c720e7a 100644 --- a/cannaiq/src/pages/IntelligencePricing.tsx +++ b/cannaiq/src/pages/IntelligencePricing.tsx @@ -8,7 +8,6 @@ import { Building2, MapPin, Package, - RefreshCw, TrendingUp, TrendingDown, BarChart3, @@ -87,56 +86,60 @@ export function IntelligencePricing() {
{/* Header */} -
+

Pricing Intelligence

Price distribution and trends by category

-
+
+ {/* State Selector */} - - - + + {/* Page Navigation */} +
+ + + +
diff --git a/cannaiq/src/pages/IntelligenceStores.tsx b/cannaiq/src/pages/IntelligenceStores.tsx index 6a098c08..427fb728 100644 --- a/cannaiq/src/pages/IntelligenceStores.tsx +++ b/cannaiq/src/pages/IntelligenceStores.tsx @@ -8,7 +8,6 @@ import { Building2, DollarSign, Package, - RefreshCw, Search, Clock, Activity, @@ -34,12 +33,19 @@ export function IntelligenceStores() { const [stores, setStores] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); - const [localStates, setLocalStates] = useState([]); + const [availableStates, setAvailableStates] = useState([]); useEffect(() => { loadStores(); }, [selectedState]); + useEffect(() => { + // Load available states from orchestrator API + api.getOrchestratorStates().then(data => { + setAvailableStates(data.states?.map((s: any) => s.state) || []); + }).catch(console.error); + }, []); + const loadStores = async () => { try { setLoading(true); @@ -47,12 +53,7 @@ export function IntelligenceStores() { state: stateParam, limit: 500, }); - const storeList = data.stores || []; - setStores(storeList); - - // Extract unique states from response for dropdown counts - const uniqueStates = [...new Set(storeList.map((s: StoreActivity) => s.state))].filter(Boolean).sort() as string[]; - setLocalStates(uniqueStates); + setStores(data.stores || []); } catch (error) { console.error('Failed to load stores:', error); } finally { @@ -110,35 +111,60 @@ export function IntelligenceStores() {
{/* Header */} -
+

Store Activity

Per-store SKU counts, snapshots, and crawl frequency

-
- - - +
+ {/* State Selector */} + + + {/* Page Navigation */} +
+ + + +
From b7d33e1cbf09b2aae5c05a081c89546e2967411e Mon Sep 17 00:00:00 2001 From: Kelly Date: Wed, 10 Dec 2025 23:42:50 -0700 Subject: [PATCH 4/6] fix(admin): Clean up store detail and intelligence pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove Update dropdown from DispensaryDetail page - Remove Crawl Now button from StoreDetailPage - Change "Last Crawl" to "Last Updated" on both detail pages - Tone down emerald colors on StoreDetailPage (use gray borders/tabs) - Simplify THC/CBD/Stock badges to plain text - Remove duplicate state dropdown from IntelligenceStores filters - Make store rows clickable in IntelligenceStores 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cannaiq/src/pages/DispensaryDetail.tsx | 43 +----------- cannaiq/src/pages/IntelligenceBrands.tsx | 4 +- cannaiq/src/pages/IntelligencePricing.tsx | 4 +- cannaiq/src/pages/IntelligenceStores.tsx | 26 +------- cannaiq/src/pages/StoreDetailPage.tsx | 81 ++++++----------------- 5 files changed, 29 insertions(+), 129 deletions(-) diff --git a/cannaiq/src/pages/DispensaryDetail.tsx b/cannaiq/src/pages/DispensaryDetail.tsx index 3464a3a4..37ec1d88 100644 --- a/cannaiq/src/pages/DispensaryDetail.tsx +++ b/cannaiq/src/pages/DispensaryDetail.tsx @@ -204,47 +204,6 @@ export function DispensaryDetail() { Back to Dispensaries - {/* Update Dropdown */} -
- - - {showUpdateDropdown && !isUpdating && ( -
- - - - -
- )} -
{/* Dispensary Header */} @@ -266,7 +225,7 @@ export function DispensaryDetail() {
- Last Crawl Date: + Last Updated: {dispensary.last_menu_scrape ? new Date(dispensary.last_menu_scrape).toLocaleDateString('en-US', { diff --git a/cannaiq/src/pages/IntelligenceBrands.tsx b/cannaiq/src/pages/IntelligenceBrands.tsx index 5b2b54c0..d9e255ad 100644 --- a/cannaiq/src/pages/IntelligenceBrands.tsx +++ b/cannaiq/src/pages/IntelligenceBrands.tsx @@ -140,14 +140,14 @@ export function IntelligenceBrands() {
- Showing {filteredStores.length} of {stores.length} stores @@ -273,7 +253,7 @@ export function IntelligenceStores() { navigate(`/admin/orchestrator/stores?storeId=${store.id}`)} + onClick={() => navigate(`/stores/list/${store.id}`)} > {store.name} diff --git a/cannaiq/src/pages/StoreDetailPage.tsx b/cannaiq/src/pages/StoreDetailPage.tsx index 8d968e12..e6a1f917 100644 --- a/cannaiq/src/pages/StoreDetailPage.tsx +++ b/cannaiq/src/pages/StoreDetailPage.tsx @@ -153,29 +153,6 @@ export function StoreDetailPage() { Back to Stores - {/* Update Button */} -
- - - {showUpdateDropdown && !isUpdating && ( -
- -
- )} -
{/* Store Header */} @@ -200,7 +177,7 @@ export function StoreDetailPage() {
- Last Crawl: + Last Updated: {lastCrawl?.completed_at ? new Date(lastCrawl.completed_at).toLocaleDateString('en-US', { @@ -212,15 +189,6 @@ export function StoreDetailPage() { }) : 'Never'} - {lastCrawl?.status && ( - - {lastCrawl.status} - - )}
@@ -282,8 +250,8 @@ export function StoreDetailPage() { setStockFilter('in_stock'); setSearchQuery(''); }} - className={`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${ - stockFilter === 'in_stock' ? 'border-blue-500' : 'border-gray-200' + className={`bg-white rounded-lg border p-4 hover:border-gray-300 hover:shadow-md transition-all cursor-pointer text-left ${ + stockFilter === 'in_stock' ? 'border-gray-400' : 'border-gray-200' }`} >
@@ -303,8 +271,8 @@ export function StoreDetailPage() { setStockFilter('out_of_stock'); setSearchQuery(''); }} - className={`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${ - stockFilter === 'out_of_stock' ? 'border-blue-500' : 'border-gray-200' + className={`bg-white rounded-lg border p-4 hover:border-gray-300 hover:shadow-md transition-all cursor-pointer text-left ${ + stockFilter === 'out_of_stock' ? 'border-gray-400' : 'border-gray-200' }`} >
@@ -320,8 +288,8 @@ export function StoreDetailPage() {
diff --git a/cannaiq/src/pages/DispensaryDetail.tsx b/cannaiq/src/pages/DispensaryDetail.tsx index 37ec1d88..d59d0bda 100644 --- a/cannaiq/src/pages/DispensaryDetail.tsx +++ b/cannaiq/src/pages/DispensaryDetail.tsx @@ -290,7 +290,7 @@ export function DispensaryDetail() { )} @@ -492,57 +492,31 @@ export function DispensaryDetail() { `$${product.regular_price}` ) : '-'} - - {product.quantity != null ? ( - 0 ? 'badge-info' : 'badge-error'}`}> - {product.quantity} - - ) : '-'} + + {product.quantity != null ? product.quantity : '-'} - - {product.thc_percentage ? ( - {product.thc_percentage}% - ) : '-'} + + {product.thc_percentage ? `${product.thc_percentage}%` : '-'} - - {product.cbd_percentage ? ( - {product.cbd_percentage}% - ) : '-'} + + {product.cbd_percentage ? `${product.cbd_percentage}%` : '-'} - - {product.strain_type ? ( - {product.strain_type} - ) : '-'} + + {product.strain_type || '-'} - - {product.in_stock ? ( - Yes - ) : product.in_stock === false ? ( - No - ) : '-'} + + {product.in_stock ? 'Yes' : product.in_stock === false ? 'No' : '-'} {product.updated_at ? formatDate(product.updated_at) : '-'} -
- {product.dutchie_url && ( - - Dutchie - - )} - -
+ ))} diff --git a/cannaiq/src/pages/DispensarySchedule.tsx b/cannaiq/src/pages/DispensarySchedule.tsx new file mode 100644 index 00000000..6dc18cd1 --- /dev/null +++ b/cannaiq/src/pages/DispensarySchedule.tsx @@ -0,0 +1,378 @@ +import { useEffect, useState } from 'react'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import { Layout } from '../components/Layout'; +import { api } from '../lib/api'; +import { + ArrowLeft, + Clock, + Calendar, + CheckCircle, + XCircle, + AlertCircle, + Package, + Timer, + Building2, +} from 'lucide-react'; + +interface CrawlHistoryItem { + id: number; + runId: string | null; + profileKey: string | null; + crawlerModule: string | null; + stateAtStart: string | null; + stateAtEnd: string | null; + totalSteps: number; + durationMs: number | null; + success: boolean; + errorMessage: string | null; + productsFound: number | null; + startedAt: string | null; + completedAt: string | null; +} + +interface NextSchedule { + scheduleId: number; + jobName: string; + enabled: boolean; + baseIntervalMinutes: number; + jitterMinutes: number; + nextRunAt: string | null; + lastRunAt: string | null; + lastStatus: string | null; +} + +interface Dispensary { + id: number; + name: string; + dba_name: string | null; + slug: string; + state: string; + city: string; + menu_type: string | null; + platform_dispensary_id: string | null; + last_menu_scrape: string | null; +} + +export function DispensarySchedule() { + const { state, city, slug } = useParams(); + const navigate = useNavigate(); + const [dispensary, setDispensary] = useState(null); + const [history, setHistory] = useState([]); + const [nextSchedule, setNextSchedule] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadScheduleData(); + }, [slug]); + + const loadScheduleData = async () => { + setLoading(true); + try { + // First get the dispensary to get the ID + const dispData = await api.getDispensary(slug!); + if (dispData?.id) { + const data = await api.getStoreCrawlHistory(dispData.id); + setDispensary(data.dispensary); + setHistory(data.history || []); + setNextSchedule(data.nextSchedule); + } + } catch (error) { + console.error('Failed to load schedule data:', error); + } finally { + setLoading(false); + } + }; + + const formatDate = (dateStr: string | null) => { + if (!dateStr) return 'Never'; + const date = new Date(dateStr); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const formatTimeAgo = (dateStr: string | null) => { + if (!dateStr) return 'Never'; + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMinutes = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffMinutes < 1) return 'Just now'; + if (diffMinutes < 60) return `${diffMinutes}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays === 1) return 'Yesterday'; + if (diffDays < 7) return `${diffDays} days ago`; + return date.toLocaleDateString(); + }; + + const formatTimeUntil = (dateStr: string | null) => { + if (!dateStr) return 'Not scheduled'; + const date = new Date(dateStr); + const now = new Date(); + const diffMs = date.getTime() - now.getTime(); + + if (diffMs < 0) return 'Overdue'; + + const diffMinutes = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMinutes / 60); + + if (diffMinutes < 60) return `in ${diffMinutes}m`; + return `in ${diffHours}h ${diffMinutes % 60}m`; + }; + + const formatDuration = (ms: number | null) => { + if (!ms) return '-'; + if (ms < 1000) return `${ms}ms`; + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + if (minutes < 1) return `${seconds}s`; + return `${minutes}m ${seconds % 60}s`; + }; + + const formatInterval = (baseMinutes: number, jitterMinutes: number) => { + const hours = Math.floor(baseMinutes / 60); + const mins = baseMinutes % 60; + let base = hours > 0 ? `${hours}h` : ''; + if (mins > 0) base += `${mins}m`; + return `Every ${base} (+/- ${jitterMinutes}m jitter)`; + }; + + if (loading) { + return ( + +
+
+

Loading schedule...

+
+
+ ); + } + + if (!dispensary) { + return ( + +
+

Dispensary not found

+
+
+ ); + } + + // Stats from history + const successCount = history.filter(h => h.success).length; + const failureCount = history.filter(h => !h.success).length; + const lastSuccess = history.find(h => h.success); + const avgDuration = history.length > 0 + ? Math.round(history.reduce((sum, h) => sum + (h.durationMs || 0), 0) / history.length) + : 0; + + return ( + +
+ {/* Header */} +
+ +
+ + {/* Dispensary Info */} +
+
+
+ +
+
+

+ {dispensary.dba_name || dispensary.name} +

+

+ {dispensary.city}, {dispensary.state} - Crawl Schedule & History +

+
+ Slug: {dispensary.slug} + {dispensary.menu_type && ( + + {dispensary.menu_type} + + )} +
+
+
+
+ + {/* Next Scheduled Crawl */} + {nextSchedule && ( +
+

+ + Upcoming Schedule +

+
+
+

Next Run

+

+ {formatTimeUntil(nextSchedule.nextRunAt)} +

+

+ {formatDate(nextSchedule.nextRunAt)} +

+
+
+

Interval

+

+ {formatInterval(nextSchedule.baseIntervalMinutes, nextSchedule.jitterMinutes)} +

+
+
+

Last Run

+

+ {formatTimeAgo(nextSchedule.lastRunAt)} +

+
+
+

Last Status

+

+ {nextSchedule.lastStatus || '-'} +

+
+
+
+ )} + + {/* Stats Summary */} +
+
+
+ +
+

Successful Runs

+

{successCount}

+
+
+
+
+
+ +
+

Failed Runs

+

{failureCount}

+
+
+
+
+
+ +
+

Avg Duration

+

{formatDuration(avgDuration)}

+
+
+
+
+
+ +
+

Last Products Found

+

+ {lastSuccess?.productsFound?.toLocaleString() || '-'} +

+
+
+
+
+ + {/* Crawl History Table */} +
+
+

+ + Crawl History +

+
+
+ + + + + + + + + + + + + {history.length === 0 ? ( + + + + ) : ( + history.map((item) => ( + + + + + + + + + )) + )} + +
StatusStartedDurationProductsStateError
+ No crawl history available +
+ + {item.success ? ( + + ) : ( + + )} + {item.success ? 'Success' : 'Failed'} + + +
{formatDate(item.startedAt)}
+
{formatTimeAgo(item.startedAt)}
+
+ {formatDuration(item.durationMs)} + + {item.productsFound?.toLocaleString() || '-'} + + {item.stateAtEnd || item.stateAtStart || '-'} + + {item.errorMessage ? ( + + {item.errorMessage.substring(0, 50)}... + + ) : '-'} +
+
+
+
+
+ ); +} + +export default DispensarySchedule; diff --git a/cannaiq/src/pages/IntelligenceBrands.tsx b/cannaiq/src/pages/IntelligenceBrands.tsx index d9e255ad..ab4d751f 100644 --- a/cannaiq/src/pages/IntelligenceBrands.tsx +++ b/cannaiq/src/pages/IntelligenceBrands.tsx @@ -31,7 +31,7 @@ export function IntelligenceBrands() { const [brands, setBrands] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); - const [sortBy, setSortBy] = useState<'stores' | 'skus' | 'name'>('stores'); + const [sortBy, setSortBy] = useState<'stores' | 'skus' | 'name' | 'states'>('stores'); useEffect(() => { loadBrands(); @@ -68,6 +68,8 @@ export function IntelligenceBrands() { return b.skuCount - a.skuCount; case 'name': return a.brandName.localeCompare(b.brandName); + case 'states': + return b.states.length - a.states.length; default: return 0; } @@ -252,6 +254,7 @@ export function IntelligenceBrands() { > + From 459ad7d9c9bac2d94c32c7353229485f44874c27 Mon Sep 17 00:00:00 2001 From: Kelly Date: Wed, 10 Dec 2025 23:54:28 -0700 Subject: [PATCH 6/6] fix(tasks): Fix missing column errors in task queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change 'active' to 'is_active' in states table query (store-discovery.ts) - Remove non-existent 'active' column check from worker_tasks query (task-service.ts) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- backend/src/tasks/handlers/store-discovery.ts | 2 +- backend/src/tasks/task-service.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/tasks/handlers/store-discovery.ts b/backend/src/tasks/handlers/store-discovery.ts index 66380aa2..017888ad 100644 --- a/backend/src/tasks/handlers/store-discovery.ts +++ b/backend/src/tasks/handlers/store-discovery.ts @@ -25,7 +25,7 @@ export async function handleStoreDiscovery(ctx: TaskContext): Promise r.code); diff --git a/backend/src/tasks/task-service.ts b/backend/src/tasks/task-service.ts index d9198690..586ea5be 100644 --- a/backend/src/tasks/task-service.ts +++ b/backend/src/tasks/task-service.ts @@ -170,7 +170,6 @@ class TaskService { WHERE id = ( SELECT id FROM worker_tasks WHERE status = 'pending' - AND active = true AND (scheduled_for IS NULL OR scheduled_for <= NOW()) -- Exclude stores that already have an active task AND (dispensary_id IS NULL OR dispensary_id NOT IN (