Update configuration and enhance CLI logging functionality
Some checks failed
farmcontrol/farmcontrol-server/pipeline/head There was a failure building this commit

- Change log level in development config from "debug" to "info" for improved clarity.
- Add new dependencies: chalk and cli-spinners for enhanced CLI output.
- Introduce command line utility functions for better logging and spinner management.
- Refactor various components to utilize the new CLI logging methods, improving user feedback during operations.
- Update package and lock files to reflect new dependencies.
This commit is contained in:
Tom Butcher 2026-07-27 02:03:17 +01:00
parent 20080929f9
commit ee9895d562
17 changed files with 676 additions and 296 deletions

View File

@ -26,6 +26,8 @@
"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",
@ -50,9 +52,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",

18
pnpm-lock.yaml generated
View File

@ -14,6 +14,12 @@ 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
@ -1765,6 +1771,10 @@ 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'}
@ -1802,6 +1812,10 @@ 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==}
@ -6402,6 +6416,8 @@ 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:
@ -6445,6 +6461,8 @@ 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

203
src/commandlineutils.js Normal file
View File

@ -0,0 +1,203 @@
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,6 +3,7 @@ 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);
@ -22,7 +23,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(
@ -30,7 +31,7 @@ function getProductionConfigPath() {
"Library", "Library",
"Application Support", "Application Support",
"farmcontrol-server", "farmcontrol-server",
"config.json" "config.json",
); );
case "linux": case "linux":
default: default:
@ -48,7 +49,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(
@ -56,14 +57,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",
); );
} }
} }
@ -104,7 +105,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)) {
@ -113,7 +114,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",
); );
} }
@ -122,7 +123,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`,
); );
} }
@ -134,9 +135,9 @@ export function loadConfig() {
} }
// Save config file // Save config file
export function saveConfig(newConfig) { export async 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");
@ -153,8 +154,10 @@ export 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,6 +1,7 @@
// 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";
@ -138,9 +139,6 @@ 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" };
@ -234,29 +232,37 @@ export class DocumentPrinterClient {
} }
async initialize() { async initialize() {
logger.info("Running document printer initialization..."); startWaiting("Running document printer initialization...", logger);
this.state = { type: "initializing", message: null }; try {
this.isOnline = true; this.state = { type: "initializing", message: null };
this.connectedAt = new Date(); this.isOnline = true;
await this.updateDocumentPrinterState(); this.connectedAt = new Date();
if (this.printerInterface && this.printerInterface.initialize) {
const result = await this.printerInterface.initialize();
if (result.error) {
logger.error(
`Error initializing document printer ${this.documentPrinter.name}:`,
result.error,
);
this.state = { type: "offline", message: result.error };
await this.updateDocumentPrinterState();
return false;
}
this.state = { type: "standby", message: null };
await this.updateDocumentPrinterState(); await this.updateDocumentPrinterState();
this.eventUpdateInterval = setInterval( if (this.printerInterface && this.printerInterface.initialize) {
this.handleEventUpdate.bind(this), const result = await this.printerInterface.initialize();
3000, if (result.error) {
); logger.error(
return true; `Error initializing document printer ${this.documentPrinter.name}:`,
result.error,
);
this.state = { type: "offline", message: result.error };
await this.updateDocumentPrinterState();
await stopWaiting();
return false;
}
this.state = { type: "standby", message: null };
await this.updateDocumentPrinterState();
this.eventUpdateInterval = setInterval(
this.handleEventUpdate.bind(this),
3000,
);
await stopWaiting();
return true;
}
await stopWaiting();
} catch (error) {
await stopWaiting();
throw error;
} }
} }

View File

@ -2,6 +2,7 @@ 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();
@ -16,7 +17,7 @@ export class DocumentPrinterManager {
} }
async reloadDocumentPrinters() { async reloadDocumentPrinters() {
logger.info("Reloading document printers..."); startWaiting("Reloading document printers...", logger);
try { try {
this.documentPrinters = await this.socketClient.listObjects({ this.documentPrinters = await this.socketClient.listObjects({
objectType: "documentPrinter", objectType: "documentPrinter",
@ -62,7 +63,9 @@ 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,6 +2,7 @@
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");
@ -119,16 +120,18 @@ export default class CupsInterface {
} }
async disconnect() { async disconnect() {
logger.info(`Disconnecting from CUPS printer...`); startWaiting("Disconnecting from CUPS printer...", logger);
this.isConnected = false; this.isConnected = false;
this.cupsPrinter = null; this.cupsPrinter = null;
await stopWaiting();
return { success: true }; return { success: true };
} }
async initialize() { async initialize() {
logger.info(`Initializing CUPS printer...`); startWaiting("Initializing CUPS printer...", logger);
if (!this.isConnected || !this.cupsPrinter) { if (!this.isConnected || !this.cupsPrinter) {
await stopWaiting();
return { error: "Printer is not connected" }; return { error: "Printer is not connected" };
} }
@ -157,14 +160,17 @@ 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,5 +1,6 @@
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;
@ -22,8 +23,8 @@ export async function setupIPC() {
return; return;
} }
ipcMain.on("getData", (event) => { ipcMain.on("getData", async () => {
logger.info("Getting data..."); startWaiting("Getting data...", logger);
try { try {
// Get the global socket client instance // Get the global socket client instance
const socketClient = global.socketClient; const socketClient = global.socketClient;
@ -35,6 +36,7 @@ export async function setupIPC() {
sendIPC("setHost", {}); sendIPC("setHost", {});
sendIPC("setPrinters", []); sendIPC("setPrinters", []);
sendIPC("setDocumentPrinters", []); sendIPC("setDocumentPrinters", []);
await stopWaiting();
return; return;
} }
@ -46,8 +48,9 @@ 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);
@ -55,6 +58,7 @@ export async function setupIPC() {
sendIPC("setLoading", false); sendIPC("setLoading", false);
sendIPC("setHost", {}); sendIPC("setHost", {});
sendIPC("setPrinters", []); sendIPC("setPrinters", []);
await stopWaiting();
} }
}); });
@ -83,7 +87,7 @@ export async function setupIPC() {
// OTP authentication handler // OTP authentication handler
ipcMain.on("authenticateOTP", async (event, otp) => { ipcMain.on("authenticateOTP", async (event, otp) => {
logger.info("Authenticating with OTP..."); startWaiting("Authenticating with OTP...", logger);
try { try {
const socketClient = global.socketClient; const socketClient = global.socketClient;
if (socketClient) { if (socketClient) {
@ -92,7 +96,9 @@ 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,6 +4,7 @@ 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();
@ -21,7 +22,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;
} }
@ -49,8 +50,8 @@ export async function createElectronWindow() {
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? path.join(__dirname, "preload.js") ? path.join(__dirname, "preload.js")
: isRunningFromBuild : isRunningFromBuild
? path.join(__dirname, "preload.js") ? path.join(__dirname, "preload.js")
: path.join(__dirname, "..", "build", "electron", "preload.js"); : path.join(__dirname, "..", "build", "electron", "preload.js");
const win = new BrowserWindow({ const win = new BrowserWindow({
width: 900, width: 900,
@ -71,7 +72,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") {
logger.info("Loading development url..."); startWaiting("Loading development url...", logger);
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
@ -79,12 +80,14 @@ 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", () => { win.webContents.on("did-finish-load", async () => {
await stopWaiting();
resolve(win); resolve(win);
}); });
} }

View File

@ -1,9 +1,8 @@
process.env.FC_HEADLESS_ENTRY = "1"; process.env.FC_HEADLESS_ENTRY = "1";
import("./index.js") import("./index.js").then(({ init }) =>
.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,163 +1,130 @@
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 { LOCAL_SERVER_PORT, LocalServer } from "./localserver/localserver.js"; import { 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 isHeadless = process.argv.includes("--headless"); const cli = createCliLogger(logger);
const isInfo = process.argv.includes("--info");
const printerHostInfo = (host) => {
var ref = host?._reference || "Unknown";
if (ref != "Unknown") {
ref = "HST:" + ref;
}
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 = {}) { export async function init(options = {}) {
const headless = options.headless ?? isHeadless; const headless = options.headless ?? isHeadless;
cli.blank();
logger.info("⌛ Checking if Farm Control Server is running..."); startWaiting("Checking if Farm Control Server is running...", logger);
let runningServer;
const runningServer = await checkRunning(); try {
runningServer = await checkRunning();
} catch (err) {
await stopWaiting();
throw err;
}
if (otpCode != undefined || isInfo == true) { if (otpCode != undefined || isInfo == true) {
logger.info("✔ Farm Control Server is running."); cli.success("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();
logger.info("-----FarmControl Server-----"); cli.sectionHeader("FarmControl Server");
logger.info( cli.label(
"Version:", "Version:",
runningServer.version ? `v${runningServer.version}` : "n/a", runningServer.version ? `v${runningServer.version}` : "n/a",
); );
logger.info("Build:", buildNumber === "dev" ? "dev" : `b${buildNumber}`); cli.label("Build:", buildNumber === "dev" ? "dev" : `b${buildNumber}`);
logger.info( cli.label("Authenticated:", cli.yesNo(runningServer.authenticated == true));
"Authenticated:", cli.sectionFooter();
runningServer.authenticated == true ? "Yes" : "No", cli.blank();
); if (runningServer.authenticated == false) {
logger.info("------------Host------------"); cli.sectionHeader("Authenticate");
printerHostInfo(runningServer.host); printAuthCommand(cli);
logger.info("----------------------------"); cli.sectionFooter();
} else {
cli.sectionHeader("Host");
printerHostInfo(runningServer.host, cli);
cli.sectionFooter();
}
cli.blank();
return runningServer; return runningServer;
} }
if (otpCode != undefined) { if (otpCode != undefined) {
logger.info("🔒 Authenticating with OTP..."); cli.blank();
const authenticatedServer = await authenticateWithOtp(otpCode); startWaiting("Authenticating with OTP...", logger);
let authenticatedServer;
try {
authenticatedServer = await authenticateWithOtp(otpCode);
await stopWaiting();
} catch (err) {
await stopWaiting();
throw err;
}
if (authenticatedServer.valid == false) { if (authenticatedServer.valid == false) {
logger.error( cli.failure("Failed to authenticate!");
"✘ Failed to authenticate with OTP -", cli.failure(authenticatedServer.error);
authenticatedServer.error,
);
} else { } else {
logger.info("✔ Authenticated with OTP."); cli.success("Authenticated with OTP.");
logger.info("------------Host------------"); cli.blank();
printerHostInfo(authenticatedServer.host); cli.sectionHeader("Host");
logger.info("----------------------------"); printerHostInfo(authenticatedServer.host, cli);
cli.sectionFooter();
cli.blank();
return authenticatedServer; return authenticatedServer;
} }
cli.blank();
return authenticatedServer; return authenticatedServer;
} }
if (runningServer != false) { if (runningServer != false) {
logger.warn("✘ Farm Control Server is already running."); cli.warn("✘ Farm Control Server is already running.");
cli.blank();
return runningServer; return runningServer;
} }
if (!headless) { if (!headless) {
// Create Electron window first startWaiting("Creating electron window...", logger);
logger.info("Creating electron window..."); try {
await createElectronWindow().catch((err) => { await createElectronWindow().catch((err) => {
logger.warn("Failed to create Electron window:", err); cli.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) => {
logger.warn("Failed to setup IPC:", err); cli.warn("Failed to setup IPC:", err);
}); });
} else { } else {
logger.info("Running in headless mode. Skipping window creation."); cli.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);
@ -165,10 +132,12 @@ export async function init(options = {}) {
socketClient.connect(); socketClient.connect();
process.on("SIGINT", () => { process.on("SIGINT", async () => {
logger.info("Shutting down..."); startWaiting("Shutting down...", logger);
cli.blank();
socketClient.disconnect(); socketClient.disconnect();
localServer.stop(); localServer.stop();
await stopWaiting();
process.exit(0); process.exit(0);
}); });
} }
@ -180,6 +149,7 @@ 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,6 +3,7 @@ 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;
@ -41,13 +42,22 @@ export class LocalServer {
} }
try { try {
logger.info("Authenticating with OTP via local server..."); startWaiting("Authenticating with OTP via local server...", logger);
notPrompting(); let result;
const result = await this.socketClient.authenticateWithOtp(otp, { try {
retryOnFailure: false, notPrompting();
}); result = await this.socketClient.authenticateWithOtp(otp, {
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,7 +3,8 @@ 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;
@ -15,7 +16,6 @@ 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,12 +146,9 @@ export class PrinterDatabase {
connectedAt, connectedAt,
}); });
logger.info(`Updated printer ${this.printer.name} state:`, { logger.info(
type: state.type, `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")})`,
progress: state.progress, );
online,
previousState: updatedPrinter.state,
});
return updatedPrinter; return updatedPrinter;
} catch (error) { } catch (error) {

View File

@ -5,6 +5,7 @@ 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";
@ -151,7 +152,6 @@ 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,96 +238,108 @@ export class PrinterClient {
} }
async initialize() { async initialize() {
logger.info("Running printer initialization..."); startWaiting("Running printer initialization...", logger);
this.state = { type: "initializing", message: null }; try {
this.isOnline = true; this.state = { type: "initializing", message: null };
this.connectedAt = new Date(); this.isOnline = true;
await this.getInfo(); this.connectedAt = new Date();
await this.getFiles(); await this.getInfo();
await this.getCurrentFilament(); await this.getFiles();
await this.loadCurrentFilament(); await this.getCurrentFilament();
await this.updateSubscriptions(); await this.loadCurrentFilament();
await this.getPrinterState(); await this.updateSubscriptions();
await this.syncSubJobs(); await this.getPrinterState();
this.eventUpdateInterval = setInterval( await this.syncSubJobs();
this.handleEventUpdate.bind(this), this.eventUpdateInterval = setInterval(
500, this.handleEventUpdate.bind(this),
); 500,
);
await stopWaiting();
} catch (error) {
await stopWaiting();
throw error;
}
} }
async getInfo() { async getInfo() {
logger.info("Getting printer info..."); startWaiting("Getting printer info...", logger);
try { try {
// Get server info
const serverResult = await this.jsonRpc.callMethod("server.info");
this.isOnline = true;
this.klippyState = { type: serverResult.klippy_state };
logger.info(
"Server:",
`Moonraker ${serverResult.moonraker_version} (${this.printer.name})`,
`State: ${this.klippyState.type}`,
);
try { try {
const klippyResult = await this.jsonRpc.callMethod("printer.info"); // Get server info
const serverResult = await this.jsonRpc.callMethod("server.info");
this.isOnline = true;
this.klippyState = { type: serverResult.klippy_state };
logger.info( logger.info(
`Klippy info for ${this.printer.name}: ${klippyResult.hostname}, ${klippyResult.software_version}`, "Server:",
); `Moonraker ${serverResult.moonraker_version} (${this.printer.name})`,
// Update firmware version in database `State: ${this.klippyState.type}`,
await this.database.updatePrinterFirmware(
klippyResult.software_version,
);
logger.info(
`Updated firmware version for ${this.printer.name} to ${klippyResult.software_version}`,
); );
if (klippyResult.state === "error" && klippyResult.state_message) { try {
logger.error( const klippyResult = await this.jsonRpc.callMethod("printer.info");
`Klippy error for ${this.printer.name}: ${klippyResult.state_message}`, logger.info(
this.database.addAlert({ `Klippy info for ${this.printer.name}: ${klippyResult.hostname}, ${klippyResult.software_version}`,
code: "klippyError",
type: "error",
message: klippyResult.state_message,
actions: ["restart", "restartFirmware"],
}),
); );
} // Update firmware version in database
await this.database.updatePrinterFirmware(
if (klippyResult.state === "shutdown" && klippyResult.state_message) { klippyResult.software_version,
logger.error( );
`Klippy error for ${this.printer.name}: ${klippyResult.state_message}`, logger.info(
this.database.addAlert({ `Updated firmware version for ${this.printer.name} to ${klippyResult.software_version}`,
code: "klippyError",
type: "error",
message: klippyResult.state_message,
actions: ["restart", "restartFirmware"],
}),
); );
}
if (klippyResult.state === "startup" && klippyResult.state_message) { if (klippyResult.state === "error" && klippyResult.state_message) {
logger.warn( logger.error(
`Klippy startup message for ${this.printer.name}: ${klippyResult.state_message}`, `Klippy error for ${this.printer.name}: ${klippyResult.state_message}`,
this.database.addAlert({ this.database.addAlert({
type: "info", code: "klippyError",
code: "klippyStartup", type: "error",
message: klippyResult.state_message, message: klippyResult.state_message,
priority: 8, actions: ["restart", "restartFirmware"],
timestamp: new Date(), }),
}), );
}
if (klippyResult.state === "shutdown" && klippyResult.state_message) {
logger.error(
`Klippy error for ${this.printer.name}: ${klippyResult.state_message}`,
this.database.addAlert({
code: "klippyError",
type: "error",
message: klippyResult.state_message,
actions: ["restart", "restartFirmware"],
}),
);
}
if (klippyResult.state === "startup" && klippyResult.state_message) {
logger.warn(
`Klippy startup message for ${this.printer.name}: ${klippyResult.state_message}`,
this.database.addAlert({
type: "info",
code: "klippyStartup",
message: klippyResult.state_message,
priority: 8,
timestamp: new Date(),
}),
);
}
} catch (error) {
logger.error(
`Error getting Klippy info for ${this.printer.name}:`,
error,
); );
} }
} catch (error) { } catch (error) {
logger.error( logger.error(
`Error getting Klippy info for ${this.printer.name}:`, `Error getting server info for ${this.printer.name}:`,
error, error,
); );
} }
await stopWaiting();
} catch (error) { } catch (error) {
logger.error( await stopWaiting();
`Error getting server info for ${this.printer.name}:`, throw error;
error,
);
} }
} }

View File

@ -3,6 +3,7 @@ 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();
@ -17,7 +18,7 @@ export class PrinterManager {
} }
async reloadPrinters() { async reloadPrinters() {
logger.info("Reloading printers..."); startWaiting("Reloading printers...", logger);
try { try {
this.printers = await this.socketClient.listObjects({ this.printers = await this.socketClient.listObjects({
objectType: "printer", objectType: "printer",
@ -64,7 +65,9 @@ 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 = [];
} }
@ -155,39 +158,44 @@ export class PrinterManager {
// Close all printer connections // Close all printer connections
async closeAllConnections() { async closeAllConnections() {
logger.info( startWaiting(
`Closing all printer connections... current count: ${this.printerClients.size}`, `Closing all printer connections... current count: ${this.printerClients.size}`,
logger,
); );
try {
const clients = Array.from(this.printerClients.values());
// Take a snapshot so any mutations during disconnects don't affect iteration for (const printerClient of clients) {
const clients = Array.from(this.printerClients.values()); try {
// Ensure we never auto-reconnect after a manual close-all
for (const printerClient of clients) { printerClient.shouldReconnect = false;
try { await printerClient.disconnect();
// Ensure we never auto-reconnect after a manual close-all logger.info(
printerClient.shouldReconnect = false; `Disconnected printer client ${printerClient?.id || "unknown"}`,
await printerClient.disconnect(); );
logger.info( } catch (error) {
`Disconnected printer client ${printerClient?.id || "unknown"}`, logger.error(
); `Failed to disconnect printer client ${
} catch (error) { printerClient?.id || "unknown"
logger.error( }:`,
`Failed to disconnect printer client ${ error,
printerClient?.id || "unknown" );
}:`, }
error,
);
} }
console.log("Printer clients:", this.printerClients);
// Clear local references so no stale clients remain
this.printerClients.clear();
this.printers = [];
logger.info(
`All printer connections closed. Remaining clients: ${this.printerClients.size}`,
);
await stopWaiting();
} catch (error) {
await stopWaiting();
throw error;
} }
console.log("Printer clients:", this.printerClients);
// Clear local references so no stale clients remain
this.printerClients.clear();
this.printers = [];
logger.info(
`All printer connections closed. Remaining clients: ${this.printerClients.size}`,
);
} }
} }

View File

@ -9,6 +9,7 @@ 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();
@ -51,9 +52,9 @@ export class SocketClient {
this.reconnectTimeout = null; this.reconnectTimeout = null;
} }
logger.info("Will attempt to reconnect in 3 seconds..."); startWaiting("Will attempt to reconnect in 3 seconds...", logger);
this.reconnectTimeout = setTimeout(() => { this.reconnectTimeout = setTimeout(async () => {
logger.info("Attempting to reconnect..."); await stopWaiting();
this.connect(); this.connect();
}, 3000); }, 3000);
} }
@ -94,9 +95,10 @@ export class SocketClient {
}); });
} }
connect() { async connect() {
try { try {
logger.info(`Connecting to Socket.IO server: ${config.url}`); await stopWaiting();
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,
@ -110,6 +112,7 @@ 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);
} }
} }
@ -150,18 +153,18 @@ export class SocketClient {
} }
async authenticateWithOtp(otp = undefined, options = {}) { async authenticateWithOtp(otp = undefined, options = {}) {
const { retryOnFailure = otp == undefined } = options; const { retryOnFailure = otp == undefined, logs = true } = options;
if (otp == undefined) { if (otp == undefined) {
otp = await askOtp(); otp = await askOtp();
} }
await this.waitForConnection(); await this.waitForConnection();
return this.authenticate({ otp }, { retryOnFailure }); return this.authenticate({ otp }, { retryOnFailure, logs });
} }
async authenticate(authenticationData, options = {}) { async authenticate(authenticationData, options = {}) {
const { retryOnFailure = true } = options; const { retryOnFailure = true, logs = true } = options;
await this.waitForConnection(); await this.waitForConnection();
@ -171,7 +174,9 @@ export class SocketClient {
return; return;
} }
logger.debug("Host authenticating..."); if (logs) {
logger.debug("Host authenticating...");
}
this.socket.emit( this.socket.emit(
"authenticate", "authenticate",
authenticationData, authenticationData,
@ -179,7 +184,9 @@ export class SocketClient {
if (verifyResult.valid == false) { if (verifyResult.valid == false) {
this.authenticated = false; this.authenticated = false;
sendIPC("setAuthenticated", false); sendIPC("setAuthenticated", false);
logger.error("Host not authenticated:", verifyResult.error); if (logs) {
logger.error("Host not authenticated:", verifyResult.error);
}
if (retryOnFailure) { if (retryOnFailure) {
const retryResult = await this.authenticateWithOtp(); const retryResult = await this.authenticateWithOtp();
@ -191,7 +198,9 @@ export class SocketClient {
return; return;
} }
logger.info("Host authenticated."); if (logs) {
logger.info("Host authenticated.");
}
this.authenticated = true; this.authenticated = true;
sendIPC("setAuthenticated", true); sendIPC("setAuthenticated", true);
sendIPC("setLoading", false); sendIPC("setLoading", false);
@ -212,7 +221,9 @@ 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 };
saveConfig(config); if (logs) {
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 });
@ -226,7 +237,6 @@ 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) => {
@ -237,7 +247,7 @@ export class SocketClient {
callback({ success: true }); callback({ success: true });
} }
handleHostUpdate(data) { async handleHostUpdate(data) {
if (data._id != this.id) { if (data._id != this.id) {
return; return;
} }
@ -249,7 +259,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 };
saveConfig(config); await saveConfig(config);
} }
} }
@ -298,7 +308,8 @@ export class SocketClient {
} }
} }
handleConnect() { async 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);
@ -326,7 +337,8 @@ export class SocketClient {
this.authenticate({ id: config.host.id, authCode: config.host.authCode }); this.authenticate({ id: config.host.id, authCode: config.host.authCode });
} }
handleError(error) { async 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);

122
src/spinner.js Normal file
View File

@ -0,0 +1,122 @@
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();
}