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:
13
server.js
13
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);
|
||||
|
||||
@@ -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 } }
|
||||
|
||||
28
src/cache.js
28
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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user