Update timezone settings from Asia/Jakarta to Asia/Dili across configuration files and codebase. Enhance logging for crawling events and improve error handling in the CrawlingService. Add a new workspace configuration file for easier project management.

This commit is contained in:
Sony Surahmn
2025-12-12 14:25:47 +07:00
parent e5baaf003a
commit 4b75f11beb
9 changed files with 443 additions and 226 deletions

View File

@ -1,37 +1,36 @@
{ {
"app": { "app": {
"name": "SVC-HCM-CRAWLER", "name": "SVC-HCM-CRAWLER",
"description": "Hikvision Device Crawling Service", "description": "Hikvision Device Crawling Service",
"version": "1.0.0", "version": "1.0.0",
"env": "Development" "env": "Development"
}, },
"crawler": { "crawler": {
"interval": "*/1 * * * *", "interval": "*/1 * * * *",
"maxResults": 10, "maxResults": 10,
"timeout": 10000 "timeout": 10000
}, },
"database": { "database": {
"hcm": { "hcm": {
"engine": "mysql", "engine": "mysql",
"host": "127.0.0.1", "host": "127.0.0.1",
"port": "3306", "port": "3306",
"username": "root", "username": "root",
"password": "r00t@dm1n05", "password": "r00t@dm1n05",
"database": "tt_hcm", "database": "tt_hcm",
"logging": false, "logging": false,
"sync": false "sync": false
}
},
"bridge": {
"host": "127.0.0.1",
"port": "3000",
"endpoint": "/api/dooraccess/logs",
"timeout": 30000
},
"hikvision": {
"defaultUsername": "admin",
"defaultPassword": "Passwordhik_1",
"defaultPort": 80
} }
}, }
"bridge": {
"host": "127.0.0.1",
"port": "3000",
"endpoint": "/api/dooraccess/logs",
"timeout": 30000
},
"hikvision": {
"defaultUsername": "admin",
"defaultPassword": "Passwordhik_1",
"defaultPort": 80
}
}

View File

@ -1,37 +1,36 @@
{ {
"app": { "app": {
"name": "SVC-HCM-CRAWLER", "name": "SVC-HCM-CRAWLER",
"description": "Hikvision Device Crawling Service", "description": "Hikvision Device Crawling Service",
"version": "1.0.0", "version": "1.0.0",
"env": "Production" "env": "Production"
}, },
"crawler": { "crawler": {
"interval": "*/1 * * * *", "interval": "*/1 * * * *",
"maxResults": 10, "maxResults": 10,
"timeout": 10000 "timeout": 10000
}, },
"database": { "database": {
"hcm": { "hcm": {
"engine": "mysql", "engine": "mysql",
"host": "127.0.0.1", "host": "127.0.0.1",
"port": "3306", "port": "3306",
"username": "apphcm", "username": "apphcm",
"password": "$ppHCMTT#2024", "password": "$ppHCMTT#2024",
"database": "dbhcm", "database": "dbhcm",
"logging": false, "logging": false,
"sync": false "sync": false
}
},
"bridge": {
"host": "127.0.0.1",
"port": "3000",
"endpoint": "/api/dooraccess/logs",
"timeout": 30000
},
"hikvision": {
"defaultUsername": "admin",
"defaultPassword": "tt#2025Timor",
"defaultPort": 80
} }
}, }
"bridge": {
"host": "127.0.0.1",
"port": "3000",
"endpoint": "/api/dooraccess/logs",
"timeout": 30000
},
"hikvision": {
"defaultUsername": "admin",
"defaultPassword": "Passwordhik_1",
"defaultPort": 80
}
}

View File

@ -12,17 +12,16 @@ module.exports = {
log_date_format: "YYYY-MM-DD HH:mm:ss Z", log_date_format: "YYYY-MM-DD HH:mm:ss Z",
merge_logs: true, merge_logs: true,
env: { env: {
NODE_ENV: "default", NODE_ENV: "development",
TZ: "Asia/Jakarta" TZ: "Asia/Dili",
}, },
env_production: { env_production: {
NODE_ENV: "prod", NODE_ENV: "prod",
TZ: "Asia/Jakarta" TZ: "Asia/Dili",
}, },
autorestart: true, autorestart: true,
max_restarts: 10, max_restarts: 10,
min_uptime: "10s" min_uptime: "10s",
} },
] ],
}; };

View File

@ -5,7 +5,7 @@
"exec": "ts-node src/index.ts", "exec": "ts-node src/index.ts",
"env": { "env": {
"NODE_ENV": "default", "NODE_ENV": "default",
"TZ": "Asia/Jakarta" "TZ": "Asia/Dili"
} }
} }

View File

@ -1,45 +1,46 @@
{ {
"name": "svc-hcm-crawler", "name": "svc-hcm-crawler",
"version": "1.0.0", "version": "1.0.0",
"description": "Hikvision Device Crawling Service for HCM Bridge", "description": "Hikvision Device Crawling Service for HCM Bridge",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"scripts": { "scripts": {
"start": "TZ='Asia/Jakarta' node dist/src/index.js", "start": "TZ='Asia/Dili' node dist/src/index.js",
"dev": "nodemon", "dev": "nodemon",
"build": "tsc", "build": "tsc",
"pm2:start": "pm2 start ecosystem.config.js", "pm2:start": "pm2 start ecosystem.config.js",
"pm2:stop": "pm2 stop svc-hcm-crawler", "pm2:start:prod": "pm2 start ecosystem.config.js --env production",
"pm2:restart": "pm2 restart svc-hcm-crawler", "pm2:stop": "pm2 stop svc-hcm-crawler",
"pm2:delete": "pm2 delete svc-hcm-crawler", "pm2:restart": "pm2 restart svc-hcm-crawler",
"pm2:logs": "pm2 logs svc-hcm-crawler" "pm2:restart:prod": "pm2 restart ecosystem.config.js --env production",
}, "pm2:delete": "pm2 delete svc-hcm-crawler",
"keywords": [ "pm2:logs": "pm2 logs svc-hcm-crawler"
"hikvision", },
"crawler", "keywords": [
"hcm" "hikvision",
], "crawler",
"author": "STS", "hcm"
"license": "ISC", ],
"dependencies": { "author": "STS",
"@types/xml2js": "^0.4.14", "license": "ISC",
"axios": "^1.7.8", "dependencies": {
"config": "^3.3.12", "@types/xml2js": "^0.4.14",
"entity": "file:../entity", "axios": "^1.7.8",
"moment-timezone": "~0.5.46", "config": "^3.3.12",
"mysql2": "^3.12.0", "entity": "file:../entity",
"node-cron": "^4.1.0", "moment-timezone": "~0.5.46",
"reflect-metadata": "^0.2.2", "mysql2": "^3.12.0",
"tslog": "^4.9.3", "node-cron": "^4.1.0",
"typeorm": "^0.3.28", "reflect-metadata": "^0.2.2",
"typescript": "^5.9.3", "tslog": "^4.9.3",
"uuid": "^11.0.3", "typeorm": "^0.3.28",
"xml2js": "^0.6.2" "typescript": "^5.9.3",
}, "uuid": "^11.0.3",
"devDependencies": { "xml2js": "^0.6.2"
"@types/node": "^20.0.0", },
"@types/node-cron": "^3.0.11", "devDependencies": {
"nodemon": "^3.0.0", "@types/node": "^20.0.0",
"ts-node": "^10.9.0" "@types/node-cron": "^3.0.11",
} "nodemon": "^3.0.0",
} "ts-node": "^10.9.0"
}
}

View File

@ -27,13 +27,12 @@ export class OrmHelper {
entities: entities, entities: entities,
subscribers: [], subscribers: [],
migrations: [], migrations: [],
extra: {
query: "SET TIMEZONE='Asia/Jakarta';",
},
}); });
try { try {
await OrmHelper.DB.initialize(); await OrmHelper.DB.initialize();
// Set timezone for MySQL connection (Asia/Dili = UTC+9)
await OrmHelper.DB.query("SET time_zone = '+09:00'");
log.info("Database initialized successfully"); log.info("Database initialized successfully");
} catch (error: any) { } catch (error: any) {
log.error("Database initialization failed:", error); log.error("Database initialization failed:", error);
@ -41,4 +40,3 @@ export class OrmHelper {
} }
} }
} }

View File

@ -6,7 +6,7 @@ import { OrmHelper } from "./helpers/orm";
import { CrawlingService } from "./services/crawling"; import { CrawlingService } from "./services/crawling";
// Set timezone // Set timezone
moment.tz.setDefault("Asia/Jakarta"); moment.tz.setDefault("Asia/Dili");
const log: Logger<ILogObj> = new Logger({ const log: Logger<ILogObj> = new Logger({
name: "[CrawlerIndex]", name: "[CrawlerIndex]",
@ -38,35 +38,89 @@ async function startCrawlingService() {
// Start cron job // Start cron job
const cronInterval = config.get("crawler.interval"); const cronInterval = config.get("crawler.interval");
log.info(`Starting crawling service with interval: ${cronInterval}`); log.info(`Starting crawling service with interval: ${cronInterval}`);
log.info(`Timezone: Asia/Dili`);
const crawlingJob = cron.schedule(cronInterval, async () => { // Schedule cron job with timezone support
if (isCrawlingRunning) { // node-cron v3+ supports timezone option
log.warn("Crawling service is still running. Skipping this schedule."); const crawlingJob = cron.schedule(
return; cronInterval,
async () => {
const scheduleTime = moment()
.tz("Asia/Dili")
.format("YYYY-MM-DD HH:mm:ss");
const utcTime = new Date().toISOString();
if (isCrawlingRunning) {
log.warn(
`[Cron] ⚠️ Crawling service is still running. Skipping schedule at ${scheduleTime} (UTC: ${utcTime})`
);
return;
}
log.info(`[Cron] ⏰ ========== CRON TRIGGERED ==========`);
log.info(`[Cron] ⏰ Schedule time: ${scheduleTime} (UTC: ${utcTime})`);
isCrawlingRunning = true;
try {
await CrawlingService.runCrawling();
const completeTime = moment()
.tz("Asia/Dili")
.format("YYYY-MM-DD HH:mm:ss");
log.info(`[Cron] ✅ Crawling service completed at ${completeTime}`);
} catch (error) {
const errorTime = moment()
.tz("Asia/Dili")
.format("YYYY-MM-DD HH:mm:ss");
log.error(
`[Cron] ❌ Crawling service failed at ${errorTime}:`,
error
);
} finally {
isCrawlingRunning = false;
log.info(`[Cron] ==========================================`);
}
},
{
timezone: "Asia/Dili",
} }
log.info(`Running crawling service at: ${new Date().toLocaleString()}`);
isCrawlingRunning = true;
try {
await CrawlingService.runCrawling();
log.info(`Crawling service completed at: ${new Date().toLocaleString()}`);
} catch (error) {
log.error("Crawling service failed:", error);
} finally {
isCrawlingRunning = false;
}
});
log.info(
`Crawling service started. App: ${config.get("app.name")} v${config.get("app.version")}`
); );
log.info(`Environment: ${config.get("app.env")}`);
log.info(`Cron schedule: ${cronInterval}`);
// Run immediately on startup (optional) // Verify cron job is scheduled
// Uncomment the line below if you want to run crawling immediately on startup if (crawlingJob) {
// await CrawlingService.runCrawling(); const nextRun = moment()
.tz("Asia/Dili")
.add(1, "minute")
.startOf("minute")
.format("YYYY-MM-DD HH:mm:ss");
const currentTime = moment()
.tz("Asia/Dili")
.format("YYYY-MM-DD HH:mm:ss");
log.info("═══════════════════════════════════════════════════════════");
log.info(`[Cron] ✅ Cron job scheduled successfully`);
log.info(
`[Cron] App: ${config.get("app.name")} v${config.get("app.version")}`
);
log.info(`[Cron] Environment: ${config.get("app.env")}`);
log.info(`[Cron] Schedule pattern: ${cronInterval} (every minute)`);
log.info(`[Cron] Timezone: Asia/Dili`);
log.info(`[Cron] Current time: ${currentTime}`);
log.info(`[Cron] Next run scheduled at: ${nextRun}`);
log.info("═══════════════════════════════════════════════════════════");
} else {
log.error("[Cron] ❌ Failed to schedule cron job");
throw new Error("Failed to schedule cron job");
}
// Run immediately on startup to verify everything works
log.info("[Cron] Running initial crawl on startup...");
try {
await CrawlingService.runCrawling();
log.info("[Cron] ✅ Initial crawl completed successfully");
} catch (error) {
log.error("[Cron] ❌ Initial crawl failed:", error);
// Don't exit - let the cron job continue
}
} catch (error) { } catch (error) {
log.error("Failed to start crawling service:", error); log.error("Failed to start crawling service:", error);
process.exit(1); process.exit(1);
@ -106,4 +160,3 @@ process.on("uncaughtException", (error) => {
// Start the service // Start the service
startCrawlingService(); startCrawlingService();

View File

@ -945,16 +945,26 @@ export class CrawlingService {
const url = `http://${deviceIp}:${port}/ISAPI/AccessControl/AcsEvent?format=json`; const url = `http://${deviceIp}:${port}/ISAPI/AccessControl/AcsEvent?format=json`;
// Get device MAC address (cache it for subsequent requests) // Get device MAC address (cache it for subsequent requests)
log.info(`[Crawl Device] Fetching MAC address for device ${deviceIp}...`);
console.log( console.log(
`[CrawlingService] Getting MAC address for device ${deviceIp}...` `[CrawlingService] Getting MAC address for device ${deviceIp}...`
); );
const macStartTime = Date.now();
const macAddress = await this.getDeviceInfo(deviceIp, username, password); const macAddress = await this.getDeviceInfo(deviceIp, username, password);
const macDuration = Date.now() - macStartTime;
if (!macAddress) { if (!macAddress) {
log.warn(
`[Crawl Device] ⚠️ Could not get MAC address for device ${deviceIp} (took ${macDuration}ms). Events will be saved without MAC address.`
);
console.warn( console.warn(
`[CrawlingService] Warning: Could not get MAC address for device ${deviceIp}. Events will be saved without MAC address.` `[CrawlingService] Warning: Could not get MAC address for device ${deviceIp}. Events will be saved without MAC address.`
); );
} else { } else {
log.info(
`[Crawl Device] ✓ MAC address retrieved: ${macAddress} for device ${deviceIp} (took ${macDuration}ms)`
);
console.log( console.log(
`[CrawlingService] Successfully retrieved MAC address ${macAddress} for device ${deviceIp}` `[CrawlingService] Successfully retrieved MAC address ${macAddress} for device ${deviceIp}`
); );
@ -973,37 +983,75 @@ export class CrawlingService {
}; };
log.info( log.info(
`Crawling device ${deviceIp} from position ${state.lastPosition}` `[Crawl Device] Starting crawl for device ${deviceIp} from position ${state.lastPosition} (SearchID: ${state.searchID})`
); );
// Make API call with digest auth // Make API call with digest auth
log.info(
`[Crawl Device] Making API request to ${url} with maxResults: ${requestPayload.AcsEventCond.maxResults}`
);
console.log(`[CrawlingService] Making request to ${url}`); console.log(`[CrawlingService] Making request to ${url}`);
const requestStartTime = Date.now();
const response: HikvisionResponse = await this.makeDigestRequest( const response: HikvisionResponse = await this.makeDigestRequest(
url, url,
username, username,
password, password,
requestPayload requestPayload
); );
const requestDuration = Date.now() - requestStartTime;
log.info(
`[Crawl Device] API request completed in ${requestDuration}ms for device ${deviceIp}`
);
if (!response || !response.AcsEvent || !response.AcsEvent.InfoList) { if (!response || !response.AcsEvent || !response.AcsEvent.InfoList) {
log.warn(`No events found for device ${deviceIp}`); log.warn(
`[Crawl Device] ⚠️ No events found for device ${deviceIp} - Response status: ${
response?.AcsEvent?.responseStatusStrg || "N/A"
}`
);
return; return;
} }
const events = response.AcsEvent.InfoList; const events = response.AcsEvent.InfoList;
log.info(`Found ${events.length} events from device ${deviceIp}`); log.info(
`[Crawl Device] Found ${events.length} event(s) from device ${deviceIp} - Total matches: ${response.AcsEvent.totalMatches}, Status: ${response.AcsEvent.responseStatusStrg}`
);
// Process each event // Process each event
log.info(
`[Crawl Device] Processing ${
events.length
} event(s) from device ${deviceIp} (MAC: ${macAddress || "NONE"})`
);
console.log( console.log(
`[CrawlingService] Processing ${ `[CrawlingService] Processing ${
events.length events.length
} events for device ${deviceIp} with MAC: ${macAddress || "NONE"}` } events for device ${deviceIp} with MAC: ${macAddress || "NONE"}`
); );
for (const event of events) {
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < events.length; i++) {
const event = events[i];
try { try {
console.log( log.info(
`[CrawlingService] Mapping event for employee ${event.employeeNoString} from device ${deviceIp}` `[Crawl Device] Processing event ${i + 1}/${
events.length
} - Employee: ${
event.employeeNoString
}, Device: ${deviceIp}, Time: ${event.time}, Status: ${
event.attendanceStatus
}`
); );
console.log(
`[CrawlingService] Mapping event ${i + 1}/${
events.length
} for employee ${event.employeeNoString} from device ${deviceIp}`
);
// Map to webhook format (now async to get employee accessdoorid) // Map to webhook format (now async to get employee accessdoorid)
const webhookEvent = await this.mapHikvisionToWebhook( const webhookEvent = await this.mapHikvisionToWebhook(
event, event,
@ -1011,93 +1059,107 @@ export class CrawlingService {
macAddress macAddress
); );
log.info(
`[Crawl Device] Mapped event - EmployeeID: ${
webhookEvent.AccessControllerEvent?.employeeNoString
}, DeviceName: ${
webhookEvent.AccessControllerEvent?.deviceName
}, MAC: ${webhookEvent.macAddress || "EMPTY"}, DateTime: ${
webhookEvent.dateTime
}`
);
console.log( console.log(
`[CrawlingService] Sending event to internal endpoint. MAC in payload: ${ `[CrawlingService] Sending event ${i + 1}/${
events.length
} to internal endpoint. MAC in payload: ${
webhookEvent.macAddress || "EMPTY" webhookEvent.macAddress || "EMPTY"
}` }`
); );
// Call internal endpoint // Call internal endpoint
await this.sendToInternalEndpoint(webhookEvent); await this.sendToInternalEndpoint(webhookEvent);
} catch (error) { successCount++;
console.error( } catch (error: any) {
`[CrawlingService] Error processing event from device ${deviceIp}:`, errorCount++;
error const errorMsg = error?.message || String(error);
log.error(
`[Crawl Device] ✗ Error processing event ${i + 1}/${
events.length
} from device ${deviceIp} - Employee: ${
event.employeeNoString
}, Time: ${event.time}, Error: ${errorMsg}`
);
console.error(
`[CrawlingService] Error processing event ${i + 1}/${
events.length
} from device ${deviceIp}:`,
errorMsg
); );
log.error(`Error processing event from device ${deviceIp}:`, error);
} }
} }
log.info(
`[Crawl Device] Completed processing ${events.length} event(s) from device ${deviceIp} - Success: ${successCount}, Errors: ${errorCount}`
);
// Update position for next crawl // Update position for next crawl
state.lastPosition += events.length; state.lastPosition += events.length;
log.info(
`[Crawl Device] Updated crawl position for device ${deviceIp} - New position: ${state.lastPosition}, Total matches: ${response.AcsEvent.totalMatches}, Status: ${response.AcsEvent.responseStatusStrg}`
);
// If there are more matches, we'll continue in next crawl // If there are more matches, we'll continue in next crawl
if (response.AcsEvent.responseStatusStrg === "MORE") { if (response.AcsEvent.responseStatusStrg === "MORE") {
log.info( log.info(
`More events available for device ${deviceIp}. Total: ${response.AcsEvent.totalMatches}` `[Crawl Device] ⏭️ More events available for device ${deviceIp} - Total: ${response.AcsEvent.totalMatches}, Current position: ${state.lastPosition}`
); );
} else { } else {
// Reset position if no more events // Reset position if no more events
log.info(
`[Crawl Device] ✅ All events processed for device ${deviceIp} - Resetting position to 0`
);
state.lastPosition = 0; state.lastPosition = 0;
state.searchID = uuidv4(); state.searchID = uuidv4();
} }
} catch (error) { } catch (error: any) {
log.error(`Error crawling device ${deviceIp}:`, error); const errorMsg = error?.message || String(error);
log.error(
`[Crawl Device] ❌ Error crawling device ${deviceIp}: ${errorMsg}`
);
if (error?.response) {
log.error(
`[Crawl Device] Response details - Status: ${
error.response.status
}, Data: ${JSON.stringify(error.response.data)}`
);
}
// Reset state on error // Reset state on error
if (crawlingState[deviceIp]) { if (crawlingState[deviceIp]) {
log.info(
`[Crawl Device] Resetting crawl state for device ${deviceIp} due to error`
);
crawlingState[deviceIp].lastPosition = 0; crawlingState[deviceIp].lastPosition = 0;
crawlingState[deviceIp].searchID = uuidv4(); crawlingState[deviceIp].searchID = uuidv4();
} }
} }
} }
/**
* Check if event already exists in database (duplicate check)
*/
static async isDuplicateEvent(eventData: any): Promise<boolean> {
try {
const accessdoorid = parseInt(
eventData.AccessControllerEvent?.employeeNoString || "0"
);
const datelogs = moment.parseZone(eventData.dateTime).toDate();
const devicename = eventData.AccessControllerEvent?.deviceName || "";
const macaddress = eventData.macAddress || "";
// Check if a record with the same accessdoorid, datelogs (within 1 second), devicename, and macaddress exists
const existing = await OrmHelper.DB.query(
`
SELECT _idx
FROM tbl_attendancedoorlogs
WHERE accessdoorid = ?
AND devicename = ?
AND macaddress = ?
AND ABS(TIMESTAMPDIFF(SECOND, datelogs, ?)) <= 1
AND isdeleted = 0
LIMIT 1
`,
[accessdoorid, devicename, macaddress, datelogs]
);
return existing && existing.length > 0;
} catch (error) {
log.error("Error checking duplicate event:", error);
// If there's an error checking, assume it's not a duplicate to avoid blocking valid events
return false;
}
}
/** /**
* Send event to internal endpoint * Send event to internal endpoint
*/ */
static async sendToInternalEndpoint(eventData: any): Promise<void> { static async sendToInternalEndpoint(eventData: any): Promise<void> {
const employeeId =
eventData.AccessControllerEvent?.employeeNoString || "N/A";
const deviceName = eventData.AccessControllerEvent?.deviceName || "N/A";
const dateTime = eventData.dateTime || "N/A";
const macAddress = eventData.macAddress || "N/A";
const ipAddress = eventData.ipAddress || "N/A";
try { try {
// Check for duplicates before sending log.info(
const isDuplicate = await this.isDuplicateEvent(eventData); `[Send Event] Preparing to send event - EmployeeID: ${employeeId}, Device: ${deviceName}, DateTime: ${dateTime}, IP: ${ipAddress}, MAC: ${macAddress}`
if (isDuplicate) { );
log.info(
`Skipping duplicate event: accessdoorid=${eventData.AccessControllerEvent?.employeeNoString}, datelogs=${eventData.dateTime}, devicename=${eventData.AccessControllerEvent?.deviceName}`
);
return;
}
const bridgeHost = config.get("bridge.host"); const bridgeHost = config.get("bridge.host");
const bridgePort = config.get("bridge.port"); const bridgePort = config.get("bridge.port");
@ -1106,7 +1168,12 @@ export class CrawlingService {
const url = `http://${bridgeHost}:${bridgePort}${bridgeEndpoint}`; const url = `http://${bridgeHost}:${bridgePort}${bridgeEndpoint}`;
log.info(
`[Send Event] Sending to bridge - URL: ${url}, EmployeeID: ${employeeId}, Device: ${deviceName}, DateTime: ${dateTime}`
);
// Send as form data with event_log field (matching the webhook format) // Send as form data with event_log field (matching the webhook format)
const startTime = Date.now();
await axios.post( await axios.post(
url, url,
{ event_log: JSON.stringify(eventData) }, { event_log: JSON.stringify(eventData) },
@ -1117,10 +1184,23 @@ export class CrawlingService {
}, },
} }
); );
const duration = Date.now() - startTime;
log.info(`Successfully sent event to bridge endpoint: ${url}`); log.info(
} catch (error) { `[Send Event] ✅ SUCCESS - Sent to bridge in ${duration}ms - EmployeeID: ${employeeId}, Device: ${deviceName}, DateTime: ${dateTime}, IP: ${ipAddress}, MAC: ${macAddress}`
log.error("Error sending to internal endpoint:", error); );
} catch (error: any) {
const errorMsg = error?.message || String(error);
const statusCode = error?.response?.status || "N/A";
log.error(
`[Send Event] ❌ FAILED - EmployeeID: ${employeeId}, Device: ${deviceName}, DateTime: ${dateTime}, IP: ${ipAddress}, MAC: ${macAddress}, Error: ${errorMsg}, Status: ${statusCode}`
);
if (error?.response?.data) {
log.error(
`[Send Event] Response data:`,
JSON.stringify(error.response.data)
);
}
throw error; throw error;
} }
} }
@ -1129,67 +1209,138 @@ export class CrawlingService {
* Run crawling for all devices that need it * Run crawling for all devices that need it
*/ */
static async runCrawling(): Promise<void> { static async runCrawling(): Promise<void> {
const startTime = Date.now();
const timestamp = moment().format("YYYY-MM-DD HH:mm:ss");
try { try {
log.info("═══════════════════════════════════════════════════════════");
log.info(`[Crawling Service] 🚀 Starting crawling cycle at ${timestamp}`);
log.info("═══════════════════════════════════════════════════════════");
console.log("[CrawlingService] Starting crawling service..."); console.log("[CrawlingService] Starting crawling service...");
log.info("Starting crawling service...");
// Check database connection // Check database connection
if (!OrmHelper.DB || !OrmHelper.DB.isInitialized) { if (!OrmHelper.DB || !OrmHelper.DB.isInitialized) {
const errorMsg = "Database not initialized"; const errorMsg = "Database not initialized";
console.error(`[CrawlingService] ${errorMsg}`); console.error(`[CrawlingService] ${errorMsg}`);
log.error(errorMsg); log.error(`[Crawling Service] ❌ ${errorMsg}`);
throw new Error(errorMsg); throw new Error(errorMsg);
} }
log.info(`[Crawling Service] ✓ Database connection verified`);
const devices = await this.getCrawlingDevices(); const devices = await this.getCrawlingDevices();
console.log(`[CrawlingService] Found ${devices.length} devices to crawl`); console.log(`[CrawlingService] Found ${devices.length} devices to crawl`);
log.info(`Found ${devices.length} devices to crawl`); log.info(`[Crawling Service] Found ${devices.length} device(s) to crawl`);
if (devices.length === 0) { if (devices.length === 0) {
console.log( console.log(
"[CrawlingService] No devices to crawl (check if devices have brand='HIKVISION' and flag=1)" "[CrawlingService] No devices to crawl (check if devices have brand='HIKVISION' and flag=1)"
); );
log.info("No devices to crawl"); log.info(
`[Crawling Service] ⚠️ No devices found - Check if devices have brand='HIKVISION' and (flag=1 OR iscrawling=1)`
);
return; return;
} }
// Log device details // Log device details
devices.forEach((device) => { log.info(`[Crawling Service] Device list:`);
devices.forEach((device, index) => {
log.info(
` ${index + 1}. IP: ${device.deviceip}, DeviceID: ${
device.deviceid
}, Location: ${device.location || "N/A"}, Flag: ${
device.flag || "N/A"
}, IsCrawling: ${device.iscrawling || "N/A"}`
);
console.log( console.log(
`[CrawlingService] Device found: ${device.deviceip} (flag: ${device.flag}, iscrawling: ${device.iscrawling})` `[CrawlingService] Device ${index + 1}: ${device.deviceip} (flag: ${
device.flag
}, iscrawling: ${device.iscrawling})`
); );
}); });
let totalDevicesProcessed = 0;
let totalDevicesSuccess = 0;
let totalDevicesError = 0;
// Crawl each device // Crawl each device
for (const device of devices) { for (let i = 0; i < devices.length; i++) {
const device = devices[i];
try { try {
console.log(`[CrawlingService] Crawling device: ${device.deviceip}`); log.info(
`[Crawling Service] ───────────────────────────────────────────────────`
);
log.info(
`[Crawling Service] Processing device ${i + 1}/${devices.length}: ${
device.deviceip
}`
);
console.log(
`[CrawlingService] Crawling device ${i + 1}/${devices.length}: ${
device.deviceip
}`
);
const deviceStartTime = Date.now();
await this.crawlDevice(device); await this.crawlDevice(device);
const deviceDuration = Date.now() - deviceStartTime;
totalDevicesProcessed++;
totalDevicesSuccess++;
log.info(
`[Crawling Service] ✓ Device ${device.deviceip} completed in ${deviceDuration}ms`
);
// Add small delay between devices to avoid overwhelming // Add small delay between devices to avoid overwhelming
await new Promise((resolve) => setTimeout(resolve, 1000)); if (i < devices.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
} catch (error: any) { } catch (error: any) {
totalDevicesProcessed++;
totalDevicesError++;
const errorMsg = error?.message || String(error); const errorMsg = error?.message || String(error);
log.error(
`[Crawling Service] ✗ Device ${device.deviceip} failed: ${errorMsg}`
);
console.error( console.error(
`[CrawlingService] Error crawling device ${device.deviceip}:`, `[CrawlingService] Error crawling device ${device.deviceip}:`,
errorMsg errorMsg
); );
if (error?.response) { if (error?.response) {
log.error(
`[Crawling Service] Response status: ${
error.response.status
}, Response data: ${JSON.stringify(error.response.data)}`
);
console.error( console.error(
`[CrawlingService] Response status: ${error.response.status}`, `[CrawlingService] Response status: ${error.response.status}`,
`Response data:`, `Response data:`,
JSON.stringify(error.response.data) JSON.stringify(error.response.data)
); );
} }
log.error(`Error crawling device ${device.deviceip}:`, error);
} }
} }
const totalDuration = Date.now() - startTime;
log.info("═══════════════════════════════════════════════════════════");
log.info(
`[Crawling Service] ✅ Crawling cycle completed in ${totalDuration}ms`
);
log.info(
`[Crawling Service] Summary - Total: ${totalDevicesProcessed}, Success: ${totalDevicesSuccess}, Errors: ${totalDevicesError}`
);
log.info("═══════════════════════════════════════════════════════════");
console.log("[CrawlingService] Crawling service completed"); console.log("[CrawlingService] Crawling service completed");
log.info("Crawling service completed");
} catch (error: any) { } catch (error: any) {
const totalDuration = Date.now() - startTime;
const errorMsg = error?.message || String(error); const errorMsg = error?.message || String(error);
log.error("═══════════════════════════════════════════════════════════");
log.error(
`[Crawling Service] ❌ Fatal error after ${totalDuration}ms: ${errorMsg}`
);
log.error("═══════════════════════════════════════════════════════════");
console.error("[CrawlingService] Error in crawling service:", errorMsg); console.error("[CrawlingService] Error in crawling service:", errorMsg);
log.error("Error in crawling service:", error);
throw error; // Re-throw so the caller can handle it throw error; // Re-throw so the caller can handle it
} }
} }

View File

@ -0,0 +1,17 @@
{
"folders": [
{
"path": "../../../tt_hcm"
},
{
"path": "../../../svc-hcm"
},
{
"path": "../../../entity"
},
{
"path": "../.."
}
],
"settings": {}
}