443 lines
17 KiB
JavaScript
443 lines
17 KiB
JavaScript
// 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');
|
|
|
|
const fastify = Fastify({
|
|
logger: true
|
|
});
|
|
|
|
const isDev = false;
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Check if token is expired
|
|
if (apiClient.isTokenExpired(appConfig.tokenExpiry)) {
|
|
console.log('Token expired, please re-login');
|
|
this.isPolling = false;
|
|
return;
|
|
}
|
|
|
|
// 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'));
|
|
|
|
// 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);
|
|
|
|
// Make poller available globally for restart after settings change
|
|
fastify.decorate('orderPoller', poller);
|
|
fastify.decorate('printWorker', printWorker);
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Start polling after server is up
|
|
poller.start();
|
|
// Start print queue worker
|
|
printWorker.start();
|
|
|
|
// Handle shutdown gracefully (PM2 reload-friendly)
|
|
const gracefulShutdown = async () => {
|
|
console.log('\nShutting down gracefully...');
|
|
poller.stop();
|
|
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();
|
|
|