Compare commits
16
Commits
6e891392a7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a44206c112 | ||
|
|
ab5bb1fd11 | ||
|
|
b6bd0f42f6 | ||
|
|
0b824f8cd2 | ||
|
|
bb429ba566 | ||
|
|
77708fa8e6 | ||
|
|
773e654616 | ||
|
|
90817cb2f8 | ||
|
|
0dcedf5d76 | ||
|
|
068435ffb9 | ||
|
|
f7ebd7a5c5 | ||
|
|
6a23577b08 | ||
|
|
f0d9a55a56 | ||
|
|
64f77187d8 | ||
|
|
8a73d266d8 | ||
|
|
bc30a8a0e4 |
No files matched your search
@@ -2,10 +2,10 @@ FROM oven/bun:1-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY backend/package*.json ./
|
||||
COPY package*.json ./
|
||||
RUN bun install --production
|
||||
|
||||
COPY backend/ .
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
|
||||
@@ -214,11 +214,17 @@ router.get('/agents/list', async (req, res) => {
|
||||
|
||||
const agents = await db.collection('agent_registry')
|
||||
.find(filter)
|
||||
.project({ uuid: 1, label: 1, _id: 0 })
|
||||
.project({ uuid: 1, label: 1, protected_label: 1, _id: 0 })
|
||||
.sort({ uuid: 1 })
|
||||
.toArray();
|
||||
|
||||
res.json({ ok: true, data: agents });
|
||||
|
||||
// Use protected_label if available to prevent proxy overwrite bug
|
||||
const mappedAgents = agents.map(a => ({
|
||||
uuid: a.uuid,
|
||||
label: a.protected_label || a.label || a.uuid
|
||||
}));
|
||||
|
||||
res.json({ ok: true, data: mappedAgents });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
@@ -239,6 +245,27 @@ router.get('/agents/:uuid/subnets', async (req, res) => {
|
||||
// ─── PUT /api/dashboard/agents/:uuid/subnets ─────────────────────────────────
|
||||
// Simpan konfigurasi subnet yang diizinkan untuk agent tertentu
|
||||
// Body: { allowed_subnets: ["192.168.1", "10.21"] }
|
||||
function ipToLong(ip) {
|
||||
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
||||
}
|
||||
|
||||
function ipMatchesSubnets(ip, subnets) {
|
||||
if (!subnets || subnets.length === 0) return true;
|
||||
if (!ip) return false;
|
||||
return subnets.some(subnet => {
|
||||
if (subnet.includes('/')) {
|
||||
try {
|
||||
const [range, bitsStr] = subnet.split('/');
|
||||
const bits = parseInt(bitsStr, 10);
|
||||
if (isNaN(bits) || bits < 0 || bits > 32) return false;
|
||||
const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
|
||||
return (ipToLong(ip) & mask) === (ipToLong(range) & mask);
|
||||
} catch (e) { return false; }
|
||||
} else { return ip === subnet; }
|
||||
});
|
||||
}
|
||||
|
||||
// PUT /api/dashboard/agents/:uuid/subnets
|
||||
router.put('/agents/:uuid/subnets', async (req, res) => {
|
||||
try {
|
||||
const allowedRoles = ['SUPER_ADMIN', 'TENANT_ADMIN', 'COMPANY_ADMIN'];
|
||||
@@ -251,6 +278,21 @@ router.put('/agents/:uuid/subnets', async (req, res) => {
|
||||
{ uuid: req.params.uuid },
|
||||
{ $set: { allowed_subnets: subnets, subnets_updated_at: new Date() } }
|
||||
);
|
||||
|
||||
// Auto-cleanup background task
|
||||
if (subnets.length > 0) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const ips = await db.collection('devicestats').distinct('ip_address', { agent_uuid: req.params.uuid });
|
||||
const invalidIps = ips.filter(ip => !ipMatchesSubnets(ip, subnets));
|
||||
if (invalidIps.length > 0) {
|
||||
await db.collection('devicestats').deleteMany({ agent_uuid: req.params.uuid, ip_address: { $in: invalidIps } });
|
||||
await db.collection('flows').deleteMany({ agent_uuid: req.params.uuid, src_ip: { $in: invalidIps } });
|
||||
}
|
||||
} catch (e) { console.error('Auto-cleanup error:', e); }
|
||||
}, 100);
|
||||
}
|
||||
|
||||
res.json({ ok: true, data: { allowed_subnets: subnets } });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
- INTERNAL_API_URL=${INTERNAL_API_URL:-http://backone-inspect-backend-bun-0:3001}
|
||||
- INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:3001}
|
||||
- JWT_SECRET=${JWT_SECRET:-super-secret-backone-key}
|
||||
- MONGODB_URI=${MONGODB_URI}
|
||||
- BACKONE_API_KEY=${NETIFY_API_KEY:-sk_db_source2}
|
||||
|
||||
@@ -13,6 +13,10 @@ async function fetchFlows(limit = 500, agentUuid = null, siteUuid = null, interv
|
||||
backoneFetch('/data/stats/top/tls_sni/download', { filter_interval: intervalMinutes, settings_limit: 50 }, agentUuid, siteUuid),
|
||||
]);
|
||||
if (!raw || !Array.isArray(raw)) return null;
|
||||
|
||||
// Safety check: Filter flows strictly to the requested agent to prevent Org-level pollution
|
||||
const filteredRaw = agentUuid ? raw.filter(r => r.agent_uuid === agentUuid) : raw;
|
||||
|
||||
const sniList = [];
|
||||
if (sniRaw && Array.isArray(sniRaw)) {
|
||||
for (const r of sniRaw) {
|
||||
@@ -22,7 +26,7 @@ async function fetchFlows(limit = 500, agentUuid = null, siteUuid = null, interv
|
||||
}
|
||||
}
|
||||
}
|
||||
return raw.map(r => {
|
||||
return filteredRaw.map(r => {
|
||||
const port = r.remote_port ?? null;
|
||||
const portService = port ? (stats.PORT_SERVICE_MAP[port] ?? `Port ${port}`) : null;
|
||||
const appLabel = r.application?.label || portService;
|
||||
|
||||
@@ -37,7 +37,8 @@ function fixDates(obj) {
|
||||
async function backoneFetch(endpoint, params = {}, agentUuid = null, siteUuid = null) {
|
||||
if (agentUuid) {
|
||||
const agentId = agentMap[agentUuid];
|
||||
if (agentId) {
|
||||
// Only use filter_agents if agentId is a number or numeric string
|
||||
if (agentId && !isNaN(Number(agentId))) {
|
||||
params.filter_agents = `[${agentId}]`;
|
||||
} else {
|
||||
params.settings_agent = agentUuid;
|
||||
|
||||
+12
-4
@@ -34,6 +34,8 @@ async function collectAllAgents() {
|
||||
const totalAgents = agents.length;
|
||||
console.log(`[Collector] Processing ${totalAgents} unique agents globally`);
|
||||
|
||||
// Step 1: Upsert technical metadata only (NEVER touch label field during updates)
|
||||
// This prevents ANY version of proxy from overwriting a custom label set by the UI.
|
||||
await Promise.allSettled(agents.map(a =>
|
||||
AgentRegistry.findOneAndUpdate(
|
||||
{ uuid: a.uuid },
|
||||
@@ -41,18 +43,24 @@ async function collectAllAgents() {
|
||||
$set: {
|
||||
uuid: a.uuid,
|
||||
serial: a.serial || a.uuid,
|
||||
site_uuid: 'global', // Store globally
|
||||
site_uuid: 'global',
|
||||
provisioned: a.provisioned ?? true,
|
||||
activated: a.activated ?? false,
|
||||
last_seen_at: a.last_seen_at ?? null,
|
||||
},
|
||||
$setOnInsert: {
|
||||
label: a.label,
|
||||
}
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
)
|
||||
));
|
||||
|
||||
// Step 2: Set default label ONLY for agents that have no label yet (brand new agents).
|
||||
// We NEVER overwrite an existing label — even if it was set by an older proxy version.
|
||||
await Promise.allSettled(agents.map(a =>
|
||||
AgentRegistry.updateOne(
|
||||
{ uuid: a.uuid, $or: [{ label: { $exists: false } }, { label: null }, { label: '' }] },
|
||||
{ $set: { label: a.label } }
|
||||
)
|
||||
));
|
||||
console.log(`[Collector] ✓ ${agents.length} agents upserted into registry globally`);
|
||||
|
||||
for (const agent of agents) {
|
||||
|
||||
+69
-1
@@ -2,6 +2,7 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Pruning process for BackOne MongoDB data retention.
|
||||
// Removes telemetry records older than 30 days to conserve database space.
|
||||
// Also removes device/flow records that are outside the agent's configured subnet.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
@@ -31,6 +32,73 @@ async function pruneOldData() {
|
||||
console.error(`[Collector] [Retention] ✗ Failed to prune ${name}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Also prune out-of-subnet data
|
||||
await pruneOutOfSubnetData();
|
||||
}
|
||||
|
||||
module.exports = { pruneOldData };
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Subnet-based cleanup: remove devices/flows that are outside the agent's
|
||||
// configured allowed_subnets. Runs after every collection cycle automatically.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function _ipToLong(ip) {
|
||||
return ip.split('.').reduce((acc, o) => (acc << 8) + parseInt(o, 10), 0) >>> 0;
|
||||
}
|
||||
|
||||
function _ipMatchesSubnets(ip, subnets) {
|
||||
if (!ip || ip.includes(':')) return false; // skip IPv6
|
||||
if (!subnets || subnets.length === 0) return true; // no restriction
|
||||
return subnets.some(s => {
|
||||
if (s.includes('/')) {
|
||||
try {
|
||||
const [r, b] = s.split('/');
|
||||
const bits = parseInt(b, 10);
|
||||
if (isNaN(bits) || bits < 0 || bits > 32) return false;
|
||||
const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
|
||||
return (_ipToLong(ip) & mask) === (_ipToLong(r) & mask);
|
||||
} catch { return false; }
|
||||
}
|
||||
return ip.startsWith(s + '.') || ip === s;
|
||||
});
|
||||
}
|
||||
|
||||
async function pruneOutOfSubnetData() {
|
||||
try {
|
||||
const db = mongoose.connection.db;
|
||||
const agents = await db.collection('agent_registry')
|
||||
.find({ allowed_subnets: { $exists: true, $not: { $size: 0 } } })
|
||||
.toArray();
|
||||
|
||||
for (const agent of agents) {
|
||||
const uuid = agent.uuid;
|
||||
const subnets = (agent.allowed_subnets || []).map(s => s.trim()).filter(Boolean);
|
||||
if (subnets.length === 0) continue;
|
||||
|
||||
// devicestats
|
||||
const devIps = await db.collection('devicestats').distinct('ip_address', { agent_uuid: uuid });
|
||||
const badDevIps = devIps.filter(ip => !_ipMatchesSubnets(ip, subnets));
|
||||
if (badDevIps.length > 0) {
|
||||
const r1 = await db.collection('devicestats').deleteMany({ agent_uuid: uuid, ip_address: { $in: badDevIps } });
|
||||
const r2 = await db.collection('deviceappstats').deleteMany({ agent_uuid: uuid, ip_address: { $in: badDevIps } });
|
||||
if (r1.deletedCount + r2.deletedCount > 0) {
|
||||
console.log(`[Collector] [Subnet] ${uuid}: removed ${r1.deletedCount} device + ${r2.deletedCount} deviceapp records (${badDevIps.length} bad IPs)`);
|
||||
}
|
||||
}
|
||||
|
||||
// flows
|
||||
const flowIps = await db.collection('flows').distinct('src_ip', { agent_uuid: uuid });
|
||||
const badFlowIps = flowIps.filter(ip => !_ipMatchesSubnets(ip, subnets));
|
||||
if (badFlowIps.length > 0) {
|
||||
const r = await db.collection('flows').deleteMany({ agent_uuid: uuid, src_ip: { $in: badFlowIps } });
|
||||
if (r.deletedCount > 0) {
|
||||
console.log(`[Collector] [Subnet] ${uuid}: removed ${r.deletedCount} flow records (${badFlowIps.length} bad IPs)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Collector] [Subnet] pruneOutOfSubnetData error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { pruneOldData, pruneOutOfSubnetData };
|
||||
@@ -103,7 +103,7 @@ export function getAgentColumns({
|
||||
className: "w-[10%] text-center",
|
||||
accessor: (row) => {
|
||||
const uuid = row.uuid || row.serial;
|
||||
const uptime = uptimeMap[uuid] ?? 100;
|
||||
const uptime = uptimeMap[uuid] ?? 0;
|
||||
let color = "text-emerald-400";
|
||||
if (uptime < 90) color = "text-red-400";
|
||||
else if (uptime < 98) color = "text-amber-400";
|
||||
|
||||
@@ -99,8 +99,8 @@ export default function AgentsPage() {
|
||||
return hum === 'Online' || hum === 'Flows Detected' || (hum && hum.startsWith('Last seen:'));
|
||||
}).length;
|
||||
const offlineAgents = totalAgents - onlineAgents;
|
||||
const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 100);
|
||||
const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 100;
|
||||
const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 0);
|
||||
const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 0;
|
||||
|
||||
// External Accounts di halaman Agents = akun eksternal teknikal/operasional saja
|
||||
// AGENT_VIEWER = akun network agent (sudah ada di tabel agents) → BUKAN akun eksternal
|
||||
@@ -175,11 +175,10 @@ export default function AgentsPage() {
|
||||
.filter(loc => agents.some(a => (a.uuid || a.serial) === loc.agent_uuid))
|
||||
.map(loc => {
|
||||
const agent = agents.find(a => (a.uuid || a.serial) === loc.agent_uuid);
|
||||
const dynamicLabel = resolveAgentLabel(loc.agent_uuid, managedUsers);
|
||||
const agentLabel = agent
|
||||
? (resolveAgentLabel(loc.agent_uuid, managedUsers) !== loc.agent_uuid
|
||||
? resolveAgentLabel(loc.agent_uuid, managedUsers)
|
||||
: (agent.label || loc.label || loc.agent_uuid))
|
||||
: (loc.label || loc.agent_uuid);
|
||||
? (agent.label || (dynamicLabel !== loc.agent_uuid ? dynamicLabel : loc.agent_uuid))
|
||||
: (loc.label || (dynamicLabel !== loc.agent_uuid ? dynamicLabel : loc.agent_uuid));
|
||||
const isOnline = !!(agent?.last_seen_at?.human === 'Online' ||
|
||||
agent?.last_seen_at?.human === 'Flows Detected' ||
|
||||
(agent?.last_seen_at?.human && agent.last_seen_at.human.startsWith('Last seen:')));
|
||||
|
||||
@@ -50,10 +50,18 @@ export default function SummaryPage() {
|
||||
|
||||
const mapData = useMemo(() => {
|
||||
if (!countries.data) return [];
|
||||
|
||||
// Jika summary menunjukkan tidak ada traffic sama sekali, jangan tampilkan globe
|
||||
const totalTraffic = (summary.data?.bandwidth_down || 0) + (summary.data?.bandwidth_up || 0);
|
||||
if (totalTraffic === 0) return [];
|
||||
|
||||
const origin = getOriginLocation();
|
||||
|
||||
return countries.data.reduce((acc: any[], row) => {
|
||||
// Hanya tampilkan negara yang punya traffic aktual > 0
|
||||
const traffic = (row.download || 0) + (row.upload || 0);
|
||||
if (traffic === 0) return acc;
|
||||
|
||||
const dest = getDestinationLocation(row.country_code, row.country_name);
|
||||
if (dest) {
|
||||
acc.push({
|
||||
@@ -63,7 +71,7 @@ export default function SummaryPage() {
|
||||
endLng: dest.lng,
|
||||
fromLabel: origin.label,
|
||||
toLabel: dest.label,
|
||||
value: row.download + row.upload,
|
||||
value: traffic,
|
||||
download: row.download,
|
||||
upload: row.upload,
|
||||
countryCode: row.country_code
|
||||
@@ -71,7 +79,7 @@ export default function SummaryPage() {
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}, [countries.data]);
|
||||
}, [countries.data, summary.data]);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* GET /api/dashboard/agents/list
|
||||
*
|
||||
* Override route: bypass backend container dan baca langsung dari MongoDB.
|
||||
* Prioritaskan protected_label agar label tidak teroverwrite oleh proxy lama.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
|
||||
const MONGODB_URI = process.env.MONGODB_URI || '';
|
||||
|
||||
async function connectMongo() {
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
await mongoose.connect(MONGODB_URI, { serverSelectionTimeoutMS: 5000 });
|
||||
}
|
||||
return mongoose.connection.db!;
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
// Verify JWT from cookie
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
let user: any;
|
||||
try {
|
||||
user = jwt.verify(token, JWT_SECRET) as any;
|
||||
} catch {
|
||||
return NextResponse.json({ ok: false, error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const db = await connectMongo();
|
||||
|
||||
const companyRoles = ['COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER'];
|
||||
const isCompanyRole = companyRoles.includes(user?.role);
|
||||
|
||||
const filter: any = {};
|
||||
|
||||
if (isCompanyRole) {
|
||||
const agentUuids: string[] = user?.agent_uuids || [];
|
||||
if (agentUuids.length === 0) {
|
||||
return NextResponse.json({ ok: true, data: [] });
|
||||
}
|
||||
filter.uuid = { $in: agentUuids };
|
||||
} else {
|
||||
// Admin/SUPER_ADMIN: filter by site UUID
|
||||
const siteUuidHeader = req.headers.get('x-backone-site-uuid');
|
||||
const effectiveRole = user?._originalRole || user?.role;
|
||||
const isGlobalUser = effectiveRole === 'SUPER_ADMIN' || effectiveRole === 'EXECUTIVE';
|
||||
|
||||
const envSites = (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID || '')
|
||||
.split(',').map((s: string) => s.trim()).filter(Boolean);
|
||||
|
||||
if (isGlobalUser && siteUuidHeader && siteUuidHeader !== 'all' && envSites.includes(siteUuidHeader)) {
|
||||
filter.site_uuid = { $in: [siteUuidHeader, 'global'] };
|
||||
} else if (envSites.length > 0) {
|
||||
filter.site_uuid = { $in: [...envSites, 'global'] };
|
||||
}
|
||||
}
|
||||
|
||||
const agents = await db.collection('agent_registry')
|
||||
.find(filter)
|
||||
.project({ uuid: 1, label: 1, protected_label: 1, _id: 0 })
|
||||
.sort({ uuid: 1 })
|
||||
.toArray();
|
||||
|
||||
// Prioritaskan protected_label untuk mencegah UUID overwrite dari proxy lama
|
||||
const mappedAgents = agents.map((a: any) => ({
|
||||
uuid: a.uuid,
|
||||
label: a.protected_label || a.label || a.uuid,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ ok: true, data: mappedAgents });
|
||||
} catch (err: any) {
|
||||
console.error('[agents/list override]', err.message);
|
||||
return NextResponse.json({ ok: false, error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -52,10 +52,11 @@ export function Sidebar({ isMobile = false, onClose }: SidebarProps) {
|
||||
// Selalu pakai site_uuid dari DB (mereka tidak bisa ganti site).
|
||||
const globalRoles = ['SUPER_ADMIN', 'EXECUTIVE', 'SOC_ANALYST', 'ENGINEER'];
|
||||
const isGlobalRole = globalRoles.includes(data.user.role || '');
|
||||
const storedSiteUuid = localStorage.getItem('backone_site_uuid');
|
||||
|
||||
if (isGlobalRole) {
|
||||
const siteToUse = storedSiteUuid || 'all';
|
||||
// Karena UI site selector sudah diganti menjadi Agent Selector (View As),
|
||||
// Global roles harus selalu diset ke 'all' agar tidak tersangkut di filter site sebelumnya (dari akun lain).
|
||||
const siteToUse = 'all';
|
||||
localStorage.setItem('backone_site_uuid', siteToUse);
|
||||
setSelectedSite(siteToUse);
|
||||
} else if (data.user.site_uuid) {
|
||||
|
||||
@@ -30,10 +30,19 @@ export async function updateAgent(agentId: string, label: string) {
|
||||
return { success: false, message: "Agent tidak ditemukan di database lokal." };
|
||||
}
|
||||
|
||||
// Write both label AND protected_label.
|
||||
// protected_label is ONLY set by the UI — the Proxy NEVER touches it.
|
||||
// This ensures custom names survive even if the Proxy overwrites label field.
|
||||
await db.collection('agent_registry').updateOne(
|
||||
{ _id: agent._id },
|
||||
{ $set: { label: label } }
|
||||
{ $set: { label: label, protected_label: label } }
|
||||
);
|
||||
|
||||
// Bismillah: Hapus semua data duplikat/hantu milik agent ini agar tidak ditimpa oleh proxy lawas/bug ganda
|
||||
await db.collection('agent_registry').deleteMany({
|
||||
$or: [{ uuid: agentId }, { serial: agentId }],
|
||||
_id: { $ne: agent._id }
|
||||
});
|
||||
|
||||
// Also try to update the summaries if possible, so historical displays match
|
||||
await db.collection('summaries').updateMany(
|
||||
|
||||
@@ -89,7 +89,8 @@ export async function fetchAgentsFromMongo(
|
||||
}
|
||||
|
||||
// 3. Optimize status check and last seen per agent using parallel indexed queries
|
||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 3600000);
|
||||
// Agent dianggap Online jika ada flow dalam 1 jam terakhir
|
||||
const oneHourAgoQuery = new Date(Date.now() - 60 * 60000);
|
||||
|
||||
const agentResults = await Promise.all(
|
||||
agentSource.map(async (item: any, idx: number) => {
|
||||
@@ -99,7 +100,7 @@ export async function fetchAgentsFromMongo(
|
||||
// Execute status checks for active flow, latest flow, and latest summary in parallel
|
||||
const [activeFlow, latestFlow, latestSummary] = await Promise.all([
|
||||
db.collection('flows').findOne(
|
||||
{ agent_uuid: uuid, timestamp: { $gte: twentyFourHoursAgo } },
|
||||
{ agent_uuid: uuid, timestamp: { $gte: oneHourAgoQuery } },
|
||||
{ projection: { _id: 1 } }
|
||||
),
|
||||
db.collection('flows').findOne(
|
||||
@@ -112,8 +113,6 @@ export async function fetchAgentsFromMongo(
|
||||
),
|
||||
]);
|
||||
|
||||
const isOnline = !!activeFlow;
|
||||
|
||||
const registryLastSeenRaw = item.last_seen_at;
|
||||
let lastSeenDate: Date | null = null;
|
||||
if (registryLastSeenRaw?.date) {
|
||||
@@ -137,19 +136,26 @@ export async function fetchAgentsFromMongo(
|
||||
}
|
||||
}
|
||||
|
||||
const isOnline = !!activeFlow;
|
||||
|
||||
let statusHuman = 'No Flows Detected';
|
||||
if (isOnline) {
|
||||
if (lastSeenDate) {
|
||||
const now = new Date();
|
||||
const isToday = lastSeenDate.getDate() === now.getDate() &&
|
||||
lastSeenDate.getMonth() === now.getMonth() &&
|
||||
lastSeenDate.getFullYear() === now.getFullYear();
|
||||
const timeStr = lastSeenDate.toLocaleTimeString('id-ID', { timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit' }) + ' WIB';
|
||||
const dateStr = lastSeenDate.toLocaleDateString('id-ID', { timeZone: 'Asia/Jakarta', day: '2-digit', month: '2-digit' });
|
||||
statusHuman = isToday ? `Last seen: ${timeStr}` : `Last seen: ${dateStr} ${timeStr}`;
|
||||
if (lastSeenDate) {
|
||||
const now = new Date();
|
||||
const isToday = lastSeenDate.getDate() === now.getDate() &&
|
||||
lastSeenDate.getMonth() === now.getMonth() &&
|
||||
lastSeenDate.getFullYear() === now.getFullYear();
|
||||
const timeStr = lastSeenDate.toLocaleTimeString('id-ID', { timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit' }) + ' WIB';
|
||||
const dateStr = lastSeenDate.toLocaleDateString('id-ID', { timeZone: 'Asia/Jakarta', day: '2-digit', month: '2-digit' });
|
||||
|
||||
if (isOnline) {
|
||||
// Online: cukup tampilkan jam saja (hari ini pasti)
|
||||
statusHuman = `Last seen: ${timeStr}`;
|
||||
} else {
|
||||
statusHuman = 'Last seen: Just now';
|
||||
// Offline: selalu tampilkan tanggal + jam lengkap
|
||||
statusHuman = `Offline (Last seen: ${dateStr} ${timeStr})`;
|
||||
}
|
||||
} else if (isOnline) {
|
||||
statusHuman = 'Last seen: Just now';
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -160,7 +166,7 @@ export async function fetchAgentsFromMongo(
|
||||
organization_uuid: '',
|
||||
provisioned: item.provisioned ?? true,
|
||||
activated: true,
|
||||
label: item.label || uuid,
|
||||
label: item.protected_label || item.label || uuid,
|
||||
created_at: { human: 'N/A', date: '', unix_time: 0 },
|
||||
updated_at: { human: 'N/A', date: '', unix_time: 0 },
|
||||
last_seen_at: {
|
||||
|
||||
Reference in new issue
Block a user