Files
cannaiq/backend/src/tasks/handlers/analytics-refresh.ts
Kelly 89c262ee20 feat(tasks): Add unified task-based worker architecture
Replace fragmented job systems (job_schedules, dispensary_crawl_jobs, SyncOrchestrator)
with a single unified task queue:

- Add worker_tasks table with atomic task claiming via SELECT FOR UPDATE SKIP LOCKED
- Add TaskService for CRUD, claiming, and capacity metrics
- Add TaskWorker with role-based handlers (resync, discovery, analytics)
- Add /api/tasks endpoints for management and migration from legacy systems
- Add TasksDashboard UI and integrate task counts into main dashboard
- Add comprehensive documentation

Task roles: store_discovery, entry_point_discovery, product_discovery, product_resync, analytics_refresh

Run workers with: WORKER_ROLE=product_resync npx tsx src/tasks/task-worker.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 16:27:03 -07:00

93 lines
2.8 KiB
TypeScript

/**
* Analytics Refresh Handler
*
* Refreshes materialized views and pre-computed analytics tables.
* Should run daily or on-demand after major data changes.
*/
import { TaskContext, TaskResult } from '../task-worker';
export async function handleAnalyticsRefresh(ctx: TaskContext): Promise<TaskResult> {
const { pool } = ctx;
console.log(`[AnalyticsRefresh] Starting analytics refresh...`);
const refreshed: string[] = [];
const failed: string[] = [];
// List of materialized views to refresh
const materializedViews = [
'mv_state_metrics',
'mv_brand_metrics',
'mv_category_metrics',
'v_brand_summary',
'v_dashboard_stats',
];
for (const viewName of materializedViews) {
try {
// Heartbeat before each refresh
await ctx.heartbeat();
// Check if view exists
const existsResult = await pool.query(`
SELECT EXISTS (
SELECT 1 FROM pg_matviews WHERE matviewname = $1
UNION
SELECT 1 FROM pg_views WHERE viewname = $1
) as exists
`, [viewName]);
if (!existsResult.rows[0].exists) {
console.log(`[AnalyticsRefresh] View ${viewName} does not exist, skipping`);
continue;
}
// Try to refresh (only works for materialized views)
try {
await pool.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY ${viewName}`);
refreshed.push(viewName);
console.log(`[AnalyticsRefresh] Refreshed ${viewName}`);
} catch (refreshError: any) {
// Try non-concurrent refresh
try {
await pool.query(`REFRESH MATERIALIZED VIEW ${viewName}`);
refreshed.push(viewName);
console.log(`[AnalyticsRefresh] Refreshed ${viewName} (non-concurrent)`);
} catch (nonConcurrentError: any) {
// Not a materialized view or other error
console.log(`[AnalyticsRefresh] ${viewName} is not a materialized view or refresh failed`);
}
}
} catch (error: any) {
console.error(`[AnalyticsRefresh] Error refreshing ${viewName}:`, error.message);
failed.push(viewName);
}
}
// Run analytics capture functions if they exist
const captureFunctions = [
'capture_brand_snapshots',
'capture_category_snapshots',
];
for (const funcName of captureFunctions) {
try {
await pool.query(`SELECT ${funcName}()`);
console.log(`[AnalyticsRefresh] Executed ${funcName}()`);
} catch (error: any) {
// Function might not exist
console.log(`[AnalyticsRefresh] ${funcName}() not available`);
}
}
console.log(`[AnalyticsRefresh] Complete: ${refreshed.length} refreshed, ${failed.length} failed`);
return {
success: failed.length === 0,
refreshed,
failed,
error: failed.length > 0 ? `Failed to refresh: ${failed.join(', ')}` : undefined,
};
}