feat: align admin & agent dashboard summaries with Netify real totals and speeds
This commit is contained in:
1 parent
952c299b37
commit
cd31d1954c
7 files changed
+264
-11
No files matched your search
+26
-4
@@ -75,6 +75,17 @@ function initSchema() {
|
||||
// Column already exists, safe to ignore
|
||||
}
|
||||
|
||||
// Alter bandwidth_timeline table to add new speed columns dynamically if they do not exist
|
||||
try {
|
||||
d.exec("ALTER TABLE bandwidth_timeline ADD COLUMN download_speed INTEGER DEFAULT 0");
|
||||
} catch (_) {}
|
||||
try {
|
||||
d.exec("ALTER TABLE bandwidth_timeline ADD COLUMN upload_speed INTEGER DEFAULT 0");
|
||||
} catch (_) {}
|
||||
try {
|
||||
d.exec("ALTER TABLE bandwidth_timeline ADD COLUMN flow_speed INTEGER DEFAULT 0");
|
||||
} catch (_) {}
|
||||
|
||||
const adminExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'admin'").get();
|
||||
if (adminExists.count === 0) {
|
||||
const hash = bcrypt.hashSync('admin123', 10);
|
||||
@@ -554,7 +565,9 @@ function initSchema() {
|
||||
site_uuid TEXT,
|
||||
fetched_at TEXT NOT NULL,
|
||||
total_download INTEGER DEFAULT 0, total_upload INTEGER DEFAULT 0,
|
||||
total_flows INTEGER DEFAULT 0, active_devices INTEGER DEFAULT 0
|
||||
total_flows INTEGER DEFAULT 0, active_devices INTEGER DEFAULT 0,
|
||||
download_speed INTEGER DEFAULT 0, upload_speed INTEGER DEFAULT 0,
|
||||
flow_speed INTEGER DEFAULT 0
|
||||
)`);
|
||||
|
||||
d.exec(`CREATE TABLE IF NOT EXISTS geoip_cache (
|
||||
@@ -766,10 +779,19 @@ function insertBandwidthTimeline(summary, fetchedAt, siteUuid) {
|
||||
const d = getDB();
|
||||
d.prepare(`
|
||||
INSERT INTO bandwidth_timeline
|
||||
(site_uuid, fetched_at, total_download, total_upload, total_flows, active_devices)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
(site_uuid, fetched_at, total_download, total_upload, total_flows, active_devices, download_speed, upload_speed, flow_speed)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
siteUuid, fetchedAt, summary.download ?? 0, summary.upload ?? 0, summary.flows ?? 0, summary.devices ?? 0);
|
||||
siteUuid,
|
||||
fetchedAt,
|
||||
summary.download ?? 0,
|
||||
summary.upload ?? 0,
|
||||
summary.flows ?? 0,
|
||||
summary.devices ?? 0,
|
||||
summary.download_speed ?? 0,
|
||||
summary.upload_speed ?? 0,
|
||||
summary.flow_speed ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
// ─── QUERY FUNCTIONS ──────────────────────────────────────────────────────────
|
||||
|
||||
+33
-2
@@ -17,6 +17,16 @@ function headersSite(useApiKey = false) {
|
||||
}
|
||||
|
||||
function getMockDataForPath(path, params = {}) {
|
||||
if (path.includes('/data/stats/summary')) {
|
||||
return {
|
||||
download: 218648938113,
|
||||
upload: 33551656812,
|
||||
flow_hourly_count: 2970165,
|
||||
download_speed: 2530000,
|
||||
upload_speed: 390000,
|
||||
flow_speed: 123756
|
||||
};
|
||||
}
|
||||
if (path.includes('/data/stats/top/application/download')) {
|
||||
return [
|
||||
{ application: { id: 1, label: 'YouTube', tag: 'video', category: { label: 'Streaming' } }, download: Math.floor(Math.random() * 50000000) + 10000000, flows: Math.floor(Math.random() * 200) + 50 },
|
||||
@@ -575,7 +585,20 @@ async function fetchTopDomains(interval = 1440, limit = 50) {
|
||||
|
||||
// ─── BANDWIDTH SUMMARY untuk timeline ─────────────────────────────────────────
|
||||
async function fetchBandwidthSummary(interval = 30) {
|
||||
// Pakai interval PENDEK (30 menit) untuk timeline supaya ada variasi naik turun
|
||||
const rawSummary = await netifyFetch('/data/stats/summary', { filter_interval: interval });
|
||||
if (rawSummary) {
|
||||
return {
|
||||
download: rawSummary.download ?? 0,
|
||||
upload: rawSummary.upload ?? 0,
|
||||
flows: rawSummary.flow_hourly_count ?? 0,
|
||||
download_speed: rawSummary.download_speed ?? 0,
|
||||
upload_speed: rawSummary.upload_speed ?? 0,
|
||||
flow_speed: rawSummary.flow_speed ?? 0,
|
||||
devices: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to old way (aggregate top 100 application stats)
|
||||
const [dlData, ulData] = await Promise.all([
|
||||
netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }),
|
||||
netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: 100 }),
|
||||
@@ -585,7 +608,15 @@ async function fetchBandwidthSummary(interval = 30) {
|
||||
const upload = (ulData ?? []).reduce((s, r) => s + (r.upload ?? 0), 0);
|
||||
const flows = (dlData ?? []).reduce((s, r) => s + (r.flows ?? 0), 0);
|
||||
|
||||
return { download, upload, flows, devices: dlData?.length ?? 0 };
|
||||
return {
|
||||
download,
|
||||
upload,
|
||||
flows,
|
||||
download_speed: 0,
|
||||
upload_speed: 0,
|
||||
flow_speed: 0,
|
||||
devices: dlData?.length ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
// ─── THREATS — gunakan flows dengan filter anomali sebagai fallback ────────────
|
||||
|
||||
@@ -6,10 +6,35 @@ const { runPoll } = require('../scheduler');
|
||||
|
||||
// GET /api/dashboard/summary — kartu ringkasan (top of page)
|
||||
router.get('/summary', (req, res) => {
|
||||
const stats = db.getStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||
const timeline = db.getBandwidthTimeline(1, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||
const isAgent = req.user?.role === 'AGENT_VIEWER';
|
||||
const stats = db.getStats(req.user?.site_uuid, isAgent ? req.user?.agent_uuid : null);
|
||||
const timeline = db.getBandwidthTimeline(1, req.user?.site_uuid, isAgent ? req.user?.agent_uuid : null);
|
||||
const latest = timeline[0] ?? {};
|
||||
|
||||
let bandwidth_down = latest.total_download ?? 0;
|
||||
let bandwidth_up = latest.total_upload ?? 0;
|
||||
let active_flows = stats.activeFlows;
|
||||
let download_speed = latest.download_speed ?? 0;
|
||||
let upload_speed = latest.upload_speed ?? 0;
|
||||
let flow_speed = latest.flow_speed ?? 0;
|
||||
|
||||
if (isAgent) {
|
||||
const siteTimeline = db.getBandwidthTimeline(1, req.user?.site_uuid, null);
|
||||
const siteLatest = siteTimeline[0] ?? {};
|
||||
const siteStats = db.getStats(req.user?.site_uuid, null);
|
||||
|
||||
const download_ratio = siteLatest.total_download > 0 ? (bandwidth_down / siteLatest.total_download) : 0;
|
||||
const upload_ratio = siteLatest.total_upload > 0 ? (bandwidth_up / siteLatest.total_upload) : 0;
|
||||
const flow_ratio = siteStats.activeFlows > 0 ? (stats.activeFlows / siteStats.activeFlows) : download_ratio;
|
||||
|
||||
active_flows = Math.round((siteLatest.total_flows ?? 0) * flow_ratio);
|
||||
download_speed = Math.round((siteLatest.download_speed ?? 0) * download_ratio);
|
||||
upload_speed = Math.round((siteLatest.upload_speed ?? 0) * upload_ratio);
|
||||
flow_speed = Math.round((siteLatest.flow_speed ?? 0) * flow_ratio);
|
||||
} else {
|
||||
active_flows = latest.total_flows ?? stats.activeFlows;
|
||||
}
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
data: {
|
||||
@@ -17,9 +42,12 @@ router.get('/summary', (req, res) => {
|
||||
total_threats: stats.totalThreats,
|
||||
total_events: stats.totalEvents,
|
||||
last_fetch: stats.lastFetch,
|
||||
bandwidth_down: latest.total_download ?? 0,
|
||||
bandwidth_up: latest.total_upload ?? 0,
|
||||
active_flows: stats.activeFlows,
|
||||
bandwidth_down,
|
||||
bandwidth_up,
|
||||
active_flows,
|
||||
download_speed,
|
||||
upload_speed,
|
||||
flow_speed,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
|
||||
// Require the database module to mock it before loading the router
|
||||
const db = require('../database');
|
||||
|
||||
const originalGetStats = db.getStats;
|
||||
const originalGetTimeline = db.getBandwidthTimeline;
|
||||
|
||||
test.afterEach(() => {
|
||||
db.getStats = originalGetStats;
|
||||
db.getBandwidthTimeline = originalGetTimeline;
|
||||
});
|
||||
|
||||
test('GET /summary scales metrics proportionally for AGENT_VIEWER based on database traffic share', async () => {
|
||||
// Mock db.getStats
|
||||
db.getStats = (siteUuid, agentUuid) => {
|
||||
if (agentUuid === '8A-V3-PB-85') {
|
||||
return { totalDevices: 5, totalThreats: 1, totalEvents: 10, lastFetch: '2026-07-02T01:00:00Z', activeFlows: 50 };
|
||||
}
|
||||
// Site wide
|
||||
return { totalDevices: 20, totalThreats: 2, totalEvents: 50, lastFetch: '2026-07-02T01:00:00Z', activeFlows: 200 };
|
||||
};
|
||||
|
||||
// Mock db.getBandwidthTimeline
|
||||
db.getBandwidthTimeline = (points, siteUuid, agentUuid) => {
|
||||
if (agentUuid === '8A-V3-PB-85') {
|
||||
return [{ fetched_at: '2026-07-02T01:00:00Z', total_download: 50000000, total_upload: 10000000 }];
|
||||
}
|
||||
// Site wide
|
||||
return [{
|
||||
fetched_at: '2026-07-02T01:00:00Z',
|
||||
total_download: 200000000,
|
||||
total_upload: 40000000,
|
||||
total_flows: 100000,
|
||||
download_speed: 8000000,
|
||||
upload_speed: 2000000,
|
||||
flow_speed: 120000
|
||||
}];
|
||||
};
|
||||
|
||||
// Load the dashboard router
|
||||
const router = require('../routes/dashboard');
|
||||
const layer = router.stack.find(l => l.route && l.route.path === '/summary');
|
||||
assert.ok(layer, 'Dashboard router should have a /summary route');
|
||||
const handler = layer.route.stack[0].handle;
|
||||
|
||||
// Mock req and res for AGENT_VIEWER
|
||||
const req = {
|
||||
user: {
|
||||
role: 'AGENT_VIEWER',
|
||||
agent_uuid: '8A-V3-PB-85',
|
||||
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e'
|
||||
}
|
||||
};
|
||||
|
||||
let responseData = null;
|
||||
const res = {
|
||||
json(payload) {
|
||||
responseData = payload;
|
||||
}
|
||||
};
|
||||
|
||||
// Execute the route handler
|
||||
await handler(req, res);
|
||||
|
||||
// Assert proportional scaling is correct
|
||||
assert.ok(responseData);
|
||||
assert.strictEqual(responseData.ok, true);
|
||||
|
||||
const data = responseData.data;
|
||||
assert.ok(data);
|
||||
// Unaltered metrics
|
||||
assert.strictEqual(data.total_devices, 5);
|
||||
assert.strictEqual(data.total_threats, 1);
|
||||
assert.strictEqual(data.total_events, 10);
|
||||
assert.strictEqual(data.bandwidth_down, 50000000);
|
||||
assert.strictEqual(data.bandwidth_up, 10000000);
|
||||
|
||||
// Scaled metrics (agent has 25% share of downloads, uploads, and flows)
|
||||
assert.strictEqual(data.active_flows, 25000);
|
||||
assert.strictEqual(data.download_speed, 2000000);
|
||||
assert.strictEqual(data.upload_speed, 500000);
|
||||
assert.strictEqual(data.flow_speed, 30000);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const axios = require('axios');
|
||||
|
||||
// Save original axios get method
|
||||
const originalGet = axios.get;
|
||||
|
||||
function mockAxiosGet(mockFn) {
|
||||
axios.get = mockFn;
|
||||
}
|
||||
|
||||
function restoreAxiosGet() {
|
||||
axios.get = originalGet;
|
||||
}
|
||||
|
||||
test.afterEach(() => {
|
||||
restoreAxiosGet();
|
||||
});
|
||||
|
||||
test('fetchBandwidthSummary retrieves /data/stats/summary with correct filter_interval and outputs all metrics', async () => {
|
||||
const netify = require('../netify');
|
||||
|
||||
mockAxiosGet(async (url, config) => {
|
||||
if (url.endsWith('/data/stats/summary')) {
|
||||
// Verify query params
|
||||
assert.strictEqual(config.params?.filter_interval, 1440);
|
||||
|
||||
return {
|
||||
data: {
|
||||
status_code: 0,
|
||||
status_message: 'Success.',
|
||||
data: {
|
||||
download: 218648938113,
|
||||
upload: 33551656812,
|
||||
flow_hourly_count: 2970165,
|
||||
download_speed: 2530000,
|
||||
upload_speed: 390000,
|
||||
flow_speed: 123756
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected mock URL: ${url}`);
|
||||
});
|
||||
|
||||
const summary = await netify.fetchBandwidthSummary(1440);
|
||||
|
||||
assert.ok(summary);
|
||||
assert.strictEqual(summary.download, 218648938113);
|
||||
assert.strictEqual(summary.upload, 33551656812);
|
||||
assert.strictEqual(summary.flows, 2970165);
|
||||
assert.strictEqual(summary.download_speed, 2530000);
|
||||
assert.strictEqual(summary.upload_speed, 390000);
|
||||
assert.strictEqual(summary.flow_speed, 123756);
|
||||
});
|
||||
Reference in new issue
Block a user