feat: novo dashboard admin com KPIs, tendencias e ranking
- Adiciona src/admin-dashboard.js com lazy loading - KPIs: hoje vs media 30 dias por fluxo - Graficos de tendencia 30 dias (consolidado e por fluxo) - Ranking top 5 agentes com filtro de periodo - Adiciona sistema de cache (src/cache.js) - Cache com TTL e auto-refresh periodico (5-10min) - APIs: /admin/api/kpis, /admin/api/trend, /admin/api/top-agentes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
164
src/cache.js
Normal file
164
src/cache.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Cache System - Stale-While-Revalidate
|
||||
*
|
||||
* Mantém dados em memória com TTL e atualização periódica.
|
||||
* Retorna dados do cache imediatamente enquanto atualiza em background.
|
||||
*/
|
||||
|
||||
const cache = new Map();
|
||||
const refreshIntervals = new Map();
|
||||
|
||||
const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutos
|
||||
const REFRESH_INTERVAL = 5 * 60 * 1000; // Atualiza a cada 5 minutos
|
||||
|
||||
/**
|
||||
* Armazena valor no cache
|
||||
* @param {string} key - Chave do cache
|
||||
* @param {any} value - Valor a armazenar
|
||||
* @param {number} ttl - Time-to-live em ms (opcional)
|
||||
*/
|
||||
function set(key, value, ttl = DEFAULT_TTL) {
|
||||
cache.set(key, {
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
ttl
|
||||
});
|
||||
console.log(`[Cache] SET ${key} (TTL: ${ttl/1000}s)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera valor do cache
|
||||
* @param {string} key - Chave do cache
|
||||
* @returns {any|null} - Valor ou null se não encontrado/expirado
|
||||
*/
|
||||
function get(key) {
|
||||
const entry = cache.get(key);
|
||||
if (!entry) return null;
|
||||
|
||||
const age = Date.now() - entry.timestamp;
|
||||
const isStale = age > entry.ttl;
|
||||
|
||||
if (isStale) {
|
||||
console.log(`[Cache] STALE ${key} (age: ${Math.round(age/1000)}s)`);
|
||||
}
|
||||
|
||||
return {
|
||||
value: entry.value,
|
||||
isStale,
|
||||
age: Math.round(age / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se cache existe (mesmo que stale)
|
||||
*/
|
||||
function has(key) {
|
||||
return cache.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove do cache
|
||||
*/
|
||||
function del(key) {
|
||||
cache.delete(key);
|
||||
if (refreshIntervals.has(key)) {
|
||||
clearInterval(refreshIntervals.get(key));
|
||||
refreshIntervals.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa todo o cache
|
||||
*/
|
||||
function clear() {
|
||||
cache.clear();
|
||||
refreshIntervals.forEach(interval => clearInterval(interval));
|
||||
refreshIntervals.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra função para refresh periódico
|
||||
* @param {string} key - Chave do cache
|
||||
* @param {Function} fetchFn - Função async que busca os dados
|
||||
* @param {number} interval - Intervalo de refresh em ms
|
||||
*/
|
||||
function registerAutoRefresh(key, fetchFn, interval = REFRESH_INTERVAL) {
|
||||
// Limpa interval anterior se existir
|
||||
if (refreshIntervals.has(key)) {
|
||||
clearInterval(refreshIntervals.get(key));
|
||||
}
|
||||
|
||||
// Função de refresh
|
||||
const refresh = async () => {
|
||||
try {
|
||||
console.log(`[Cache] REFRESH ${key}`);
|
||||
const value = await fetchFn();
|
||||
set(key, value);
|
||||
} catch (err) {
|
||||
console.error(`[Cache] REFRESH ERROR ${key}:`, err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Faz refresh inicial
|
||||
refresh();
|
||||
|
||||
// Agenda refreshes periódicos
|
||||
const intervalId = setInterval(refresh, interval);
|
||||
refreshIntervals.set(key, intervalId);
|
||||
|
||||
console.log(`[Cache] AUTO-REFRESH registered for ${key} (every ${interval/1000}s)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: get-or-fetch com stale-while-revalidate
|
||||
* Retorna cache (mesmo stale) imediatamente e atualiza em background se stale
|
||||
*/
|
||||
async function getOrFetch(key, fetchFn, ttl = DEFAULT_TTL) {
|
||||
const cached = get(key);
|
||||
|
||||
if (cached) {
|
||||
// Se stale, atualiza em background
|
||||
if (cached.isStale) {
|
||||
fetchFn().then(value => set(key, value, ttl)).catch(err => {
|
||||
console.error(`[Cache] Background fetch error for ${key}:`, err.message);
|
||||
});
|
||||
}
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
// Não tem cache, busca e aguarda
|
||||
const value = await fetchFn();
|
||||
set(key, value, ttl);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats do cache
|
||||
*/
|
||||
function stats() {
|
||||
const entries = [];
|
||||
cache.forEach((entry, key) => {
|
||||
const age = Date.now() - entry.timestamp;
|
||||
entries.push({
|
||||
key,
|
||||
age: Math.round(age / 1000),
|
||||
ttl: entry.ttl / 1000,
|
||||
isStale: age > entry.ttl
|
||||
});
|
||||
});
|
||||
return {
|
||||
size: cache.size,
|
||||
entries
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
set,
|
||||
get,
|
||||
has,
|
||||
del,
|
||||
clear,
|
||||
registerAutoRefresh,
|
||||
getOrFetch,
|
||||
stats
|
||||
};
|
||||
Reference in New Issue
Block a user