feat(orchestrator): Add crawl_enabled filter to stores page
- Backend: Filter stores by crawl_enabled (default: enabled only) - API: Support crawl_enabled param in getOrchestratorStores - UI: Add Enabled/Disabled/All filter toggle buttons - UI: Show crawl status icon in stores table 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,12 +117,13 @@ router.get('/states', async (_req: Request, res: Response) => {
|
||||
* Returns list of stores with orchestrator status info
|
||||
* Query params:
|
||||
* - state: Filter by state (e.g., "AZ")
|
||||
* - crawl_enabled: Filter by crawl status (default: true, use "all" to show all, "false" for disabled only)
|
||||
* - limit: Max results (default 100)
|
||||
* - offset: Pagination offset
|
||||
*/
|
||||
router.get('/stores', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { state, limit = '100', offset = '0' } = req.query;
|
||||
const { state, crawl_enabled, limit = '100', offset = '0' } = req.query;
|
||||
|
||||
let whereClause = 'WHERE 1=1';
|
||||
const params: any[] = [];
|
||||
@@ -134,6 +135,16 @@ router.get('/stores', async (req: Request, res: Response) => {
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Filter by crawl_enabled - defaults to showing only enabled
|
||||
if (crawl_enabled === 'false' || crawl_enabled === '0') {
|
||||
whereClause += ` AND (d.crawl_enabled = false OR d.crawl_enabled IS NULL)`;
|
||||
} else if (crawl_enabled === 'all') {
|
||||
// Show all (no filter)
|
||||
} else {
|
||||
// Default: show only enabled
|
||||
whereClause += ` AND d.crawl_enabled = true`;
|
||||
}
|
||||
|
||||
params.push(parseInt(limit as string, 10), parseInt(offset as string, 10));
|
||||
|
||||
const { rows } = await pool.query(`
|
||||
@@ -145,6 +156,7 @@ router.get('/stores', async (req: Request, res: Response) => {
|
||||
d.menu_type as provider,
|
||||
d.platform_dispensary_id,
|
||||
d.last_crawl_at,
|
||||
d.crawl_enabled,
|
||||
dcp.id as profile_id,
|
||||
dcp.profile_key,
|
||||
COALESCE(dcp.status, dcp.config->>'status', 'legacy') as crawler_status,
|
||||
@@ -187,6 +199,7 @@ router.get('/stores', async (req: Request, res: Response) => {
|
||||
provider_raw: r.provider || null,
|
||||
provider_display: getProviderDisplayName(r.provider),
|
||||
platformDispensaryId: r.platform_dispensary_id,
|
||||
crawlEnabled: r.crawl_enabled ?? false,
|
||||
status: r.crawler_status || (r.platform_dispensary_id ? 'legacy' : 'pending'),
|
||||
profileId: r.profile_id,
|
||||
profileKey: r.profile_key,
|
||||
|
||||
@@ -1216,9 +1216,10 @@ class ApiClient {
|
||||
}>('/api/admin/orchestrator/states');
|
||||
}
|
||||
|
||||
async getOrchestratorStores(params?: { state?: string; limit?: number; offset?: number }) {
|
||||
async getOrchestratorStores(params?: { state?: string; crawl_enabled?: string; limit?: number; offset?: number }) {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.state && params.state !== 'all') searchParams.append('state', params.state);
|
||||
if (params?.crawl_enabled) searchParams.append('crawl_enabled', params.crawl_enabled);
|
||||
if (params?.limit) searchParams.append('limit', params.limit.toString());
|
||||
if (params?.offset) searchParams.append('offset', params.offset.toString());
|
||||
const queryString = searchParams.toString() ? `?${searchParams.toString()}` : '';
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
AlertTriangle,
|
||||
XCircle,
|
||||
RefreshCw,
|
||||
Power,
|
||||
PowerOff,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface StoreInfo {
|
||||
@@ -27,6 +29,7 @@ interface StoreInfo {
|
||||
lastSuccessAt: string | null;
|
||||
lastFailureAt: string | null;
|
||||
productCount: number;
|
||||
crawlEnabled?: boolean;
|
||||
}
|
||||
|
||||
const STATUS_FILTERS: Record<string, { label: string; match: (s: string) => boolean; icon: React.ReactNode; color: string }> = {
|
||||
@@ -70,15 +73,19 @@ export function OrchestratorStores() {
|
||||
const [totalStores, setTotalStores] = useState(0);
|
||||
|
||||
const statusFilter = searchParams.get('status') || 'all';
|
||||
const crawlEnabledFilter = searchParams.get('crawl_enabled') || 'true'; // Default to enabled only
|
||||
|
||||
useEffect(() => {
|
||||
loadStores();
|
||||
}, []);
|
||||
}, [crawlEnabledFilter]);
|
||||
|
||||
const loadStores = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await api.getOrchestratorStores({ limit: 500 });
|
||||
const data = await api.getOrchestratorStores({
|
||||
limit: 500,
|
||||
crawl_enabled: crawlEnabledFilter,
|
||||
});
|
||||
setStores(data.stores || []);
|
||||
setTotalStores(data.total || 0);
|
||||
} catch (error) {
|
||||
@@ -88,6 +95,20 @@ export function OrchestratorStores() {
|
||||
}
|
||||
};
|
||||
|
||||
const setCrawlEnabledFilter = (value: string) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (value === 'true') {
|
||||
newParams.delete('crawl_enabled'); // Default is enabled
|
||||
} else {
|
||||
newParams.set('crawl_enabled', value);
|
||||
}
|
||||
// Preserve status filter if set
|
||||
if (statusFilter !== 'all') {
|
||||
newParams.set('status', statusFilter);
|
||||
}
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
const filteredStores = stores.filter((store) => {
|
||||
const filter = STATUS_FILTERS[statusFilter];
|
||||
return filter ? filter.match(store.status) : true;
|
||||
@@ -184,12 +205,51 @@ export function OrchestratorStores() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Crawl Enabled Filter */}
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm font-medium text-gray-600">Crawl Status:</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setCrawlEnabledFilter('true')}
|
||||
className={`btn btn-sm gap-2 ${
|
||||
crawlEnabledFilter === 'true' ? 'btn-success' : 'btn-outline'
|
||||
}`}
|
||||
>
|
||||
<Power className={`w-4 h-4 ${crawlEnabledFilter === 'true' ? 'text-white' : 'text-green-600'}`} />
|
||||
Enabled
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCrawlEnabledFilter('false')}
|
||||
className={`btn btn-sm gap-2 ${
|
||||
crawlEnabledFilter === 'false' ? 'btn-error' : 'btn-outline'
|
||||
}`}
|
||||
>
|
||||
<PowerOff className={`w-4 h-4 ${crawlEnabledFilter === 'false' ? 'text-white' : 'text-red-600'}`} />
|
||||
Disabled
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCrawlEnabledFilter('all')}
|
||||
className={`btn btn-sm gap-2 ${
|
||||
crawlEnabledFilter === 'all' ? 'btn-primary' : 'btn-outline'
|
||||
}`}
|
||||
>
|
||||
<Building2 className={`w-4 h-4 ${crawlEnabledFilter === 'all' ? 'text-white' : 'text-gray-600'}`} />
|
||||
All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Filter Tabs */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{Object.entries(STATUS_FILTERS).map(([key, { label, icon, color }]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setSearchParams(key === 'all' ? {} : { status: key })}
|
||||
onClick={() => {
|
||||
const newParams = new URLSearchParams();
|
||||
if (key !== 'all') newParams.set('status', key);
|
||||
if (crawlEnabledFilter !== 'true') newParams.set('crawl_enabled', crawlEnabledFilter);
|
||||
setSearchParams(newParams);
|
||||
}}
|
||||
className={`btn btn-sm gap-2 ${
|
||||
statusFilter === key ? 'btn-primary' : 'btn-outline'
|
||||
}`}
|
||||
@@ -213,6 +273,7 @@ export function OrchestratorStores() {
|
||||
<th>City</th>
|
||||
<th>State</th>
|
||||
<th>Provider</th>
|
||||
<th>Crawl</th>
|
||||
<th>Status</th>
|
||||
<th>Last Success</th>
|
||||
<th>Products</th>
|
||||
@@ -221,13 +282,13 @@ export function OrchestratorStores() {
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center py-8">
|
||||
<td colSpan={8} className="text-center py-8">
|
||||
<div className="inline-block animate-spin rounded-full h-6 w-6 border-2 border-blue-500 border-t-transparent"></div>
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredStores.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center py-8 text-gray-500">
|
||||
<td colSpan={8} className="text-center py-8 text-gray-500">
|
||||
No stores match this filter
|
||||
</td>
|
||||
</tr>
|
||||
@@ -246,6 +307,15 @@ export function OrchestratorStores() {
|
||||
{store.provider_display || store.provider || 'Menu'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span title={store.crawlEnabled ? 'Crawl Enabled' : 'Crawl Disabled'}>
|
||||
{store.crawlEnabled ? (
|
||||
<Power className="w-4 h-4 text-green-600" />
|
||||
) : (
|
||||
<PowerOff className="w-4 h-4 text-red-400" />
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td>{getStatusPill(store.status)}</td>
|
||||
<td className="text-xs text-green-600">
|
||||
{formatTimeAgo(store.lastSuccessAt)}
|
||||
|
||||
Reference in New Issue
Block a user