fix: chart.js legend error + smart cache for date-range queries

- Fix forecast chart confidence band fill config (fill:'+1' → explicit target)
  to resolve 't.legend.handleEvent' TypeError in Chart.js 4.4.1
- Add getOrFetchRange() to cache.js: auto TTL based on data age
  (end date >10 days old → 24h cache, recent → 5min cache)
- Apply smart cache to 6 heavy endpoints: bi, bi/revenue, bi/strategic,
  providers, providers/failed, providers/trend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-16 22:48:53 -05:00
parent 844f931076
commit 6bfd21a111
3 changed files with 44 additions and 11 deletions

View File

@@ -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);

View File

@@ -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 } }

View File

@@ -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
};