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>
93 lines
2.8 KiB
TypeScript
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,
|
|
};
|
|
}
|