diff --git a/server.js b/server.js index 4bec7c4..c30e287 100644 --- a/server.js +++ b/server.js @@ -357,7 +357,7 @@ app.get('/admin/api/bi', requireRole('admin'), async (req, res) => { return row ? row.nome : null; }; - const data = await fetchBIData(start, end, getAgenteName); + const data = await cache.getOrFetchRange('bi', start, end, () => fetchBIData(start, end, getAgenteName)); res.json(data); } catch (err) { console.error('Admin BI API error:', err); @@ -369,7 +369,8 @@ app.get('/admin/api/bi/revenue', requireRole('admin'), async (req, res) => { try { const { start, end, granularity } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); - const data = await fetchRevenueAnalytics(start, end, granularity || 'dia'); + const g = granularity || 'dia'; + const data = await cache.getOrFetchRange(`bi-rev-${g}`, start, end, () => fetchRevenueAnalytics(start, end, g)); res.json(data); } catch (err) { console.error('Revenue API error:', err); @@ -381,7 +382,7 @@ app.get('/admin/api/bi/strategic', requireRole('admin'), async (req, res) => { try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); - const data = await fetchBIStrategic(start, end); + const data = await cache.getOrFetchRange('bi-strat', start, end, () => fetchBIStrategic(start, end)); res.json(data); } catch (err) { console.error('Strategic BI API error:', err); @@ -917,7 +918,7 @@ app.get('/admin/api/providers', requireRole('admin'), async (req, res) => { try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); - const data = await fetchProviderPerformance(start, end); + const data = await cache.getOrFetchRange('providers', start, end, () => fetchProviderPerformance(start, end)); res.json(data); } catch (err) { console.error('Provider API error:', err); @@ -929,7 +930,7 @@ app.get('/admin/api/providers/failed', requireRole('admin'), async (req, res) => try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); - const data = await fetchFailedTransactions(start, end); + const data = await cache.getOrFetchRange('providers-fail', start, end, () => fetchFailedTransactions(start, end)); res.json(data); } catch (err) { console.error('Failed TX API error:', err); @@ -941,7 +942,7 @@ app.get('/admin/api/providers/trend', requireRole('admin'), async (req, res) => try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); - const data = await fetchProviderTrend(start, end); + const data = await cache.getOrFetchRange('providers-trend', start, end, () => fetchProviderTrend(start, end)); res.json(data); } catch (err) { console.error('Provider Trend API error:', err); diff --git a/src/admin-bi.js b/src/admin-bi.js index 9127576..a559915 100644 --- a/src/admin-bi.js +++ b/src/admin-bi.js @@ -2142,15 +2142,19 @@ async function loadForecast() { data: { labels: allLabels, datasets: [ - { label: 'Historical', data: histFull, borderColor: theme.blue, backgroundColor: 'transparent', borderWidth: 2, pointRadius: 0, tension: 0.3 }, - { label: 'Forecast', data: predValues, borderColor: theme.green, backgroundColor: 'transparent', borderWidth: 2, borderDash: [6,3], pointRadius: 0, tension: 0.3 }, - { label: 'Upper 95%', data: upperValues, borderColor: 'transparent', backgroundColor: theme.green + '15', fill: '+1', pointRadius: 0 }, - { label: 'Lower 95%', data: lowerValues, borderColor: 'transparent', backgroundColor: 'transparent', fill: false, pointRadius: 0 } + { label: 'Confidence 95%', data: upperValues, borderColor: 'transparent', backgroundColor: theme.green + '15', fill: { target: 3, above: theme.green + '15' }, pointRadius: 0, tension: 0.3 }, + { label: 'Historical', data: histFull, borderColor: theme.blue, backgroundColor: 'transparent', borderWidth: 2, pointRadius: 0, tension: 0.3, fill: false }, + { label: 'Forecast', data: predValues, borderColor: theme.green, backgroundColor: 'transparent', borderWidth: 2, borderDash: [6,3], pointRadius: 0, tension: 0.3, fill: false }, + { data: lowerValues, borderColor: 'transparent', backgroundColor: 'transparent', pointRadius: 0, tension: 0.3, fill: false } ] }, options: { responsive: true, maintainAspectRatio: false, - plugins: { legend: { position: 'top', labels: { color: theme.text, usePointStyle: true, pointStyle: 'line', font: { size: 11 } } } }, + plugins: { + legend: { position: 'top', labels: { color: theme.text, usePointStyle: true, pointStyle: 'line', font: { size: 11 }, filter: function(item) { return item.text !== undefined; } } }, + tooltip: { filter: function(item) { return item.dataset.label !== undefined; } }, + filler: { propagate: false } + }, scales: { x: { ticks: { color: theme.text, maxTicksLimit: 15, font: { size: 10 } }, grid: { color: theme.grid } }, y: { ticks: { color: theme.text, callback: function(v){return '$' + (v>=1000 ? Math.round(v/1000)+'K' : v);} }, grid: { color: theme.grid } } diff --git a/src/cache.js b/src/cache.js index a41f4b4..d2fbdcf 100644 --- a/src/cache.js +++ b/src/cache.js @@ -132,6 +132,32 @@ async function getOrFetch(key, fetchFn, ttl = DEFAULT_TTL) { return value; } +/** + * Smart TTL para queries com date range: + * - end date > 10 dias atrás → dados imutáveis → TTL 24h + * - end date recente → dados mudam → TTL curto (5 min) + */ +const IMMUTABLE_TTL = 24 * 60 * 60 * 1000; // 24h +const FRESH_TTL = 5 * 60 * 1000; // 5 min +const STALE_DAYS = 10; + +function getDateRangeTTL(endDate) { + const end = new Date(endDate + 'T23:59:59'); + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - STALE_DAYS); + return end < cutoff ? IMMUTABLE_TTL : FRESH_TTL; +} + +/** + * Cache-aware fetch para queries com date range. + * Key = prefix + start + end. TTL automático baseado na idade dos dados. + */ +async function getOrFetchRange(prefix, start, end, fetchFn) { + const key = `${prefix}:${start}:${end}`; + const ttl = getDateRangeTTL(end); + return getOrFetch(key, fetchFn, ttl); +} + /** * Stats do cache */ @@ -160,5 +186,7 @@ module.exports = { clear, registerAutoRefresh, getOrFetch, + getOrFetchRange, + getDateRangeTTL, stats };