2025-10-23 19:02:56 -04:00
|
|
|
// Load environment variables
|
|
|
|
|
require('dotenv').config();
|
|
|
|
|
const { checkAndUpdate } = require('./updater');
|
|
|
|
|
|
|
|
|
|
const Fastify = require('fastify');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const database = require('./database');
|
|
|
|
|
const config = require('./config');
|
|
|
|
|
const apiClient = require('./api-client');
|
|
|
|
|
const printer = require('./printer');
|
|
|
|
|
const PrintQueueWorker = require('./print-worker');
|
2026-03-01 17:10:03 -05:00
|
|
|
const AbandonedCallPoller = require('./abandoned-call-poller');
|
2025-10-23 19:02:56 -04:00
|
|
|
|
|
|
|
|
const fastify = Fastify({
|
|
|
|
|
logger: true
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-01 17:12:39 -05:00
|
|
|
const isDev = false;
|
2025-10-23 19:02:56 -04:00
|
|
|
|
|
|
|
|
// Order Poller Class
|
|
|
|
|
class OrderPoller {
|
|
|
|
|
constructor(apiClient, database, printer) {
|
|
|
|
|
this.apiClient = apiClient;
|
|
|
|
|
this.db = database;
|
|
|
|
|
this.printer = printer;
|
|
|
|
|
this.intervalId = null;
|
|
|
|
|
this.isPolling = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async start() {
|
|
|
|
|
console.log('Starting order poller...');
|
|
|
|
|
|
|
|
|
|
// Initial poll
|
|
|
|
|
this.poll();
|
|
|
|
|
|
|
|
|
|
// Schedule recurring polls
|
|
|
|
|
this.scheduleNextPoll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scheduleNextPoll() {
|
|
|
|
|
const appConfig = config.getAll();
|
|
|
|
|
const interval = parseInt(appConfig.pollingInterval, 10) || 15000;
|
|
|
|
|
|
|
|
|
|
if (this.intervalId) {
|
|
|
|
|
clearTimeout(this.intervalId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.intervalId = setTimeout(() => {
|
|
|
|
|
this.poll();
|
|
|
|
|
this.scheduleNextPoll();
|
|
|
|
|
}, interval);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async poll() {
|
|
|
|
|
if (this.isPolling) {
|
|
|
|
|
console.log('Poll already in progress, skipping...');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.isPolling = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const appConfig = config.getAll();
|
|
|
|
|
|
|
|
|
|
// Check if configured
|
|
|
|
|
if (!appConfig.authToken || !appConfig.selectedBotId) {
|
|
|
|
|
console.log('Not configured yet, skipping poll');
|
|
|
|
|
this.isPolling = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 17:10:03 -05:00
|
|
|
// Attempt token refresh if expired or near expiry
|
|
|
|
|
if (apiClient.isTokenNearExpiry(appConfig.tokenExpiry, 7)) {
|
|
|
|
|
const refreshed = await apiClient.ensureValidToken();
|
|
|
|
|
if (!refreshed) {
|
|
|
|
|
console.log('Token expired and refresh failed, please re-login');
|
|
|
|
|
this.isPolling = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Re-read config after refresh so the poll uses the new token
|
|
|
|
|
Object.assign(appConfig, config.getAll());
|
2025-10-23 19:02:56 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get last synced order ID
|
|
|
|
|
const lastOrder = this.db.getLastOrder();
|
|
|
|
|
const afterId = lastOrder ? lastOrder.order_id : 0;
|
|
|
|
|
|
|
|
|
|
console.log(`Polling for new orders (afterId: ${afterId})...`);
|
|
|
|
|
|
|
|
|
|
// Fetch new orders from API
|
|
|
|
|
const result = await this.apiClient.getOrders(
|
|
|
|
|
appConfig.authToken,
|
|
|
|
|
appConfig.selectedBotId,
|
|
|
|
|
afterId,
|
|
|
|
|
{ includeCanceled: true, limit: 50 }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
|
console.error('Failed to fetch orders:', result.message);
|
|
|
|
|
this.isPolling = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const orders = result.orders || [];
|
|
|
|
|
console.log(`Received ${orders.length} orders from API`);
|
|
|
|
|
|
|
|
|
|
// Process each order
|
|
|
|
|
for (const order of orders) {
|
|
|
|
|
const existingOrder = this.db.getOrderById(order.id);
|
|
|
|
|
|
|
|
|
|
if (!existingOrder) {
|
|
|
|
|
// New order - save and print
|
|
|
|
|
console.log(`New order detected: #${order.id}`);
|
|
|
|
|
this.db.insertOrder(order);
|
|
|
|
|
|
|
|
|
|
// Initialize printer if needed
|
|
|
|
|
if (!this.printer.printer) {
|
|
|
|
|
this.printer.initializePrinter(appConfig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add to print queue with deduplication check
|
|
|
|
|
const newJobId = this.db.addToPrintQueue(order.id, 'new');
|
|
|
|
|
if (!newJobId) {
|
|
|
|
|
console.log(`Print job not created for order #${order.id} (duplicate prevention)`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.db.markPrintJobProcessing(newJobId);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Use per-printer config system
|
|
|
|
|
const printerConfigs = this.db.getEnabledPrinters();
|
|
|
|
|
let result;
|
|
|
|
|
|
|
|
|
|
if (printerConfigs && printerConfigs.length > 0) {
|
|
|
|
|
result = await this.printer.printOrderReceiptWithPrinterConfigs(order, printerConfigs, 'new');
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback to legacy system
|
|
|
|
|
if (!this.printer.printer || !this.printer.config) {
|
|
|
|
|
const currentConfig = require('./config').getAll();
|
|
|
|
|
this.printer.initializePrinter(currentConfig);
|
|
|
|
|
}
|
|
|
|
|
result = await this.printer.printOrderReceipt(order, 'new');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result && result.success) {
|
|
|
|
|
this.db.markOrderPrinted(order.id);
|
|
|
|
|
this.db.markPrintJobCompleted(newJobId);
|
|
|
|
|
// Cleanup any other pending jobs for this order+type
|
|
|
|
|
this.db.cleanupDuplicateJobs(newJobId, order.id, 'new');
|
|
|
|
|
console.log(`✓ Receipt printed for order #${order.id}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.error(`✗ Print result indicates failure for order #${order.id}:`, result && result.error ? result.error : 'Unknown error');
|
|
|
|
|
this.db.markPrintJobPending(newJobId);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`✗ Failed to print order #${order.id}:`, error.message);
|
|
|
|
|
console.log(' Order saved to database. You can reprint from dashboard.');
|
|
|
|
|
this.db.markPrintJobPending(newJobId);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Check for status changes (cancellation status from backend)
|
|
|
|
|
const statusChanged = existingOrder.status !== order.status;
|
|
|
|
|
|
|
|
|
|
if (statusChanged) {
|
|
|
|
|
console.log(`Order #${order.id} status changed: ${existingOrder.status} → ${order.status}`);
|
|
|
|
|
|
|
|
|
|
this.db.updateOrder(order);
|
|
|
|
|
|
|
|
|
|
// Print cancellation receipt if order was canceled
|
|
|
|
|
if (order.status === 'canceled' && existingOrder.status !== 'canceled') {
|
|
|
|
|
console.log(`Order #${order.id} was canceled - printing cancellation receipt...`);
|
|
|
|
|
|
|
|
|
|
// Update local status to 'canceled' so it shows on the dashboard
|
|
|
|
|
this.db.updateOrderStatus(order.id, 'canceled');
|
|
|
|
|
|
|
|
|
|
// Check if cancellation already printed to prevent duplicates
|
|
|
|
|
if (this.db.hasPrintedCancellation(order.id)) {
|
|
|
|
|
console.log(`Skipping duplicate cancellation print for order #${order.id}`);
|
|
|
|
|
} else {
|
|
|
|
|
// Add to print queue with deduplication check
|
|
|
|
|
const jobId = this.db.addToPrintQueue(order.id, 'canceled');
|
|
|
|
|
if (!jobId) {
|
|
|
|
|
console.log(`Cancellation job not created for order #${order.id} (duplicate prevention)`);
|
|
|
|
|
} else {
|
|
|
|
|
this.db.markPrintJobProcessing(jobId);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const cancelReason = order.cancellationReason || order.order?.cancellationReason || 'Order canceled';
|
|
|
|
|
|
|
|
|
|
// Use per-printer config system
|
|
|
|
|
const printerConfigs = this.db.getEnabledPrinters();
|
|
|
|
|
let result;
|
|
|
|
|
|
|
|
|
|
if (printerConfigs && printerConfigs.length > 0) {
|
|
|
|
|
result = await this.printer.printOrderReceiptWithPrinterConfigs(order, printerConfigs, 'canceled', { reason: cancelReason });
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback to legacy system
|
|
|
|
|
if (!this.printer.printer || !this.printer.config) {
|
|
|
|
|
const currentConfig = require('./config').getAll();
|
|
|
|
|
this.printer.initializePrinter(currentConfig);
|
|
|
|
|
}
|
|
|
|
|
result = await this.printer.printOrderReceipt(order, 'canceled', { reason: cancelReason });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result && result.success) {
|
|
|
|
|
this.db.markOrderPrinted(order.id);
|
|
|
|
|
this.db.markPrintJobCompleted(jobId);
|
|
|
|
|
// Cleanup any other pending jobs for this order+type
|
|
|
|
|
this.db.cleanupDuplicateJobs(jobId, order.id, 'canceled');
|
|
|
|
|
console.log(`✓ Cancellation receipt printed for order #${order.id}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.error(`✗ Cancellation print failed for order #${order.id}:`, result && result.error ? result.error : 'Unknown error');
|
|
|
|
|
this.db.markPrintJobPending(jobId);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`✗ Failed to print cancellation for order #${order.id}:`, error.message);
|
|
|
|
|
console.log(' Order updated in database. You can reprint from dashboard.');
|
|
|
|
|
this.db.markPrintJobPending(jobId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reconciliation fetch: scan recent orders window to catch updates (e.g., cancellations)
|
|
|
|
|
try {
|
|
|
|
|
const recentResult = await this.apiClient.getOrders(
|
|
|
|
|
appConfig.authToken,
|
|
|
|
|
appConfig.selectedBotId,
|
|
|
|
|
0,
|
|
|
|
|
{ includeCanceled: true, limit: 200 }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!recentResult.error) {
|
|
|
|
|
const recentOrders = recentResult.orders || [];
|
|
|
|
|
for (const order of recentOrders) {
|
|
|
|
|
const existingOrder = this.db.getOrderById(order.id);
|
|
|
|
|
if (!existingOrder) {
|
|
|
|
|
// Skip new orders here; they are handled by the main afterId fetch
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for status changes (cancellation status from backend)
|
|
|
|
|
const statusChanged = existingOrder.status !== order.status;
|
|
|
|
|
|
|
|
|
|
if (statusChanged) {
|
|
|
|
|
console.log(`(reconcile) Order #${order.id} status changed: ${existingOrder.status} → ${order.status}`);
|
|
|
|
|
|
|
|
|
|
this.db.updateOrder(order);
|
|
|
|
|
|
|
|
|
|
// Print cancellation receipt if order was canceled
|
|
|
|
|
if (order.status === 'canceled' && existingOrder.status !== 'canceled') {
|
|
|
|
|
console.log(`(reconcile) Order #${order.id} was canceled - printing cancellation receipt...`);
|
|
|
|
|
|
|
|
|
|
// Update local status to 'canceled' so it shows on the dashboard
|
|
|
|
|
this.db.updateOrderStatus(order.id, 'canceled');
|
|
|
|
|
|
|
|
|
|
// Check if cancellation already printed to prevent duplicates
|
|
|
|
|
if (this.db.hasPrintedCancellation(order.id)) {
|
|
|
|
|
console.log(`(reconcile) Skipping duplicate cancellation print for order #${order.id}`);
|
|
|
|
|
} else {
|
|
|
|
|
// Add to print queue with deduplication check
|
|
|
|
|
const jobId = this.db.addToPrintQueue(order.id, 'canceled');
|
|
|
|
|
if (!jobId) {
|
|
|
|
|
console.log(`(reconcile) Cancellation job not created for order #${order.id} (duplicate prevention)`);
|
|
|
|
|
} else {
|
|
|
|
|
this.db.markPrintJobProcessing(jobId);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const cancelReason = order.cancellationReason || order.order?.cancellationReason || 'Order canceled';
|
|
|
|
|
|
|
|
|
|
// Use per-printer config system
|
|
|
|
|
const printerConfigs = this.db.getEnabledPrinters();
|
|
|
|
|
let result;
|
|
|
|
|
|
|
|
|
|
if (printerConfigs && printerConfigs.length > 0) {
|
|
|
|
|
result = await this.printer.printOrderReceiptWithPrinterConfigs(order, printerConfigs, 'canceled', { reason: cancelReason });
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback to legacy system
|
|
|
|
|
if (!this.printer.printer || !this.printer.config) {
|
|
|
|
|
const currentConfig = require('./config').getAll();
|
|
|
|
|
this.printer.initializePrinter(currentConfig);
|
|
|
|
|
}
|
|
|
|
|
result = await this.printer.printOrderReceipt(order, 'canceled', { reason: cancelReason });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result && result.success) {
|
|
|
|
|
this.db.markOrderPrinted(order.id);
|
|
|
|
|
this.db.markPrintJobCompleted(jobId);
|
|
|
|
|
// Cleanup any other pending jobs for this order+type
|
|
|
|
|
this.db.cleanupDuplicateJobs(jobId, order.id, 'canceled');
|
|
|
|
|
console.log(`✓ Cancellation receipt printed for order #${order.id}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.error(`✗ Cancellation print failed for order #${order.id}:`, result && result.error ? result.error : 'Unknown error');
|
|
|
|
|
this.db.markPrintJobPending(jobId);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`✗ Failed to print cancellation for order #${order.id}:`, error.message);
|
|
|
|
|
console.log(' Order updated in database. You can reprint from dashboard.');
|
|
|
|
|
this.db.markPrintJobPending(jobId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (reconcileErr) {
|
|
|
|
|
console.error('Reconciliation fetch error:', reconcileErr.message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Poll error:', error.message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.isPolling = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
|
if (this.intervalId) {
|
|
|
|
|
clearTimeout(this.intervalId);
|
|
|
|
|
this.intervalId = null;
|
|
|
|
|
}
|
|
|
|
|
console.log('Order poller stopped');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
restart() {
|
|
|
|
|
this.stop();
|
|
|
|
|
this.start();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Main initialization
|
|
|
|
|
async function main() {
|
|
|
|
|
try {
|
|
|
|
|
// Initialize database
|
|
|
|
|
database.init();
|
|
|
|
|
console.log('Database initialized');
|
|
|
|
|
|
|
|
|
|
// Register plugins
|
|
|
|
|
await fastify.register(require('@fastify/view'), {
|
|
|
|
|
engine: {
|
|
|
|
|
ejs: require('ejs')
|
|
|
|
|
},
|
|
|
|
|
root: path.join(__dirname, 'views')
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await fastify.register(require('@fastify/static'), {
|
|
|
|
|
root: path.join(__dirname, 'public'),
|
|
|
|
|
prefix: '/public/'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await fastify.register(require('@fastify/cookie'), {
|
|
|
|
|
secret: process.env.COOKIE_SECRET || 'kitchen-agent-secret-key-change-in-production',
|
|
|
|
|
hook: 'onRequest'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await fastify.register(require('@fastify/formbody'));
|
|
|
|
|
|
|
|
|
|
await fastify.register(require('@fastify/multipart'), {
|
|
|
|
|
limits: {
|
|
|
|
|
fileSize: 5 * 1024 * 1024 // 5MB limit for logo uploads
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Register routes
|
|
|
|
|
await fastify.register(require('./routes/auth'));
|
|
|
|
|
await fastify.register(require('./routes/dashboard'));
|
|
|
|
|
await fastify.register(require('./routes/settings'));
|
|
|
|
|
await fastify.register(require('./routes/orders'));
|
2026-03-01 17:10:03 -05:00
|
|
|
await fastify.register(require('./routes/abandoned-calls'));
|
2025-10-23 19:02:56 -04:00
|
|
|
|
|
|
|
|
// Initialize printer with config
|
|
|
|
|
const appConfig = config.getAll();
|
|
|
|
|
if (appConfig.printerType && appConfig.printerPath) {
|
|
|
|
|
try {
|
|
|
|
|
printer.initializePrinter(appConfig);
|
|
|
|
|
console.log('Printer initialized successfully');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to initialize printer:', error.message);
|
|
|
|
|
console.log('Printer can be configured later in Settings');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.log('Printer not configured - configure in Settings');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start order poller
|
|
|
|
|
const poller = new OrderPoller(apiClient, database, printer);
|
|
|
|
|
const printWorker = new PrintQueueWorker(database, config, printer);
|
2026-03-01 17:10:03 -05:00
|
|
|
const abandonedCallPoller = new AbandonedCallPoller(database, printer);
|
2025-10-23 19:02:56 -04:00
|
|
|
|
2026-03-01 17:10:03 -05:00
|
|
|
// Make pollers available globally for restart after settings change
|
2025-10-23 19:02:56 -04:00
|
|
|
fastify.decorate('orderPoller', poller);
|
|
|
|
|
fastify.decorate('printWorker', printWorker);
|
2026-03-01 17:10:03 -05:00
|
|
|
fastify.decorate('abandonedCallPoller', abandonedCallPoller);
|
2025-10-23 19:02:56 -04:00
|
|
|
|
|
|
|
|
// Start server
|
|
|
|
|
const port = parseInt(process.env.PORT, 10) || 3000;
|
|
|
|
|
const host = process.env.HOST || '0.0.0.0';
|
|
|
|
|
|
|
|
|
|
await fastify.listen({ port, host });
|
|
|
|
|
|
|
|
|
|
const addresses = fastify.server.address();
|
|
|
|
|
console.log('\n=================================================');
|
|
|
|
|
console.log('Think Link AI Kitchen Agent is running!');
|
|
|
|
|
console.log(`Access at: http://localhost:${port}`);
|
|
|
|
|
console.log(`Or use your computer's IP address from other devices`);
|
|
|
|
|
console.log('=================================================\n');
|
|
|
|
|
|
|
|
|
|
// Kick off auto-update check (at boot and on interval)
|
|
|
|
|
if (isDev) {
|
|
|
|
|
console.log('Dev mode detected: skipping auto-update checks');
|
|
|
|
|
} else {
|
|
|
|
|
try { checkAndUpdate(); } catch (_) {}
|
|
|
|
|
const envIntervalRaw = process.env.UPDATE_CHECK_INTERVAL_MS;
|
|
|
|
|
const envIntervalMs = envIntervalRaw ? parseInt(envIntervalRaw, 10) : NaN;
|
|
|
|
|
const updateIntervalMs = (!Number.isNaN(envIntervalMs) && envIntervalMs > 0) ? envIntervalMs : (5 * 60 * 1000);
|
|
|
|
|
setInterval(() => { try { checkAndUpdate(); } catch (e) { fastify.log.error(e); } }, updateIntervalMs);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 17:10:03 -05:00
|
|
|
// Attempt token refresh on startup in case it expired while offline
|
|
|
|
|
try {
|
|
|
|
|
const refreshedToken = await apiClient.ensureValidToken();
|
|
|
|
|
if (refreshedToken) {
|
|
|
|
|
console.log('Startup token check passed');
|
|
|
|
|
} else if (config.get('authToken')) {
|
|
|
|
|
console.log('Startup token refresh failed — user may need to re-login');
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Startup token refresh error:', err.message);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 19:02:56 -04:00
|
|
|
// Start polling after server is up
|
|
|
|
|
poller.start();
|
|
|
|
|
// Start print queue worker
|
|
|
|
|
printWorker.start();
|
2026-03-01 17:10:03 -05:00
|
|
|
// Start abandoned call poller
|
|
|
|
|
abandonedCallPoller.start();
|
|
|
|
|
|
|
|
|
|
// Periodic token refresh (every 6 hours)
|
|
|
|
|
const TOKEN_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
|
|
|
const tokenRefreshTimer = setInterval(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await apiClient.ensureValidToken();
|
|
|
|
|
if (result) {
|
|
|
|
|
console.log('Periodic token refresh: OK');
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Periodic token refresh error:', err.message);
|
|
|
|
|
}
|
|
|
|
|
}, TOKEN_REFRESH_INTERVAL_MS);
|
2025-10-23 19:02:56 -04:00
|
|
|
|
|
|
|
|
// Handle shutdown gracefully (PM2 reload-friendly)
|
|
|
|
|
const gracefulShutdown = async () => {
|
|
|
|
|
console.log('\nShutting down gracefully...');
|
|
|
|
|
poller.stop();
|
2026-03-01 17:10:03 -05:00
|
|
|
try { abandonedCallPoller.stop(); } catch (_) {}
|
|
|
|
|
clearInterval(tokenRefreshTimer);
|
2025-10-23 19:02:56 -04:00
|
|
|
try { printWorker.stop(); } catch (_) {}
|
|
|
|
|
try { database.close(); } catch (_) {}
|
|
|
|
|
try { await fastify.close(); } catch (_) {}
|
|
|
|
|
process.exit(0);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
process.on('SIGINT', gracefulShutdown);
|
|
|
|
|
process.on('SIGTERM', gracefulShutdown);
|
|
|
|
|
process.on('message', (msg) => { if (msg === 'shutdown') gracefulShutdown(); });
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to start server:', error);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start the application
|
|
|
|
|
main();
|
|
|
|
|
|