diff --git a/docker-compose.yml b/docker-compose.yml
index 202929b..af80bd8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,6 +11,7 @@ services:
# Netbird VPN Setup Key
- NETBIRD_SETUP_KEY=${NETBIRD_SETUP_KEY:-14A782C8-24D2-46A9-B427-A422854E9B50}
- NETBIRD_MANAGEMENT_URL=${NETBIRD_MANAGEMENT_URL:-https://netbird.cambioreal.com}
+ - NETBIRD_HOSTNAME=${NETBIRD_HOSTNAME:-bi-ccc}
# MySQL RDS Connection (via Netbird)
- MYSQL_URL=${MYSQL_URL}
@@ -20,9 +21,12 @@ services:
# App Config
- SESSION_SECRET=${SESSION_SECRET:-bi-agentes-secret-key-change-me}
- PORT=3080
+ - AWESOME_API_TOKEN=${AWESOME_API_TOKEN:-2dbcf6a26f9bd9016859a2d31f99fbd8fc9ac4e9e8e440d94002ac3b436a747a}
volumes:
# Persist SQLite database
- ./data:/app/data
+ # Persist NetBird config (keeps same peer/IP between restarts)
+ - ./netbird:/var/lib/netbird
cap_add:
# Required for Netbird VPN
- NET_ADMIN
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
index 6f7a90b..e22e5a2 100644
--- a/docker-entrypoint.sh
+++ b/docker-entrypoint.sh
@@ -16,7 +16,9 @@ if [ -n "$NETBIRD_SETUP_KEY" ]; then
sleep 3
# Connect using setup key and management URL
- netbird up --setup-key "$NETBIRD_SETUP_KEY" --management-url "$MGMT_URL" &
+ # Use custom hostname for stable DNS name (default: bi-ccc)
+ NETBIRD_HOSTNAME="${NETBIRD_HOSTNAME:-bi-ccc}"
+ netbird up --setup-key "$NETBIRD_SETUP_KEY" --management-url "$MGMT_URL" --hostname "$NETBIRD_HOSTNAME" &
# Wait for connection
echo "Waiting for Netbird connection..."
@@ -24,6 +26,11 @@ if [ -n "$NETBIRD_SETUP_KEY" ]; then
# Check connection status
netbird status || echo "Netbird connection pending..."
+
+ # Add route for local network to bypass VPN tunnel
+ # This ensures responses to LAN clients go via Docker bridge, not VPN
+ echo "Adding local network route..."
+ ip route add 192.168.88.0/24 via 172.23.0.1 dev eth0 2>/dev/null || true
else
echo "WARNING: NETBIRD_SETUP_KEY not set. Database connection may fail."
fi
diff --git a/public/admin-login.html b/public/admin-login.html
new file mode 100644
index 0000000..f411e87
--- /dev/null
+++ b/public/admin-login.html
@@ -0,0 +1,192 @@
+
+
+
+
+
+BI Agentes - Admin Login
+
+
+
+
+
+
+
+
diff --git a/public/login.html b/public/login.html
index 6217a4b..0c063ff 100644
--- a/public/login.html
+++ b/public/login.html
@@ -136,6 +136,22 @@
font-size: 12px;
color: var(--text-secondary);
}
+
+ /* Mobile responsive */
+ @media (max-width: 480px) {
+ .login-container { padding: 16px; }
+ .login-card { padding: 28px 24px; border-radius: 12px; }
+ .login-header { margin-bottom: 24px; }
+ .login-header .logo { width: 140px; margin-bottom: 16px; }
+ .login-header .app-name { font-size: 16px; padding: 6px 16px; }
+ .login-header .subtitle { font-size: 10px; letter-spacing: 1.5px; }
+ .form-group { margin-bottom: 16px; }
+ .form-group label { font-size: 11px; }
+ .form-group input { padding: 11px 14px; font-size: 14px; border-radius: 8px; }
+ .btn-login { padding: 12px; font-size: 14px; border-radius: 8px; }
+ .error-msg { font-size: 12px; padding: 10px 12px; }
+ .footer { margin-top: 20px; font-size: 11px; }
+ }
diff --git a/server.js b/server.js
index d06fae1..4dee13b 100644
--- a/server.js
+++ b/server.js
@@ -11,7 +11,7 @@ const express = require('express');
const session = require('express-session');
const path = require('path');
const { authenticate, requireAuth, requireRole, createAgente, createUser } = require('./src/auth');
-const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes } = require('./src/queries');
+const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod } = require('./src/queries');
const { buildHTML } = require('./src/dashboard');
const { buildAdminHTML } = require('./src/admin-panel');
const { buildAdminHomeHTML } = require('./src/admin-home');
@@ -38,10 +38,17 @@ app.use('/public', express.static(path.join(__dirname, 'public')));
// --- Unified Login Routes ---
+// Helper function to get redirect URL based on role
+function getRedirectByRole(role) {
+ if (role === 'admin') return '/corporate'; // Admin vai direto pro Corporate Dashboard
+ if (role === 'corporate') return '/corporate';
+ return '/dashboard';
+}
+
// Root -> login page (or redirect if logged in)
app.get('/', (req, res) => {
if (req.session?.user) {
- return res.redirect(req.session.user.role === 'admin' ? '/admin' : '/dashboard');
+ return res.redirect(getRedirectByRole(req.session.user.role));
}
res.redirect('/login');
});
@@ -49,7 +56,7 @@ app.get('/', (req, res) => {
// Login page
app.get('/login', (req, res) => {
if (req.session?.user) {
- return res.redirect(req.session.user.role === 'admin' ? '/admin' : '/dashboard');
+ return res.redirect(getRedirectByRole(req.session.user.role));
}
res.sendFile(path.join(__dirname, 'public', 'login.html'));
});
@@ -72,11 +79,7 @@ app.post('/login', async (req, res) => {
};
// Redirect based on role
- if (user.role === 'admin') {
- res.redirect('/admin');
- } else {
- res.redirect('/dashboard');
- }
+ res.redirect(getRedirectByRole(user.role));
} catch (err) {
console.error('Login error:', err);
res.redirect(`/login?error=1&email=${emailParam}`);
@@ -114,22 +117,10 @@ app.get('/dashboard', requireRole('agente'), async (req, res) => {
}
});
-// --- Admin Routes ---
+// --- Admin Routes (User Management - admin only) ---
-// Admin home (admin only) - Fast daily overview
-app.get('/admin', requireRole('admin'), async (req, res) => {
- try {
- const stats = await fetchDailyStats();
- const html = buildAdminHomeHTML(stats, req.session.user);
- res.send(html);
- } catch (err) {
- console.error('Admin home error:', err);
- res.status(500).send('Erro ao carregar home admin: ' + err.message);
- }
-});
-
-// Admin agents management (admin only)
-app.get('/admin/agentes', requireRole('admin'), (req, res) => {
+// Admin home - User management panel (admin only)
+app.get('/admin', requireRole('admin'), (req, res) => {
try {
const agentes = db.prepare('SELECT * FROM agentes ORDER BY id DESC').all();
const html = buildAdminHTML(agentes, req.session.user);
@@ -140,33 +131,101 @@ app.get('/admin/agentes', requireRole('admin'), (req, res) => {
}
});
-// Admin Dashboard - KPIs, Tendências e Ranking (com lazy load)
-app.get('/admin/dashboard', requireRole('admin'), async (req, res) => {
+// Alias: /admin/usuarios -> /admin
+app.get('/admin/usuarios', requireRole('admin'), (req, res) => {
+ res.redirect('/admin');
+});
+
+// Legacy route - redirect to /admin
+app.get('/admin/agentes', requireRole('admin'), (req, res) => {
+ res.redirect('/admin');
+});
+
+// --- Corporate Routes (Dashboard + Emulation - corporate and admin) ---
+
+// Corporate Dashboard - Full KPIs, Trends and Ranking
+app.get('/corporate', requireRole('corporate', 'admin'), async (req, res) => {
try {
const user = req.session.user;
- const html = buildAdminDashboardHTML({ nome: user.nome, email: user.email });
+ const html = buildAdminDashboardHTML(user);
res.send(html);
} catch (err) {
- console.error('Admin dashboard error:', err);
- res.status(500).send('Erro ao carregar dashboard admin: ' + err.message);
+ console.error('Corporate dashboard error:', err);
+ res.status(500).send('Erro ao carregar dashboard corporate: ' + err.message);
}
});
-// API endpoint for admin dashboard data (admin only)
-app.get('/admin/api/data', requireRole('admin'), async (req, res) => {
+// Legacy route - redirect to /corporate
+app.get('/corporate/dashboard', requireRole('corporate', 'admin'), (req, res) => {
+ res.redirect('/corporate');
+});
+
+// Corporate emulate agent - view dashboard as specific agent
+app.get('/corporate/emular/:agente_id', requireRole('corporate', 'admin'), async (req, res) => {
+ try {
+ const agenteId = parseInt(req.params.agente_id);
+ const agente = db.prepare('SELECT * FROM agentes WHERE agente_id = ?').get(agenteId);
+
+ if (!agente) {
+ return res.status(404).send('Agente nao encontrado');
+ }
+
+ const { rowsBrlUsd, rowsUsdBrl } = await fetchTransacoes(agenteId);
+ const data = serialize(rowsBrlUsd, rowsUsdBrl);
+ const html = buildHTML(data, {
+ nome: agente.nome + ' (Emulando)',
+ agente_id: agenteId,
+ email: agente.email,
+ emulatorRole: req.session.user.role // Pass the emulator's role
+ }, true, null, false, true); // isEmulating = true
+ res.send(html);
+ } catch (err) {
+ console.error('Corporate emulate error:', err);
+ res.status(500).send('Erro ao emular agente: ' + err.message);
+ }
+});
+
+// Legacy route - redirect to /corporate/emular
+app.get('/admin/emular/:agente_id', requireRole('admin'), (req, res) => {
+ res.redirect(`/corporate/emular/${req.params.agente_id}`);
+});
+
+// --- Live Rate Proxy (caches for 3s to avoid rate limiting) ---
+let _rateCache = { data: null, ts: 0 };
+app.get('/api/cotacao', async (req, res) => {
+ try {
+ const now = Date.now();
+ if (_rateCache.data && now - _rateCache.ts < 3000) {
+ return res.json(_rateCache.data);
+ }
+ const token = process.env.AWESOME_API_TOKEN || '';
+ const url = 'https://economia.awesomeapi.com.br/json/last/USD-BRL,EUR-BRL' + (token ? '?token=' + token : '');
+ const resp = await fetch(url);
+ const json = await resp.json();
+ _rateCache = { data: json, ts: now };
+ res.json(json);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// --- Corporate API Routes (dashboard data - corporate and admin) ---
+
+// API endpoint for corporate dashboard data
+app.get('/corporate/api/data', requireRole('corporate', 'admin'), async (req, res) => {
try {
const dias = parseInt(req.query.dias) || 90;
const { rowsBrlUsd, rowsUsdBrl } = await fetchAllTransacoes(dias);
const data = serialize(rowsBrlUsd, rowsUsdBrl);
res.json({ success: true, data, count: data.length });
} catch (err) {
- console.error('Admin API error:', err);
+ console.error('Corporate API error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// API: KPIs (hoje vs média 30 dias) - com cache
-app.get('/admin/api/kpis', requireRole('admin'), async (req, res) => {
+app.get('/corporate/api/kpis', requireRole('corporate', 'admin'), async (req, res) => {
try {
const data = await cache.getOrFetch('kpis', fetchKPIs, 5 * 60 * 1000);
res.json({ success: true, data });
@@ -177,7 +236,7 @@ app.get('/admin/api/kpis', requireRole('admin'), async (req, res) => {
});
// API: Tendência 30 dias - com cache
-app.get('/admin/api/trend', requireRole('admin'), async (req, res) => {
+app.get('/corporate/api/trend', requireRole('corporate', 'admin'), async (req, res) => {
try {
const data = await cache.getOrFetch('trend30', fetchTrend30Days, 10 * 60 * 1000);
res.json({ success: true, data });
@@ -188,7 +247,7 @@ app.get('/admin/api/trend', requireRole('admin'), async (req, res) => {
});
// API: Top 5 agentes - com cache por período
-app.get('/admin/api/top-agentes', requireRole('admin'), async (req, res) => {
+app.get('/corporate/api/top-agentes', requireRole('corporate', 'admin'), async (req, res) => {
try {
const dias = parseInt(req.query.dias) || 30;
const cacheKey = `top-agentes-${dias}`;
@@ -212,30 +271,59 @@ app.get('/admin/api/top-agentes', requireRole('admin'), async (req, res) => {
}
});
-// Admin emulate agent - view dashboard as specific agent (admin only)
-app.get('/admin/emular/:agente_id', requireRole('admin'), async (req, res) => {
+// API: Corporate Dashboard - KPIs por período
+app.get('/corporate/api/kpis-period', requireRole('corporate', 'admin'), async (req, res) => {
try {
- const agenteId = parseInt(req.params.agente_id);
- const agente = db.prepare('SELECT * FROM agentes WHERE agente_id = ?').get(agenteId);
-
- if (!agente) {
- return res.status(404).send('Agente nao encontrado');
+ const { inicio, fim } = req.query;
+ if (!inicio || !fim) {
+ return res.status(400).json({ success: false, error: 'Parametros inicio e fim sao obrigatorios' });
}
-
- const { rowsBrlUsd, rowsUsdBrl } = await fetchTransacoes(agenteId);
- const data = serialize(rowsBrlUsd, rowsUsdBrl);
- const html = buildHTML(data, {
- nome: agente.nome + ' (Emulando)',
- agente_id: agenteId,
- email: agente.email
- }, true, null, false, true); // isEmulating = true
- res.send(html);
+ const data = await fetchKPIsByPeriod(inicio, fim);
+ res.json({ success: true, data });
} catch (err) {
- console.error('Admin emulate error:', err);
- res.status(500).send('Erro ao emular agente: ' + err.message);
+ console.error('Corporate KPIs API error:', err);
+ res.status(500).json({ success: false, error: err.message });
}
});
+// API: Corporate Dashboard - Tendência por período
+app.get('/corporate/api/trend-period', requireRole('corporate', 'admin'), async (req, res) => {
+ try {
+ const { inicio, fim } = req.query;
+ if (!inicio || !fim) {
+ return res.status(400).json({ success: false, error: 'Parametros inicio e fim sao obrigatorios' });
+ }
+ const data = await fetchTrendByPeriod(inicio, fim);
+ res.json({ success: true, data });
+ } catch (err) {
+ console.error('Corporate Trend API error:', err);
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+// Legacy API routes - redirect to /corporate/api/*
+app.get('/admin/api/data', requireRole('admin'), (req, res) => {
+ res.redirect(`/corporate/api/data?${new URLSearchParams(req.query)}`);
+});
+app.get('/admin/api/kpis', requireRole('admin'), (req, res) => {
+ res.redirect('/corporate/api/kpis');
+});
+app.get('/admin/api/trend', requireRole('admin'), (req, res) => {
+ res.redirect('/corporate/api/trend');
+});
+app.get('/admin/api/top-agentes', requireRole('admin'), (req, res) => {
+ res.redirect(`/corporate/api/top-agentes?${new URLSearchParams(req.query)}`);
+});
+app.get('/admin/api/corporate/kpis', requireRole('admin'), (req, res) => {
+ res.redirect(`/corporate/api/kpis-period?${new URLSearchParams(req.query)}`);
+});
+app.get('/admin/api/corporate/trend', requireRole('admin'), (req, res) => {
+ res.redirect(`/corporate/api/trend-period?${new URLSearchParams(req.query)}`);
+});
+app.get('/admin/dashboard', requireRole('admin'), (req, res) => {
+ res.redirect('/corporate/dashboard');
+});
+
// Create user (admin only)
app.post('/admin/agentes', requireRole('admin'), async (req, res) => {
const { nome, email, agente_id, senha, role } = req.body;
@@ -245,7 +333,8 @@ app.post('/admin/agentes', requireRole('admin'), async (req, res) => {
}
const userRole = role || 'agente';
- const agenteId = userRole === 'admin' ? 0 : (agente_id || 0);
+ // Admin and Corporate don't need agente_id
+ const agenteId = (userRole === 'admin' || userRole === 'corporate') ? 0 : (agente_id || 0);
if (userRole === 'agente' && !agente_id) {
return res.status(400).json({ error: 'Agente ID e obrigatorio para agentes' });
diff --git a/src/admin-dashboard.js b/src/admin-dashboard.js
index 312fb10..48cf746 100644
--- a/src/admin-dashboard.js
+++ b/src/admin-dashboard.js
@@ -1,13 +1,110 @@
/**
- * Admin Dashboard - KPIs, Tendências e Ranking
- * Lazy loading para performance
+ * Admin Dashboard Corporate - KPIs, Tendências e Detalhes
+ * Filtros por período: Este Mês, Mês Anterior, Últimos 2 Meses, ou período customizado
*/
const { buildHeader, buildFooter, buildHead } = require('./ui-template');
-function buildAdminDashboardHTML(admin) {
- const pageScripts = ``;
+function buildAdminDashboardHTML(user) {
+ // Support both admin and corporate roles
+ const role = user.role || 'corporate';
+ const pageScripts = '
+function loadAllData() { updatePeriodInfo(); loadKPIs(); loadTrend(); loadRanking(); }
+document.addEventListener('DOMContentLoaded', loadAllData);
+<\/script>