feat(site): implement dynamic site selection dropdown in sidebar and enable multi-site monitoring

This commit is contained in:
Rafif-Riqullah-Siregar committed 2026-07-13 16:57:45 +07:00
1 parent be6bc14190
commit 4882108068
125 files changed
+5666 -4305

No files matched your search

+3 -2
View File
@@ -40,7 +40,8 @@ const dashCode = readFile('backend/routes/dashboard.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');
readFile('backend/routes/dashboard/events.js') +
readFile('backend/routes/dashboard/agents.js');
const authCode = readFile('backend/routes/auth.js');
test('server.js tidak memanggil ingestionService', !serverCode.includes('ingestionService'));
@@ -77,7 +78,7 @@ test("scheduler.js mendukung mode 'all'", schedulerCode.includes("
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'));
test('collector.js tag data dengan site_uuid', collectorCode.includes('site_uuid: SITE_UUID') || collectorCode.includes('site_uuid: SITE_UUID') || collectorCode.includes('site_uuid: siteUuid'));
console.log('');
// ─── TEST GROUP 4: Docker Compose 2 Container Groups ─────────────────────
+13 -2
View File
@@ -18,11 +18,22 @@ console.log('\n╔════════════════════
console.log('║ FINAL REVIEW — Original Request Deliverables ║');
console.log('╚══════════════════════════════════════════════════════╝\n');
const dash = r('backend/routes/dashboard.js');
const dash = r('backend/routes/dashboard.js') +
r('backend/routes/dashboard/helpers.js') +
r('backend/routes/dashboard/summary.js') +
r('backend/routes/dashboard/apps.js') +
r('backend/routes/dashboard/devices.js') +
r('backend/routes/dashboard/flows.js') +
r('backend/routes/dashboard/threats.js') +
r('backend/routes/dashboard/geo.js') +
r('backend/routes/dashboard/tls.js') +
r('backend/routes/dashboard/telemetry.js') +
r('backend/routes/dashboard/events.js') +
r('backend/routes/dashboard/agents.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 coll = r('proxy/collector.js') + r('proxy/collectorHelperDpi2.js');
const compose = r('docker-compose.yml');
const envFile = r('.env.local');
const proxyIdx = r('proxy/index.js');
-67
View File
@@ -1,67 +0,0 @@
// 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
@@ -1,273 +0,0 @@
// 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
@@ -1,92 +0,0 @@
// 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}`);