feat: Responsive admin UI, SEO pages, and click analytics

## Responsive Admin UI
- Layout.tsx: Mobile sidebar drawer with hamburger menu
- Dashboard.tsx: 2-col grid on mobile, responsive stats cards
- OrchestratorDashboard.tsx: Responsive table with hidden columns
- PagesTab.tsx: Responsive filters and table

## SEO Pages
- New /admin/seo section with state landing pages
- SEO page generation and management
- State page content with dispensary/product counts

## Click Analytics
- Product click tracking infrastructure
- Click analytics dashboard

## Other Changes
- Consumer features scaffolding (alerts, deals, favorites)
- Health panel component
- Workers dashboard improvements
- Legacy DutchieAZ pages removed

🤖 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-07 22:48:21 -07:00
parent 38d3ea1408
commit 3bc0effa33
74 changed files with 12295 additions and 807 deletions

View File

@@ -269,13 +269,12 @@ router.get('/dispensaries/:id/profile', async (req: Request, res: Response) => {
dcp.dispensary_id,
dcp.profile_key,
dcp.profile_name,
dcp.platform,
dcp.crawler_type,
dcp.version,
dcp.status,
dcp.config,
dcp.enabled,
dcp.sandbox_attempt_count,
dcp.next_retry_at,
dcp.sandbox_attempts,
dcp.created_at,
dcp.updated_at,
d.name as dispensary_name,
@@ -318,13 +317,12 @@ router.get('/dispensaries/:id/profile', async (req: Request, res: Response) => {
id: profile.id,
profileKey: profile.profile_key,
profileName: profile.profile_name,
platform: profile.platform,
crawlerType: profile.crawler_type,
version: profile.version,
status: profile.status || profile.config?.status || 'unknown',
config: profile.config,
enabled: profile.enabled,
sandboxAttemptCount: profile.sandbox_attempt_count,
nextRetryAt: profile.next_retry_at,
sandboxAttempts: profile.sandbox_attempts || [],
createdAt: profile.created_at,
updatedAt: profile.updated_at,
},
@@ -349,7 +347,7 @@ router.get('/dispensaries/:id/crawler-module', async (req: Request, res: Respons
// Get the profile key for this dispensary
const { rows } = await pool.query(`
SELECT profile_key, platform
SELECT profile_key, crawler_type
FROM dispensary_crawler_profiles
WHERE dispensary_id = $1 AND enabled = true
ORDER BY updated_at DESC
@@ -364,14 +362,14 @@ router.get('/dispensaries/:id/crawler-module', async (req: Request, res: Respons
}
const profileKey = rows[0].profile_key;
const platform = rows[0].platform || 'dutchie';
const crawlerType = rows[0].crawler_type || 'dutchie';
// Construct file path
const modulePath = path.join(
__dirname,
'..',
'crawlers',
platform,
crawlerType,
'stores',
`${profileKey}.ts`
);
@@ -381,7 +379,7 @@ router.get('/dispensaries/:id/crawler-module', async (req: Request, res: Respons
return res.status(404).json({
error: `Crawler module file not found: ${profileKey}.ts`,
hasModule: false,
expectedPath: `crawlers/${platform}/stores/${profileKey}.ts`,
expectedPath: `crawlers/${crawlerType}/stores/${profileKey}.ts`,
});
}
@@ -391,9 +389,9 @@ router.get('/dispensaries/:id/crawler-module', async (req: Request, res: Respons
res.json({
hasModule: true,
profileKey,
platform,
crawlerType,
fileName: `${profileKey}.ts`,
filePath: `crawlers/${platform}/stores/${profileKey}.ts`,
filePath: `crawlers/${crawlerType}/stores/${profileKey}.ts`,
content,
lines: content.split('\n').length,
});