Compare commits

..

No commits in common. "ee9895d562ef9c0f93c8a925103cd8d28881e7da" and "e04c06493bd8ca0f46e8fff6b075b7eed38589a3" have entirely different histories.

19 changed files with 299 additions and 681 deletions

2
Jenkinsfile vendored
View File

@ -1,5 +1,5 @@
properties([ properties([
buildDiscarder(logRotator(numToKeepStr: '20')) buildDiscarder(logRotator(numToKeepStr: '25'))
]) ])
def writeBuildMetadata() { def writeBuildMetadata() {

View File

@ -26,8 +26,6 @@
"dependencies": { "dependencies": {
"axios": "^1.16.1", "axios": "^1.16.1",
"canvas": "^3.2.3", "canvas": "^3.2.3",
"chalk": "^6.0.0",
"cli-spinners": "^3.4.0",
"etcd3": "^1.1.2", "etcd3": "^1.1.2",
"express": "^5.2.1", "express": "^5.2.1",
"form-data": "^4.0.5", "form-data": "^4.0.5",
@ -52,9 +50,9 @@
"antd": "^5.29.2", "antd": "^5.29.2",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"esbuild": "^0.25.12",
"electron": "^38.7.1", "electron": "^38.7.1",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"esbuild": "^0.25.12",
"jest": "^30.4.2", "jest": "^30.4.2",
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
"pkg": "^5.8.1", "pkg": "^5.8.1",

View File

@ -9,6 +9,4 @@ systemctl enable farmcontrol-server.service
# Start the service # Start the service
systemctl start farmcontrol-server.service systemctl start farmcontrol-server.service
sleep 3
farmcontrol-server --info farmcontrol-server --info

18
pnpm-lock.yaml generated
View File

@ -14,12 +14,6 @@ importers:
canvas: canvas:
specifier: ^3.2.3 specifier: ^3.2.3
version: 3.2.3 version: 3.2.3
chalk:
specifier: ^6.0.0
version: 6.0.0
cli-spinners:
specifier: ^3.4.0
version: 3.4.0
etcd3: etcd3:
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2 version: 1.1.2
@ -1771,10 +1765,6 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
chalk@6.0.0:
resolution: {integrity: sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==}
engines: {node: '>=22'}
char-regex@1.0.2: char-regex@1.0.2:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1812,10 +1802,6 @@ packages:
classnames@2.5.1: classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
cli-spinners@3.4.0:
resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==}
engines: {node: '>=18.20'}
cliui@7.0.4: cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
@ -6416,8 +6402,6 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
chalk@6.0.0: {}
char-regex@1.0.2: {} char-regex@1.0.2: {}
chokidar@3.6.0: chokidar@3.6.0:
@ -6461,8 +6445,6 @@ snapshots:
classnames@2.5.1: {} classnames@2.5.1: {}
cli-spinners@3.4.0: {}
cliui@7.0.4: cliui@7.0.4:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3

View File

@ -1,203 +0,0 @@
import axios from "axios";
import chalk from "chalk";
import { LOCAL_SERVER_PORT } from "./localserver/localserver.js";
import {
createSpinner,
registerSpinner,
stopWaiting as stopSpinnerWaiting,
} from "./spinner.js";
const LOG_SECTION_WIDTH = 55;
let sharedCli = null;
export function getCli() {
return sharedCli;
}
export { startWaiting, stopWaiting } from "./spinner.js";
export function formatState(stateType) {
switch (stateType) {
case "online":
return chalk.green("Online");
case "offline":
return chalk.dim("Offline");
case "error":
return chalk.red("Error");
case "inactive":
return chalk.dim("Inactive");
case "connecting":
return chalk.yellow("Connecting");
case "printing":
return chalk.blue("Printing");
case "paused":
return chalk.yellow("Paused");
case "cancelled":
return chalk.red("Cancelled");
case "completed":
return chalk.green("Completed");
default:
return chalk.dim("Unknown");
}
}
export function createCliLogger(logger) {
const spinner = createSpinner();
registerSpinner(spinner);
function withSpinnerStopped(fn) {
return async (...args) => {
await stopSpinnerWaiting();
return fn(...args);
};
}
function logSectionHeader(title) {
const padding = LOG_SECTION_WIDTH - title.length;
const left = Math.floor(padding / 2);
const right = padding - left;
logger.info(
chalk.dim.cyan("-".repeat(left)) +
chalk.bold.white(title) +
chalk.dim.cyan("-".repeat(right)),
);
}
function logSectionFooter() {
logger.info(chalk.dim.cyan("-".repeat(LOG_SECTION_WIDTH)));
}
const cli = {
waiting(message) {
spinner.start(message, logger);
},
async stopWaiting() {
await stopSpinnerWaiting();
},
blank: withSpinnerStopped(() => {
process.stdout.write(" \n");
}),
success: withSpinnerStopped((message, ...rest) => {
logger.info(chalk.bold.white(``) + chalk.bold.green(message), ...rest);
}),
failure: withSpinnerStopped((message, ...rest) => {
logger.error(chalk.bold.white(``) + chalk.bold.red(message), ...rest);
}),
warn: withSpinnerStopped((message, ...rest) => {
logger.warn(chalk.yellow(message), ...rest);
}),
info: withSpinnerStopped((message, ...rest) => {
logger.info(message, ...rest);
}),
secure: withSpinnerStopped((message) => {
logger.info(chalk.cyan(`🔒 ${message}`));
}),
label: withSpinnerStopped((key, value) => {
logger.info(chalk.bold(key), value);
}),
yesNo(value) {
return value ? chalk.green("Yes") : chalk.yellow("No");
},
formatState,
sectionHeader: withSpinnerStopped(logSectionHeader),
sectionFooter: withSpinnerStopped(logSectionFooter),
};
sharedCli = cli;
return cli;
}
export function getArgValue(flag) {
const withEquals = process.argv.find((arg) => arg.startsWith(`${flag}=`));
if (withEquals) {
return withEquals.slice(flag.length + 1);
}
const index = process.argv.indexOf(flag);
if (index !== -1 && index + 1 < process.argv.length) {
return process.argv[index + 1];
}
return undefined;
}
export const isHeadless = process.argv.includes("--headless");
export const isInfo = process.argv.includes("--info");
export const otpCode = getArgValue("--otp") || undefined;
export function printerHostInfo(host, cli) {
let ref = host?._reference || "Unknown";
if (ref !== "Unknown") {
ref = `HST:${ref}`;
}
cli.label("Reference:", ref);
cli.label("Name:", host?.name || "Unknown");
if (host?.tags?.length > 0) {
cli.label("Tags:", chalk.dim(`- `) + chalk.blue(`${host.tags[0]}`));
for (let i = 0; i < host.tags.length - 1; i++) {
const tag = host.tags[i + 1];
cli.info(chalk.dim(` - `) + chalk.blue(`${tag}`));
}
} else {
cli.label("Tags:", chalk.dim("n/a"));
}
cli.label("State:", cli.formatState(host?.state?.type));
}
export function printAuthCommand(cli) {
cli.info(
"Run '" +
chalk.bold.white("farmcontrol-server") +
" " +
chalk.bold.magentaBright("--otp") +
" " +
chalk.dim.white("<otp>") +
"' to authenticate.",
);
}
export async function checkRunning() {
try {
const response = await axios.get(
`http://127.0.0.1:${LOCAL_SERVER_PORT}/info`,
{ timeout: 1000 },
);
if (response.data?.running === true) {
return response.data;
}
} catch {
return false;
}
}
export async function authenticateWithOtp(code) {
try {
const response = await axios.post(
`http://127.0.0.1:${LOCAL_SERVER_PORT}/otpAuth`,
{ otp: code },
{ timeout: 30000 },
);
return response.data;
} catch (err) {
if (err.response?.data) {
return err.response.data;
}
throw new Error("Failed to authenticate with OTP");
}
}

View File

@ -3,7 +3,6 @@ import fs from "fs";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import log4js from "log4js"; import log4js from "log4js";
import { startWaiting, stopWaiting } from "./spinner.js";
// Determine environment // Determine environment
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -23,7 +22,7 @@ function getProductionConfigPath() {
return path.join( return path.join(
process.env.PROGRAMDATA || "C:\\ProgramData", process.env.PROGRAMDATA || "C:\\ProgramData",
"farmcontrol-server", "farmcontrol-server",
"config.json", "config.json"
); );
case "darwin": case "darwin":
return path.join( return path.join(
@ -31,7 +30,7 @@ function getProductionConfigPath() {
"Library", "Library",
"Application Support", "Application Support",
"farmcontrol-server", "farmcontrol-server",
"config.json", "config.json"
); );
case "linux": case "linux":
default: default:
@ -49,7 +48,7 @@ function getDevelopmentConfigPath() {
return path.join( return path.join(
process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"), process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"),
"farmcontrol-server", "farmcontrol-server",
"config.json", "config.json"
); );
case "darwin": case "darwin":
return path.join( return path.join(
@ -57,14 +56,14 @@ function getDevelopmentConfigPath() {
"Library", "Library",
"Application Support", "Application Support",
"farmcontrol-server", "farmcontrol-server",
"config.json", "config.json"
); );
default: default:
return path.join( return path.join(
homeDir, homeDir,
".config", ".config",
"farmcontrol-server", "farmcontrol-server",
"config.json", "config.json"
); );
} }
} }
@ -105,7 +104,7 @@ export function loadConfig() {
try { try {
if (!fs.existsSync(CONFIG_PATH)) { if (!fs.existsSync(CONFIG_PATH)) {
logger.info( logger.info(
`Configuration file not found at ${CONFIG_PATH}. Creating a new one.`, `Configuration file not found at ${CONFIG_PATH}. Creating a new one.`
); );
const configDir = path.dirname(CONFIG_PATH); const configDir = path.dirname(CONFIG_PATH);
if (!fs.existsSync(configDir)) { if (!fs.existsSync(configDir)) {
@ -114,7 +113,7 @@ export function loadConfig() {
fs.writeFileSync( fs.writeFileSync(
CONFIG_PATH, CONFIG_PATH,
JSON.stringify(DEFAULT_CONFIG, null, 2), JSON.stringify(DEFAULT_CONFIG, null, 2),
"utf8", "utf8"
); );
} }
@ -123,7 +122,7 @@ export function loadConfig() {
if (!config[NODE_ENV]) { if (!config[NODE_ENV]) {
throw new Error( throw new Error(
`Configuration for environment '${NODE_ENV}' not found in config.json`, `Configuration for environment '${NODE_ENV}' not found in config.json`
); );
} }
@ -135,9 +134,9 @@ export function loadConfig() {
} }
// Save config file // Save config file
export async function saveConfig(newConfig) { export function saveConfig(newConfig) {
startWaiting("Saving...", logger);
try { try {
logger.info("Saving...");
let config = {}; let config = {};
if (fs.existsSync(CONFIG_PATH)) { if (fs.existsSync(CONFIG_PATH)) {
const configData = fs.readFileSync(CONFIG_PATH, "utf8"); const configData = fs.readFileSync(CONFIG_PATH, "utf8");
@ -154,10 +153,8 @@ export async function saveConfig(newConfig) {
// Write back to file with 2-space indentation // Write back to file with 2-space indentation
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8"); fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
await stopWaiting();
logger.info(`Configuration for '${NODE_ENV}' saved successfully.`); logger.info(`Configuration for '${NODE_ENV}' saved successfully.`);
} catch (err) { } catch (err) {
await stopWaiting();
logger.error("Error saving config:", err); logger.error("Error saving config:", err);
throw err; throw err;
} }

View File

@ -1,7 +1,6 @@
// documentprinterclient.js - Handles connection to a single document printer // documentprinterclient.js - Handles connection to a single document printer
import { loadConfig } from "../config.js"; import { loadConfig } from "../config.js";
import log4js from "log4js"; import log4js from "log4js";
import { startWaiting, stopWaiting } from "../spinner.js";
import CupsPrinterInterface from "./interfaces/cupsinterface.js"; import CupsPrinterInterface from "./interfaces/cupsinterface.js";
import ReceiptInterface from "./interfaces/receiptinterface.js"; import ReceiptInterface from "./interfaces/receiptinterface.js";
import { sendIPC } from "../electron/ipc.js"; import { sendIPC } from "../electron/ipc.js";
@ -139,6 +138,9 @@ export class DocumentPrinterClient {
async connect() { async connect() {
if (this.active == false) { if (this.active == false) {
logger.info(
`Document printer ${this.id} is not active, skipping connection`,
);
this.shouldReconnect = false; this.shouldReconnect = false;
this.isOnline = false; this.isOnline = false;
this.state = { type: "inactive" }; this.state = { type: "inactive" };
@ -232,8 +234,7 @@ export class DocumentPrinterClient {
} }
async initialize() { async initialize() {
startWaiting("Running document printer initialization...", logger); logger.info("Running document printer initialization...");
try {
this.state = { type: "initializing", message: null }; this.state = { type: "initializing", message: null };
this.isOnline = true; this.isOnline = true;
this.connectedAt = new Date(); this.connectedAt = new Date();
@ -247,7 +248,6 @@ export class DocumentPrinterClient {
); );
this.state = { type: "offline", message: result.error }; this.state = { type: "offline", message: result.error };
await this.updateDocumentPrinterState(); await this.updateDocumentPrinterState();
await stopWaiting();
return false; return false;
} }
this.state = { type: "standby", message: null }; this.state = { type: "standby", message: null };
@ -256,14 +256,8 @@ export class DocumentPrinterClient {
this.handleEventUpdate.bind(this), this.handleEventUpdate.bind(this),
3000, 3000,
); );
await stopWaiting();
return true; return true;
} }
await stopWaiting();
} catch (error) {
await stopWaiting();
throw error;
}
} }
async handleEventUpdate() { async handleEventUpdate() {

View File

@ -2,7 +2,6 @@ import { loadConfig } from "../config.js";
import log4js from "log4js"; import log4js from "log4js";
import { sendIPC } from "../electron/ipc.js"; import { sendIPC } from "../electron/ipc.js";
import { DocumentPrinterClient } from "./documentprinterclient.js"; import { DocumentPrinterClient } from "./documentprinterclient.js";
import { startWaiting, stopWaiting } from "../spinner.js";
const config = loadConfig(); const config = loadConfig();
@ -17,7 +16,7 @@ export class DocumentPrinterManager {
} }
async reloadDocumentPrinters() { async reloadDocumentPrinters() {
startWaiting("Reloading document printers...", logger); logger.info("Reloading document printers...");
try { try {
this.documentPrinters = await this.socketClient.listObjects({ this.documentPrinters = await this.socketClient.listObjects({
objectType: "documentPrinter", objectType: "documentPrinter",
@ -63,9 +62,7 @@ export class DocumentPrinterManager {
); );
} }
} }
await stopWaiting();
} catch (error) { } catch (error) {
await stopWaiting();
logger.error("Failed to update document printers:", error); logger.error("Failed to update document printers:", error);
this.documentPrinters = []; this.documentPrinters = [];
} }

View File

@ -2,7 +2,6 @@
import log4js from "log4js"; import log4js from "log4js";
import { loadConfig } from "../../config.js"; import { loadConfig } from "../../config.js";
import ipp from "ipp"; import ipp from "ipp";
import { startWaiting, stopWaiting } from "../../spinner.js";
const config = loadConfig(); const config = loadConfig();
const logger = log4js.getLogger("CUPS Printer Interface"); const logger = log4js.getLogger("CUPS Printer Interface");
@ -120,18 +119,16 @@ export default class CupsInterface {
} }
async disconnect() { async disconnect() {
startWaiting("Disconnecting from CUPS printer...", logger); logger.info(`Disconnecting from CUPS printer...`);
this.isConnected = false; this.isConnected = false;
this.cupsPrinter = null; this.cupsPrinter = null;
await stopWaiting();
return { success: true }; return { success: true };
} }
async initialize() { async initialize() {
startWaiting("Initializing CUPS printer...", logger); logger.info(`Initializing CUPS printer...`);
if (!this.isConnected || !this.cupsPrinter) { if (!this.isConnected || !this.cupsPrinter) {
await stopWaiting();
return { error: "Printer is not connected" }; return { error: "Printer is not connected" };
} }
@ -160,17 +157,14 @@ export default class CupsInterface {
// Printer states: 3 = idle, 4 = processing, 5 = stopped // Printer states: 3 = idle, 4 = processing, 5 = stopped
if (printerState === 5) { if (printerState === 5) {
logger.warn(`Printer ${this.name} is stopped: ${stateMessage}`); logger.warn(`Printer ${this.name} is stopped: ${stateMessage}`);
await stopWaiting();
return { error: `Printer is stopped: ${stateMessage}` }; return { error: `Printer is stopped: ${stateMessage}` };
} }
logger.info( logger.info(
`CUPS printer ${this.name} initialized successfully. State: ${printerState}` `CUPS printer ${this.name} initialized successfully. State: ${printerState}`
); );
await stopWaiting();
return true; return true;
} catch (error) { } catch (error) {
await stopWaiting();
logger.error(`Failed to initialize CUPS printer ${this.name}:`, error); logger.error(`Failed to initialize CUPS printer ${this.name}:`, error);
return { error: error.message || "Failed to initialize CUPS printer" }; return { error: error.message || "Failed to initialize CUPS printer" };
} }

View File

@ -1,6 +1,5 @@
import log4js from "log4js"; import log4js from "log4js";
import { notPrompting } from "../utils.js"; import { notPrompting } from "../utils.js";
import { startWaiting, stopWaiting } from "../spinner.js";
const logger = log4js.getLogger("IPC"); const logger = log4js.getLogger("IPC");
let mainWindow = null; let mainWindow = null;
@ -23,8 +22,8 @@ export async function setupIPC() {
return; return;
} }
ipcMain.on("getData", async () => { ipcMain.on("getData", (event) => {
startWaiting("Getting data...", logger); logger.info("Getting data...");
try { try {
// Get the global socket client instance // Get the global socket client instance
const socketClient = global.socketClient; const socketClient = global.socketClient;
@ -36,7 +35,6 @@ export async function setupIPC() {
sendIPC("setHost", {}); sendIPC("setHost", {});
sendIPC("setPrinters", []); sendIPC("setPrinters", []);
sendIPC("setDocumentPrinters", []); sendIPC("setDocumentPrinters", []);
await stopWaiting();
return; return;
} }
@ -48,9 +46,8 @@ export async function setupIPC() {
sendIPC("setPrinters", socketClient.printerManager.printers || []); sendIPC("setPrinters", socketClient.printerManager.printers || []);
sendIPC( sendIPC(
"setDocumentPrinters", "setDocumentPrinters",
socketClient.documentPrinterManager.documentPrinters || [], socketClient.documentPrinterManager.documentPrinters || []
); );
await stopWaiting();
} catch (error) { } catch (error) {
logger.error("Error getting printer data:", error); logger.error("Error getting printer data:", error);
sendIPC("setAuthenticated", false); sendIPC("setAuthenticated", false);
@ -58,7 +55,6 @@ export async function setupIPC() {
sendIPC("setLoading", false); sendIPC("setLoading", false);
sendIPC("setHost", {}); sendIPC("setHost", {});
sendIPC("setPrinters", []); sendIPC("setPrinters", []);
await stopWaiting();
} }
}); });
@ -87,7 +83,7 @@ export async function setupIPC() {
// OTP authentication handler // OTP authentication handler
ipcMain.on("authenticateOTP", async (event, otp) => { ipcMain.on("authenticateOTP", async (event, otp) => {
startWaiting("Authenticating with OTP...", logger); logger.info("Authenticating with OTP...");
try { try {
const socketClient = global.socketClient; const socketClient = global.socketClient;
if (socketClient) { if (socketClient) {
@ -96,9 +92,7 @@ export async function setupIPC() {
} else { } else {
logger.error("Socket client not available for OTP authentication"); logger.error("Socket client not available for OTP authentication");
} }
await stopWaiting();
} catch (error) { } catch (error) {
await stopWaiting();
logger.error("Error during OTP authentication:", error); logger.error("Error during OTP authentication:", error);
} }
}); });

View File

@ -4,7 +4,6 @@ import path from "path";
import { loadConfig } from "../config.js"; import { loadConfig } from "../config.js";
import log4js from "log4js"; import log4js from "log4js";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { startWaiting, stopWaiting } from "../spinner.js";
// Load configuration // Load configuration
const config = loadConfig(); const config = loadConfig();
@ -22,7 +21,7 @@ export async function createElectronWindow() {
} catch (error) { } catch (error) {
logger.warn( logger.warn(
"Electron not available, skipping window creation. Error:", "Electron not available, skipping window creation. Error:",
error, error
); );
return; return;
} }
@ -72,7 +71,7 @@ export async function createElectronWindow() {
logger.info("Preload Script", preloadPath); logger.info("Preload Script", preloadPath);
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
startWaiting("Loading development url...", logger); logger.info("Loading development url...");
win.loadURL("http://localhost:5287"); // Vite dev server win.loadURL("http://localhost:5287"); // Vite dev server
} else { } else {
// In production, the built files will be in the build/electron directory // In production, the built files will be in the build/electron directory
@ -80,14 +79,12 @@ export async function createElectronWindow() {
? path.join(__dirname, "index.html") ? path.join(__dirname, "index.html")
: path.join(__dirname, "..", "build", "electron", "index.html"); : path.join(__dirname, "..", "build", "electron", "index.html");
startWaiting("Loading production file...", logger);
logger.info("Loading production file:", htmlPath); logger.info("Loading production file:", htmlPath);
win.loadFile(htmlPath); win.loadFile(htmlPath);
} }
// Resolve the promise when the window is ready // Resolve the promise when the window is ready
win.webContents.on("did-finish-load", async () => { win.webContents.on("did-finish-load", () => {
await stopWaiting();
resolve(win); resolve(win);
}); });
} }

View File

@ -1,6 +1,7 @@
process.env.FC_HEADLESS_ENTRY = "1"; process.env.FC_HEADLESS_ENTRY = "1";
import("./index.js").then(({ init }) => import("./index.js")
.then(({ init }) =>
init({ headless: true }).catch((err) => { init({ headless: true }).catch((err) => {
console.error(err.message); console.error(err.message);
process.exit(1); process.exit(1);

View File

@ -1,130 +1,163 @@
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import path from "node:path"; import path from "node:path";
import { loadConfig } from "./config.js"; import { loadConfig } from "./config.js";
import axios from "axios";
import log4js from "log4js"; import log4js from "log4js";
import { createElectronWindow } from "./electron/window.js"; import { createElectronWindow } from "./electron/window.js";
import { setupIPC } from "./electron/ipc.js"; import { setupIPC } from "./electron/ipc.js";
import { LocalServer } from "./localserver/localserver.js"; import { LOCAL_SERVER_PORT, LocalServer } from "./localserver/localserver.js";
import { SocketClient } from "./socket/socketclient.js"; import { SocketClient } from "./socket/socketclient.js";
import {
authenticateWithOtp,
checkRunning,
createCliLogger,
isHeadless,
isInfo,
otpCode,
printAuthCommand,
printerHostInfo,
startWaiting,
stopWaiting,
} from "./commandlineutils.js";
if (!process.env.NODE_ENV) { if (!process.env.NODE_ENV) {
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
} }
// Load configuration
const config = loadConfig(); const config = loadConfig();
const logger = log4js.getLogger("App"); const logger = log4js.getLogger("App");
logger.level = config.logLevel; logger.level = config.logLevel;
const cli = createCliLogger(logger); const isHeadless = process.argv.includes("--headless");
const isInfo = process.argv.includes("--info");
export async function init(options = {}) { const printerHostInfo = (host) => {
const headless = options.headless ?? isHeadless; var ref = host?._reference || "Unknown";
cli.blank();
startWaiting("Checking if Farm Control Server is running...", logger); if (ref != "Unknown") {
let runningServer; ref = "HST:" + ref;
try {
runningServer = await checkRunning();
} catch (err) {
await stopWaiting();
throw err;
} }
logger.info("Reference:", ref);
logger.info("Name:", host?.name || "Unknown");
if (host?.tags?.length > 0) {
logger.info("Tags: -", host?.tags?.[0]);
for (var i = 0; i < host?.tags?.length - 1; i++) {
const tag = host?.tags[i + 1];
logger.info(" -", tag);
}
} else {
logger.info("Tags: n/a");
}
logger.info("State:", host?.state?.type || "Unknown");
};
function getArgValue(flag) {
const withEquals = process.argv.find((arg) => arg.startsWith(`${flag}=`));
if (withEquals) {
return withEquals.slice(flag.length + 1);
}
const index = process.argv.indexOf(flag);
if (index !== -1 && index + 1 < process.argv.length) {
return process.argv[index + 1];
}
return undefined;
}
const otpCode = getArgValue("--otp") || undefined;
async function checkRunning() {
try {
const response = await axios.get(
`http://127.0.0.1:${LOCAL_SERVER_PORT}/info`,
{ timeout: 1000 },
);
if (response.data?.running === true) {
return response.data;
}
} catch (err) {
return false;
}
}
async function authenticateWithOtp(otpCode) {
try {
const response = await axios.post(
`http://127.0.0.1:${LOCAL_SERVER_PORT}/otpAuth`,
{ otp: otpCode },
{ timeout: 30000 },
);
return response.data;
} catch (err) {
if (err.response?.data) {
return err.response.data;
}
throw new Error("Failed to authenticate with OTP");
}
}
export async function init(options = {}) {
const headless = options.headless ?? isHeadless;
logger.info("⌛ Checking if Farm Control Server is running...");
const runningServer = await checkRunning();
if (otpCode != undefined || isInfo == true) { if (otpCode != undefined || isInfo == true) {
cli.success("Farm Control Server is running."); logger.info("✔ Farm Control Server is running.");
} }
if (isInfo) { if (isInfo) {
const buildNumber = const buildNumber =
runningServer.buildNumber ?? runningServer.build ?? "n/a"; runningServer.buildNumber ?? runningServer.build ?? "n/a";
cli.blank();
cli.sectionHeader("FarmControl Server"); logger.info("-----FarmControl Server-----");
cli.label( logger.info(
"Version:", "Version:",
runningServer.version ? `v${runningServer.version}` : "n/a", runningServer.version ? `v${runningServer.version}` : "n/a",
); );
cli.label("Build:", buildNumber === "dev" ? "dev" : `b${buildNumber}`); logger.info("Build:", buildNumber === "dev" ? "dev" : `b${buildNumber}`);
cli.label("Authenticated:", cli.yesNo(runningServer.authenticated == true)); logger.info(
cli.sectionFooter(); "Authenticated:",
cli.blank(); runningServer.authenticated == true ? "Yes" : "No",
if (runningServer.authenticated == false) { );
cli.sectionHeader("Authenticate"); logger.info("------------Host------------");
printAuthCommand(cli); printerHostInfo(runningServer.host);
cli.sectionFooter(); logger.info("----------------------------");
} else {
cli.sectionHeader("Host");
printerHostInfo(runningServer.host, cli);
cli.sectionFooter();
}
cli.blank();
return runningServer; return runningServer;
} }
if (otpCode != undefined) { if (otpCode != undefined) {
cli.blank(); logger.info("🔒 Authenticating with OTP...");
startWaiting("Authenticating with OTP...", logger); const authenticatedServer = await authenticateWithOtp(otpCode);
let authenticatedServer;
try {
authenticatedServer = await authenticateWithOtp(otpCode);
await stopWaiting();
} catch (err) {
await stopWaiting();
throw err;
}
if (authenticatedServer.valid == false) { if (authenticatedServer.valid == false) {
cli.failure("Failed to authenticate!"); logger.error(
cli.failure(authenticatedServer.error); "✘ Failed to authenticate with OTP -",
authenticatedServer.error,
);
} else { } else {
cli.success("Authenticated with OTP."); logger.info("✔ Authenticated with OTP.");
cli.blank(); logger.info("------------Host------------");
cli.sectionHeader("Host"); printerHostInfo(authenticatedServer.host);
printerHostInfo(authenticatedServer.host, cli); logger.info("----------------------------");
cli.sectionFooter();
cli.blank();
return authenticatedServer; return authenticatedServer;
} }
cli.blank();
return authenticatedServer; return authenticatedServer;
} }
if (runningServer != false) { if (runningServer != false) {
cli.warn("✘ Farm Control Server is already running."); logger.warn("✘ Farm Control Server is already running.");
cli.blank();
return runningServer; return runningServer;
} }
if (!headless) { if (!headless) {
startWaiting("Creating electron window...", logger); // Create Electron window first
try { logger.info("Creating electron window...");
await createElectronWindow().catch((err) => { await createElectronWindow().catch((err) => {
cli.warn("Failed to create Electron window:", err); logger.warn("Failed to create Electron window:", err);
}); });
await stopWaiting();
} catch (err) {
await stopWaiting();
throw err;
}
// Setup IPC communication after window is created
setupIPC().catch((err) => { setupIPC().catch((err) => {
cli.warn("Failed to setup IPC:", err); logger.warn("Failed to setup IPC:", err);
}); });
} else { } else {
cli.info("Running in headless mode. Skipping window creation."); logger.info("Running in headless mode. Skipping window creation.");
} }
const socketClient = new SocketClient(); const socketClient = new SocketClient();
// Make socket client globally accessible for IPC handlers
global.socketClient = socketClient; global.socketClient = socketClient;
const localServer = new LocalServer(socketClient); const localServer = new LocalServer(socketClient);
@ -132,12 +165,10 @@ export async function init(options = {}) {
socketClient.connect(); socketClient.connect();
process.on("SIGINT", async () => { process.on("SIGINT", () => {
startWaiting("Shutting down...", logger); logger.info("Shutting down...");
cli.blank();
socketClient.disconnect(); socketClient.disconnect();
localServer.stop(); localServer.stop();
await stopWaiting();
process.exit(0); process.exit(0);
}); });
} }
@ -149,7 +180,6 @@ const isMainModule =
if (isMainModule && !process.env.FC_HEADLESS_ENTRY) { if (isMainModule && !process.env.FC_HEADLESS_ENTRY) {
init().catch((err) => { init().catch((err) => {
logger.error(err.message); logger.error(err.message);
cli.blank();
process.exit(1); process.exit(1);
}); });
} }

View File

@ -3,7 +3,6 @@ import log4js from "log4js";
import { notPrompting } from "../utils.js"; import { notPrompting } from "../utils.js";
import { loadConfig } from "../config.js"; import { loadConfig } from "../config.js";
import { getServerVersionInfo } from "../serverVersion.js"; import { getServerVersionInfo } from "../serverVersion.js";
import { startWaiting, stopWaiting } from "../spinner.js";
export const LOCAL_SERVER_PORT = 47291; export const LOCAL_SERVER_PORT = 47291;
@ -42,22 +41,13 @@ export class LocalServer {
} }
try { try {
startWaiting("Authenticating with OTP via local server...", logger); logger.info("Authenticating with OTP via local server...");
let result;
try {
notPrompting(); notPrompting();
result = await this.socketClient.authenticateWithOtp(otp, { const result = await this.socketClient.authenticateWithOtp(otp, {
retryOnFailure: false, retryOnFailure: false,
logs: false,
}); });
await stopWaiting();
} catch (err) {
await stopWaiting();
throw err;
}
if (!result.valid) { if (!result.valid) {
logger.error("OTP authentication failed:", result.error);
res.status(401).json(result); res.status(401).json(result);
return; return;
} }

View File

@ -3,8 +3,7 @@ import log4js from "log4js";
import NodeCache from "node-cache"; import NodeCache from "node-cache";
import { loadConfig } from "../config.js"; import { loadConfig } from "../config.js";
import { sendIPC } from "../electron/ipc.js"; import { sendIPC } from "../electron/ipc.js";
import chalk from "chalk";
import { formatState } from "../commandlineutils.js";
const config = loadConfig(); const config = loadConfig();
const logger = log4js.getLogger("Printer Database"); const logger = log4js.getLogger("Printer Database");
logger.level = config.logLevel; logger.level = config.logLevel;
@ -16,6 +15,7 @@ export class PrinterDatabase {
this.id = this.printer._id; this.id = this.printer._id;
// Initialize cache with 30 second TTL // Initialize cache with 30 second TTL
this.printerCache = new NodeCache({ stdTTL: 30 }); this.printerCache = new NodeCache({ stdTTL: 30 });
logger.info("Initialized PrinterDatabase with socket manager");
} }
async getPrinter() { async getPrinter() {
@ -146,9 +146,12 @@ export class PrinterDatabase {
connectedAt, connectedAt,
}); });
logger.info( logger.info(`Updated printer ${this.printer.name} state:`, {
`Updated printer: ${chalk.bold.white(this.printer.name)}, State: ${chalk.bold.white(formatState(this.printer.state.type))}${state?.progress ? ` (${state.progress})` : ""} (${online ? chalk.green("Online") : chalk.dim.white("Offline")})`, type: state.type,
); progress: state.progress,
online,
previousState: updatedPrinter.state,
});
return updatedPrinter; return updatedPrinter;
} catch (error) { } catch (error) {

View File

@ -5,7 +5,6 @@ import { loadConfig } from "../config.js";
import { PrinterDatabase } from "./database.js"; import { PrinterDatabase } from "./database.js";
import { PrinterFileManager } from "./printerfilemanager.js"; import { PrinterFileManager } from "./printerfilemanager.js";
import log4js from "log4js"; import log4js from "log4js";
import { startWaiting, stopWaiting } from "../spinner.js";
import axios from "axios"; import axios from "axios";
import FormData from "form-data"; import FormData from "form-data";
import _ from "lodash"; import _ from "lodash";
@ -152,6 +151,7 @@ export class PrinterClient {
async connect() { async connect() {
if (this.active == false) { if (this.active == false) {
logger.info(`Printer ${this.id} is not active, skipping connection`);
this.shouldReconnect = false; this.shouldReconnect = false;
this.isOnline = false; this.isOnline = false;
this.state = { type: "inactive" }; this.state = { type: "inactive" };
@ -238,8 +238,7 @@ export class PrinterClient {
} }
async initialize() { async initialize() {
startWaiting("Running printer initialization...", logger); logger.info("Running printer initialization...");
try {
this.state = { type: "initializing", message: null }; this.state = { type: "initializing", message: null };
this.isOnline = true; this.isOnline = true;
this.connectedAt = new Date(); this.connectedAt = new Date();
@ -254,16 +253,10 @@ export class PrinterClient {
this.handleEventUpdate.bind(this), this.handleEventUpdate.bind(this),
500, 500,
); );
await stopWaiting();
} catch (error) {
await stopWaiting();
throw error;
}
} }
async getInfo() { async getInfo() {
startWaiting("Getting printer info...", logger); logger.info("Getting printer info...");
try {
try { try {
// Get server info // Get server info
const serverResult = await this.jsonRpc.callMethod("server.info"); const serverResult = await this.jsonRpc.callMethod("server.info");
@ -336,11 +329,6 @@ export class PrinterClient {
error, error,
); );
} }
await stopWaiting();
} catch (error) {
await stopWaiting();
throw error;
}
} }
async getFiles() { async getFiles() {

View File

@ -3,7 +3,6 @@ import { PrinterClient } from "./printerclient.js";
import { loadConfig } from "../config.js"; import { loadConfig } from "../config.js";
import log4js from "log4js"; import log4js from "log4js";
import { sendIPC } from "../electron/ipc.js"; import { sendIPC } from "../electron/ipc.js";
import { startWaiting, stopWaiting } from "../spinner.js";
// Load configuration // Load configuration
const config = loadConfig(); const config = loadConfig();
@ -18,7 +17,7 @@ export class PrinterManager {
} }
async reloadPrinters() { async reloadPrinters() {
startWaiting("Reloading printers...", logger); logger.info("Reloading printers...");
try { try {
this.printers = await this.socketClient.listObjects({ this.printers = await this.socketClient.listObjects({
objectType: "printer", objectType: "printer",
@ -65,9 +64,7 @@ export class PrinterManager {
logger.debug("Printers added:", addedPrintersCount); logger.debug("Printers added:", addedPrintersCount);
logger.debug("Printers removed:", removedPrintersCount); logger.debug("Printers removed:", removedPrintersCount);
await stopWaiting();
} catch (error) { } catch (error) {
await stopWaiting();
logger.error("Failed to update printers:", error); logger.error("Failed to update printers:", error);
this.printers = []; this.printers = [];
} }
@ -158,11 +155,11 @@ export class PrinterManager {
// Close all printer connections // Close all printer connections
async closeAllConnections() { async closeAllConnections() {
startWaiting( logger.info(
`Closing all printer connections... current count: ${this.printerClients.size}`, `Closing all printer connections... current count: ${this.printerClients.size}`,
logger,
); );
try {
// Take a snapshot so any mutations during disconnects don't affect iteration
const clients = Array.from(this.printerClients.values()); const clients = Array.from(this.printerClients.values());
for (const printerClient of clients) { for (const printerClient of clients) {
@ -192,10 +189,5 @@ export class PrinterManager {
logger.info( logger.info(
`All printer connections closed. Remaining clients: ${this.printerClients.size}`, `All printer connections closed. Remaining clients: ${this.printerClients.size}`,
); );
await stopWaiting();
} catch (error) {
await stopWaiting();
throw error;
}
} }
} }

View File

@ -9,7 +9,6 @@ import { PrinterManager } from "../printer/printermanager.js";
import { HostManager } from "../host/hostmanager.js"; import { HostManager } from "../host/hostmanager.js";
import { FileManager } from "../files/filemanager.js"; import { FileManager } from "../files/filemanager.js";
import { DocumentPrinterManager } from "../documentprinter/documentprintermanager.js"; import { DocumentPrinterManager } from "../documentprinter/documentprintermanager.js";
import { startWaiting, stopWaiting } from "../spinner.js";
const config = loadConfig(); const config = loadConfig();
@ -52,9 +51,9 @@ export class SocketClient {
this.reconnectTimeout = null; this.reconnectTimeout = null;
} }
startWaiting("Will attempt to reconnect in 3 seconds...", logger); logger.info("Will attempt to reconnect in 3 seconds...");
this.reconnectTimeout = setTimeout(async () => { this.reconnectTimeout = setTimeout(() => {
await stopWaiting(); logger.info("Attempting to reconnect...");
this.connect(); this.connect();
}, 3000); }, 3000);
} }
@ -95,10 +94,9 @@ export class SocketClient {
}); });
} }
async connect() { connect() {
try { try {
await stopWaiting(); logger.info(`Connecting to Socket.IO server: ${config.url}`);
startWaiting(`Connecting to Socket.IO server: ${config.url}`, logger);
this.socket = io(config.url, { this.socket = io(config.url, {
auth: { type: "host" }, auth: { type: "host" },
reconnection: false, reconnection: false,
@ -112,7 +110,6 @@ export class SocketClient {
sendIPC("setAuthenticated", false); sendIPC("setAuthenticated", false);
this.setupSocketEventHandlers(); this.setupSocketEventHandlers();
} catch (error) { } catch (error) {
await stopWaiting();
logger.error("Failed to create Socket.IO connection:", error); logger.error("Failed to create Socket.IO connection:", error);
} }
} }
@ -153,18 +150,18 @@ export class SocketClient {
} }
async authenticateWithOtp(otp = undefined, options = {}) { async authenticateWithOtp(otp = undefined, options = {}) {
const { retryOnFailure = otp == undefined, logs = true } = options; const { retryOnFailure = otp == undefined } = options;
if (otp == undefined) { if (otp == undefined) {
otp = await askOtp(); otp = await askOtp();
} }
await this.waitForConnection(); await this.waitForConnection();
return this.authenticate({ otp }, { retryOnFailure, logs }); return this.authenticate({ otp }, { retryOnFailure });
} }
async authenticate(authenticationData, options = {}) { async authenticate(authenticationData, options = {}) {
const { retryOnFailure = true, logs = true } = options; const { retryOnFailure = true } = options;
await this.waitForConnection(); await this.waitForConnection();
@ -174,9 +171,7 @@ export class SocketClient {
return; return;
} }
if (logs) {
logger.debug("Host authenticating..."); logger.debug("Host authenticating...");
}
this.socket.emit( this.socket.emit(
"authenticate", "authenticate",
authenticationData, authenticationData,
@ -184,9 +179,7 @@ export class SocketClient {
if (verifyResult.valid == false) { if (verifyResult.valid == false) {
this.authenticated = false; this.authenticated = false;
sendIPC("setAuthenticated", false); sendIPC("setAuthenticated", false);
if (logs) {
logger.error("Host not authenticated:", verifyResult.error); logger.error("Host not authenticated:", verifyResult.error);
}
if (retryOnFailure) { if (retryOnFailure) {
const retryResult = await this.authenticateWithOtp(); const retryResult = await this.authenticateWithOtp();
@ -198,9 +191,7 @@ export class SocketClient {
return; return;
} }
if (logs) {
logger.info("Host authenticated."); logger.info("Host authenticated.");
}
this.authenticated = true; this.authenticated = true;
sendIPC("setAuthenticated", true); sendIPC("setAuthenticated", true);
sendIPC("setLoading", false); sendIPC("setLoading", false);
@ -221,9 +212,7 @@ export class SocketClient {
this.id = this.host._id; this.id = this.host._id;
config.host = { id: this.id, authCode: this.host.authCode }; config.host = { id: this.id, authCode: this.host.authCode };
if (logs) { saveConfig(config);
await saveConfig(config);
}
this.sendDeviceInfo(); this.sendDeviceInfo();
this.subscribeToObjectEvents(); this.subscribeToObjectEvents();
this.subscribeToObjectUpdates({ objectType: "host", _id: this.id }); this.subscribeToObjectUpdates({ objectType: "host", _id: this.id });
@ -237,6 +226,7 @@ export class SocketClient {
} }
handleHostAction(action, callback) { handleHostAction(action, callback) {
console.log("RUNNING HOST ACTION");
switch (action.type) { switch (action.type) {
case "reloadPrinters": case "reloadPrinters":
this.printerManager.updatePrinters().catch((error) => { this.printerManager.updatePrinters().catch((error) => {
@ -247,7 +237,7 @@ export class SocketClient {
callback({ success: true }); callback({ success: true });
} }
async handleHostUpdate(data) { handleHostUpdate(data) {
if (data._id != this.id) { if (data._id != this.id) {
return; return;
} }
@ -259,7 +249,7 @@ export class SocketClient {
if (data.object?.authCode) { if (data.object?.authCode) {
config.host = { id: this.id, authCode: data.object.authCode }; config.host = { id: this.id, authCode: data.object.authCode };
await saveConfig(config); saveConfig(config);
} }
} }
@ -308,8 +298,7 @@ export class SocketClient {
} }
} }
async handleConnect() { handleConnect() {
await stopWaiting();
logger.info("Connected to FarmControl Api."); logger.info("Connected to FarmControl Api.");
this.connected = true; this.connected = true;
sendIPC("setConnected", true); sendIPC("setConnected", true);
@ -337,8 +326,7 @@ export class SocketClient {
this.authenticate({ id: config.host.id, authCode: config.host.authCode }); this.authenticate({ id: config.host.id, authCode: config.host.authCode });
} }
async handleError(error) { handleError(error) {
await stopWaiting();
logger.error("Connection error:", error.message); logger.error("Connection error:", error.message);
this.loading = false; this.loading = false;
sendIPC("setLoading", false); sendIPC("setLoading", false);

View File

@ -1,122 +0,0 @@
import chalk from "chalk";
import cliSpinners from "cli-spinners";
import log4js from "log4js";
import { writeSync } from "node:fs";
import { colouredLayout } from "log4js/lib/layouts.js";
let spinner = null;
let consoleLogPatched = false;
function patchConsoleLog() {
if (consoleLogPatched) {
return;
}
const originalLog = console.log.bind(console);
console.log = (...args) => {
spinner?.stopSync();
originalLog(...args);
};
consoleLogPatched = true;
}
function formatLogPrefix(logger) {
const categoryName = logger?.category ?? "App";
return colouredLayout({
startTime: new Date(),
level: log4js.levels.INFO,
categoryName,
data: [],
});
}
function clearSpinnerLineSync() {
if (!process.stderr.isTTY) {
return;
}
try {
writeSync(process.stderr.fd, "\r\x1b[2K");
} catch {
process.stderr.write("\r\x1b[2K");
}
}
export function createSpinner() {
const { frames, interval } = cliSpinners.dots;
let timer = null;
let frameIndex = 0;
let message = "";
let active = false;
let activeLogger = null;
function render() {
if (!active) {
return;
}
const frame = frames[frameIndex];
frameIndex = (frameIndex + 1) % frames.length;
const prefix = formatLogPrefix(activeLogger);
const line = `${prefix}${frame} ${chalk.bold.cyan(message)}`;
process.stderr.write(`\r\x1b[2K${line}`);
}
function stopSync() {
if (timer) {
clearInterval(timer);
timer = null;
}
const wasActive = active;
active = false;
activeLogger = null;
if (wasActive) {
clearSpinnerLineSync();
}
}
async function stop() {
await new Promise((resolve) => setTimeout(resolve, 15));
stopSync();
}
function start(text, logger, timeout = true) {
if (timeout == true) {
setTimeout(() => {
start(text, logger, false);
}, 10);
return;
}
stopSync();
message = text;
frameIndex = 0;
activeLogger = logger;
if (!process.stderr.isTTY) {
const prefix = formatLogPrefix(activeLogger);
process.stderr.write(
`${prefix}${frames[0]} ${chalk.bold.cyan(message)}\n`,
);
return;
}
active = true;
render();
timer = setInterval(render, interval);
}
return { start, stop, stopSync };
}
export function registerSpinner(instance) {
spinner = instance;
patchConsoleLog();
}
export function startWaiting(message, logger) {
spinner?.start(message, logger);
}
export async function stopWaiting() {
await spinner?.stop();
}