import { fallbackStageProgress, stageLabel, taskTypeLabel } from '@/lib/task-workflow'; import type { Task, TaskNotificationAction, TaskNotificationStat, TaskNotificationView } from '@/lib/types'; type TaskCore = Omit; function asRecord(value: unknown) { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; } function asString(value: unknown) { return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null; } function asNumber(value: unknown) { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string' && value.trim().length > 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } function formatInteger(value: number) { return new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value); } function buildProgress(task: TaskCore) { const currentProgress = task.stage_context?.progress ?? fallbackStageProgress(task); if (!currentProgress || currentProgress.total <= 0) { return null; } const current = Math.min(Math.max(Math.trunc(currentProgress.current), 0), Math.trunc(currentProgress.total)); const total = Math.max(Math.trunc(currentProgress.total), 1); const percent = total > 0 ? Math.min(100, Math.max(0, Math.round((current / total) * 100))) : null; return { current, total, unit: currentProgress.unit, percent }; } function makeStat(label: string, value: number | string | null | undefined): TaskNotificationStat | null { if (value === null || value === undefined) { return null; } if (typeof value === 'number') { return { label, value: formatInteger(value) }; } const normalized = value.trim(); return normalized ? { label, value: normalized } : null; } function buildStats(task: TaskCore): TaskNotificationStat[] { const result = asRecord(task.result); const counters = task.stage_context?.counters ?? {}; const stats: Array = []; switch (task.task_type) { case 'sync_filings': stats.push( makeStat('Fetched', asNumber(result?.fetched) ?? counters.fetched ?? task.stage_context?.progress?.total ?? null), makeStat('Inserted', asNumber(result?.inserted) ?? counters.inserted ?? null), makeStat('Updated', asNumber(result?.updated) ?? counters.updated ?? null), makeStat('Hydrated', asNumber(result?.taxonomySnapshotsHydrated) ?? counters.hydrated ?? null), makeStat('Failed', asNumber(result?.taxonomySnapshotsFailed) ?? counters.failed ?? null) ); break; case 'refresh_prices': stats.push( makeStat('Tickers', asNumber(result?.totalTickers) ?? task.stage_context?.progress?.total ?? null), makeStat('Updated', asNumber(result?.updatedCount) ?? counters.updatedCount ?? null), makeStat('Holdings', counters.holdings ?? null) ); break; case 'analyze_filing': stats.push( makeStat('Ticker', asString(result?.ticker) ?? task.stage_context?.subject?.ticker ?? null), makeStat('Form', asString(result?.filingType) ?? null), makeStat('Model', asString(result?.model) ?? null) ); break; case 'index_search': stats.push( makeStat('Sources', asNumber(result?.sourcesCollected) ?? counters.sourcesCollected ?? task.stage_context?.progress?.total ?? null), makeStat('Indexed', asNumber(result?.indexed) ?? counters.indexed ?? null), makeStat('Chunks', asNumber(result?.chunksEmbedded) ?? counters.chunksEmbedded ?? null), makeStat('Skipped', asNumber(result?.skipped) ?? counters.skipped ?? null), makeStat('Deleted', asNumber(result?.deleted) ?? counters.deleted ?? null) ); break; case 'portfolio_insights': { const summary = asRecord(result?.summary); stats.push( makeStat('Positions', asNumber(summary?.positions) ?? counters.holdings ?? null), makeStat('Provider', asString(result?.provider) ?? null), makeStat('Model', asString(result?.model) ?? null) ); break; } } if (stats.every((stat) => stat === null)) { const fallbackStats: Array = []; for (const [label, value] of Object.entries(counters)) { fallbackStats.push(makeStat(label, value)); } return fallbackStats.filter((stat): stat is TaskNotificationStat => Boolean(stat)); } return stats.filter((stat): stat is TaskNotificationStat => Boolean(stat)); } function buildTaskHref(task: TaskCore) { const result = asRecord(task.result); const payload = asRecord(task.payload); const ticker = asString(result?.ticker) ?? task.stage_context?.subject?.ticker ?? asString(payload?.ticker); const accessionNumber = asString(result?.accessionNumber) ?? task.stage_context?.subject?.accessionNumber ?? asString(payload?.accessionNumber); return { ticker, accessionNumber }; } function buildActions(task: TaskCore): TaskNotificationAction[] { const { ticker, accessionNumber } = buildTaskHref(task); const actions: TaskNotificationAction[] = []; switch (task.task_type) { case 'sync_filings': actions.push({ id: 'open_filings', label: 'Open filings', href: ticker ? `/filings?ticker=${encodeURIComponent(ticker)}` : '/filings', primary: true }); break; case 'analyze_filing': if (ticker && accessionNumber) { actions.push({ id: 'open_analysis_report', label: 'Open summary', href: `/analysis/reports/${encodeURIComponent(ticker)}/${encodeURIComponent(accessionNumber)}`, primary: true }); } actions.push({ id: 'open_filings', label: 'Open filings', href: ticker ? `/filings?ticker=${encodeURIComponent(ticker)}` : '/filings', primary: actions.length === 0 }); break; case 'refresh_prices': case 'portfolio_insights': actions.push({ id: 'open_portfolio', label: 'Open portfolio', href: '/portfolio', primary: true }); break; case 'index_search': actions.push({ id: 'open_search', label: 'Open search', href: ticker ? `/search?ticker=${encodeURIComponent(ticker)}` : '/search', primary: true }); break; } actions.push({ id: 'open_details', label: 'Open details', href: null }); return actions; } function buildStatusLine(task: TaskCore, progress: TaskNotificationView['progress']) { switch (task.status) { case 'queued': return 'Queued for execution'; case 'running': return progress?.percent !== null && progress?.percent !== undefined ? `Running ${stageLabel(task.stage).toLowerCase()} ยท ${progress.percent}%` : `Running ${stageLabel(task.stage).toLowerCase()}`; case 'completed': return 'Finished successfully'; case 'failed': return task.stage !== 'failed' ? `Failed during ${stageLabel(task.stage).toLowerCase()}` : 'Failed'; } } export function buildTaskNotification(task: TaskCore): TaskNotificationView { const progress = buildProgress(task); const detailLine = task.stage_detail ?? task.error; return { title: taskTypeLabel(task.task_type), statusLine: buildStatusLine(task, progress), detailLine, tone: task.status === 'failed' ? 'error' : task.status === 'completed' ? 'success' : 'info', progress, stats: buildStats(task), actions: buildActions(task) }; }