feat: complete deployment codebase for BackOne DPI dashboard with dynamic telemetry, rebranding, database size tracking, and modular design

This commit is contained in:
Rafif-Riqullah-Siregar committed 2026-07-10 11:52:58 +07:00
1 parent 6cf3c6c7e5
commit c316f3171b
226 files changed
+21152 -8397

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
// test/architecture_test.js
// TDD: Verifikasi arsitektur 2 container BackOne DPI
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
function readFile(relPath) {
return fs.readFileSync(path.join(ROOT, relPath), 'utf8');
}
let passed = 0;
let failed = 0;
function test(name, condition, detail = '') {
if (condition) {
console.log(` ✓ PASS: ${name}`);
passed++;
} else {
console.log(` ✗ FAIL: ${name}${detail ? ' — ' + detail : ''}`);
failed++;
}
}
console.log('\n══════════════════════════════════════════════════════');
console.log(' BackOne DPI — TDD Architecture Verification');
console.log('══════════════════════════════════════════════════════\n');
// ─── TEST GROUP 1: Backend bersih dari SQLite/Netify API ───────────────────
console.log('[GROUP 1] Backend — Isolasi dari SQLite & Netify API');
const serverCode = readFile('backend/server.js');
const dashCode = readFile('backend/routes/dashboard.js') +
readFile('backend/routes/dashboard/helpers.js') +
readFile('backend/routes/dashboard/summary.js') +
readFile('backend/routes/dashboard/apps.js') +
readFile('backend/routes/dashboard/devices.js') +
readFile('backend/routes/dashboard/flows.js') +
readFile('backend/routes/dashboard/threats.js') +
readFile('backend/routes/dashboard/geo.js') +
readFile('backend/routes/dashboard/tls.js') +
readFile('backend/routes/dashboard/telemetry.js') +
readFile('backend/routes/dashboard/events.js');
const authCode = readFile('backend/routes/auth.js');
test('server.js tidak memanggil ingestionService', !serverCode.includes('ingestionService'));
test('server.js tidak import database.js (SQLite)', !serverCode.includes("require('./database')") && !serverCode.includes("require('../database')"));
test('server.js tidak memanggil startScheduler', !serverCode.includes('startScheduler'));
test('dashboard.js tidak import database.js', !dashCode.includes("require('../database')"));
test('dashboard.js tidak import scheduler.js lama', !dashCode.includes("require('../scheduler')"));
test('dashboard.js tidak memanggil netify.ai API', !dashCode.includes('netify.ai') && !dashCode.includes('fetchTopApps'));
test('auth.js tidak import database.js', !authCode.includes("require('../database')"));
test('auth.js tidak menggunakan better-sqlite3', !authCode.includes('better-sqlite3') && !authCode.includes('getDB()'));
console.log('');
// ─── TEST GROUP 2: Multi-tenant isolation ──────────────────────────────────
console.log('[GROUP 2] Dashboard — Multi-Tenant Agent UUID Isolation');
test('dashboard.js punya getBaseFilter() helper', dashCode.includes('function getBaseFilter'));
test('getBaseFilter menambahkan agent_uuid filter', dashCode.includes('filter.agent_uuid = req.user.agent_uuid'));
test('getBaseFilter menambahkan site_uuid filter', dashCode.includes('filter.site_uuid = req.user.site_uuid') || dashCode.includes('filter.site_uuid ='));
test('/apps pakai getBaseFilter', dashCode.includes("router.get('/apps'") && dashCode.includes('getBaseFilter'));
test('/devices pakai getBaseFilter', dashCode.includes("router.get('/devices'") && dashCode.includes('getBaseFilter'));
test('/flows pakai getBaseFilter', dashCode.includes("router.get('/flows'") && dashCode.includes('getBaseFilter'));
test('/threats pakai getBaseFilter', dashCode.includes("router.get('/threats'") && dashCode.includes('getBaseFilter'));
test('/summary pakai getBaseFilter', dashCode.includes("router.get('/summary'") && dashCode.includes('getBaseFilter'));
test('/agents endpoint hanya untuk SUPER_ADMIN', dashCode.includes("SUPER_ADMIN") && dashCode.includes("router.get('/agents'"));
console.log('');
// ─── TEST GROUP 3: Proxy 2 mode ────────────────────────────────────────────
console.log('[GROUP 3] Proxy — 2 Mode Pengambilan Data');
const schedulerCode = readFile('proxy/scheduler.js');
const collectorCode = readFile('proxy/collector.js');
test('scheduler.js membaca PROXY_COLLECT_MODE env', schedulerCode.includes('PROXY_COLLECT_MODE'));
test('scheduler.js membaca PROXY_AGENT_UUID env', schedulerCode.includes('PROXY_AGENT_UUID'));
test("scheduler.js mendukung mode 'all'", schedulerCode.includes("COLLECT_MODE === 'agent'"));
test('collector.js export collectAllAgents()', collectorCode.includes('collectAllAgents'));
test('collector.js export collectSpecificAgent()', collectorCode.includes('collectSpecificAgent'));
test('collector.js tag data dengan agent_uuid', collectorCode.includes('agent_uuid: agentUuid'));
test('collector.js tag data dengan site_uuid', collectorCode.includes('site_uuid: SITE_UUID') || collectorCode.includes('site_uuid: SITE_UUID'));
console.log('');
// ─── TEST GROUP 4: Docker Compose 2 Container Groups ─────────────────────
console.log('[GROUP 4] Docker Compose — 2 Container Groups + 2 Networks');
const composeCode = readFile('docker-compose.yml');
test('docker-compose.yml punya service: mongodb', composeCode.includes('mongodb:'));
test('docker-compose.yml punya service: proxy', composeCode.includes('proxy:'));
test('docker-compose.yml punya service: backend', composeCode.includes('backend:'));
test('docker-compose.yml punya service: frontend', composeCode.includes('frontend:'));
test('docker-compose.yml punya network: backone-infra', composeCode.includes('backone-infra'));
test('docker-compose.yml punya network: backone-app', composeCode.includes('backone-app'));
test('proxy connect ke backone-infra (Container 1)', composeCode.includes('backone-infra'));
test('backend connect ke backone-app (Container 2)', composeCode.includes('backone-app'));
test('proxy depends on mongodb', composeCode.includes('service_healthy'));
test('env PROXY_COLLECT_MODE di docker-compose', composeCode.includes('PROXY_COLLECT_MODE'));
test('env PROXY_AGENT_UUID di docker-compose', composeCode.includes('PROXY_AGENT_UUID'));
test('PROXY_URL dikirim ke backend', composeCode.includes('PROXY_URL=http://proxy:4000'));
console.log('');
// ─── TEST GROUP 5: MongoDB Schemas sinkron ────────────────────────────────
console.log('[GROUP 5] MongoDB Schemas — Proxy & Backend Sinkron');
const proxySchema = readFile('proxy/models/Schemas.js');
const backendSchema = readFile('backend/models/Schemas.js');
const sharedFields = ['agent_uuid', 'site_uuid', 'timestamp', 'bandwidth_down', 'bandwidth_up', 'ip_address', 'mac_address', 'flow_id', 'src_ip', 'dst_ip', 'threat_type', 'severity'];
for (const field of sharedFields) {
test(`Field '${field}' ada di kedua schema`, proxySchema.includes(field) && backendSchema.includes(field));
}
console.log('');
// ─── SUMMARY ──────────────────────────────────────────────────────────────
console.log('══════════════════════════════════════════════════════');
console.log(` Results: ${passed} PASSED | ${failed} FAILED`);
console.log('══════════════════════════════════════════════════════\n');
if (failed > 0) process.exit(1);
+103
View File
@@ -0,0 +1,103 @@
// test/field_alignment.js
// Check field name alignment between backend and frontend
const fs = require('fs');
const dash = fs.readFileSync('backend/routes/dashboard.js','utf8') +
fs.readFileSync('backend/routes/dashboard/helpers.js','utf8') +
fs.readFileSync('backend/routes/dashboard/summary.js','utf8') +
fs.readFileSync('backend/routes/dashboard/apps.js','utf8') +
fs.readFileSync('backend/routes/dashboard/devices.js','utf8') +
fs.readFileSync('backend/routes/dashboard/flows.js','utf8') +
fs.readFileSync('backend/routes/dashboard/threats.js','utf8') +
fs.readFileSync('backend/routes/dashboard/geo.js','utf8') +
fs.readFileSync('backend/routes/dashboard/tls.js','utf8') +
fs.readFileSync('backend/routes/dashboard/telemetry.js','utf8') +
fs.readFileSync('backend/routes/dashboard/events.js','utf8');
const api = fs.readFileSync('src/lib/api.ts','utf8');
console.log('=== Field Name Alignment: Backend ↔ Frontend ===\n');
function check(label, backendHas, frontendExpects, mismatchDesc = '') {
if (backendHas && frontendExpects) {
console.log(` ✓ PASS: ${label} is aligned`);
} else if (backendHas && !frontendExpects) {
console.log(` ⚠ WARNING: Backend defines ${label} but Frontend doesn't expect it`);
} else if (!backendHas && frontendExpects) {
console.log(` ✗ FAIL: ${label} is missing in backend routing!`);
if (mismatchDesc) console.log(` → ${mismatchDesc}`);
}
}
// /protocols
console.log('[/protocols]');
check(
'protocol_label',
dash.includes('protocol_label'),
api.includes('protocol_label'),
'Backend doesn\'t return protocol_label to frontend'
);
console.log('');
// /timeline
console.log('[/timeline]');
check(
'fetched_at',
dash.includes('fetched_at'),
api.includes('fetched_at'),
'Backend sends timestamp, frontend reads fetched_at'
);
console.log('');
// /countries
console.log('[/countries]');
check(
'country_name',
dash.includes('country_name'),
api.includes('country_name'),
'Backend does not return country_name'
);
check(
'country_code',
dash.includes('country_code'),
api.includes('country_code'),
'Backend does not return country_code'
);
console.log('');
// /dns
console.log('[/dns]');
check(
'domain',
dash.includes('domain'),
api.includes('domain'),
'Backend does not map domain field'
);
console.log('');
// /summary
console.log('[/summary]');
check('total_devices', dash.includes('total_devices'), api.includes('total_devices'));
check('bandwidth_down', dash.includes('bandwidth_down'), api.includes('bandwidth_down'));
check('download_speed', dash.includes('download_speed'), api.includes('download_speed'));
console.log('');
// /apps
console.log('[/apps]');
check('app_label', dash.includes('app_label'), api.includes('app_label'));
console.log('');
// /flows
console.log('[/flows]');
check('src_ip', dash.includes('src_ip'), api.includes('src_ip'));
check('dst_port', dash.includes('dst_port'), api.includes('dst_port'));
console.log('');
// app-categories
console.log('[/app-categories]');
check(
'category_label',
dash.includes('category_label'),
api.includes('category_label'),
'Backend sends label, frontend expects category_label'
);
console.log('');
+137
View File
@@ -0,0 +1,137 @@
// test/final_review.js
// Final Review — Verifikasi semua deliverable dari original request
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
function r(f) { return fs.readFileSync(path.join(ROOT, f), 'utf8'); }
let ok = 0, fail = 0;
function check(label, condition) {
const mark = condition ? '✓' : '✗ FAIL';
console.log(` ${mark}: ${label}`);
condition ? ok++ : fail++;
}
console.log('\n╔══════════════════════════════════════════════════════╗');
console.log('║ FINAL REVIEW — Original Request Deliverables ║');
console.log('╚══════════════════════════════════════════════════════╝\n');
const dash = r('backend/routes/dashboard.js');
const auth = r('backend/routes/auth.js');
const server = r('backend/server.js');
const sched = r('proxy/scheduler.js');
const coll = r('proxy/collector.js');
const compose = r('docker-compose.yml');
const envFile = r('.env.local');
const proxyIdx = r('proxy/index.js');
const proxySchema = r('proxy/models/Schemas.js');
const pkg = JSON.parse(r('backend/package.json'));
// ─── DELIVERABLE 1: Proxy 2 Mode ───────────────────────────────────────────
console.log('📦 DELIVERABLE 1: Proxy — 2 Mode Pengambilan Data');
check('Mode ALL: collectAllAgents() ada', coll.includes('collectAllAgents'));
check('Mode AGENT: collectSpecificAgent() ada', coll.includes('collectSpecificAgent'));
check('Scheduler baca PROXY_COLLECT_MODE env', sched.includes('PROXY_COLLECT_MODE'));
check('Scheduler baca PROXY_AGENT_UUID env', sched.includes('PROXY_AGENT_UUID'));
check('Jika mode=agent → collectSpecificAgent', sched.includes("COLLECT_MODE === 'agent'"));
check('Jika mode=all → collectAllAgents', sched.includes('collectAllAgents'));
console.log('');
// ─── DELIVERABLE 2: Data Isolation per Agent UUID ──────────────────────────
console.log('📦 DELIVERABLE 2: Isolasi Data per Network Agent UUID di MongoDB');
check('Proxy tag dokumen dengan agent_uuid', coll.includes('agent_uuid: agentUuid'));
check('Proxy tag dokumen dengan site_uuid', coll.includes('site_uuid'));
check('Proxy simpan IP address per agent', coll.includes('ip_address'));
check('Proxy simpan MAC address per agent', coll.includes('mac_address'));
check('Proxy simpan timestamp per agent', coll.includes('timestamp'));
check('Proxy simpan bandwidth per agent', coll.includes('bandwidth_down') || coll.includes('download'));
check('Summary schema punya agent_uuid', proxySchema.includes('agent_uuid'));
check('Flow schema punya agent_uuid', proxySchema.includes('FlowSchema'));
check('Device schema punya mac_address', proxySchema.includes('mac_address'));
check('Threat schema ada', proxySchema.includes('ThreatSchema'));
console.log('');
// ─── DELIVERABLE 3: Backend READ-ONLY dari MongoDB ─────────────────────────
console.log('📦 DELIVERABLE 3: Backend Hanya Baca MongoDB (bukan tulis/ingestion)');
check('server.js tidak ada ingestionService', !server.includes('ingestionService'));
check('server.js tidak ada startScheduler', !server.includes('startScheduler'));
check('server.js tidak ada import SQLite database.js', !server.includes("'./database'"));
check('dashboard.js tidak ada db.getStats SQLite', !dash.includes('db.getStats'));
check('dashboard.js tidak ada db.getBandwidthTimeline', !dash.includes('getBandwidthTimeline'));
check('dashboard.js tidak ada db.getLatest SQLite', !dash.includes('db.getLatest'));
check('auth.js tidak import database.js', !auth.includes("require('../database')"));
check('auth.js tidak ada better-sqlite3', !auth.includes('better-sqlite3'));
check('package.json tidak ada sqlite3', !pkg.dependencies['sqlite3'] && !pkg.dependencies['better-sqlite3']);
console.log('');
// ─── DELIVERABLE 4: Multi-tenant isolasi per agent ─────────────────────────
console.log('📦 DELIVERABLE 4: Multi-Tenant Isolation (Backend filter per agent_uuid)');
check('getBaseFilter() helper ada', dash.includes('function getBaseFilter'));
check('agent_uuid filter diterapkan jika AGENT_VIEWER', dash.includes('filter.agent_uuid = req.user.agent_uuid'));
check('site_uuid filter diterapkan', dash.includes('filter.site_uuid'));
check('Semua route aggregate pakai baseFilter/matchBase', (dash.match(/matchBase|getBaseFilter/g) || []).length > 15);
check('/agents hanya SUPER_ADMIN', dash.includes("role !== 'SUPER_ADMIN'"));
check('SUPER_ADMIN lihat semua data (no agent filter)', dash.includes('AGENT_VIEWER'));
check('View-As mode didukung di server.js', server.includes('AGENT_VIEWER') && server.includes('view-as'));
console.log('');
// ─── DELIVERABLE 5: 2 Docker Container Groups ──────────────────────────────
console.log('📦 DELIVERABLE 5: 2 Docker Container Groups (Container 1: Proxy+MongoDB, Container 2: Backend+Frontend)');
check('Container 1: Service mongodb', compose.includes('mongodb:'));
check('Container 1: Service proxy', compose.includes('proxy:'));
check('Container 2: Service backend', compose.includes('backend:'));
check('Container 2: Service frontend', compose.includes('frontend:'));
check('Network backone-infra (Container 1)', compose.includes('backone-infra'));
check('Network backone-app (Container 2)', compose.includes('backone-app'));
check('Proxy di backone-infra (isolated)', compose.includes('backone-infra'));
check('Backend di backone-app (isolated)', compose.includes('backone-app'));
check('MongoDB di kedua network (shared)', compose.includes('backone-infra') && compose.includes('backone-app'));
check('Proxy depends on mongodb healthcheck', compose.includes('service_healthy'));
check('PROXY_COLLECT_MODE di docker-compose', compose.includes('PROXY_COLLECT_MODE'));
check('PROXY_AGENT_UUID di docker-compose', compose.includes('PROXY_AGENT_UUID'));
check('PROXY_URL ke backend (untuk manual refresh)', compose.includes('PROXY_URL'));
console.log('');
// ─── DELIVERABLE 6: Proxy API Endpoints ────────────────────────────────────
console.log('📦 DELIVERABLE 6: Proxy REST API (port 4000)');
check('GET /health', proxyIdx.includes("'/health'"));
check('GET /status', proxyIdx.includes("'/status'"));
check('GET /agents (list all agent UUIDs di MongoDB)', proxyIdx.includes("'/agents'"));
check('POST /collect/all (trigger semua agent)', proxyIdx.includes("'/collect/all'"));
check('POST /collect/:agentUuid (trigger spesifik)', proxyIdx.includes("'/collect/:agentUuid'"));
console.log('');
// ─── DELIVERABLE 7: Proxy pisah dari backend ───────────────────────────────
console.log('📦 DELIVERABLE 7: Proxy Terpisah dari Backend (sesuai pertanyaan user)');
check('Proxy punya Dockerfile sendiri', fs.existsSync(path.join(ROOT, 'proxy/Dockerfile')));
check('Proxy punya package.json sendiri', fs.existsSync(path.join(ROOT, 'proxy/package.json')));
check('Proxy di folder terpisah (/proxy/)', fs.existsSync(path.join(ROOT, 'proxy/index.js')));
check('Backend tidak mengimport dari proxy/', !server.includes("'../proxy'") && !dash.includes("'../proxy'"));
check('Proxy tidak mengimport dari backend/', !r('proxy/index.js').includes("'../backend'"));
check('Proxy hanya komunikasi ke MongoDB (tidak ke backend)', !r('proxy/collector.js').includes('3001'));
check('Backend hanya POST ke proxy (bukan import)', dash.includes('axios.post') || server.includes('PROXY_URL'));
console.log('');
// ─── DELIVERABLE 8: .env.local dokumentasi ─────────────────────────────────
console.log('📦 DELIVERABLE 8: Environment Variable Dokumentasi');
check('PROXY_COLLECT_MODE terdokumentasi', envFile.includes('PROXY_COLLECT_MODE'));
check('PROXY_AGENT_UUID terdokumentasi', envFile.includes('PROXY_AGENT_UUID'));
check('Penjelasan Mode 1 (all)', envFile.includes('Mode 1'));
check('Penjelasan Mode 2 (agent spesifik)', envFile.includes('Mode 2'));
check('MONGODB_URI ada', envFile.includes('MONGODB_URI'));
check('PROXY_CRON_SCHEDULE ada', envFile.includes('PROXY_CRON_SCHEDULE'));
console.log('');
// ─── SUMMARY ───────────────────────────────────────────────────────────────
console.log('══════════════════════════════════════════════════════');
console.log(` FINAL: ${ok} PASSED | ${fail} FAILED`);
if (fail === 0) {
console.log(' STATUS: ✓ SEMUA DELIVERABLE TERPENUHI');
} else {
console.log(' STATUS: ✗ ADA YANG PERLU DIPERBAIKI — lihat item FAIL di atas');
}
console.log('══════════════════════════════════════════════════════\n');
if (fail > 0) process.exit(1);
+67
View File
@@ -0,0 +1,67 @@
// Script to patch api.ts with timeRange support
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, '..', 'src', 'lib', 'api.ts');
let content = fs.readFileSync(filePath, 'utf8');
// 1. Fix fetcher to accept timeRange
const oldFetcher = `// Fetcher Function
export async function fetcher<T>(endpoint: string): Promise<T> {
const url = \`/api/dashboard\${endpoint}\`;
// Tambahkan X-View-As-Agent header jika admin dalam mode view-as
const viewAsHeaders = getViewAsHeaders();
const res = await fetch(url, { headers: viewAsHeaders });
if (!res.ok) {
throw new Error(\`API error: \${res.status} \${res.statusText}\`);
}
const json = await res.json();
if (!json.ok) {
throw new Error(json.message || 'API responded with ok: false');
}
return json.data;
}`;
const newFetcher = `// Fetcher Function — supports optional timeRange for time-filtered queries
export async function fetcher<T>(endpoint: string, timeRange?: string): Promise<T> {
// Append timeRange param if provided (appended before X-View-As header)
let url = \`/api/dashboard\${endpoint}\`;
if (timeRange) {
const sep = url.includes('?') ? '&' : '?';
url += sep + 'timeRange=' + timeRange;
}
// X-View-As-Agent header for admin view-as mode
const viewAsHeaders = getViewAsHeaders();
const res = await fetch(url, { headers: viewAsHeaders });
if (!res.ok) {
throw new Error(\`API error: \${res.status} \${res.statusText}\`);
}
const json = await res.json();
if (!json.ok) {
throw new Error(json.message || 'API responded with ok: false');
}
return json.data;
}`;
// Normalize CRLF → LF for matching
const normalized = content.replace(/\r\n/g, '\n');
const oldNorm = oldFetcher.replace(/\r\n/g, '\n');
const newNorm = newFetcher.replace(/\r\n/g, '\n');
if (!normalized.includes(oldNorm)) {
console.error('ERROR: Could not find fetcher function in api.ts');
console.log('Content around line 170:');
console.log(normalized.split('\n').slice(168, 186).join('\n'));
process.exit(1);
}
const patched = normalized.replace(oldNorm, newNorm);
// Write back with CRLF (Windows)
fs.writeFileSync(filePath, patched.replace(/\n/g, '\r\n'), 'utf8');
console.log('✓ api.ts patched successfully');
console.log('✓ fetcher now accepts optional timeRange parameter');
// Verify
const verify = fs.readFileSync(filePath, 'utf8');
console.log('✓ timeRange in fetcher:', verify.includes('timeRange?: string') ? 'YES' : 'NO');
+273
View File
@@ -0,0 +1,273 @@
// Script to update all API hooks to accept timeRange parameter
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, '..', 'src', 'lib', 'api.ts');
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
let count = 0;
// Helper to replace hook signatures and add timeRange to useEffect deps
function patchHook(content, hookSignature, fetchCall, newFetchCall, depArray, newDepArray) {
if (content.includes(hookSignature)) {
content = content.replace(hookSignature, hookSignature.replace(')', ', timeRange?: string)'));
if (fetchCall && newFetchCall) {
content = content.replace(fetchCall, newFetchCall);
}
if (depArray && newDepArray) {
content = content.replace(depArray, newDepArray);
}
count++;
}
return content;
}
// useDashboardSummary
content = content.replace(
'export function useDashboardSummary() {',
'export function useDashboardSummary(timeRange?: string) {'
);
content = content.replace(
" fetcher<DashboardSummary>('/summary')\n .then(setData)",
" fetcher<DashboardSummary>('/summary', timeRange)\n .then(setData)"
);
content = content.replace(
" }, []);\n\n return { data, error, isLoading };\n}\n\nexport function useTimeline",
" }, [timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTimeline"
);
// useTimeline
content = content.replace(
'export function useTimeline(points: number = 60) {',
'export function useTimeline(points: number = 60, timeRange?: string) {'
);
content = content.replace(
" fetcher<TimelinePoint[]>(`/timeline?points=${points}`)",
" fetcher<TimelinePoint[]>(`/timeline?points=${points}`, timeRange)"
);
content = content.replace(
" }, [points]);\n\n return { data, error, isLoading };\n}\n\nexport function useTopApps",
" }, [points, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTopApps"
);
// useTopApps
content = content.replace(
'export function useTopApps(limit: number = 10) {',
'export function useTopApps(limit: number = 10, timeRange?: string) {'
);
content = content.replace(
" fetcher<AppStat[]>(`/apps?limit=${limit}`)",
" fetcher<AppStat[]>(`/apps?limit=${limit}`, timeRange)"
);
content = content.replace(
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useTopProtocols",
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTopProtocols"
);
// useTopProtocols
content = content.replace(
'export function useTopProtocols() {',
'export function useTopProtocols(timeRange?: string) {'
);
content = content.replace(
" fetcher<ProtocolStat[]>('/protocols')",
" fetcher<ProtocolStat[]>('/protocols', timeRange)"
);
content = content.replace(
" }, []);\n\n return { data, error, isLoading };\n}\n\nexport function useDevices",
" }, [timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useDevices"
);
// useDevices
content = content.replace(
'export function useDevices(limit: number = 50) {',
'export function useDevices(limit: number = 50, timeRange?: string) {'
);
content = content.replace(
" fetcher<DeviceStat[]>(`/devices?limit=${limit}`)",
" fetcher<DeviceStat[]>(`/devices?limit=${limit}`, timeRange)"
);
content = content.replace(
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useThreats",
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useThreats"
);
// useThreats
content = content.replace(
'export function useThreats(limit: number = 20) {',
'export function useThreats(limit: number = 20, timeRange?: string) {'
);
content = content.replace(
" fetcher<ThreatStat[]>(`/threats?limit=${limit}`)",
" fetcher<ThreatStat[]>(`/threats?limit=${limit}`, timeRange)"
);
content = content.replace(
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useAppCategories",
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useAppCategories"
);
// useAppCategories
content = content.replace(
'export function useAppCategories() {',
'export function useAppCategories(timeRange?: string) {'
);
content = content.replace(
" fetcher<AppCategoryStat[]>('/app-categories')",
" fetcher<AppCategoryStat[]>('/app-categories', timeRange)"
);
content = content.replace(
" }, []);\n\n return { data, error, isLoading };\n}\n\nexport function useContinents",
" }, [timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useContinents"
);
// useContinents
content = content.replace(
'export function useContinents() {',
'export function useContinents(timeRange?: string) {'
);
content = content.replace(
" fetcher<ContinentStat[]>('/continents')",
" fetcher<ContinentStat[]>('/continents', timeRange)"
);
content = content.replace(
" }, []);\n\n return { data, error, isLoading };\n}\n\nexport function useFlows",
" }, [timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useFlows"
);
// useFlows
content = content.replace(
'export function useFlows(limit: number = 50) {',
'export function useFlows(limit: number = 50, timeRange?: string) {'
);
content = content.replace(
" fetcher<FlowStat[]>(`/flows?limit=${limit}`)",
" fetcher<FlowStat[]>(`/flows?limit=${limit}`, timeRange)"
);
content = content.replace(
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useCountries",
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useCountries"
);
// useCountries
content = content.replace(
'export function useCountries(limit: number = 50) {',
'export function useCountries(limit: number = 50, timeRange?: string) {'
);
content = content.replace(
" fetcher<CountryStat[]>(`/countries?limit=${limit}`)",
" fetcher<CountryStat[]>(`/countries?limit=${limit}`, timeRange)"
);
content = content.replace(
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useDNS",
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useDNS"
);
// useDNS
content = content.replace(
'export function useDNS(limit: number = 50) {',
'export function useDNS(limit: number = 50, timeRange?: string) {'
);
content = content.replace(
" fetcher<DnsStat[]>(`/dns?limit=${limit}`)",
" fetcher<DnsStat[]>(`/dns?limit=${limit}`, timeRange)"
);
content = content.replace(
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useEvents",
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useEvents"
);
// useEvents
content = content.replace(
'export function useEvents(limit: number = 50) {',
'export function useEvents(limit: number = 50, timeRange?: string) {'
);
content = content.replace(
" fetcher<EventStat[]>(`/events?limit=${limit}`)",
" fetcher<EventStat[]>(`/events?limit=${limit}`, timeRange)"
);
content = content.replace(
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsVersions",
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsVersions"
);
// useTlsVersions
content = content.replace(
'export function useTlsVersions(limit: number = 10) {',
'export function useTlsVersions(limit: number = 10, timeRange?: string) {'
);
content = content.replace(
" fetcher<TlsVersionStat[]>(`/tls-versions?limit=${limit}`)",
" fetcher<TlsVersionStat[]>(`/tls-versions?limit=${limit}`, timeRange)"
);
content = content.replace(
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsCiphers",
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsCiphers"
);
// useTlsCiphers
content = content.replace(
'export function useTlsCiphers(limit: number = 15) {',
'export function useTlsCiphers(limit: number = 15, timeRange?: string) {'
);
content = content.replace(
" fetcher<TlsCipherStat[]>(`/tls-ciphers?limit=${limit}`)",
" fetcher<TlsCipherStat[]>(`/tls-ciphers?limit=${limit}`, timeRange)"
);
content = content.replace(
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsSecurity",
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsSecurity"
);
// useTlsSecurity
content = content.replace(
'export function useTlsSecurity(limit: number = 10) {',
'export function useTlsSecurity(limit: number = 10, timeRange?: string) {'
);
content = content.replace(
" fetcher<TlsSecurityStat[]>(`/tls-security?limit=${limit}`)",
" fetcher<TlsSecurityStat[]>(`/tls-security?limit=${limit}`, timeRange)"
);
content = content.replace(
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useIntelligenceStats",
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useIntelligenceStats"
);
// useIntelligenceStats
content = content.replace(
'export function useIntelligenceStats() {',
'export function useIntelligenceStats(timeRange?: string) {'
);
content = content.replace(
" fetcher<IntelligenceStats>('/intelligence/stats')",
" fetcher<IntelligenceStats>('/intelligence/stats', timeRange)"
);
content = content.replace(
" }, []);\n\n return { data, error, isLoading };\n}\n\n// ─── LOOKUP APPLICATIONS",
" }, [timeRange]);\n\n return { data, error, isLoading };\n}\n\n// ─── LOOKUP APPLICATIONS"
);
// Write back with CRLF
fs.writeFileSync(filePath, content.replace(/\n/g, '\r\n'), 'utf8');
// Verify
const verify = fs.readFileSync(filePath, 'utf8');
const hooks = [
'useDashboardSummary', 'useTimeline', 'useTopApps', 'useTopProtocols',
'useDevices', 'useThreats', 'useAppCategories', 'useContinents',
'useFlows', 'useCountries', 'useDNS', 'useEvents',
'useTlsVersions', 'useTlsCiphers', 'useTlsSecurity', 'useIntelligenceStats'
];
console.log('\n=== api.ts Patch Results ===');
let allOk = true;
for (const hook of hooks) {
const pattern = hook + '(';
const idx = verify.indexOf('export function ' + hook);
if (idx < 0) { console.log('✗ MISSING:', hook); allOk = false; continue; }
const slice = verify.slice(idx, idx + 80);
const hasTimeRange = slice.includes('timeRange');
console.log((hasTimeRange ? '✓' : '✗') + ' ' + hook + ' timeRange param');
if (!hasTimeRange) allOk = false;
}
console.log('\n' + (allOk ? '✓ ALL HOOKS UPDATED' : '✗ SOME HOOKS MISSING'));
+92
View File
@@ -0,0 +1,92 @@
// Script to update page files to use api-with-context instead of api
// This ensures all page hooks automatically use timeRange from TimeFilterContext
const fs = require('fs');
const path = require('path');
// Files that import hooks from @/lib/api (not fetch functions, just hooks)
const pageFiles = [
'src/app/(dashboard)/page.tsx',
'src/app/(dashboard)/threats/page.tsx',
'src/app/(dashboard)/flows/page.tsx',
'src/app/(dashboard)/devices/page.tsx',
'src/app/(dashboard)/apps/page.tsx',
'src/app/(dashboard)/dns/page.tsx',
'src/app/(dashboard)/events/page.tsx',
'src/app/(dashboard)/geography/page.tsx',
'src/app/(dashboard)/intelligence/page.tsx',
'src/app/(dashboard)/network-intelligence/page.tsx',
'src/app/(dashboard)/security-audit/page.tsx',
];
// Hooks that are context-aware (exist in api-with-context.ts)
const contextHooks = [
'useDashboardSummary', 'useTimeline', 'useTopApps', 'useTopProtocols',
'useDevices', 'useThreats', 'useAppCategories', 'useContinents',
'useFlows', 'useCountries', 'useDNS', 'useEvents',
'useTlsVersions', 'useTlsCiphers', 'useTlsSecurity', 'useIntelligenceStats',
];
// Types/fetch functions that should remain in @/lib/api
const apiOnly = [
'fetcher', 'fetchAgentDetails', 'fetchDeviceDetails', 'fetchAppDetails',
'fetchSecurityDevices', 'useLookupApplications',
// Type re-exports
'DashboardSummary', 'TimelinePoint', 'AppStat', 'ProtocolStat', 'DeviceStat',
'ThreatStat', 'AppCategoryStat', 'ContinentStat', 'FlowStat', 'CountryStat',
'DnsStat', 'EventStat', 'TlsVersionStat', 'TlsCipherStat', 'TlsSecurityStat',
'IntelligenceStats', 'AgentDetails', 'DeviceDetails', 'AppDetails', 'SecurityDevice',
];
let totalChanges = 0;
for (const relPath of pageFiles) {
const filePath = path.join(process.cwd(), relPath);
if (!fs.existsSync(filePath)) {
console.log('SKIP (not found):', relPath);
continue;
}
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
const original = content;
// Find all imports from @/lib/api (not api-with-context, not api-advanced)
const importRegex = /import \{([^}]+)\} from ["']@\/lib\/api["'];?/g;
let match;
while ((match = importRegex.exec(content)) !== null) {
const fullImport = match[0];
const importedItems = match[1].split(',').map(s => s.trim()).filter(Boolean);
// Categorize: hooks go to api-with-context, types/fetchers stay in api
const hooksToMove = importedItems.filter(item =>
contextHooks.includes(item) || contextHooks.includes(item.split(' ')[0]) // handle "as" aliases
);
const typesToKeep = importedItems.filter(item =>
!contextHooks.includes(item) && !contextHooks.includes(item.split(' ')[0])
);
if (hooksToMove.length === 0) continue; // nothing to move
let newImports = '';
if (typesToKeep.length > 0) {
newImports += `import { ${typesToKeep.join(', ')} } from "@/lib/api";\n`;
}
if (hooksToMove.length > 0) {
newImports += `import { ${hooksToMove.join(', ')} } from "@/lib/api-with-context";`;
}
content = content.replace(fullImport, newImports);
totalChanges++;
}
if (content !== original) {
fs.writeFileSync(filePath, content.replace(/\n/g, '\r\n'), 'utf8');
console.log('✓ Updated:', relPath);
} else {
console.log(' No changes needed:', relPath);
}
}
console.log(`\nTotal files updated: ${totalChanges}`);