fix: Static file paths and crawl_enabled API filters

- Fix static file paths for local development (./public/* instead of /app/public/*)
- Add crawl_enabled and dutchie_verified filters to /api/stores and /api/dispensaries
- Default API to return only enabled stores (crawl_enabled=true)
- Add ?crawl_enabled=false to show disabled, ?crawl_enabled=all to show all

🤖 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-08 14:07:17 -07:00
parent 5415cac2f3
commit 39aebfcb82
3 changed files with 317 additions and 68 deletions

View File

@@ -17,11 +17,13 @@ app.use(cors());
app.use(express.json()); app.use(express.json());
// Serve static images when MinIO is not configured // Serve static images when MinIO is not configured
const LOCAL_IMAGES_PATH = process.env.LOCAL_IMAGES_PATH || '/app/public/images'; // Uses ./public/images relative to working directory (works for both Docker and local dev)
const LOCAL_IMAGES_PATH = process.env.LOCAL_IMAGES_PATH || './public/images';
app.use('/images', express.static(LOCAL_IMAGES_PATH)); app.use('/images', express.static(LOCAL_IMAGES_PATH));
// Serve static downloads (plugin files, etc.) // Serve static downloads (plugin files, etc.)
const LOCAL_DOWNLOADS_PATH = process.env.LOCAL_DOWNLOADS_PATH || '/app/public/downloads'; // Uses ./public/downloads relative to working directory (works for both Docker and local dev)
const LOCAL_DOWNLOADS_PATH = process.env.LOCAL_DOWNLOADS_PATH || './public/downloads';
app.use('/downloads', express.static(LOCAL_DOWNLOADS_PATH)); app.use('/downloads', express.static(LOCAL_DOWNLOADS_PATH));
// Simple health check for load balancers/K8s probes // Simple health check for load balancers/K8s probes

View File

@@ -11,29 +11,46 @@ const VALID_MENU_TYPES = ['dutchie', 'treez', 'jane', 'weedmaps', 'leafly', 'mea
// Get all dispensaries // Get all dispensaries
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
const { menu_type, city, state } = req.query; const { menu_type, city, state, crawl_enabled, dutchie_verified } = req.query;
let query = ` let query = `
SELECT SELECT
id, id,
name, name,
company_name,
slug, slug,
address, address1,
address2,
city, city,
state, state,
zip, zipcode,
phone, phone,
website, website,
email,
dba_name, dba_name,
latitude, latitude,
longitude, longitude,
timezone,
menu_url, menu_url,
menu_type, menu_type,
platform, platform,
platform_dispensary_id, platform_dispensary_id,
c_name,
chain_slug,
enterprise_id,
description,
logo_image,
banner_image,
offer_pickup,
offer_delivery,
offer_curbside_pickup,
is_medical,
is_recreational,
status,
country,
product_count, product_count,
last_crawl_at, last_crawl_at,
crawl_enabled,
dutchie_verified,
created_at, created_at,
updated_at updated_at
FROM dispensaries FROM dispensaries
@@ -48,10 +65,10 @@ router.get('/', async (req, res) => {
params.push(menu_type); params.push(menu_type);
} }
// Filter by city if provided // Filter by city if provided (supports partial match)
if (city) { if (city) {
conditions.push(`city ILIKE $${params.length + 1}`); conditions.push(`city ILIKE $${params.length + 1}`);
params.push(city); params.push(`%${city}%`);
} }
// Filter by state if provided // Filter by state if provided
@@ -60,6 +77,27 @@ router.get('/', async (req, res) => {
params.push(state); params.push(state);
} }
// Filter by crawl_enabled - defaults to showing only enabled
if (crawl_enabled === 'false' || crawl_enabled === '0') {
// Explicitly show disabled only
conditions.push(`(crawl_enabled = false OR crawl_enabled IS NULL)`);
} else if (crawl_enabled === 'all') {
// Show all (no filter)
} else {
// Default: show only enabled
conditions.push(`crawl_enabled = true`);
}
// Filter by dutchie_verified if provided
if (dutchie_verified !== undefined) {
const verified = dutchie_verified === 'true' || dutchie_verified === '1';
if (verified) {
conditions.push(`dutchie_verified = true`);
} else {
conditions.push(`(dutchie_verified = false OR dutchie_verified IS NULL)`);
}
}
if (conditions.length > 0) { if (conditions.length > 0) {
query += ` WHERE ${conditions.join(' AND ')}`; query += ` WHERE ${conditions.join(' AND ')}`;
} }
@@ -68,7 +106,7 @@ router.get('/', async (req, res) => {
const result = await pool.query(query, params); const result = await pool.query(query, params);
res.json({ dispensaries: result.rows }); res.json({ dispensaries: result.rows, total: result.rowCount });
} catch (error) { } catch (error) {
console.error('Error fetching dispensaries:', error); console.error('Error fetching dispensaries:', error);
res.status(500).json({ error: 'Failed to fetch dispensaries' }); res.status(500).json({ error: 'Failed to fetch dispensaries' });
@@ -91,6 +129,46 @@ router.get('/stats/menu-types', async (req, res) => {
} }
}); });
// Get crawl status stats
router.get('/stats/crawl-status', async (req, res) => {
try {
const { state, city } = req.query;
let query = `
SELECT
COUNT(*) FILTER (WHERE crawl_enabled = true) as enabled_count,
COUNT(*) FILTER (WHERE crawl_enabled = false OR crawl_enabled IS NULL) as disabled_count,
COUNT(*) FILTER (WHERE dutchie_verified = true) as verified_count,
COUNT(*) FILTER (WHERE dutchie_verified = false OR dutchie_verified IS NULL) as unverified_count,
COUNT(*) as total_count
FROM dispensaries
`;
const params: any[] = [];
const conditions: string[] = [];
if (state) {
conditions.push(`state = $${params.length + 1}`);
params.push(state);
}
if (city) {
conditions.push(`city ILIKE $${params.length + 1}`);
params.push(`%${city}%`);
}
if (conditions.length > 0) {
query += ` WHERE ${conditions.join(' AND ')}`;
}
const result = await pool.query(query, params);
res.json(result.rows[0]);
} catch (error) {
console.error('Error fetching crawl status stats:', error);
res.status(500).json({ error: 'Failed to fetch crawl status stats' });
}
});
// Get single dispensary by slug or ID // Get single dispensary by slug or ID
router.get('/:slugOrId', async (req, res) => { router.get('/:slugOrId', async (req, res) => {
try { try {
@@ -101,21 +179,36 @@ router.get('/:slugOrId', async (req, res) => {
SELECT SELECT
id, id,
name, name,
company_name,
slug, slug,
address, address1,
address2,
city, city,
state, state,
zip, zipcode,
phone, phone,
website, website,
email,
dba_name, dba_name,
latitude, latitude,
longitude, longitude,
timezone,
menu_url, menu_url,
menu_type, menu_type,
platform, platform,
platform_dispensary_id, platform_dispensary_id,
c_name,
chain_slug,
enterprise_id,
description,
logo_image,
banner_image,
offer_pickup,
offer_delivery,
offer_curbside_pickup,
is_medical,
is_recreational,
status,
country,
product_count, product_count,
last_crawl_at, last_crawl_at,
raw_metadata, raw_metadata,
@@ -143,19 +236,34 @@ router.put('/:id', async (req, res) => {
const { const {
name, name,
dba_name, dba_name,
company_name,
website, website,
phone, phone,
address, email,
address1,
address2,
city, city,
state, state,
zip, zipcode,
latitude, latitude,
longitude, longitude,
timezone,
menu_url, menu_url,
menu_type, menu_type,
platform, platform,
platform_dispensary_id, platform_dispensary_id,
c_name,
chain_slug,
enterprise_id,
description,
logo_image,
banner_image,
offer_pickup,
offer_delivery,
offer_curbside_pickup,
is_medical,
is_recreational,
status,
country,
slug, slug,
} = req.body; } = req.body;
@@ -171,39 +279,69 @@ router.put('/:id', async (req, res) => {
SET SET
name = COALESCE($1, name), name = COALESCE($1, name),
dba_name = COALESCE($2, dba_name), dba_name = COALESCE($2, dba_name),
company_name = COALESCE($3, company_name), website = COALESCE($3, website),
website = COALESCE($4, website), phone = COALESCE($4, phone),
phone = COALESCE($5, phone), email = COALESCE($5, email),
address = COALESCE($6, address), address1 = COALESCE($6, address1),
city = COALESCE($7, city), address2 = COALESCE($7, address2),
state = COALESCE($8, state), city = COALESCE($8, city),
zip = COALESCE($9, zip), state = COALESCE($9, state),
latitude = COALESCE($10, latitude), zipcode = COALESCE($10, zipcode),
longitude = COALESCE($11, longitude), latitude = COALESCE($11, latitude),
menu_url = COALESCE($12, menu_url), longitude = COALESCE($12, longitude),
menu_type = COALESCE($13, menu_type), timezone = COALESCE($13, timezone),
platform = COALESCE($14, platform), menu_url = COALESCE($14, menu_url),
platform_dispensary_id = COALESCE($15, platform_dispensary_id), menu_type = COALESCE($15, menu_type),
slug = COALESCE($16, slug), platform = COALESCE($16, platform),
platform_dispensary_id = COALESCE($17, platform_dispensary_id),
c_name = COALESCE($18, c_name),
chain_slug = COALESCE($19, chain_slug),
enterprise_id = COALESCE($20, enterprise_id),
description = COALESCE($21, description),
logo_image = COALESCE($22, logo_image),
banner_image = COALESCE($23, banner_image),
offer_pickup = COALESCE($24, offer_pickup),
offer_delivery = COALESCE($25, offer_delivery),
offer_curbside_pickup = COALESCE($26, offer_curbside_pickup),
is_medical = COALESCE($27, is_medical),
is_recreational = COALESCE($28, is_recreational),
status = COALESCE($29, status),
country = COALESCE($30, country),
slug = COALESCE($31, slug),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $17 WHERE id = $32
RETURNING * RETURNING *
`, [ `, [
name, name,
dba_name, dba_name,
company_name,
website, website,
phone, phone,
address, email,
address1,
address2,
city, city,
state, state,
zip, zipcode,
latitude, latitude,
longitude, longitude,
timezone,
menu_url, menu_url,
menu_type, menu_type,
platform, platform,
platform_dispensary_id, platform_dispensary_id,
c_name,
chain_slug,
enterprise_id,
description,
logo_image,
banner_image,
offer_pickup,
offer_delivery,
offer_curbside_pickup,
is_medical,
is_recreational,
status,
country,
slug, slug,
id id
]); ]);

View File

@@ -70,7 +70,7 @@ function detectProvider(menuUrl: string | null): string {
// Get all stores (from dispensaries table) // Get all stores (from dispensaries table)
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
const { city, state, menu_type } = req.query; const { city, state, menu_type, crawl_enabled, dutchie_verified } = req.query;
let query = ` let query = `
SELECT SELECT
@@ -79,18 +79,36 @@ router.get('/', async (req, res) => {
slug, slug,
city, city,
state, state,
address, address1,
zip, address2,
zipcode,
phone, phone,
website, website,
email,
latitude, latitude,
longitude, longitude,
timezone,
menu_url, menu_url,
menu_type, menu_type,
platform, platform,
platform_dispensary_id, platform_dispensary_id,
c_name,
chain_slug,
enterprise_id,
description,
logo_image,
banner_image,
offer_pickup,
offer_delivery,
offer_curbside_pickup,
is_medical,
is_recreational,
status,
country,
product_count, product_count,
last_crawl_at, last_crawl_at,
crawl_enabled,
dutchie_verified,
created_at, created_at,
updated_at updated_at
FROM dispensaries FROM dispensaries
@@ -99,21 +117,45 @@ router.get('/', async (req, res) => {
const params: any[] = []; const params: any[] = [];
const conditions: string[] = []; const conditions: string[] = [];
// Filter by city (partial match)
if (city) { if (city) {
conditions.push(`city ILIKE $${params.length + 1}`); conditions.push(`city ILIKE $${params.length + 1}`);
params.push(city); params.push(`%${city}%`);
} }
// Filter by state
if (state) { if (state) {
conditions.push(`state = $${params.length + 1}`); conditions.push(`state = $${params.length + 1}`);
params.push(state); params.push(state);
} }
// Filter by menu_type
if (menu_type) { if (menu_type) {
conditions.push(`menu_type = $${params.length + 1}`); conditions.push(`menu_type = $${params.length + 1}`);
params.push(menu_type); params.push(menu_type);
} }
// Filter by crawl_enabled - defaults to showing only enabled
if (crawl_enabled === 'false' || crawl_enabled === '0') {
// Explicitly show disabled only
conditions.push(`(crawl_enabled = false OR crawl_enabled IS NULL)`);
} else if (crawl_enabled === 'all') {
// Show all (no filter)
} else {
// Default: show only enabled
conditions.push(`crawl_enabled = true`);
}
// Filter by dutchie_verified
if (dutchie_verified !== undefined) {
const verified = dutchie_verified === 'true' || dutchie_verified === '1';
if (verified) {
conditions.push(`dutchie_verified = true`);
} else {
conditions.push(`(dutchie_verified = false OR dutchie_verified IS NULL)`);
}
}
if (conditions.length > 0) { if (conditions.length > 0) {
query += ` WHERE ${conditions.join(' AND ')}`; query += ` WHERE ${conditions.join(' AND ')}`;
} }
@@ -129,7 +171,7 @@ router.get('/', async (req, res) => {
...calculateFreshness(row.last_crawl_at) ...calculateFreshness(row.last_crawl_at)
})); }));
res.json({ stores }); res.json({ stores, total: result.rowCount });
} catch (error) { } catch (error) {
console.error('Error fetching stores:', error); console.error('Error fetching stores:', error);
res.status(500).json({ error: 'Failed to fetch stores' }); res.status(500).json({ error: 'Failed to fetch stores' });
@@ -148,18 +190,33 @@ router.get('/:id', async (req, res) => {
slug, slug,
city, city,
state, state,
address, address1,
zip, address2,
zipcode,
phone, phone,
website, website,
email,
dba_name, dba_name,
company_name,
latitude, latitude,
longitude, longitude,
timezone,
menu_url, menu_url,
menu_type, menu_type,
platform, platform,
platform_dispensary_id, platform_dispensary_id,
c_name,
chain_slug,
enterprise_id,
description,
logo_image,
banner_image,
offer_pickup,
offer_delivery,
offer_curbside_pickup,
is_medical,
is_recreational,
status,
country,
product_count, product_count,
last_crawl_at, last_crawl_at,
raw_metadata, raw_metadata,
@@ -203,16 +260,32 @@ router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
slug, slug,
city, city,
state, state,
address, address1,
zip, address2,
zipcode,
phone, phone,
website, website,
email,
menu_url, menu_url,
menu_type, menu_type,
platform, platform,
platform_dispensary_id, platform_dispensary_id,
c_name,
chain_slug,
enterprise_id,
latitude, latitude,
longitude longitude,
timezone,
description,
logo_image,
banner_image,
offer_pickup,
offer_delivery,
offer_curbside_pickup,
is_medical,
is_recreational,
status,
country
} = req.body; } = req.body;
if (!name || !slug || !city || !state) { if (!name || !slug || !city || !state) {
@@ -221,16 +294,19 @@ router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
const result = await pool.query(` const result = await pool.query(`
INSERT INTO dispensaries ( INSERT INTO dispensaries (
name, slug, city, state, address, zip, phone, website, name, slug, city, state, address1, address2, zipcode, phone, website, email,
menu_url, menu_type, platform, platform_dispensary_id, menu_url, menu_type, platform, platform_dispensary_id, c_name, chain_slug, enterprise_id,
latitude, longitude, created_at, updated_at latitude, longitude, timezone, description, logo_image, banner_image,
offer_pickup, offer_delivery, offer_curbside_pickup, is_medical, is_recreational, status, country,
created_at, updated_at
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING * RETURNING *
`, [ `, [
name, slug, city, state, address, zip, phone, website, name, slug, city, state, address1, address2, zipcode, phone, website, email,
menu_url, menu_type, platform || 'dutchie', platform_dispensary_id, menu_url, menu_type, platform || 'dutchie', platform_dispensary_id, c_name, chain_slug, enterprise_id,
latitude, longitude latitude, longitude, timezone, description, logo_image, banner_image,
offer_pickup, offer_delivery, offer_curbside_pickup, is_medical, is_recreational, status, country || 'United States'
]); ]);
res.status(201).json(result.rows[0]); res.status(201).json(result.rows[0]);
@@ -253,16 +329,32 @@ router.put('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
slug, slug,
city, city,
state, state,
address, address1,
zip, address2,
zipcode,
phone, phone,
website, website,
email,
menu_url, menu_url,
menu_type, menu_type,
platform, platform,
platform_dispensary_id, platform_dispensary_id,
c_name,
chain_slug,
enterprise_id,
latitude, latitude,
longitude longitude,
timezone,
description,
logo_image,
banner_image,
offer_pickup,
offer_delivery,
offer_curbside_pickup,
is_medical,
is_recreational,
status,
country
} = req.body; } = req.body;
const result = await pool.query(` const result = await pool.query(`
@@ -272,23 +364,40 @@ router.put('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
slug = COALESCE($2, slug), slug = COALESCE($2, slug),
city = COALESCE($3, city), city = COALESCE($3, city),
state = COALESCE($4, state), state = COALESCE($4, state),
address = COALESCE($5, address), address1 = COALESCE($5, address1),
zip = COALESCE($6, zip), address2 = COALESCE($6, address2),
phone = COALESCE($7, phone), zipcode = COALESCE($7, zipcode),
website = COALESCE($8, website), phone = COALESCE($8, phone),
menu_url = COALESCE($9, menu_url), website = COALESCE($9, website),
menu_type = COALESCE($10, menu_type), email = COALESCE($10, email),
platform = COALESCE($11, platform), menu_url = COALESCE($11, menu_url),
platform_dispensary_id = COALESCE($12, platform_dispensary_id), menu_type = COALESCE($12, menu_type),
latitude = COALESCE($13, latitude), platform = COALESCE($13, platform),
longitude = COALESCE($14, longitude), platform_dispensary_id = COALESCE($14, platform_dispensary_id),
c_name = COALESCE($15, c_name),
chain_slug = COALESCE($16, chain_slug),
enterprise_id = COALESCE($17, enterprise_id),
latitude = COALESCE($18, latitude),
longitude = COALESCE($19, longitude),
timezone = COALESCE($20, timezone),
description = COALESCE($21, description),
logo_image = COALESCE($22, logo_image),
banner_image = COALESCE($23, banner_image),
offer_pickup = COALESCE($24, offer_pickup),
offer_delivery = COALESCE($25, offer_delivery),
offer_curbside_pickup = COALESCE($26, offer_curbside_pickup),
is_medical = COALESCE($27, is_medical),
is_recreational = COALESCE($28, is_recreational),
status = COALESCE($29, status),
country = COALESCE($30, country),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $15 WHERE id = $31
RETURNING * RETURNING *
`, [ `, [
name, slug, city, state, address, zip, phone, website, name, slug, city, state, address1, address2, zipcode, phone, website, email,
menu_url, menu_type, platform, platform_dispensary_id, menu_url, menu_type, platform, platform_dispensary_id, c_name, chain_slug, enterprise_id,
latitude, longitude, id latitude, longitude, timezone, description, logo_image, banner_image,
offer_pickup, offer_delivery, offer_curbside_pickup, is_medical, is_recreational, status, country, id
]); ]);
if (result.rows.length === 0) { if (result.rows.length === 0) {