feat: SEO template library, discovery pipeline, and orchestrator enhancements
## SEO Template Library - Add complete template library with 7 page types (state, city, category, brand, product, search, regeneration) - Add Template Library tab in SEO Orchestrator with accordion-based editors - Add template preview, validation, and variable injection engine - Add API endpoints: /api/seo/templates, preview, validate, generate, regenerate ## Discovery Pipeline - Add promotion.ts for discovery location validation and promotion - Add discover-all-states.ts script for multi-state discovery - Add promotion log migration (067) - Enhance discovery routes and types ## Orchestrator & Admin - Add crawl_enabled filter to stores page - Add API permissions page - Add job queue management - Add price analytics routes - Add markets and intelligence routes - Enhance dashboard and worker monitoring ## Infrastructure - Add migrations for worker definitions, SEO settings, field alignment - Add canonical pipeline for scraper v2 - Update hydration and sync orchestrator - Enhance multi-state query service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
137
backend/src/scripts/retry-platform-ids.ts
Normal file
137
backend/src/scripts/retry-platform-ids.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Retry resolving platform IDs for Dutchie stores that have menu_url but no platform_dispensary_id
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx src/scripts/retry-platform-ids.ts
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg';
|
||||
import dotenv from 'dotenv';
|
||||
import { resolveDispensaryIdWithDetails } from '../platforms/dutchie/queries';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL ||
|
||||
`postgresql://${process.env.CANNAIQ_DB_USER || 'dutchie'}:${process.env.CANNAIQ_DB_PASS || 'dutchie_local_pass'}@${process.env.CANNAIQ_DB_HOST || 'localhost'}:${process.env.CANNAIQ_DB_PORT || '54320'}/${process.env.CANNAIQ_DB_NAME || 'dutchie_menus'}`
|
||||
});
|
||||
|
||||
interface DispensaryRow {
|
||||
id: number;
|
||||
name: string;
|
||||
menu_url: string;
|
||||
}
|
||||
|
||||
function extractSlugFromUrl(menuUrl: string): string | null {
|
||||
// Extract slug from Dutchie URLs like:
|
||||
// https://dutchie.com/stores/Nirvana-North-Phoenix
|
||||
// https://dutchie.com/dispensary/curaleaf-dispensary-peoria
|
||||
// https://dutchie.com/embedded-menu/some-slug
|
||||
|
||||
const patterns = [
|
||||
/dutchie\.com\/stores\/([^/?]+)/i,
|
||||
/dutchie\.com\/dispensary\/([^/?]+)/i,
|
||||
/dutchie\.com\/embedded-menu\/([^/?]+)/i,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = menuUrl.match(pattern);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Retry Platform ID Resolution');
|
||||
console.log('='.repeat(60));
|
||||
console.log('');
|
||||
|
||||
// Get Dutchie dispensaries with menu_url but no platform_dispensary_id
|
||||
const result = await pool.query<DispensaryRow>(`
|
||||
SELECT id, name, menu_url
|
||||
FROM dispensaries
|
||||
WHERE menu_type = 'dutchie'
|
||||
AND menu_url IS NOT NULL AND menu_url != ''
|
||||
AND (platform_dispensary_id IS NULL OR platform_dispensary_id = '')
|
||||
ORDER BY name
|
||||
`);
|
||||
|
||||
console.log(`Found ${result.rows.length} stores to retry\n`);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
console.log('No stores need platform ID resolution.');
|
||||
await pool.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const successes: { id: number; name: string; platformId: string }[] = [];
|
||||
const failures: { id: number; name: string; slug: string | null; error: string }[] = [];
|
||||
|
||||
for (const row of result.rows) {
|
||||
console.log(`\n[${row.id}] ${row.name}`);
|
||||
console.log(` URL: ${row.menu_url}`);
|
||||
|
||||
const slug = extractSlugFromUrl(row.menu_url);
|
||||
if (!slug) {
|
||||
console.log(` ❌ Could not extract slug from URL`);
|
||||
failures.push({ id: row.id, name: row.name, slug: null, error: 'Could not extract slug' });
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(` Slug: ${slug}`);
|
||||
|
||||
try {
|
||||
const resolveResult = await resolveDispensaryIdWithDetails(slug);
|
||||
|
||||
if (resolveResult.dispensaryId) {
|
||||
console.log(` ✅ Resolved: ${resolveResult.dispensaryId}`);
|
||||
|
||||
// Update database
|
||||
await pool.query(
|
||||
'UPDATE dispensaries SET platform_dispensary_id = $1 WHERE id = $2',
|
||||
[resolveResult.dispensaryId, row.id]
|
||||
);
|
||||
console.log(` 💾 Updated database`);
|
||||
|
||||
successes.push({ id: row.id, name: row.name, platformId: resolveResult.dispensaryId });
|
||||
} else {
|
||||
const errorMsg = resolveResult.error || 'Unknown error';
|
||||
console.log(` ❌ Failed: ${errorMsg}`);
|
||||
failures.push({ id: row.id, name: row.name, slug, error: errorMsg });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(` ❌ Error: ${error.message}`);
|
||||
failures.push({ id: row.id, name: row.name, slug, error: error.message });
|
||||
}
|
||||
|
||||
// Small delay between requests
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('SUMMARY');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
console.log(`\n✅ Successes (${successes.length}):`);
|
||||
for (const s of successes) {
|
||||
console.log(` [${s.id}] ${s.name} -> ${s.platformId}`);
|
||||
}
|
||||
|
||||
console.log(`\n❌ Failures (${failures.length}):`);
|
||||
for (const f of failures) {
|
||||
console.log(` [${f.id}] ${f.name} (slug: ${f.slug || 'N/A'})`);
|
||||
console.log(` ${f.error}`);
|
||||
}
|
||||
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error('Fatal error:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user