New version of farmcontrol-server. Includes electron UI, connects only via WS and authenticates using OTP and then auth code.
54
config.json
@ -1,40 +1,18 @@
|
|||||||
{
|
{
|
||||||
"development": {
|
"development": {
|
||||||
"server": {
|
"logLevel": "debug",
|
||||||
"port": 8081,
|
"url": "http://192.168.68.53:9090",
|
||||||
"logLevel": "debug"
|
"host": {
|
||||||
},
|
"id": "68a0b5d7c873abe59a995431",
|
||||||
"auth": {
|
"authCode": "OHHRijUj-PJnsxx6qAb7hesAlB64SdFBpDrJszComy225KIQ3M3uvMMKhdVCeGfB"
|
||||||
"enabled": true,
|
|
||||||
"keycloak": {
|
|
||||||
"url": "https://auth.tombutcher.work",
|
|
||||||
"realm": "master",
|
|
||||||
"clientId": "farmcontrol-client",
|
|
||||||
"clientSecret": "GPyh59xctRX83yfKWb83ShK6VEwHIvLF"
|
|
||||||
},
|
|
||||||
"requiredRoles": []
|
|
||||||
},
|
|
||||||
"database": {
|
|
||||||
"url": "mongodb://localhost:27017/farmcontrol"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
"server": {
|
|
||||||
"port": 8081,
|
|
||||||
"logLevel": "info"
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"enabled": true,
|
|
||||||
"keycloak": {
|
|
||||||
"url": "https://auth.tombutcher.work",
|
|
||||||
"realm": "master",
|
|
||||||
"clientId": "farmcontrol-client",
|
|
||||||
"clientSecret": "GPyh59xctRX83yfKWb83ShK6VEwHIvLF"
|
|
||||||
},
|
|
||||||
"requiredRoles": []
|
|
||||||
},
|
|
||||||
"database": {
|
|
||||||
"url": "mongodb://farmcontrol.tombutcher.local:27017/farmcontrol"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"production": {
|
||||||
|
"logLevel": "info",
|
||||||
|
"url": "192.168.68.53:8001",
|
||||||
|
"host": {
|
||||||
|
"id": "",
|
||||||
|
"authCode": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16840
package-lock.json
generated
75
package.json
@ -1,31 +1,48 @@
|
|||||||
{
|
{
|
||||||
"name": "farmcontrol-server",
|
"name": "farmcontrol-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Connects to moonraker and also manages the socket connection to the printer.",
|
"description": "Connects to moonraker and also manages the socket connection to the printer.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node build/index.js",
|
"start": "node build/index.js",
|
||||||
"dev": "nodemon src/index.js",
|
"dev": "nodemon src/index.js",
|
||||||
"build": "rimraf build && mkdir build && cp -r src/* build/ && cp package.json config.json build/",
|
"dev:electron": "concurrently \"NODE_ENV=development electron .\" \"vite src/electron --port 5173\"",
|
||||||
"clean": "rimraf build"
|
"build": "rimraf build && mkdir build && cp -r src/* build/ && cp package.json config.json build/ && npm run build:electron && cp src/electron/preload.js build/electron/ && rm -rf build/electron/App.jsx build/electron/main.jsx build/electron/App.css build/electron/index.css build/electron/FarmControlLogo.jsx build/electron/vite.config.js build/electron/public build/electron/build",
|
||||||
},
|
"build:electron": "vite build src/electron --outDir build/electron",
|
||||||
"author": "Tom Butcher",
|
"clean": "rimraf build"
|
||||||
"license": "ISC",
|
},
|
||||||
"dependencies": {
|
"author": "Tom Butcher",
|
||||||
"axios": "^1.8.4",
|
"license": "ISC",
|
||||||
"express": "^5.1.0",
|
"dependencies": {
|
||||||
"jsonwebtoken": "^9.0.2",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"keycloak-connect": "^26.1.1",
|
"ant-design": "^1.0.0",
|
||||||
"log4js": "^6.9.1",
|
"antd": "^5.27.0",
|
||||||
"mongoose": "^8.13.2",
|
"axios": "^1.8.4",
|
||||||
"socket.io": "^4.8.1",
|
"etcd3": "^1.1.2",
|
||||||
"ws": "^8.18.1"
|
"express": "^5.1.0",
|
||||||
},
|
"jsonwebtoken": "^9.0.2",
|
||||||
"devDependencies": {
|
"keycloak-connect": "^26.1.1",
|
||||||
"jest": "^29.7.0",
|
"lodash": "^4.17.21",
|
||||||
"nodemon": "^3.1.9",
|
"log4js": "^6.9.1",
|
||||||
"rimraf": "^5.0.5",
|
"mongoose": "^8.13.2",
|
||||||
"supertest": "^6.3.4"
|
"prop-types": "^15.8.1",
|
||||||
}
|
"socket.io": "^4.8.1",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
|
"ws": "^8.18.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"concurrently": "^9.2.0",
|
||||||
|
"electron": "^37.2.6",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"nodemon": "^3.1.9",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"rimraf": "^5.0.5",
|
||||||
|
"supertest": "^6.3.4",
|
||||||
|
"vite": "^5.0.12",
|
||||||
|
"vite-plugin-svgo": "^2.0.0",
|
||||||
|
"vite-plugin-svgr": "^4.5.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
233
src/auth/auth.js
@ -8,143 +8,140 @@ import { loadConfig } from "../config.js";
|
|||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
const logger = log4js.getLogger("Auth");
|
const logger = log4js.getLogger("Auth");
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
export class KeycloakAuth {
|
export class KeycloakAuth {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.config = config.auth;
|
this.config = config.auth;
|
||||||
this.tokenCache = new Map(); // Cache for verified tokens
|
this.tokenCache = new Map(); // Cache for verified tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify a token with Keycloak server
|
||||||
|
async verifyToken(token) {
|
||||||
|
// Check cache first
|
||||||
|
if (this.tokenCache.has(token)) {
|
||||||
|
const cachedInfo = this.tokenCache.get(token);
|
||||||
|
if (cachedInfo.expiresAt > Date.now()) {
|
||||||
|
return { valid: true, user: cachedInfo.user };
|
||||||
|
} else {
|
||||||
|
// Token expired, remove from cache
|
||||||
|
this.tokenCache.delete(token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify a token with Keycloak server
|
try {
|
||||||
async verifyToken(token) {
|
// Verify token with Keycloak introspection endpoint
|
||||||
// Check cache first
|
const response = await axios.post(
|
||||||
if (this.tokenCache.has(token)) {
|
`${this.config.keycloak.url}/realms/${this.config.keycloak.realm}/protocol/openid-connect/token/introspect`,
|
||||||
const cachedInfo = this.tokenCache.get(token);
|
new URLSearchParams({
|
||||||
if (cachedInfo.expiresAt > Date.now()) {
|
token: token,
|
||||||
return { valid: true, user: cachedInfo.user };
|
client_id: this.config.keycloak.clientId,
|
||||||
} else {
|
client_secret: this.config.keycloak.clientSecret,
|
||||||
// Token expired, remove from cache
|
}),
|
||||||
this.tokenCache.delete(token);
|
{
|
||||||
}
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
const introspection = response.data;
|
||||||
// Verify token with Keycloak introspection endpoint
|
|
||||||
const response = await axios.post(
|
|
||||||
`${this.config.keycloak.url}/realms/${this.config.keycloak.realm}/protocol/openid-connect/token/introspect`,
|
|
||||||
new URLSearchParams({
|
|
||||||
token: token,
|
|
||||||
client_id: this.config.keycloak.clientId,
|
|
||||||
client_secret: this.config.keycloak.clientSecret,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const introspection = response.data;
|
if (!introspection.active) {
|
||||||
|
logger.info("Token is not active");
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
|
||||||
if (!introspection.active) {
|
// Verify required roles if configured
|
||||||
logger.info("Token is not active");
|
if (this.config.requiredRoles && this.config.requiredRoles.length > 0) {
|
||||||
return { valid: false };
|
const hasRequiredRole = this.checkRoles(
|
||||||
}
|
introspection,
|
||||||
|
this.config.requiredRoles
|
||||||
// Verify required roles if configured
|
);
|
||||||
if (
|
if (!hasRequiredRole) {
|
||||||
this.config.requiredRoles &&
|
logger.info("User doesn't have required roles");
|
||||||
this.config.requiredRoles.length > 0
|
return { valid: false };
|
||||||
) {
|
|
||||||
const hasRequiredRole = this.checkRoles(
|
|
||||||
introspection,
|
|
||||||
this.config.requiredRoles,
|
|
||||||
);
|
|
||||||
if (!hasRequiredRole) {
|
|
||||||
logger.info("User doesn't have required roles");
|
|
||||||
return { valid: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse token to extract user info
|
|
||||||
const decodedToken = jwt.decode(token);
|
|
||||||
const user = {
|
|
||||||
id: decodedToken.sub,
|
|
||||||
username: decodedToken.preferred_username,
|
|
||||||
email: decodedToken.email,
|
|
||||||
name: decodedToken.name,
|
|
||||||
roles: this.extractRoles(decodedToken),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cache the verified token
|
|
||||||
const expiresAt = introspection.exp * 1000; // Convert to milliseconds
|
|
||||||
this.tokenCache.set(token, { expiresAt, user });
|
|
||||||
|
|
||||||
return { valid: true, user };
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Token verification error:", error.message);
|
|
||||||
return { valid: false };
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse token to extract user info
|
||||||
|
const decodedToken = jwt.decode(token);
|
||||||
|
const user = {
|
||||||
|
id: decodedToken.sub,
|
||||||
|
username: decodedToken.preferred_username,
|
||||||
|
email: decodedToken.email,
|
||||||
|
name: decodedToken.name,
|
||||||
|
roles: this.extractRoles(decodedToken),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache the verified token
|
||||||
|
const expiresAt = introspection.exp * 1000; // Convert to milliseconds
|
||||||
|
this.tokenCache.set(token, { expiresAt, user });
|
||||||
|
|
||||||
|
return { valid: true, user };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Token verification error:", error.message);
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract roles from token
|
||||||
|
extractRoles(token) {
|
||||||
|
const roles = [];
|
||||||
|
|
||||||
|
// Extract realm roles
|
||||||
|
if (token.realm_access && token.realm_access.roles) {
|
||||||
|
roles.push(...token.realm_access.roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract client roles
|
||||||
|
if (token.resource_access) {
|
||||||
|
for (const client in token.resource_access) {
|
||||||
|
if (token.resource_access[client].roles) {
|
||||||
|
roles.push(
|
||||||
|
...token.resource_access[client].roles.map(
|
||||||
|
(role) => `${client}:${role}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has required roles
|
||||||
|
checkRoles(tokenInfo, requiredRoles) {
|
||||||
// Extract roles from token
|
// Extract roles from token
|
||||||
extractRoles(token) {
|
const userRoles = this.extractRoles(tokenInfo);
|
||||||
const roles = [];
|
|
||||||
|
|
||||||
// Extract realm roles
|
// Check if user has any of the required roles
|
||||||
if (token.realm_access && token.realm_access.roles) {
|
return requiredRoles.some((role) => userRoles.includes(role));
|
||||||
roles.push(...token.realm_access.roles);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Extract client roles
|
|
||||||
if (token.resource_access) {
|
|
||||||
for (const client in token.resource_access) {
|
|
||||||
if (token.resource_access[client].roles) {
|
|
||||||
roles.push(
|
|
||||||
...token.resource_access[client].roles.map(
|
|
||||||
(role) => `${client}:${role}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user has required roles
|
|
||||||
checkRoles(tokenInfo, requiredRoles) {
|
|
||||||
// Extract roles from token
|
|
||||||
const userRoles = this.extractRoles(tokenInfo);
|
|
||||||
|
|
||||||
// Check if user has any of the required roles
|
|
||||||
return requiredRoles.some((role) => userRoles.includes(role));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Socket.IO middleware for authentication
|
// Socket.IO middleware for authentication
|
||||||
export function createAuthMiddleware(auth) {
|
export function createAuthMiddleware(auth) {
|
||||||
return async (socket, next) => {
|
return async (socket, next) => {
|
||||||
const token = socket.handshake.auth.token;
|
const token = socket.handshake.auth.token;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return next(new Error("Authentication token is required"));
|
return next(new Error("Authentication token is required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await auth.verifyToken(token);
|
const authResult = await auth.verifyToken(token);
|
||||||
|
|
||||||
if (!authResult.valid) {
|
if (!authResult.valid) {
|
||||||
return next(new Error("Invalid authentication token"));
|
return next(new Error("Invalid authentication token"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach user information to socket
|
// Attach user information to socket
|
||||||
socket.user = authResult.user;
|
socket.user = authResult.user;
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Authentication error:", err);
|
logger.error("Authentication error:", err);
|
||||||
next(new Error("Authentication failed"));
|
next(new Error("Authentication failed"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,37 +2,65 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
import log4js from "log4js";
|
||||||
|
|
||||||
// Configure paths relative to this file
|
// Configure paths relative to this file
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const CONFIG_PATH = path.resolve(__dirname, "../config.json");
|
const CONFIG_PATH = path.resolve(__dirname, "../config.json");
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("Config");
|
||||||
|
logger.level = "info";
|
||||||
|
|
||||||
// Determine environment
|
// Determine environment
|
||||||
const NODE_ENV = process.env.NODE_ENV || 'development';
|
const NODE_ENV = process.env.NODE_ENV || "development";
|
||||||
|
|
||||||
// Load config file
|
// Load config file
|
||||||
export function loadConfig() {
|
export function loadConfig() {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(CONFIG_PATH)) {
|
if (!fs.existsSync(CONFIG_PATH)) {
|
||||||
throw new Error(`Configuration file not found at ${CONFIG_PATH}`);
|
throw new Error(`Configuration file not found at ${CONFIG_PATH}`);
|
||||||
}
|
|
||||||
|
|
||||||
const configData = fs.readFileSync(CONFIG_PATH, "utf8");
|
|
||||||
const config = JSON.parse(configData);
|
|
||||||
|
|
||||||
if (!config[NODE_ENV]) {
|
|
||||||
throw new Error(`Configuration for environment '${NODE_ENV}' not found in config.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config[NODE_ENV];
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error loading config:", err);
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const configData = fs.readFileSync(CONFIG_PATH, "utf8");
|
||||||
|
const config = JSON.parse(configData);
|
||||||
|
|
||||||
|
if (!config[NODE_ENV]) {
|
||||||
|
throw new Error(
|
||||||
|
`Configuration for environment '${NODE_ENV}' not found in config.json`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config[NODE_ENV];
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading config:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save config file
|
||||||
|
export function saveConfig(newConfig) {
|
||||||
|
try {
|
||||||
|
logger.info("Saving...");
|
||||||
|
let config = {};
|
||||||
|
if (fs.existsSync(CONFIG_PATH)) {
|
||||||
|
const configData = fs.readFileSync(CONFIG_PATH, "utf8");
|
||||||
|
config = JSON.parse(configData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current environment
|
||||||
|
config[NODE_ENV] = newConfig;
|
||||||
|
|
||||||
|
// Write back to file with 2-space indentation
|
||||||
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
|
||||||
|
logger.info(`Configuration for '${NODE_ENV}' saved successfully.`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Error saving config:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current environment
|
// Get current environment
|
||||||
export function getEnvironment() {
|
export function getEnvironment() {
|
||||||
return NODE_ENV;
|
return NODE_ENV;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
const filamentSchema = new mongoose.Schema({
|
|
||||||
name: { required: true, type: String },
|
|
||||||
barcode: { required: false, type: String },
|
|
||||||
url: { required: false, type: String },
|
|
||||||
image: { required: false, type: Buffer },
|
|
||||||
color: { required: true, type: String },
|
|
||||||
vendor: { type: Schema.Types.ObjectId, ref: "Vendor", required: true },
|
|
||||||
type: { required: true, type: String },
|
|
||||||
cost: { required: true, type: Number },
|
|
||||||
diameter: { required: true, type: Number },
|
|
||||||
density: { required: true, type: Number },
|
|
||||||
createdAt: { required: true, type: Date },
|
|
||||||
updatedAt: { required: true, type: Date },
|
|
||||||
emptySpoolWeight: { required: true, type: Number },
|
|
||||||
});
|
|
||||||
|
|
||||||
filamentSchema.virtual("id").get(function () {
|
|
||||||
return this._id.toHexString();
|
|
||||||
});
|
|
||||||
|
|
||||||
filamentSchema.set("toJSON", { virtuals: true });
|
|
||||||
|
|
||||||
export const filamentModel = mongoose.model("Filament", filamentSchema);
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
// Define the main filamentStock schema
|
|
||||||
const filamentStockSchema = new Schema(
|
|
||||||
{
|
|
||||||
state: {
|
|
||||||
type: { type: String, required: true },
|
|
||||||
percent: { type: String, required: true },
|
|
||||||
},
|
|
||||||
startingGrossWeight: { type: Number, required: true },
|
|
||||||
startingNetWeight: { type: Number, required: true },
|
|
||||||
currentGrossWeight: { type: Number, required: true },
|
|
||||||
currentNetWeight: { type: Number, required: true },
|
|
||||||
filament: { type: mongoose.Schema.Types.ObjectId, ref: "Filament" },
|
|
||||||
stockEvents: [{ type: mongoose.Schema.Types.ObjectId, ref: "StockEvent" }]
|
|
||||||
},
|
|
||||||
{ timestamps: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add virtual id getter
|
|
||||||
filamentStockSchema.virtual("id").get(function () {
|
|
||||||
return this._id.toHexString();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure JSON serialization to include virtuals
|
|
||||||
filamentStockSchema.set("toJSON", { virtuals: true });
|
|
||||||
|
|
||||||
// Create and export the model
|
|
||||||
export const filamentStockModel = mongoose.model(
|
|
||||||
"FilamentStock",
|
|
||||||
filamentStockSchema,
|
|
||||||
);
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
const gcodeFileSchema = new mongoose.Schema({
|
|
||||||
name: { required: true, type: String },
|
|
||||||
gcodeFileName: { required: false, type: String },
|
|
||||||
gcodeFileInfo: { required: true, type: Object },
|
|
||||||
size: { type: Number, required: false },
|
|
||||||
filament: { type: Schema.Types.ObjectId, ref: "Filament", required: true },
|
|
||||||
parts: [{ type: Schema.Types.ObjectId, ref: "Part", required: true }],
|
|
||||||
cost: { type: Number, required: false },
|
|
||||||
createdAt: { type: Date },
|
|
||||||
updatedAt: { type: Date },
|
|
||||||
});
|
|
||||||
|
|
||||||
gcodeFileSchema.index({ name: "text", brand: "text" });
|
|
||||||
|
|
||||||
gcodeFileSchema.virtual("id").get(function () {
|
|
||||||
return this._id.toHexString();
|
|
||||||
});
|
|
||||||
|
|
||||||
gcodeFileSchema.set("toJSON", { virtuals: true });
|
|
||||||
|
|
||||||
export const gcodeFileModel = mongoose.model("GCodeFile", gcodeFileSchema);
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
import { loadConfig } from "../config.js";
|
|
||||||
import log4js from "log4js";
|
|
||||||
// Load configuration
|
|
||||||
const config = loadConfig();
|
|
||||||
|
|
||||||
const logger = log4js.getLogger("Mongo DB");
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
function dbConnect() {
|
|
||||||
mongoose.connection.once("open", () => logger.info("Database connected."));
|
|
||||||
return mongoose.connect(config.database.url, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { dbConnect };
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
// Define the moonraker connection schema
|
|
||||||
const moonrakerSchema = new Schema(
|
|
||||||
{
|
|
||||||
host: { type: String, required: true },
|
|
||||||
port: { type: Number, required: true },
|
|
||||||
protocol: { type: String, required: true },
|
|
||||||
apiKey: { type: String, default: null, required: false },
|
|
||||||
},
|
|
||||||
{ _id: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Define the alert schema
|
|
||||||
const alertSchema = new Schema(
|
|
||||||
{
|
|
||||||
priority: { type: String, required: true }, // order to show
|
|
||||||
type: { type: String, required: true }, // selectFilament, error, info, message,
|
|
||||||
message: { type: String, required: false }
|
|
||||||
},
|
|
||||||
{ timestamps: true, _id: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Define the main printer schema
|
|
||||||
const printerSchema = new Schema(
|
|
||||||
{
|
|
||||||
name: { type: String, required: true },
|
|
||||||
online: { type: Boolean, required: true, default: false },
|
|
||||||
state: {
|
|
||||||
type: { type: String, required: true, default: "Offline" },
|
|
||||||
progress: { required: false, type: Number, default: 0 },
|
|
||||||
},
|
|
||||||
connectedAt: { type: Date, default: null },
|
|
||||||
loadedFilament: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: "Filament",
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
moonraker: { type: moonrakerSchema, required: true },
|
|
||||||
tags: [{ type: String }],
|
|
||||||
firmware: { type: String },
|
|
||||||
currentJob: { type: Schema.Types.ObjectId, ref: "PrintJob" },
|
|
||||||
currentSubJob: { type: Schema.Types.ObjectId, ref: "PrintSubJob" },
|
|
||||||
currentFilamentStock: { type: Schema.Types.ObjectId, ref: "FilamentStock" },
|
|
||||||
subJobs: [{ type: Schema.Types.ObjectId, ref: "PrintSubJob" }],
|
|
||||||
alerts: [alertSchema],
|
|
||||||
},
|
|
||||||
{ timestamps: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add virtual id getter
|
|
||||||
printerSchema.virtual("id").get(function () {
|
|
||||||
return this._id.toHexString();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure JSON serialization to include virtuals
|
|
||||||
printerSchema.set("toJSON", { virtuals: true });
|
|
||||||
|
|
||||||
// Create and export the model
|
|
||||||
export const printerModel = mongoose.model("Printer", printerSchema);
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
const printJobSchema = new mongoose.Schema({
|
|
||||||
state: {
|
|
||||||
type: { required: true, type: String },
|
|
||||||
progress: { required: false, type: Number, default: 0 },
|
|
||||||
},
|
|
||||||
subJobStats : {
|
|
||||||
required: false, type: Object
|
|
||||||
},
|
|
||||||
printers: [{ type: Schema.Types.ObjectId, ref: "Printer", required: false }],
|
|
||||||
createdAt: { required: true, type: Date },
|
|
||||||
updatedAt: { required: true, type: Date },
|
|
||||||
startedAt: { required: false, type: Date },
|
|
||||||
finishedAt: { required: false, type: Date },
|
|
||||||
gcodeFile: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: "GCodeFile",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
quantity: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
default: 1,
|
|
||||||
min: 1,
|
|
||||||
},
|
|
||||||
subJobs: [
|
|
||||||
{type: Schema.Types.ObjectId, ref: "PrintSubJob", required: false}
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
printJobSchema.virtual("id").get(function () {
|
|
||||||
return this._id.toHexString();
|
|
||||||
});
|
|
||||||
|
|
||||||
printJobSchema.set("toJSON", { virtuals: true });
|
|
||||||
|
|
||||||
export const printJobModel = mongoose.model("PrintJob", printJobSchema);
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
const printSubJobSchema = new mongoose.Schema({
|
|
||||||
printer: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: "Printer",
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
printJob: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: "PrintJob",
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
subJobId: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
gcodeFile: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: "GCodeFile",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
type: { required: true, type: String },
|
|
||||||
progress: { required: false, type: Number, default: 0 },
|
|
||||||
},
|
|
||||||
number: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now
|
|
||||||
},
|
|
||||||
startedAt: { required: false, type: Date },
|
|
||||||
finishedAt: { required: false, type: Date },
|
|
||||||
});
|
|
||||||
|
|
||||||
printSubJobSchema.virtual("id").get(function () {
|
|
||||||
return this._id.toHexString();
|
|
||||||
});
|
|
||||||
|
|
||||||
printSubJobSchema.set("toJSON", { virtuals: true });
|
|
||||||
|
|
||||||
export const printSubJobModel = mongoose.model("PrintSubJob", printSubJobSchema);
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
const stockEventSchema = new Schema(
|
|
||||||
{
|
|
||||||
type: { type: String, required: true },
|
|
||||||
value: { type: Number, required: true },
|
|
||||||
unit: { type: String, required: true},
|
|
||||||
subJob: { type: Schema.Types.ObjectId, ref: "PrintSubJob", required: false },
|
|
||||||
job: { type: Schema.Types.ObjectId, ref: "PrintJob", required: false },
|
|
||||||
filamentStock: { type: Schema.Types.ObjectId, ref: "FilamentStock", required: true },
|
|
||||||
timestamp: { type: Date, default: Date.now }
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add virtual id getter
|
|
||||||
stockEventSchema.virtual("id").get(function () {
|
|
||||||
return this._id.toHexString();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure JSON serialization to include virtuals
|
|
||||||
stockEventSchema.set("toJSON", { virtuals: true });
|
|
||||||
|
|
||||||
// Create and export the model
|
|
||||||
export const stockEventModel = mongoose.model("StockEvent", stockEventSchema);
|
|
||||||
14
src/documentprinter/documentprintermanager.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { loadConfig } from "../config.js";
|
||||||
|
import log4js from "log4js";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("Document Printer Manager");
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
export class DocumentPrinterManager {
|
||||||
|
constructor(socketClient) {
|
||||||
|
this.socketClient = socketClient;
|
||||||
|
this.documentPrinterClients = new Map();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/electron/App.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#root {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme-aware styles */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
241
src/electron/App.jsx
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import "./App.css";
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Tag,
|
||||||
|
Menu,
|
||||||
|
ConfigProvider,
|
||||||
|
theme,
|
||||||
|
Layout,
|
||||||
|
Modal,
|
||||||
|
} from "antd";
|
||||||
|
import { MenuOutlined } from "@ant-design/icons";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import merge from "lodash/merge";
|
||||||
|
import unionBy from "lodash/unionBy";
|
||||||
|
import Overview from "./pages/Overview";
|
||||||
|
import Printers from "./pages/Printers";
|
||||||
|
import Loading from "./pages/Loading";
|
||||||
|
import OTPInput from "./pages/OTPInput";
|
||||||
|
import CloudIcon from "./icons/CloudIcon";
|
||||||
|
import LockIcon from "./icons/LockIcon";
|
||||||
|
import SettingsIcon from "./icons/SettingsIcon";
|
||||||
|
import Disconnected from "./pages/Disconnected";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [host, setHost] = useState({});
|
||||||
|
const [printers, setPrinters] = useState([]);
|
||||||
|
const [documentPrinters, setDocumentPrinters] = useState([]);
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
const [authenticated, setAuthenticated] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [currentPageKey, setCurrentPageKey] = useState("overview");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
|
||||||
|
// Listen for system theme changes
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setIsDarkMode(e.matches);
|
||||||
|
console.log("CHANGE", e);
|
||||||
|
};
|
||||||
|
mediaQuery.addEventListener("change", handleChange);
|
||||||
|
setIsDarkMode(mediaQuery.matches);
|
||||||
|
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Setting up IPC listeners...");
|
||||||
|
// Set up IPC listeners when component mounts
|
||||||
|
window.electronAPI.onIPCData("setHost", (newHost) => {
|
||||||
|
console.log("Host data received:", newHost);
|
||||||
|
setHost((prev) => merge(prev, newHost));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onIPCData("setPrinters", (newPrinters) => {
|
||||||
|
console.log("Printers data:", newPrinters);
|
||||||
|
setPrinters(newPrinters);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onIPCData("setPrinter", (newPrinter) => {
|
||||||
|
console.log("Printer data:", newPrinter);
|
||||||
|
setPrinters((prev) => unionBy(prev, [newPrinter], "_id"));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onIPCData(
|
||||||
|
"setDocumentPrinters",
|
||||||
|
(newDocumentPrinters) => {
|
||||||
|
console.log("Document printers data:", newDocumentPrinters);
|
||||||
|
setDocumentPrinters((prev) =>
|
||||||
|
unionBy(prev, newDocumentPrinters, "_id")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
window.electronAPI.onIPCData("setAuthenticated", (setAuthenticated) => {
|
||||||
|
console.log("Set authenticated:", setAuthenticated);
|
||||||
|
setLoading(setAuthenticated);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onIPCData("setConnected", (isConnected) => {
|
||||||
|
console.log("Set connected:", isConnected);
|
||||||
|
setConnected(isConnected);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onIPCData("setAuthenticated", (isAuthenticated) => {
|
||||||
|
console.log("Set authenticated:", isAuthenticated);
|
||||||
|
setAuthenticated(isAuthenticated);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onIPCData("setLoading", (isLoading) => {
|
||||||
|
console.log("Set loading:", isLoading);
|
||||||
|
setLoading(isLoading);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onIPCData("setPrinters", (newPrinters) => {
|
||||||
|
console.log("Printers data:", newPrinters);
|
||||||
|
setPrinters((prev) => unionBy(prev, newPrinters, "_id"));
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Sending get data...");
|
||||||
|
// Request initial data
|
||||||
|
window.electronAPI.sendIPC("getData");
|
||||||
|
|
||||||
|
// Cleanup listeners when component unmounts
|
||||||
|
return () => {
|
||||||
|
window.electronAPI.removeAllListeners("setHost");
|
||||||
|
window.electronAPI.removeAllListeners("setPrinters");
|
||||||
|
window.electronAPI.removeAllListeners("setDocumentPrinters");
|
||||||
|
window.electronAPI.removeAllListeners("setAuthenticated");
|
||||||
|
window.electronAPI.removeAllListeners("setConnected");
|
||||||
|
window.electronAPI.removeAllListeners("setLoading");
|
||||||
|
};
|
||||||
|
}, []); // Empty dependency array means this runs once on mount
|
||||||
|
|
||||||
|
// Function to render the appropriate page based on currentPageKey and auth status
|
||||||
|
const renderCurrentPage = () => {
|
||||||
|
// If loading, show loading
|
||||||
|
if (loading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not authenticated but connected, show OTP input
|
||||||
|
if (connected === false && loading == false) {
|
||||||
|
return <Disconnected />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not authenticated but connected, show OTP input
|
||||||
|
if (authenticated === false && connected === true) {
|
||||||
|
return <OTPInput />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If authenticated and connected, show the selected page
|
||||||
|
switch (currentPageKey) {
|
||||||
|
case "overview":
|
||||||
|
return <Overview printers={printers} host={host} loading={loading} />;
|
||||||
|
case "printers":
|
||||||
|
return <Printers printers={printers} />;
|
||||||
|
case "documentPrinters":
|
||||||
|
return <div>Document Printers Page (to be implemented)</div>;
|
||||||
|
default:
|
||||||
|
return <Overview />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle menu item clicks
|
||||||
|
const handleMenuClick = ({ key }) => {
|
||||||
|
setCurrentPageKey(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mainMenuItems = [
|
||||||
|
{
|
||||||
|
key: "overview",
|
||||||
|
label: "Overview",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "printers",
|
||||||
|
label: "Printers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "documentPrinters",
|
||||||
|
label: "Document Printers",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorPrimary: "#007AFF",
|
||||||
|
colorSuccess: "#32D74B",
|
||||||
|
colorWarning: "#FF9F0A",
|
||||||
|
colorInfo: "#0A84FF",
|
||||||
|
colorLink: "#5AC8F5",
|
||||||
|
borderRadius: "10px",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Layout: {
|
||||||
|
headerBg: isDarkMode ? "#141414" : "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layout>
|
||||||
|
<Flex style={{ width: "100vw", height: "100vh" }} vertical>
|
||||||
|
<Flex
|
||||||
|
className="ant-menu-horizontal ant-menu-light"
|
||||||
|
style={{ lineHeight: "40px", padding: "0 8px 0 75px" }}
|
||||||
|
>
|
||||||
|
{loading == false && authenticated == true && connected == true ? (
|
||||||
|
<Menu
|
||||||
|
mode="horizontal"
|
||||||
|
items={mainMenuItems}
|
||||||
|
selectedKeys={[currentPageKey]}
|
||||||
|
style={{
|
||||||
|
flexWrap: "wrap",
|
||||||
|
border: 0,
|
||||||
|
lineHeight: "40px",
|
||||||
|
}}
|
||||||
|
overflowedIndicator={
|
||||||
|
<Button type="text" icon={<MenuOutlined />} />
|
||||||
|
}
|
||||||
|
onClick={handleMenuClick}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="electron-navigation" style={{ flexGrow: 1 }}></div>
|
||||||
|
<Flex align="center" gap={"small"}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<SettingsIcon />}
|
||||||
|
style={{ marginTop: "1px" }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Tag
|
||||||
|
color={authenticated ? "success" : "warning"}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
icon={<LockIcon />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tag
|
||||||
|
color={connected ? "success" : "error"}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
icon={<CloudIcon />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<div style={{ overflow: "auto", margin: "16px", height: "100%" }}>
|
||||||
|
{renderCurrentPage()}
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</Layout>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
9
src/electron/FarmControlLogo.jsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import { ReactComponent as CustomIconSvg } from "./assets/farmcontrollogo.svg";
|
||||||
|
|
||||||
|
const FarmControlLogo = (props) => (
|
||||||
|
<Icon component={CustomIconSvg} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export default FarmControlLogo;
|
||||||
33
src/electron/assets/farmcontrollogo.svg
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||||
|
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" version="1.1" viewBox="0 0 1280 113" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="matrix(1.0156 0 0 1.0156 -2.2117e-16 0)">
|
||||||
|
<g transform="matrix(.67805 0 0 .67805 -10.706 -23.499)" fill-rule="nonzero">
|
||||||
|
<path d="m15.789 187.1v-142.1c0-1.997 0.817-3.766 2.45-5.309 1.634-1.542 3.449-2.314 5.445-2.314h77.857c1.997 0 3.812 0.772 5.445 2.314 1.633 1.543 2.45 3.312 2.45 5.309v18.239c0 1.996-0.817 3.766-2.45 5.309-1.633 1.542-3.448 2.314-5.445 2.314h-49.273v35.934h43.557c1.996 0 3.811 0.771 5.444 2.314 1.634 1.542 2.45 3.312 2.45 5.308v18.24c0 1.996-0.816 3.765-2.45 5.308-1.633 1.543-3.448 2.314-5.444 2.314h-43.557v46.823c0 1.997-0.771 3.812-2.314 5.445s-3.312 2.45-5.308 2.45h-21.234c-1.997 0-3.766-0.817-5.309-2.45-1.542-1.633-2.314-3.448-2.314-5.445z"/>
|
||||||
|
<path d="m163.34 37.379h30.49c3.63 0 6.352 1.725 8.167 5.173l44.646 144.28c0 2.178-0.772 4.084-2.314 5.717-1.543 1.633-3.403 2.45-5.581 2.45h-24.229c-3.629 0-6.079-1.815-7.35-5.445l-5.717-20.961h-45.734l-5.717 20.961c-1.27 3.63-3.72 5.445-7.35 5.445h-24.228c-2.178 0-4.038-0.817-5.581-2.45s-2.314-3.539-2.314-5.717l44.646-144.28c1.814-3.448 4.537-5.173 8.166-5.173zm15.245 46.552-13.883 51.179h27.767l-13.884-51.179z"/>
|
||||||
|
<path d="m315.51 117.69c6.715 0 12.16-1.906 16.334-5.717s6.261-9.8 6.261-17.967-2.087-14.201-6.261-18.103-9.619-5.853-16.334-5.853h-12.25v47.64h12.25zm29.129 74.591-22.051-42.468-6.261 0.544h-13.067v36.751c0 1.997-0.771 3.812-2.314 5.445-1.542 1.633-3.312 2.45-5.308 2.45h-21.234c-1.996 0-3.766-0.817-5.309-2.45-1.542-1.633-2.314-3.448-2.314-5.445v-142.1c0-1.997 0.817-3.766 2.451-5.309 1.633-1.542 3.448-2.314 5.444-2.314h41.651c0.908 0 2.087 0.046 3.539 0.137 1.452 0.09 4.31 0.499 8.575 1.225 4.265 0.725 8.303 1.724 12.115 2.994 3.811 1.27 8.076 3.358 12.794 6.261 4.719 2.904 8.757 6.262 12.114 10.073 3.358 3.811 6.216 8.847 8.576 15.109 2.359 6.261 3.539 13.203 3.539 20.825 0 19.056-6.715 33.484-20.145 43.284l25.861 50.091c0 2.177-0.68 3.992-2.041 5.444s-3.131 2.178-5.309 2.178h-25.317c-2.541 0-4.537-0.907-5.989-2.722z"/>
|
||||||
|
<path d="m480.48 174.04h-3.266c-1.997 0-3.358-0.726-4.084-2.177l-33.212-60.708v75.952c0 1.997-0.771 3.812-2.314 5.445-1.542 1.633-3.312 2.45-5.308 2.45h-21.234c-1.996 0-3.766-0.817-5.309-2.45-1.542-1.633-2.314-3.448-2.314-5.445v-141.83c0-1.996 0.772-3.811 2.314-5.445 1.543-1.633 3.313-2.45 5.309-2.45h24.5c2.904 0 4.901 1.089 5.99 3.267l37.023 66.424 37.023-66.424c1.089-2.178 3.085-3.267 5.989-3.267h24.5c1.997 0 3.766 0.817 5.309 2.45 1.543 1.634 2.314 3.449 2.314 5.445v141.83c0 1.997-0.771 3.812-2.314 5.445s-3.312 2.45-5.309 2.45h-21.233c-1.997 0-3.766-0.817-5.309-2.45s-2.314-3.448-2.314-5.445v-75.952l-33.212 60.708c-0.726 1.451-1.905 2.177-3.539 2.177z"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(.67805 0 0 .67805 15.294 -23.499)" fill="url(#a)" fill-rule="nonzero">
|
||||||
|
<path d="m656.07 198c-22.686 0-40.608-7.759-53.765-23.276-13.158-15.517-19.737-34.981-19.737-58.393s6.579-42.876 19.737-58.393c13.157-15.517 31.079-23.276 53.765-23.276 39.019 0 62.703 18.149 71.052 54.446-0.363 1.996-1.316 3.72-2.859 5.172-1.542 1.452-3.312 2.178-5.308 2.178h-23.412c-2.722 0-4.809-1.361-6.261-4.083-4.356-16.153-15.426-24.229-33.212-24.229-12.16 0-21.143 4.447-26.951 13.339-5.807 8.893-8.711 20.508-8.711 34.846 0 14.156 2.904 25.725 8.711 34.709 5.808 8.984 14.791 13.475 26.951 13.475 17.786 0 28.856-8.076 33.212-24.228 1.452-2.722 3.539-4.083 6.261-4.083h23.412c1.996 0 3.766 0.725 5.308 2.177 1.543 1.452 2.496 3.176 2.859 5.173-8.349 36.297-32.033 54.446-71.052 54.446z"/>
|
||||||
|
<path d="m767.14 57.797c12.704-15.427 30.399-23.14 53.085-23.14s40.335 7.713 52.949 23.14c12.613 15.426 18.92 34.936 18.92 58.529 0 23.412-6.352 42.876-19.056 58.393s-30.309 23.276-52.813 23.276-40.108-7.759-52.812-23.276-19.056-34.981-19.056-58.393c0-23.593 6.261-43.103 18.783-58.529zm19.056 58.529c0 32.123 11.343 48.184 34.029 48.184s34.029-16.061 34.029-48.184-11.343-48.185-34.029-48.185-34.029 16.062-34.029 48.185z"/>
|
||||||
|
<path d="m1005.3 191.73-47.91-78.129v73.501c0 1.997-0.772 3.812-2.314 5.445-1.543 1.633-3.313 2.45-5.309 2.45h-21.234c-1.996 0-3.766-0.817-5.308-2.45-1.543-1.633-2.314-3.448-2.314-5.445v-141.83c0-1.996 0.771-3.811 2.314-5.445 1.542-1.633 3.312-2.45 5.308-2.45h22.867c2.723 0 4.719 1.089 5.99 3.267l50.09 82.758v-78.13c0-1.996 0.77-3.811 2.31-5.445 1.55-1.633 3.32-2.45 5.31-2.45h21.24c1.99 0 3.76 0.817 5.3 2.45 1.55 1.634 2.32 3.449 2.32 5.445v141.83c0 1.997-0.77 3.812-2.32 5.445-1.54 1.633-3.31 2.45-5.3 2.45h-25.05c-2.72 0-4.72-1.089-5.99-3.267z"/>
|
||||||
|
<path d="m1076.7 37.379h99.9c2 0 3.81 0.772 5.45 2.314 1.63 1.543 2.45 3.312 2.45 5.309v18.239c0 1.996-0.82 3.766-2.45 5.309-1.64 1.542-3.45 2.314-5.45 2.314h-31.57v116.24c0 1.997-0.78 3.812-2.32 5.445s-3.31 2.45-5.31 2.45h-21.5c-2 0-3.77-0.817-5.31-2.45s-2.32-3.448-2.32-5.445v-116.24h-31.57c-2 0-3.81-0.772-5.45-2.314-1.63-1.543-2.45-3.313-2.45-5.309v-18.239c0-1.997 0.82-3.766 2.45-5.309 1.64-1.542 3.45-2.314 5.45-2.314z"/>
|
||||||
|
<path d="m1258 117.69c6.71 0 12.16-1.906 16.33-5.717 4.18-3.811 6.27-9.8 6.27-17.967s-2.09-14.201-6.27-18.103c-4.17-3.902-9.62-5.853-16.33-5.853h-12.25v47.64h12.25zm29.13 74.591-22.05-42.468-6.26 0.544h-13.07v36.751c0 1.997-0.77 3.812-2.31 5.445-1.55 1.633-3.32 2.45-5.31 2.45h-21.24c-1.99 0-3.76-0.817-5.3-2.45-1.55-1.633-2.32-3.448-2.32-5.445v-142.1c0-1.997 0.82-3.766 2.45-5.309 1.64-1.542 3.45-2.314 5.45-2.314h41.65c0.9 0 2.08 0.046 3.54 0.137 1.45 0.09 4.31 0.499 8.57 1.225 4.27 0.725 8.3 1.724 12.12 2.994 3.81 1.27 8.07 3.358 12.79 6.261 4.72 2.904 8.76 6.262 12.11 10.073 3.36 3.811 6.22 8.847 8.58 15.109 2.36 6.261 3.54 13.203 3.54 20.825 0 19.056-6.72 33.484-20.15 43.284l25.87 50.091c0 2.177-0.68 3.992-2.05 5.444-1.36 1.452-3.13 2.178-5.31 2.178h-25.31c-2.54 0-4.54-0.907-5.99-2.722z"/>
|
||||||
|
<path d="m1360.6 57.797c12.71-15.427 30.4-23.14 53.08-23.14 22.69 0 40.34 7.713 52.95 23.14 12.62 15.426 18.92 34.936 18.92 58.529 0 23.412-6.35 42.876-19.05 58.393-12.71 15.517-30.31 23.276-52.82 23.276-22.5 0-40.1-7.759-52.81-23.276-12.7-15.517-19.05-34.981-19.05-58.393 0-23.593 6.26-43.103 18.78-58.529zm19.06 58.529c0 32.123 11.34 48.184 34.02 48.184 22.69 0 34.03-16.061 34.03-48.184s-11.34-48.185-34.03-48.185c-22.68 0-34.02 16.062-34.02 48.185z"/>
|
||||||
|
<path d="m1514.4 187.38v-142.1c0-1.996 0.77-3.811 2.31-5.445 1.55-1.633 3.32-2.45 5.31-2.45h21.24c1.99 0 3.76 0.817 5.31 2.45 1.54 1.634 2.31 3.449 2.31 5.445v116.24h49.82c1.99 0 3.81 0.771 5.44 2.314 1.64 1.542 2.45 3.312 2.45 5.308v18.24c0 1.996-0.81 3.765-2.45 5.308-1.63 1.543-3.45 2.314-5.44 2.314h-78.4c-2 0-3.82-0.771-5.45-2.314s-2.45-3.312-2.45-5.308z"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.304 0 0 1.304 -18.701 -415.07)">
|
||||||
|
<path d="m947.98 337.61h-46.26c-1.056 0-2.016-0.408-2.88-1.224s-1.296-1.752-1.296-2.808v-11.232c0-1.056 0.432-1.992 1.296-2.808s1.824-1.224 2.88-1.224h66.099c3.382 0 6.386 1.364 9.01 4.091 2.727 2.624 4.091 5.628 4.091 9.01v66.099c0 1.056-0.408 2.016-1.224 2.88s-1.752 1.296-2.808 1.296h-11.232c-1.056 0-1.992-0.432-2.808-1.296s-1.224-1.824-1.224-2.88v-46.26l-49.262 49.263c-0.747 0.747-1.714 1.137-2.902 1.171s-2.156-0.323-2.902-1.069l-7.942-7.943c-0.747-0.746-1.104-1.714-1.07-2.902 0.034-1.187 0.425-2.155 1.171-2.902l49.263-49.262z" fill-rule="nonzero"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="a" x2="1" gradientTransform="matrix(9.8182e-15 -160.34 160.34 9.8182e-15 1115.9 195)" gradientUnits="userSpaceOnUse"><stop stop-color="#00a2ff" offset="0"/><stop stop-color="#008eff" offset="1"/></linearGradient>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.7 KiB |
7
src/electron/assets/icons/checkicon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1.0502,0,0,1.0502,3,1.95893)">
|
||||||
|
<path d="M21.134,55.708C22.759,55.708 24.041,55.029 24.932,53.671L54.306,7.879C54.964,6.849 55.227,5.987 55.227,5.139C55.227,2.987 53.714,1.503 51.54,1.503C49.998,1.503 49.102,2.021 48.166,3.49L21.009,46.634L7.015,28.64C6.104,27.434 5.149,26.929 3.799,26.929C1.567,26.929 0,28.491 0,30.648C0,31.575 0.346,32.511 1.126,33.458L17.316,53.715C18.394,55.063 19.56,55.708 21.134,55.708Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 934 B |
7
src/electron/assets/icons/cloudicon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.842938,0,0,0.842938,-1.20699,10.7166)">
|
||||||
|
<path d="M17.234,50.498L58.219,50.498C69.004,50.498 77.357,42.333 77.357,31.858C77.357,21.284 68.895,13.259 57.377,13.259C53.159,4.936 45.5,0 36.011,0C23.706,0 13.354,9.339 12.075,21.965C5.656,23.887 1.432,29.266 1.432,36.013C1.432,44.099 7.374,50.498 17.234,50.498ZM17.256,44.64C11.012,44.64 7.29,41.301 7.29,36.174C7.29,31.759 10.12,28.604 15.222,27.255C17,26.812 17.651,26.016 17.821,24.103C18.751,13.486 26.384,5.858 36.011,5.858C43.345,5.858 49.09,9.961 52.635,17.093C53.419,18.674 54.336,19.211 56.288,19.211C65.99,19.211 71.473,24.864 71.473,32.006C71.473,39.129 65.818,44.64 58.439,44.64L17.256,44.64Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
10
src/electron/assets/icons/copyicon.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.804399,0,0,0.804399,5.78037,0)">
|
||||||
|
<path d="M15.46,10.251C15.46,3.526 18.935,0 25.609,0L37.353,0C40.949,0 43.76,0.934 46.097,3.327L61.681,19.184C64.149,21.714 65.003,24.337 65.003,28.446L65.003,53.213C65.003,59.932 61.528,63.464 54.854,63.464L49.549,63.464L49.549,57.342L54.32,57.342C57.356,57.342 58.881,55.738 58.881,52.833L58.881,26.958L45.094,26.958C41.253,26.958 39.216,24.946 39.216,21.079L39.216,6.122L26.118,6.122C23.081,6.122 21.582,7.752 21.582,10.632L21.582,16.032C21.407,16.028 21.225,16.027 21.039,16.027L15.46,16.027L15.46,10.251ZM44.326,20.307C44.326,21.378 44.769,21.847 45.841,21.847L57.287,21.847L44.326,8.681L44.326,20.307Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.804399,0,0,0.804399,5.78037,0)">
|
||||||
|
<path d="M0,69.24C0,75.991 3.455,79.491 10.149,79.491L39.4,79.491C46.094,79.491 49.549,75.959 49.549,69.24L49.549,45.188C49.549,41.003 49.018,39.044 46.4,36.375L29.462,19.176C26.947,16.609 24.804,16.027 21.039,16.027L10.149,16.027C3.481,16.027 0,19.528 0,26.278L0,69.24ZM6.122,68.859L6.122,26.633C6.122,23.779 7.621,22.149 10.663,22.149L20.266,22.149L20.266,39.256C20.266,43.734 22.48,45.916 26.901,45.916L43.421,45.916L43.421,68.859C43.421,71.765 41.896,73.369 38.886,73.369L10.637,73.369C7.621,73.369 6.122,71.765 6.122,68.859ZM27.506,40.517C26.216,40.517 25.666,39.972 25.666,38.677L25.666,23.351L42.57,40.517L27.506,40.517Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
15
src/electron/assets/icons/documentprintericon.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.784149,0,0,0.784149,2.8024,2.29292)">
|
||||||
|
<g transform="matrix(1,0,0,1,1.52726,3.10699)">
|
||||||
|
<path d="M58.753,9.703L58.753,11.382L53.049,11.382L53.049,9.269C53.049,6.903 51.849,5.734 49.494,5.734L21.921,5.734C19.592,5.734 18.366,6.903 18.366,9.269L18.366,11.382L12.662,11.382L12.662,9.703C12.662,3.356 16.079,0.469 21.927,0.469L49.487,0.469C55.598,0.469 58.753,3.356 58.753,9.703Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,1.52726,3.10699)">
|
||||||
|
<path d="M71.415,21.061L71.415,49.816C71.415,56.191 67.983,59.496 61.633,59.496L58.388,59.496L58.388,53.961L61.639,53.961C64.204,53.961 65.557,52.614 65.557,50.044L65.557,20.839C65.557,18.269 64.204,16.911 61.639,16.911L9.801,16.911C7.211,16.911 5.884,18.269 5.884,20.839L5.884,50.044C5.884,52.614 7.211,53.961 9.801,53.961L13.027,53.961L13.027,59.496L9.807,59.496C3.432,59.496 0,56.191 0,49.816L0,21.061C0,14.712 3.694,11.382 9.807,11.382L61.633,11.382C67.983,11.382 71.415,14.712 71.415,21.061ZM58.554,24.233C58.554,26.468 56.711,28.285 54.527,28.285C52.292,28.285 50.474,26.468 50.474,24.233C50.474,22.049 52.292,20.206 54.527,20.206C56.711,20.206 58.554,22.049 58.554,24.233Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,1.52726,3.10699)">
|
||||||
|
<path d="M19.642,69.086L51.773,69.086C56.203,69.086 58.388,67.054 58.388,62.465L58.388,37.831C58.388,33.248 56.203,31.216 51.773,31.216L19.642,31.216C15.372,31.216 13.027,33.248 13.027,37.831L13.027,62.465C13.027,67.054 15.213,69.086 19.642,69.086ZM20.958,63.693C19.464,63.693 18.688,62.948 18.688,61.423L18.688,38.853C18.688,37.329 19.464,36.615 20.958,36.615L50.483,36.615C52.002,36.615 52.727,37.329 52.727,38.853L52.727,61.423C52.727,62.948 52.002,63.693 50.483,63.693L20.958,63.693ZM25.386,46.96L46.097,46.96C47.318,46.96 48.221,46.032 48.221,44.806C48.221,43.642 47.318,42.745 46.097,42.745L25.386,42.745C24.154,42.745 23.231,43.642 23.231,44.806C23.231,46.032 24.159,46.96 25.386,46.96ZM25.386,57.596L46.097,57.596C47.318,57.596 48.221,56.674 48.221,55.504C48.221,54.304 47.318,53.376 46.097,53.376L25.386,53.376C24.159,53.376 23.231,54.304 23.231,55.504C23.231,56.674 24.154,57.596 25.386,57.596Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
7
src/electron/assets/icons/hosticon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.804972,0,0,0.804972,2,13.2492)">
|
||||||
|
<path d="M10.251,46.567L64.286,46.567C70.946,46.567 74.537,43.028 74.537,36.419L74.537,25.612C74.537,21.168 73.402,17.984 70.78,15.337L60.737,5.006C57.346,1.558 54.946,0.02 50.458,0.02L24.084,0.02C19.622,0.02 17.197,1.558 13.826,5.006L3.757,15.337C1.121,18.035 0,21.089 0,25.612L0,36.419C0,43.028 3.591,46.567 10.251,46.567ZM10.528,40.574C7.661,40.574 6.044,39.008 6.044,36.039L6.044,29.648C6.044,27.744 6.941,26.821 8.819,26.821L65.718,26.821C67.596,26.821 68.493,27.744 68.493,29.648L68.493,36.039C68.493,39.008 66.876,40.574 64.009,40.574L10.528,40.574ZM8.292,21.549C7.221,21.549 6.919,20.566 7.559,19.921L19.028,7.799C20.779,5.964 22.053,5.291 24.419,5.291L50.124,5.291C52.484,5.291 53.758,5.964 55.515,7.799L67.004,19.921C67.618,20.566 67.342,21.549 66.245,21.549L8.292,21.549ZM12.847,36.651C14.503,36.651 15.898,35.276 15.898,33.599C15.898,31.943 14.503,30.574 12.847,30.574C11.171,30.574 9.801,31.943 9.801,33.599C9.801,35.276 11.171,36.651 12.847,36.651ZM21.209,37.622C22.246,37.622 23.134,36.81 23.134,35.794L23.134,31.61C23.134,30.579 22.246,29.767 21.209,29.767C20.193,29.767 19.381,30.579 19.381,31.61L19.381,35.794C19.381,36.81 20.193,37.622 21.209,37.622ZM27.271,37.622C28.308,37.622 29.196,36.81 29.196,35.794L29.196,31.61C29.196,30.579 28.308,29.767 27.271,29.767C26.261,29.767 25.449,30.579 25.449,31.61L25.449,35.794C25.449,36.81 26.261,37.622 27.271,37.622ZM33.806,35.517C34.849,35.517 35.698,34.667 35.698,33.599C35.698,32.551 34.849,31.708 33.806,31.708C32.758,31.708 31.909,32.551 31.909,33.599C31.909,34.667 32.758,35.517 33.806,35.517ZM40.361,37.622C41.366,37.622 42.178,36.81 42.178,35.794L42.178,31.61C42.178,30.579 41.366,29.767 40.361,29.767C39.319,29.767 38.431,30.579 38.431,31.61L38.431,35.794C38.431,36.81 39.319,37.622 40.361,37.622ZM46.423,37.622C47.459,37.622 48.246,36.81 48.246,35.794L48.246,31.61C48.246,30.579 47.459,29.767 46.423,29.767C45.387,29.767 44.518,30.579 44.518,31.61L44.518,35.794C44.518,36.81 45.387,37.622 46.423,37.622ZM53.651,35.581L63.098,35.581C64.146,35.581 64.946,34.704 64.946,33.656C64.946,32.629 64.146,31.834 63.098,31.834L53.651,31.834C52.624,31.834 51.823,32.629 51.823,33.656C51.823,34.704 52.624,35.581 53.651,35.581Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
9
src/electron/assets/icons/infocircleicon.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.990847,0,0,0.990847,0,0)">
|
||||||
|
<rect x="0" y="0" width="64.753" height="64.591" style="fill-opacity:0;"/>
|
||||||
|
<path d="M32.28,64.56C50.104,64.56 64.566,50.099 64.566,32.28C64.566,14.461 50.104,0 32.28,0C14.461,0 0,14.461 0,32.28C0,50.099 14.461,64.56 32.28,64.56ZM32.28,58.201C17.947,58.201 6.359,46.613 6.359,32.28C6.359,17.947 17.947,6.359 32.28,6.359C46.613,6.359 58.207,17.947 58.207,32.28C58.207,46.613 46.613,58.201 32.28,58.201Z" style="fill-rule:nonzero;"/>
|
||||||
|
<path d="M26.817,49.476L39.395,49.476C40.778,49.476 41.862,48.471 41.862,47.072C41.862,45.752 40.778,44.704 39.395,44.704L35.914,44.704L35.914,29.94C35.914,28.095 34.991,26.874 33.251,26.874L27.365,26.874C25.977,26.874 24.898,27.927 24.898,29.247C24.898,30.647 25.977,31.652 27.365,31.652L30.523,31.652L30.523,44.704L26.817,44.704C25.434,44.704 24.35,45.752 24.35,47.072C24.35,48.471 25.434,49.476 26.817,49.476ZM32.083,22.041C34.443,22.041 36.303,20.15 36.303,17.807C36.303,15.436 34.443,13.556 32.083,13.556C29.766,13.556 27.869,15.436 27.869,17.807C27.869,20.15 29.766,22.041 32.083,22.041Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
7
src/electron/assets/icons/lockicon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.931914,0,0,0.931914,11.6234,3)">
|
||||||
|
<path d="M7.234,62.237L36.497,62.237C41.243,62.237 43.731,59.693 43.731,54.569L43.731,32.436C43.731,27.356 41.243,24.811 36.497,24.811L7.234,24.811C2.483,24.811 0,27.356 0,32.436L0,54.569C0,59.693 2.483,62.237 7.234,62.237ZM8.121,56.636C6.822,56.636 6.081,55.843 6.081,54.368L6.081,32.649C6.081,31.169 6.822,30.413 8.121,30.413L35.615,30.413C36.939,30.413 37.644,31.169 37.644,32.649L37.644,54.368C37.644,55.843 36.939,56.636 35.615,56.636L8.121,56.636ZM5.716,27.436L11.568,27.436L11.568,16.922C11.568,9.584 16.262,5.596 21.853,5.596C27.432,5.596 32.194,9.584 32.194,16.922L32.194,27.436L38.02,27.436L38.02,17.477C38.02,5.946 30.389,0 21.853,0C13.342,0 5.716,5.946 5.716,17.477L5.716,27.436Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
5
src/electron/assets/icons/printericon.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||||
|
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" version="1.1" viewBox="0 0 72 70" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m17.549 64.222c-5.878-0.024-9.043-3.093-9.043-8.994v-41.37c0-5.9 3.433-8.994 9.113-8.994h36.389c5.899 0 9.088 3.094 9.088 8.994v41.37c0 5.924-3.189 8.994-9.088 8.994h-36.389-0.07zm7.075-5.012c-1.387 0-2.108-0.692-2.108-2.108v-1.937c0-1.416 0.721-2.08 2.108-2.08h22.378c1.411 0 2.085 0.664 2.085 2.08v1.937c0 1.416-0.674 2.108-2.085 2.108h-22.378zm-10.651-36.9v33.13c0 2.281 1.125 3.525 3.324 3.632-0.028-0.318-0.042-0.652-0.042-1.002v-3.855c0-4.258 2.179-6.146 6.147-6.146h24.798c4.116 0 6.147 1.888 6.147 6.146v3.855c0 0.35-0.014 0.685-0.041 1.003 2.193-0.1 3.347-1.345 3.347-3.633v-33.13h-3.306v0.74c0 4.861-2.148 7.801-6.179 8.699l-2.826 2.826c-0.818 0.818-2.145 0.818-2.963 0l-2.83-2.83c-4.033-0.902-6.175-3.841-6.175-8.695v-0.74h-19.401zm31.291-5.442h0.056c2.346 0.02 3.584 1.281 3.584 3.65v2.744c0 2.388-1.257 3.64-3.64 3.64h-2.783c-2.407 0-3.64-1.252-3.64-3.64v-2.744c0-2.388 1.233-3.65 3.64-3.65h2.783zm12.389 0.55v-3.766c0-2.388-1.257-3.651-3.64-3.651h-36.4c-2.407 0-3.64 1.263-3.64 3.651v3.766h19.834c1.087-3.743 4.178-5.687 8.68-5.687h2.771c4.676 0 7.65 1.944 8.682 5.687h3.713z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
7
src/electron/assets/icons/settingsicon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.913503,0,0,0.913503,1,1.02599)">
|
||||||
|
<path d="M30.765,67.814L37.106,67.814C39.686,67.814 41.73,66.21 42.301,63.74L43.571,58.211L44.403,57.924L49.226,60.88C51.404,62.214 53.95,61.894 55.787,60.057L60.179,55.702C62.038,53.848 62.325,51.271 60.991,49.13L57.983,44.343L58.291,43.569L63.808,42.261C66.253,41.691 67.871,39.621 67.871,37.066L67.871,30.901C67.871,28.352 66.278,26.308 63.808,25.706L58.342,24.373L58.009,23.548L61.016,18.761C62.351,16.62 62.089,14.068 60.204,12.189L55.813,7.808C54.001,5.997 51.455,5.639 49.278,6.985L44.454,9.941L43.571,9.602L42.301,4.074C41.73,1.598 39.686,0 37.106,0L30.765,0C28.184,0 26.141,1.609 25.57,4.074L24.274,9.602L23.391,9.941L18.593,6.985C16.416,5.651 13.844,5.997 12.033,7.808L7.666,12.189C5.781,14.068 5.494,16.62 6.854,18.761L9.836,23.548L9.529,24.373L4.062,25.706C1.581,26.314 0,28.352 0,30.901L0,37.066C0,39.621 1.618,41.691 4.062,42.261L9.58,43.569L9.862,44.343L6.88,49.13C5.52,51.271 5.832,53.848 7.692,55.702L12.058,60.057C13.895,61.894 16.467,62.214 18.644,60.88L23.443,57.924L24.274,58.211L25.57,63.74C26.141,66.21 28.184,67.814 30.765,67.814ZM31.541,62.151C31.015,62.151 30.745,61.927 30.657,61.444L28.804,53.744C26.825,53.298 24.926,52.498 23.413,51.53L16.646,55.684C16.276,55.959 15.856,55.934 15.518,55.539L12.184,52.211C11.841,51.868 11.826,51.498 12.065,51.083L16.236,44.367C15.376,42.874 14.541,40.998 14.064,39.018L6.364,37.197C5.881,37.109 5.657,36.839 5.657,36.313L5.657,31.629C5.657,31.078 5.856,30.839 6.364,30.745L14.038,28.898C14.527,26.822 15.448,24.877 16.184,23.524L12.039,16.808C11.775,16.367 11.784,15.998 12.128,15.628L15.492,12.352C15.856,11.988 16.199,11.963 16.646,12.207L23.356,16.284C24.773,15.438 26.837,14.589 28.824,14.064L30.657,6.37C30.745,5.887 31.015,5.662 31.541,5.662L36.33,5.662C36.856,5.662 37.12,5.887 37.188,6.37L39.072,14.115C41.099,14.609 42.916,15.43 44.463,16.309L51.183,12.218C51.66,11.974 51.972,11.994 52.368,12.363L55.701,15.639C56.07,16.009 56.059,16.378 55.794,16.819L51.661,23.524C52.417,24.871 53.318,26.822 53.801,28.887L61.506,30.745C61.989,30.839 62.214,31.078 62.214,31.629L62.214,36.313C62.214,36.839 61.958,37.109 61.506,37.197L53.776,39.029C53.298,40.992 52.489,42.891 51.609,44.367L55.763,51.072C56.008,51.487 56.019,51.856 55.649,52.2L52.342,55.528C51.972,55.923 51.578,55.943 51.183,55.673L44.443,51.53C42.903,52.498 41.113,53.276 39.072,53.744L37.188,61.444C37.12,61.927 36.856,62.151 36.33,62.151L31.541,62.151ZM33.927,45.757C40.487,45.757 45.777,40.467 45.777,33.907C45.777,27.347 40.487,22.057 33.927,22.057C27.372,22.057 22.077,27.347 22.077,33.907C22.077,40.467 27.372,45.757 33.927,45.757ZM33.927,40.396C30.341,40.396 27.438,37.498 27.438,33.907C27.438,30.316 30.341,27.418 33.927,27.418C37.518,27.418 40.416,30.316 40.416,33.907C40.416,37.498 37.518,40.396 33.927,40.396Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
7
src/electron/assets/icons/xmarkicon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1.15242,0,0,1.15242,3,2.94855)">
|
||||||
|
<path d="M1.001,49.366C2.359,50.693 4.649,50.676 5.942,49.383L25.166,30.159L44.379,49.377C45.694,50.693 47.982,50.713 49.315,49.36C50.647,48.002 50.653,45.765 49.337,44.444L30.125,25.2L49.337,5.988C50.653,4.672 50.673,2.41 49.315,1.077C47.957,-0.281 45.694,-0.287 44.379,1.055L25.166,20.267L5.942,1.049C4.649,-0.264 2.333,-0.312 1.001,1.071C-0.326,2.43 -0.309,4.689 0.984,5.982L20.208,25.2L0.984,44.455C-0.309,45.743 -0.352,48.033 1.001,49.366Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1000 B |
40
src/electron/components/BoolDisplay.jsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Space, Tag } from "antd";
|
||||||
|
import CheckIcon from "../icons/CheckIcon";
|
||||||
|
import XMarkIcon from "../icons/XMarkIcon";
|
||||||
|
|
||||||
|
const BoolDisplay = ({
|
||||||
|
value,
|
||||||
|
yesNo,
|
||||||
|
showIcon = true,
|
||||||
|
showText = true,
|
||||||
|
showColor = true,
|
||||||
|
}) => {
|
||||||
|
var falseText = "False";
|
||||||
|
var trueText = "True";
|
||||||
|
if (yesNo) {
|
||||||
|
falseText = "No";
|
||||||
|
trueText = "Yes";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<Tag
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
color={showColor ? (value ? "success" : "error") : "default"}
|
||||||
|
icon={showIcon ? value ? <CheckIcon /> : <XMarkIcon /> : undefined}
|
||||||
|
>
|
||||||
|
{showText ? (value === true ? trueText : falseText) : null}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BoolDisplay.propTypes = {
|
||||||
|
value: PropTypes.bool.isRequired,
|
||||||
|
yesNo: PropTypes.bool,
|
||||||
|
showIcon: PropTypes.bool,
|
||||||
|
showText: PropTypes.bool,
|
||||||
|
showColor: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoolDisplay;
|
||||||
73
src/electron/components/CopyButton.jsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Button, Tooltip, message } from "antd";
|
||||||
|
import CopyIcon from "../icons/CopyIcon";
|
||||||
|
|
||||||
|
const CopyButton = ({
|
||||||
|
text,
|
||||||
|
tooltip = "Copy",
|
||||||
|
size = "small",
|
||||||
|
type = "text",
|
||||||
|
}) => {
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
|
||||||
|
const doCopy = (copyText) => {
|
||||||
|
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(copyText)
|
||||||
|
.then(() => {
|
||||||
|
messageApi.success("Copied to clipboard");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
messageApi.error("Failed to copy");
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
document.queryCommandSupported &&
|
||||||
|
document.queryCommandSupported("copy")
|
||||||
|
) {
|
||||||
|
// Legacy fallback
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = copyText;
|
||||||
|
textarea.setAttribute("readonly", "");
|
||||||
|
textarea.style.position = "absolute";
|
||||||
|
textarea.style.left = "-9999px";
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
try {
|
||||||
|
document.execCommand("copy");
|
||||||
|
messageApi.success("Copied to clipboard");
|
||||||
|
} catch (err) {
|
||||||
|
messageApi.error("Failed to copy");
|
||||||
|
}
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
} else {
|
||||||
|
messageApi.error("Copy not supported in this browser");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{contextHolder}
|
||||||
|
<Tooltip title={tooltip} arrow={false}>
|
||||||
|
<Button
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
style={{ minWidth: 25 }}
|
||||||
|
width={20}
|
||||||
|
size={size}
|
||||||
|
type={type}
|
||||||
|
onClick={() => doCopy(text)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CopyButton.propTypes = {
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
style: PropTypes.object,
|
||||||
|
tooltip: PropTypes.string,
|
||||||
|
size: PropTypes.string,
|
||||||
|
type: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CopyButton;
|
||||||
85
src/electron/components/HostInformation.jsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Typography, Descriptions, Flex } from "antd";
|
||||||
|
import CopyButton from "./CopyButton";
|
||||||
|
import BoolDislay from "./BoolDisplay";
|
||||||
|
import StateDislay from "./StateDisplay";
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const HostInformation = ({ host, bordered, size, column }) => {
|
||||||
|
return (
|
||||||
|
<Descriptions
|
||||||
|
bordere={bordered}
|
||||||
|
size={size}
|
||||||
|
column={column}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: "ID",
|
||||||
|
children: (
|
||||||
|
<Flex style={{ minWidth: 0 }}>
|
||||||
|
<Text code ellipsis>
|
||||||
|
HST:{host?._id}
|
||||||
|
</Text>
|
||||||
|
<CopyButton />
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Name",
|
||||||
|
children: <Text>{host?.name}</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Active",
|
||||||
|
children: <BoolDislay value={host?.active} yesNo={true} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "State",
|
||||||
|
children: <StateDislay state={host?.state} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "OS Platform",
|
||||||
|
children: <Text>{host?.deviceInfo?.os?.platform}</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "OS Type",
|
||||||
|
children: <Text>{host?.deviceInfo?.os?.type}</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "OS Release",
|
||||||
|
children: <Text>{host?.deviceInfo?.os?.release}</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Arch",
|
||||||
|
children: <Text>{host?.deviceInfo?.os?.arch}</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hostname",
|
||||||
|
children: <Text>{host?.deviceInfo?.os?.hostname}</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "CPU Model",
|
||||||
|
children: <Text>{host?.deviceInfo?.cpu?.model}</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "CPU Cores",
|
||||||
|
children: <Text>{host?.deviceInfo?.cpu?.cores}</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "CPU Speed",
|
||||||
|
children: <Text>{host?.deviceInfo?.cpu?.speedMHz}MHz</Text>,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
HostInformation.propTypes = {
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
style: PropTypes.object,
|
||||||
|
tooltip: PropTypes.string,
|
||||||
|
size: PropTypes.string,
|
||||||
|
type: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HostInformation;
|
||||||
29
src/electron/components/MissingPlaceholder.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Card, Flex, Typography } from "antd";
|
||||||
|
import InfoCircleIcon from "../icons/InfoCircleIcon";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const MissingPlaceholder = ({ message }) => {
|
||||||
|
return (
|
||||||
|
<Card size="small">
|
||||||
|
<Flex
|
||||||
|
justify="center"
|
||||||
|
gap={"small"}
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<Text type="secondary">
|
||||||
|
<InfoCircleIcon />
|
||||||
|
</Text>
|
||||||
|
<Text type="secondary">{message}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MissingPlaceholder.propTypes = {
|
||||||
|
message: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MissingPlaceholder;
|
||||||
60
src/electron/components/PrinterList.jsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Typography, List, Button, Flex } from "antd";
|
||||||
|
import PrinterIcon from "../icons/PrinterIcon";
|
||||||
|
import InfoCircleIcon from "../icons/InfoCircleIcon";
|
||||||
|
import StateDisplay from "./StateDisplay";
|
||||||
|
import DocumentPrinterIcon from "../icons/DocumentPrinterIcon";
|
||||||
|
import MissingPlaceholder from "./MissingPlaceholder";
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const PrinterList = ({ printers, type = "printer" }) => {
|
||||||
|
if ((printers?.length || 0) <= 0) {
|
||||||
|
return (
|
||||||
|
<MissingPlaceholder
|
||||||
|
message={`No ${
|
||||||
|
type == "printer" ? "printers" : "document printers"
|
||||||
|
} added.`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
dataSource={printers}
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
renderItem={(printer) => (
|
||||||
|
<List.Item actions={[]}>
|
||||||
|
<List.Item.Meta
|
||||||
|
description={
|
||||||
|
<Flex gap={"middle"} justify="space-between" align="center">
|
||||||
|
<Flex gap={"small"}>
|
||||||
|
<Text>
|
||||||
|
{type == "printer" && <PrinterIcon />}
|
||||||
|
{type == "documentPrinter" && <DocumentPrinterIcon />}
|
||||||
|
</Text>
|
||||||
|
<Text>{printer.name || printer._id}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={"middle"} align="center">
|
||||||
|
<StateDisplay state={printer.state} />
|
||||||
|
<Button
|
||||||
|
key="info"
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PrinterList.propTypes = {
|
||||||
|
printers: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrinterList;
|
||||||
36
src/electron/components/StateDisplay.jsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// PrinterSelect.js
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Progress, Flex, Space } from 'antd'
|
||||||
|
import StateTag from './StateTag'
|
||||||
|
|
||||||
|
const StateDisplay = ({ state, showProgress = true, showState = true }) => {
|
||||||
|
const currentState = state || {
|
||||||
|
type: 'unknown',
|
||||||
|
progress: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex gap='small' align={'center'}>
|
||||||
|
{showState && (
|
||||||
|
<Space>
|
||||||
|
<StateTag state={currentState.type} />
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
{showProgress && currentState?.progress && currentState?.progress > 0 ? (
|
||||||
|
<Progress
|
||||||
|
percent={Math.round(currentState.progress * 100)}
|
||||||
|
status='active'
|
||||||
|
style={{ width: '150px', marginBottom: '2px' }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
StateDisplay.propTypes = {
|
||||||
|
state: PropTypes.object,
|
||||||
|
showProgress: PropTypes.bool,
|
||||||
|
showState: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StateDisplay
|
||||||
107
src/electron/components/StateTag.jsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Badge, Flex, Tag } from 'antd'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
const StateTag = ({ state, showBadge = true, style = {} }) => {
|
||||||
|
const { badgeStatus, badgeText } = useMemo(() => {
|
||||||
|
let status = 'default'
|
||||||
|
let text = 'Unknown'
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'online':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Online'
|
||||||
|
break
|
||||||
|
case 'standby':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Standby'
|
||||||
|
break
|
||||||
|
case 'complete':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Complete'
|
||||||
|
break
|
||||||
|
case 'offline':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Offline'
|
||||||
|
break
|
||||||
|
case 'shutdown':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Shutdown'
|
||||||
|
break
|
||||||
|
case 'initializing':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Initializing'
|
||||||
|
break
|
||||||
|
case 'printing':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Printing'
|
||||||
|
break
|
||||||
|
case 'paused':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Paused'
|
||||||
|
break
|
||||||
|
case 'cancelled':
|
||||||
|
status = 'error'
|
||||||
|
text = 'Cancelled'
|
||||||
|
break
|
||||||
|
case 'loading':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Uploading'
|
||||||
|
break
|
||||||
|
case 'processing':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Processing'
|
||||||
|
break
|
||||||
|
case 'ready':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Ready'
|
||||||
|
break
|
||||||
|
case 'unconsumed':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Unconsumed'
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
status = 'error'
|
||||||
|
text = 'Error'
|
||||||
|
break
|
||||||
|
case 'startup':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Startup'
|
||||||
|
break
|
||||||
|
case 'draft':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Draft'
|
||||||
|
break
|
||||||
|
case 'failed':
|
||||||
|
status = 'error'
|
||||||
|
text = 'Failed'
|
||||||
|
break
|
||||||
|
case 'queued':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Queued'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
status = 'default'
|
||||||
|
text = state || 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
return { badgeStatus: status, badgeText: text }
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag color={badgeStatus} style={{ marginRight: 0, ...style }}>
|
||||||
|
<Flex gap={6}>
|
||||||
|
{showBadge && <Badge status={badgeStatus} />}
|
||||||
|
{badgeText}
|
||||||
|
</Flex>
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
StateTag.propTypes = {
|
||||||
|
state: PropTypes.string,
|
||||||
|
showBadge: PropTypes.bool,
|
||||||
|
style: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StateTag
|
||||||
6
src/electron/icons/CheckIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/checkicon.svg?react";
|
||||||
|
|
||||||
|
const CheckIcon = (props) => <Icon component={CustomIconSvg} {...props} />;
|
||||||
|
|
||||||
|
export default CheckIcon;
|
||||||
6
src/electron/icons/CloudIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/cloudicon.svg?react";
|
||||||
|
|
||||||
|
const CloudIcon = (props) => <Icon component={CustomIconSvg} {...props} />;
|
||||||
|
|
||||||
|
export default CloudIcon;
|
||||||
6
src/electron/icons/CopyIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/copyicon.svg?react";
|
||||||
|
|
||||||
|
const CopyIcon = (props) => <Icon component={CustomIconSvg} {...props} />;
|
||||||
|
|
||||||
|
export default CopyIcon;
|
||||||
8
src/electron/icons/DocumentPrinterIcon.jsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/documentprintericon.svg?react";
|
||||||
|
|
||||||
|
const DocumentPrinterIcon = (props) => (
|
||||||
|
<Icon component={CustomIconSvg} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DocumentPrinterIcon;
|
||||||
6
src/electron/icons/HostIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/hosticon.svg?react";
|
||||||
|
|
||||||
|
const HostIcon = (props) => <Icon component={CustomIconSvg} {...props} />;
|
||||||
|
|
||||||
|
export default HostIcon;
|
||||||
6
src/electron/icons/InfoCircleIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/infocircleicon.svg?react";
|
||||||
|
|
||||||
|
const InfoCircleIcon = (props) => <Icon component={CustomIconSvg} {...props} />;
|
||||||
|
|
||||||
|
export default InfoCircleIcon;
|
||||||
6
src/electron/icons/LockIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/lockicon.svg?react";
|
||||||
|
|
||||||
|
const LockIcon = (props) => <Icon component={CustomIconSvg} {...props} />;
|
||||||
|
|
||||||
|
export default LockIcon;
|
||||||
6
src/electron/icons/PrinterIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/printericon.svg?react";
|
||||||
|
|
||||||
|
const PrinterIcon = (props) => <Icon component={CustomIconSvg} {...props} />;
|
||||||
|
|
||||||
|
export default PrinterIcon;
|
||||||
6
src/electron/icons/SettingsIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/settingsicon.svg?react";
|
||||||
|
|
||||||
|
const SettingsIcon = (props) => <Icon component={CustomIconSvg} {...props} />;
|
||||||
|
|
||||||
|
export default SettingsIcon;
|
||||||
6
src/electron/icons/XMarkIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/xmarkicon.svg?react";
|
||||||
|
|
||||||
|
const XMarkIcon = (props) => <Icon component={CustomIconSvg} {...props} />;
|
||||||
|
|
||||||
|
export default XMarkIcon;
|
||||||
28
src/electron/index.css
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default button styles to let Ant Design handle them */
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper theme support */
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
19
src/electron/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<link rel="stylesheet" href="./fonts.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Farm Control Server</title>
|
||||||
|
<style>
|
||||||
|
.electron-navigation {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
112
src/electron/ipc.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import log4js from "log4js";
|
||||||
|
import { notPrompting } from "../utils.js";
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("IPC");
|
||||||
|
let mainWindow = null;
|
||||||
|
|
||||||
|
export async function setupIPC() {
|
||||||
|
// Only import Electron if we're in an Electron environment
|
||||||
|
let ipcMain;
|
||||||
|
try {
|
||||||
|
const electron = await import("electron");
|
||||||
|
ipcMain = electron.ipcMain;
|
||||||
|
mainWindow = global.mainWindow;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("Electron not available, skipping IPC setup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only proceed if we have ipcMain
|
||||||
|
if (!ipcMain) {
|
||||||
|
logger.warn("ipcMain not available, skipping IPC setup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Generic IPC handler for custom messages
|
||||||
|
console.log("SETTING GET DATA HANDLER");
|
||||||
|
ipcMain.on("getData", (event) => {
|
||||||
|
logger.info("Getting data...");
|
||||||
|
try {
|
||||||
|
// Get the global socket client instance
|
||||||
|
const socketClient = global.socketClient;
|
||||||
|
|
||||||
|
if (!socketClient) {
|
||||||
|
sendIPC("setAuthenticated", false);
|
||||||
|
sendIPC("setConnected", false);
|
||||||
|
sendIPC("setLoading", false);
|
||||||
|
sendIPC("setHost", {});
|
||||||
|
sendIPC("setPrinters", []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send individual data pieces to match renderer expectations
|
||||||
|
sendIPC("setAuthenticated", socketClient.authenticated);
|
||||||
|
sendIPC("setConnected", socketClient.connected);
|
||||||
|
sendIPC("setLoading", socketClient.loading);
|
||||||
|
sendIPC("setHost", socketClient.host || {});
|
||||||
|
sendIPC("setPrinters", socketClient.printerManager.printers || []);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error getting printer data:", error);
|
||||||
|
sendIPC("setAuthenticated", false);
|
||||||
|
sendIPC("setConnected", false);
|
||||||
|
sendIPC("setLoading", false);
|
||||||
|
sendIPC("setHost", {});
|
||||||
|
sendIPC("setPrinters", []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Window management IPC handlers
|
||||||
|
ipcMain.on("window-minimize", (event) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.minimize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on("window-maximize", (event) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
if (mainWindow.isMaximized()) {
|
||||||
|
mainWindow.unmaximize();
|
||||||
|
} else {
|
||||||
|
mainWindow.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on("window-close", (event) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// OTP authentication handler
|
||||||
|
ipcMain.on("authenticateOTP", async (event, otp) => {
|
||||||
|
logger.info("Authenticating with OTP...");
|
||||||
|
try {
|
||||||
|
const socketClient = global.socketClient;
|
||||||
|
if (socketClient) {
|
||||||
|
notPrompting();
|
||||||
|
await socketClient.authenticateWithOtp(otp);
|
||||||
|
} else {
|
||||||
|
logger.error("Socket client not available for OTP authentication");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error during OTP authentication:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("IPC handlers setup complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendIPC(channel, data) {
|
||||||
|
try {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send(channel, data);
|
||||||
|
logger.info(`Message sent to main window on channel: ${channel}`, data);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`No main window available, cannot send message on channel: ${channel}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error sending message on channel ${channel}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/electron/main.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "./index.css";
|
||||||
|
import App from "./App.jsx";
|
||||||
|
import "antd/dist/reset.css";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
27
src/electron/pages/Disconnected.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Flex, Result, Button } from "antd";
|
||||||
|
|
||||||
|
const Disconnected = ({}) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<Flex gap="large" align="center">
|
||||||
|
<Result
|
||||||
|
title="Disconnected From Server"
|
||||||
|
subTitle="The host cannot connect to the server."
|
||||||
|
>
|
||||||
|
<Flex justify="center">
|
||||||
|
<Button>Reconnect</Button>
|
||||||
|
</Flex>
|
||||||
|
</Result>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Disconnected.propTypes = {};
|
||||||
|
|
||||||
|
export default Disconnected;
|
||||||
25
src/electron/pages/Loading.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Typography, Spin, Flex } from "antd";
|
||||||
|
import { LoadingOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const Loading = ({}) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<Flex gap="large" align="center">
|
||||||
|
<Text style={{ fontSize: 32 }}>
|
||||||
|
<LoadingOutlined />
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Loading.propTypes = {};
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
46
src/electron/pages/OTPInput.jsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Flex, Result, Input } from "antd";
|
||||||
|
|
||||||
|
const OTPInput = ({}) => {
|
||||||
|
const [otp, setOtp] = useState("");
|
||||||
|
|
||||||
|
const handleOtpChange = (value) => {
|
||||||
|
setOtp(value);
|
||||||
|
|
||||||
|
// Check if all 6 digits have been entered
|
||||||
|
if (value.length === 6) {
|
||||||
|
// Send IPC command to authenticate with OTP
|
||||||
|
if (window.electronAPI) {
|
||||||
|
window.electronAPI.sendIPC("authenticateOTP", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<Flex gap="large" align="center">
|
||||||
|
<Result
|
||||||
|
title="Enter Passcode To Continue"
|
||||||
|
subTitle="Please enter a one time passcode in order to authenticate the host."
|
||||||
|
>
|
||||||
|
<Flex justify="center">
|
||||||
|
<Input.OTP
|
||||||
|
size="large"
|
||||||
|
value={otp}
|
||||||
|
onChange={handleOtpChange}
|
||||||
|
length={6}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Result>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
OTPInput.propTypes = {};
|
||||||
|
|
||||||
|
export default OTPInput;
|
||||||
67
src/electron/pages/Overview.jsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Space, Card, Typography, Flex } from "antd";
|
||||||
|
import HostInformation from "../components/HostInformation.jsx";
|
||||||
|
import PrinterIcon from "../icons/PrinterIcon.jsx";
|
||||||
|
import HostIcon from "../icons/HostIcon.jsx";
|
||||||
|
import DocumentPrinterIcon from "../icons/DocumentPrinterIcon.jsx";
|
||||||
|
import PrinterList from "../components/PrinterList.jsx";
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const Overview = ({ loading, host, printers, documentPrinters }) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
vertical
|
||||||
|
size="large"
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
gap={"middle"}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<HostIcon />
|
||||||
|
Host Information
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
style={{ minWidth: "400px", flexShrink: 1 }}
|
||||||
|
>
|
||||||
|
<HostInformation
|
||||||
|
host={host}
|
||||||
|
size={"small"}
|
||||||
|
column={{ xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Flex gap={"middle"} wrap style={{ flexGrow: 1 }}>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<PrinterIcon />
|
||||||
|
Printers
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
style={{ minWidth: "400px", flexGrow: 1, flex: "1 1 0%" }}
|
||||||
|
>
|
||||||
|
<PrinterList printers={printers} type="printer" />
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<DocumentPrinterIcon />
|
||||||
|
Document Printers
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
style={{ minWidth: "400px", flexGrow: 1, flex: "1 1 0%" }}
|
||||||
|
>
|
||||||
|
<PrinterList printers={documentPrinters} type="documentPrinter" />
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Overview.propTypes = {};
|
||||||
|
|
||||||
|
export default Overview;
|
||||||
25
src/electron/pages/Printers.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Space, Card, Flex } from "antd";
|
||||||
|
import PrinterIcon from "../icons/PrinterIcon.jsx";
|
||||||
|
import PrinterList from "../components/PrinterList.jsx";
|
||||||
|
|
||||||
|
const Printers = ({ printers }) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
vertical
|
||||||
|
size="large"
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
gap={"middle"}
|
||||||
|
>
|
||||||
|
<Flex gap={"middle"} wrap style={{ flexGrow: 1 }}>
|
||||||
|
<Card size="small" style={{ minWidth: "400px", flexGrow: 1 }}>
|
||||||
|
<PrinterList printers={printers} type="printer" />
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Printers.propTypes = {};
|
||||||
|
|
||||||
|
export default Printers;
|
||||||
21
src/electron/preload.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
// Expose protected methods that allow the renderer process to use
|
||||||
|
// the ipcRenderer without exposing the entire object
|
||||||
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
|
onIPCData: (channel, callback) => {
|
||||||
|
ipcRenderer.on(channel, (event, data) => callback(data));
|
||||||
|
},
|
||||||
|
// Send messages to main process
|
||||||
|
sendIPC: (channel, data) => {
|
||||||
|
console.log("SEND IPC", channel);
|
||||||
|
ipcRenderer.send(channel, data);
|
||||||
|
},
|
||||||
|
// Window management
|
||||||
|
minimize: () => ipcRenderer.send("window-minimize"),
|
||||||
|
maximize: () => ipcRenderer.send("window-maximize"),
|
||||||
|
close: () => ipcRenderer.send("window-close"),
|
||||||
|
removeAllListeners: (channel) => {
|
||||||
|
ipcRenderer.removeAllListeners(channel);
|
||||||
|
},
|
||||||
|
});
|
||||||
119
src/electron/public/fonts.css
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'DM Sans';
|
||||||
|
src:
|
||||||
|
url('./DMSans-Regular.woff2') format('woff2'),
|
||||||
|
url('./DMSans-Regular.woff') format('woff');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Sans';
|
||||||
|
src:
|
||||||
|
url('./DMSans-Bold.woff2') format('woff2'),
|
||||||
|
url('./DMSans-Bold.woff') format('woff');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Sans';
|
||||||
|
src:
|
||||||
|
url('./DMSans-Italic.woff2') format('woff2'),
|
||||||
|
url('./DMSans-Italic.woff') format('woff');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Sans';
|
||||||
|
src:
|
||||||
|
url('./DMSans-Medium.woff2') format('woff2'),
|
||||||
|
url('./DMSans-Medium.woff') format('woff');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Sans';
|
||||||
|
src:
|
||||||
|
url('./DMSans-MediumItalic.woff2') format('woff2'),
|
||||||
|
url('./DMSans-MediumItalic.woff') format('woff');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Sans';
|
||||||
|
src:
|
||||||
|
url('./DMSans-BoldItalic.woff2') format('woff2'),
|
||||||
|
url('./DMSans-BoldItalic.woff') format('woff');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
src:
|
||||||
|
url('./DMMono-Regular.woff2') format('woff2'),
|
||||||
|
url('./DMMono-Regular.woff') format('woff');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
src:
|
||||||
|
url('./DMMono-Medium.woff2') format('woff2'),
|
||||||
|
url('./DMMono-Medium.woff') format('woff');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
src:
|
||||||
|
url('./DMMono-MediumItalic.woff2') format('woff2'),
|
||||||
|
url('./DMMono-MediumItalic.woff') format('woff');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
src:
|
||||||
|
url('./DMMono-Light.woff2') format('woff2'),
|
||||||
|
url('./DMMono-Light.woff') format('woff');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
src:
|
||||||
|
url('./DMMono-LightItalic.woff2') format('woff2'),
|
||||||
|
url('./DMMono-LightItalic.woff') format('woff');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
src:
|
||||||
|
url('./DMMono-Italic.woff2') format('woff2'),
|
||||||
|
url('./DMMono-Italic.woff') format('woff');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
BIN
src/electron/public/fonts/DMMono-Italic.ttf
Normal file
BIN
src/electron/public/fonts/DMMono-Italic.woff
Normal file
BIN
src/electron/public/fonts/DMMono-Italic.woff2
Normal file
BIN
src/electron/public/fonts/DMMono-Light.ttf
Normal file
BIN
src/electron/public/fonts/DMMono-Light.woff
Normal file
BIN
src/electron/public/fonts/DMMono-Light.woff2
Normal file
BIN
src/electron/public/fonts/DMMono-LightItalic.ttf
Normal file
BIN
src/electron/public/fonts/DMMono-LightItalic.woff
Normal file
BIN
src/electron/public/fonts/DMMono-LightItalic.woff2
Normal file
BIN
src/electron/public/fonts/DMMono-Medium.ttf
Normal file
BIN
src/electron/public/fonts/DMMono-Medium.woff
Normal file
BIN
src/electron/public/fonts/DMMono-Medium.woff2
Normal file
BIN
src/electron/public/fonts/DMMono-MediumItalic.ttf
Normal file
BIN
src/electron/public/fonts/DMMono-MediumItalic.woff
Normal file
BIN
src/electron/public/fonts/DMMono-MediumItalic.woff2
Normal file
BIN
src/electron/public/fonts/DMMono-Regular.ttf
Normal file
BIN
src/electron/public/fonts/DMMono-Regular.woff
Normal file
BIN
src/electron/public/fonts/DMMono-Regular.woff2
Normal file
BIN
src/electron/public/fonts/DMSans-Bold.ttf
Normal file
BIN
src/electron/public/fonts/DMSans-Bold.woff
Normal file
BIN
src/electron/public/fonts/DMSans-Bold.woff2
Normal file
BIN
src/electron/public/fonts/DMSans-BoldItalic.ttf
Normal file
BIN
src/electron/public/fonts/DMSans-BoldItalic.woff
Normal file
BIN
src/electron/public/fonts/DMSans-BoldItalic.woff2
Normal file
BIN
src/electron/public/fonts/DMSans-Italic.ttf
Normal file
BIN
src/electron/public/fonts/DMSans-Italic.woff
Normal file
BIN
src/electron/public/fonts/DMSans-Italic.woff2
Normal file
BIN
src/electron/public/fonts/DMSans-Medium.ttf
Normal file
BIN
src/electron/public/fonts/DMSans-Medium.woff
Normal file
BIN
src/electron/public/fonts/DMSans-Medium.woff2
Normal file
BIN
src/electron/public/fonts/DMSans-MediumItalic.ttf
Normal file
BIN
src/electron/public/fonts/DMSans-MediumItalic.woff
Normal file
BIN
src/electron/public/fonts/DMSans-MediumItalic.woff2
Normal file
BIN
src/electron/public/fonts/DMSans-Regular.ttf
Normal file
BIN
src/electron/public/fonts/DMSans-Regular.woff
Normal file
BIN
src/electron/public/fonts/DMSans-Regular.woff2
Normal file
22
src/electron/vite.config.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import path from "path";
|
||||||
|
import svgr from "vite-plugin-svgr";
|
||||||
|
import svgo from "vite-plugin-svgo";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), svgr(), svgo()],
|
||||||
|
root: path.resolve(__dirname),
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, "../../build/electron"),
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: path.resolve(__dirname, "index.html"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
},
|
||||||
|
});
|
||||||
91
src/electron/window.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
// Remove CommonJS requires and use ES module imports
|
||||||
|
import path from "path";
|
||||||
|
import { loadConfig } from "../config.js";
|
||||||
|
import log4js from "log4js";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
// Load configuration
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("Electron");
|
||||||
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
|
export async function createElectronWindow() {
|
||||||
|
// Only import Electron if we're in an Electron environment
|
||||||
|
let app, BrowserWindow;
|
||||||
|
try {
|
||||||
|
const electron = (await import("electron")).default;
|
||||||
|
app = electron.app;
|
||||||
|
BrowserWindow = electron.BrowserWindow;
|
||||||
|
logger.trace("Imported electron");
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
"Electron not available, skipping window creation. Error:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only proceed if we have app
|
||||||
|
if (!app) {
|
||||||
|
logger.warn("Electron app not available, skipping window creation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// __dirname workaround for ES modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
function createWindow() {
|
||||||
|
logger.debug("Creating browser window...");
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 900,
|
||||||
|
height: 600,
|
||||||
|
resizable: true,
|
||||||
|
frame: false,
|
||||||
|
titleBarStyle: "hiddenInset",
|
||||||
|
trafficLightPosition: { x: 14, y: 12 },
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload:
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? path.join(__dirname, "preload.js")
|
||||||
|
: path.join(__dirname, "..", "build", "electron", "preload.js"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make the window globally accessible for IPC
|
||||||
|
global.mainWindow = win;
|
||||||
|
|
||||||
|
logger.info("Preload Script", path.join(__dirname, "preload.js"));
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
logger.info("Loading development url...");
|
||||||
|
win.loadURL("http://localhost:5173"); // Vite dev server
|
||||||
|
} else {
|
||||||
|
// In production, the built files will be in the build/electron directory
|
||||||
|
win.loadFile(
|
||||||
|
path.join(__dirname, "..", "build", "electron", "index.html")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the promise when the window is ready
|
||||||
|
win.webContents.on("did-finish-load", () => {
|
||||||
|
resolve(win);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
app.on("activate", function () {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("window-all-closed", function () {
|
||||||
|
if (process.platform !== "darwin") app.quit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
5
src/host/hostmanager.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class HostManager {
|
||||||
|
constructor(socketClient) {
|
||||||
|
this.socketClient = socketClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/index.js
@ -1,38 +1,36 @@
|
|||||||
import { loadConfig } from "./config.js";
|
import { loadConfig } from "./config.js";
|
||||||
import { dbConnect } from "./database/mongo.js";
|
|
||||||
import { PrinterManager } from "./printer/printermanager.js";
|
|
||||||
import { SocketManager } from "./socket/socketmanager.js";
|
|
||||||
import { KeycloakAuth } from "./auth/auth.js";
|
|
||||||
import express from "express";
|
|
||||||
import log4js from "log4js";
|
import log4js from "log4js";
|
||||||
|
import { createElectronWindow } from "./electron/window.js";
|
||||||
|
import { setupIPC } from "./electron/ipc.js";
|
||||||
|
import { SocketClient } from "./socket/socketclient.js";
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
const logger = log4js.getLogger("FarmControl Server");
|
const logger = log4js.getLogger("App");
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
// Create Express app
|
export async function init() {
|
||||||
const app = express();
|
// Create Electron window first
|
||||||
|
logger.info("Creating electron window...");
|
||||||
|
await createElectronWindow().catch((err) => {
|
||||||
|
logger.warn("Failed to create Electron window:", err);
|
||||||
|
});
|
||||||
|
|
||||||
// Connect to database
|
// Setup IPC communication after window is created
|
||||||
dbConnect();
|
setupIPC().catch((err) => {
|
||||||
|
logger.warn("Failed to setup IPC:", err);
|
||||||
|
});
|
||||||
|
const socketClient = new SocketClient();
|
||||||
|
// Make socket client globally accessible for IPC handlers
|
||||||
|
global.socketClient = socketClient;
|
||||||
|
socketClient.connect();
|
||||||
|
|
||||||
// Setup Keycloak Integration
|
process.on("SIGINT", () => {
|
||||||
const keycloakAuth = new KeycloakAuth(config);
|
|
||||||
|
|
||||||
// Create printer manager
|
|
||||||
const printerManager = new PrinterManager(config);
|
|
||||||
const socketManager = new SocketManager(config, printerManager, keycloakAuth);
|
|
||||||
printerManager.setSocketManager(socketManager);
|
|
||||||
|
|
||||||
// Start Express server
|
|
||||||
app.listen(config.server.port, () => {
|
|
||||||
logger.info(`Server listening on port ${config.server.port}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
logger.info("Shutting down...");
|
logger.info("Shutting down...");
|
||||||
printerManager.closeAllConnections();
|
socketClient.disconnect();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|||||||
@ -6,91 +6,90 @@ import log4js from "log4js";
|
|||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
const logger = log4js.getLogger("JSON RPC");
|
const logger = log4js.getLogger("JSON RPC");
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
export class JsonRPC {
|
export class JsonRPC {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.idCounter = 0;
|
this.idCounter = 0;
|
||||||
this.methods = {};
|
this.methods = {};
|
||||||
this.pendingRequests = {};
|
this.pendingRequests = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a unique ID for RPC requests
|
// Generate a unique ID for RPC requests
|
||||||
generateId() {
|
generateId() {
|
||||||
return this.idCounter++;
|
return this.idCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register a method to handle incoming notifications/responses
|
// Register a method to handle incoming notifications/responses
|
||||||
registerMethod(methodName, callback) {
|
registerMethod(methodName, callback) {
|
||||||
this.methods[methodName] = callback;
|
this.methods[methodName] = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process incoming messages
|
// Process incoming messages
|
||||||
processMessage(message) {
|
processMessage(message) {
|
||||||
|
if (message.method && this.methods[message.method]) {
|
||||||
|
// Handle method call or notification
|
||||||
|
this.methods[message.method](message.params);
|
||||||
|
logger.trace(`JSON-RPC notification: ${message.method}`);
|
||||||
|
} else if (message.id !== undefined) {
|
||||||
|
// Handle response to a previous request
|
||||||
|
const rpcPromise = this.pendingRequests[message.id];
|
||||||
|
if (rpcPromise) {
|
||||||
|
if (message.error) {
|
||||||
|
logger.error(`Error in JSON-RPC response: ${message.error}`);
|
||||||
|
rpcPromise.reject(message.error);
|
||||||
|
} else {
|
||||||
|
logger.debug(`JSON-RPC response: OK`);
|
||||||
|
logger.trace("Result:", message.result);
|
||||||
|
|
||||||
if (message.method && this.methods[message.method]) {
|
rpcPromise.resolve(message.result);
|
||||||
// Handle method call or notification
|
|
||||||
this.methods[message.method](message.params);
|
|
||||||
logger.trace(`JSON-RPC notification: ${message.method}`);
|
|
||||||
} else if (message.id !== undefined) {
|
|
||||||
// Handle response to a previous request
|
|
||||||
const rpcPromise = this.pendingRequests[message.id];
|
|
||||||
if (rpcPromise) {
|
|
||||||
if (message.error) {
|
|
||||||
logger.error(`Error in JSON-RPC response: ${message.error}`);
|
|
||||||
rpcPromise.reject(message.error);
|
|
||||||
} else {
|
|
||||||
logger.debug(`JSON-RPC response: OK`);
|
|
||||||
logger.trace("Result:", message.result);
|
|
||||||
|
|
||||||
rpcPromise.resolve(message.result);
|
|
||||||
}
|
|
||||||
delete this.pendingRequests[message.id];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// If it's a notification without a registered method, ignore it
|
delete this.pendingRequests[message.id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// If it's a notification without a registered method, ignore it
|
||||||
|
}
|
||||||
|
|
||||||
// Call a method without parameters
|
// Call a method without parameters
|
||||||
callMethod(method) {
|
callMethod(method) {
|
||||||
return this.callMethodWithKwargs(method, {});
|
return this.callMethodWithKwargs(method, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call a method with parameters
|
// Call a method with parameters
|
||||||
callMethodWithKwargs(method, params) {
|
callMethodWithKwargs(method, params) {
|
||||||
logger.debug(`Calling method: ${method}`);
|
logger.debug(`Calling method: ${method}`);
|
||||||
logger.trace("Params:", params);
|
logger.trace("Params:", params);
|
||||||
const id = this.generateId();
|
const id = this.generateId();
|
||||||
const request = {
|
const request = {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
method: method,
|
method: method,
|
||||||
params: params,
|
params: params,
|
||||||
id: id,
|
id: id,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.pendingRequests[id] = { resolve, reject };
|
this.pendingRequests[id] = { resolve, reject };
|
||||||
// The actual sending of the message is done by the WebSocket connection
|
// The actual sending of the message is done by the WebSocket connection
|
||||||
// This just prepares the message and returns a promise
|
// This just prepares the message and returns a promise
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.send(JSON.stringify(request));
|
this.socket.send(JSON.stringify(request));
|
||||||
} else {
|
} else {
|
||||||
// If socket is not directly attached to this instance, the caller
|
// If socket is not directly attached to this instance, the caller
|
||||||
// is responsible for sending the serialized request
|
// is responsible for sending the serialized request
|
||||||
this.lastRequest = JSON.stringify(request);
|
this.lastRequest = JSON.stringify(request);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// For external socket handling
|
// For external socket handling
|
||||||
getLastRequest() {
|
getLastRequest() {
|
||||||
const req = this.lastRequest;
|
const req = this.lastRequest;
|
||||||
this.lastRequest = null;
|
this.lastRequest = null;
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Associate a WebSocket with this RPC instance for direct communication
|
// Associate a WebSocket with this RPC instance for direct communication
|
||||||
setSocket(socket) {
|
setSocket(socket) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,104 +2,98 @@
|
|||||||
import { JsonRPC } from "./jsonrpc.js";
|
import { JsonRPC } from "./jsonrpc.js";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import { loadConfig } from "../config.js";
|
import { loadConfig } from "../config.js";
|
||||||
import { printerModel } from "../database/printer.schema.js";
|
|
||||||
import { printSubJobModel } from "../database/printsubjob.schema.js";
|
|
||||||
import { PrinterDatabase } from "./database.js";
|
import { PrinterDatabase } from "./database.js";
|
||||||
import log4js from "log4js";
|
import log4js from "log4js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
|
import _ from "lodash";
|
||||||
// Load configuration
|
// Load configuration
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
const logger = log4js.getLogger("Printer Client");
|
const logger = log4js.getLogger("Printer Client");
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
export class PrinterClient {
|
export class PrinterClient {
|
||||||
constructor(printer, printerManager, socketManager) {
|
constructor(printer, printerManager) {
|
||||||
this.id = printer.id;
|
this.id = printer._id;
|
||||||
this.name = printer.name;
|
this.printer = printer;
|
||||||
this.printerManager = printerManager;
|
this.printerManager = printerManager;
|
||||||
this.socketManager = socketManager;
|
this.socketClient = printerManager.socketClient;
|
||||||
this.state = { type: 'offline '};
|
this.database = new PrinterDatabase(this.socketClient, this.printer);
|
||||||
this.klippyState = { type: 'offline'};
|
this.state = { type: "offline " };
|
||||||
|
this.klippyState = { type: "offline" };
|
||||||
this.config = printer.moonraker;
|
this.config = printer.moonraker;
|
||||||
this.version = printer.version;
|
this.version = printer.version;
|
||||||
this.jsonRpc = new JsonRPC();
|
this.jsonRpc = new JsonRPC();
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.connectionId = null;
|
this.connectionId = null;
|
||||||
this.currentJobId = null;
|
this.currentJobId = null;
|
||||||
|
this.temperatureObject = {};
|
||||||
this.currentJobState = { type: "unknown", progress: 0 };
|
this.currentJobState = { type: "unknown", progress: 0 };
|
||||||
this.currentSubJobId = null;
|
this.currentSubJobId = null;
|
||||||
this.currentSubJobState = null;
|
this.currentSubJobState = null;
|
||||||
this.currentFilamentStockId = printer.currentFilamentStock?._id.toString() || null;
|
this.currentFilamentStockId =
|
||||||
this.currentFilamentStockDensity = printer.currentFilamentStock?.filament?.density || null
|
printer.currentFilamentStock?._id.toString() || null;
|
||||||
|
this.currentFilamentStockDensity =
|
||||||
|
printer.currentFilamentStock?.filament?.density || null;
|
||||||
this.registerEventHandlers();
|
this.registerEventHandlers();
|
||||||
|
this.subscribeToActions();
|
||||||
this.baseSubscription = {
|
this.baseSubscription = {
|
||||||
print_stats: null,
|
print_stats: null,
|
||||||
display_status: null,
|
display_status: null,
|
||||||
'filament_switch_sensor fsensor': null,
|
"filament_switch_sensor fsensor": null,
|
||||||
output_pin: null
|
output_pin: null,
|
||||||
|
extruder: null,
|
||||||
|
heater_bed: null,
|
||||||
};
|
};
|
||||||
this.subscriptions = new Map();
|
this.subscriptions = new Map();
|
||||||
this.queuedJobIds = [];
|
this.queuedJobIds = [];
|
||||||
this.isOnline = printer.online;
|
this.isOnline = printer.online;
|
||||||
this.subJobIsCancelling = false;
|
this.subJobIsCancelling = false;
|
||||||
this.subJobCancelId = null;
|
this.subJobCancelId = null;
|
||||||
this.database = new PrinterDatabase(socketManager);
|
|
||||||
this.filamentDetected = false;
|
this.filamentDetected = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEventHandlers() {
|
registerEventHandlers() {
|
||||||
// Register event handlers for Moonraker notifications
|
// Register event handlers for Moonraker notifications
|
||||||
this.jsonRpc.registerMethod(
|
this.jsonRpc.registerMethod(
|
||||||
"notify_gcode_response",
|
"notify_gcode_response"
|
||||||
//this.handleGcodeResponse.bind(this),
|
//this.handleGcodeResponse.bind(this),
|
||||||
);
|
);
|
||||||
this.jsonRpc.registerMethod(
|
this.jsonRpc.registerMethod(
|
||||||
"notify_status_update",
|
"notify_status_update",
|
||||||
this.handleStatusUpdate.bind(this),
|
this.handleStatusUpdate.bind(this)
|
||||||
);
|
);
|
||||||
this.jsonRpc.registerMethod(
|
this.jsonRpc.registerMethod(
|
||||||
"notify_klippy_disconnected",
|
"notify_klippy_disconnected",
|
||||||
this.handleKlippyDisconnected.bind(this),
|
this.handleKlippyDisconnected.bind(this)
|
||||||
);
|
);
|
||||||
this.jsonRpc.registerMethod(
|
this.jsonRpc.registerMethod(
|
||||||
"notify_klippy_ready",
|
"notify_klippy_ready",
|
||||||
this.handleKlippyReady.bind(this),
|
this.handleKlippyReady.bind(this)
|
||||||
);
|
);
|
||||||
this.jsonRpc.registerMethod(
|
this.jsonRpc.registerMethod(
|
||||||
"notify_filelist_changed",
|
"notify_filelist_changed",
|
||||||
this.handleFileListChanged.bind(this),
|
this.handleFileListChanged.bind(this)
|
||||||
);
|
);
|
||||||
this.jsonRpc.registerMethod(
|
this.jsonRpc.registerMethod(
|
||||||
"notify_metadata_update",
|
"notify_metadata_update",
|
||||||
this.handleMetadataUpdate.bind(this),
|
this.handleMetadataUpdate.bind(this)
|
||||||
);
|
);
|
||||||
this.jsonRpc.registerMethod(
|
this.jsonRpc.registerMethod(
|
||||||
"notify_power_changed",
|
"notify_power_changed",
|
||||||
this.handlePowerChanged.bind(this),
|
this.handlePowerChanged.bind(this)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrinterConnectionConfig() {
|
subscribeToActions() {
|
||||||
try {
|
this.socketClient.subscribeToObjectActions({
|
||||||
const config = await this.database.getPrinterConfig(this.id);
|
objectType: "printer",
|
||||||
if (config) {
|
_id: this.id,
|
||||||
this.config = config;
|
});
|
||||||
logger.info(`Reloaded connection config! (${this.name})`);
|
|
||||||
logger.debug(this.config);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to get printer connection config! (${this.name}):`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
await this.getPrinterConnectionConfig();
|
|
||||||
const { protocol, host, port } = this.config;
|
const { protocol, host, port } = this.config;
|
||||||
const wsUrl = `${protocol}://${host}:${port}/websocket`;
|
const wsUrl = `${protocol}://${host}:${port}/websocket`;
|
||||||
|
|
||||||
@ -109,8 +103,10 @@ export class PrinterClient {
|
|||||||
|
|
||||||
this.jsonRpc.setSocket(this.socket);
|
this.jsonRpc.setSocket(this.socket);
|
||||||
|
|
||||||
|
await this.database.clearAlerts();
|
||||||
|
|
||||||
this.socket.on("open", () => {
|
this.socket.on("open", () => {
|
||||||
logger.info(`Connected to Moonraker (${this.name})`);
|
logger.info(`Connected to Moonraker (${this.printer.name})`);
|
||||||
this.isOnline = true;
|
this.isOnline = true;
|
||||||
this.identifyConnection();
|
this.identifyConnection();
|
||||||
});
|
});
|
||||||
@ -120,7 +116,7 @@ export class PrinterClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on("close", () => {
|
this.socket.on("close", () => {
|
||||||
logger.info(`Disconnected from Moonraker (${this.name})`);
|
logger.info(`Disconnected from Moonraker (${this.printer.name})`);
|
||||||
this.isOnline = false;
|
this.isOnline = false;
|
||||||
this.state = { type: "offline" };
|
this.state = { type: "offline" };
|
||||||
this.updatePrinterState();
|
this.updatePrinterState();
|
||||||
@ -130,7 +126,7 @@ export class PrinterClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on("error", (error) => {
|
this.socket.on("error", (error) => {
|
||||||
logger.error(`Moonraker connection error (${this.name}):`, error);
|
logger.error(`Moonraker connection error (${this.printer.name}):`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,19 +142,22 @@ export class PrinterClient {
|
|||||||
args.api_key = this.config.apiKey;
|
args.api_key = this.config.apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Identifying connection... (${this.name})`);
|
logger.debug(`Identifying connection... (${this.printer.name})`);
|
||||||
|
|
||||||
this.jsonRpc
|
this.jsonRpc
|
||||||
.callMethodWithKwargs("server.connection.identify", args)
|
.callMethodWithKwargs("server.connection.identify", args)
|
||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
this.connectionId = result.connection_id;
|
this.connectionId = result.connection_id;
|
||||||
logger.info(
|
logger.info(
|
||||||
`Connection identified with ID: ${this.connectionId} (${this.name})`,
|
`Connection identified with ID: ${this.connectionId} (${this.printer.name})`
|
||||||
);
|
);
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error(`Error identifying connection (${this.name}):`, error);
|
logger.error(
|
||||||
|
`Error identifying connection (${this.printer.name}):`,
|
||||||
|
error
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,54 +178,62 @@ export class PrinterClient {
|
|||||||
this.klippyState = { type: serverResult.klippy_state };
|
this.klippyState = { type: serverResult.klippy_state };
|
||||||
logger.info(
|
logger.info(
|
||||||
"Server:",
|
"Server:",
|
||||||
`Moonraker ${serverResult.moonraker_version} (${this.name})`,
|
`Moonraker ${serverResult.moonraker_version} (${this.printer.name})`,
|
||||||
`State: ${this.klippyState.type}`,
|
`State: ${this.klippyState.type}`
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const klippyResult = await this.jsonRpc.callMethod("printer.info");
|
const klippyResult = await this.jsonRpc.callMethod("printer.info");
|
||||||
logger.info(
|
logger.info(
|
||||||
`Klippy info for ${this.name}: ${klippyResult.hostname}, ${klippyResult.software_version}`,
|
`Klippy info for ${this.printer.name}: ${klippyResult.hostname}, ${klippyResult.software_version}`
|
||||||
);
|
);
|
||||||
// Update firmware version in database
|
// Update firmware version in database
|
||||||
await this.database.updatePrinterFirmware(this.id, klippyResult.software_version);
|
await this.database.updatePrinterFirmware(
|
||||||
|
klippyResult.software_version
|
||||||
|
);
|
||||||
logger.info(
|
logger.info(
|
||||||
`Updated firmware version for ${this.name} to ${klippyResult.software_version}`,
|
`Updated firmware version for ${this.printer.name} to ${klippyResult.software_version}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (klippyResult.state === "error" && klippyResult.state_message) {
|
if (klippyResult.state === "error" && klippyResult.state_message) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Klippy error for ${this.name}: ${klippyResult.state_message}`,
|
`Klippy error for ${this.printer.name}: ${klippyResult.state_message}`,
|
||||||
this.database.addAlert(this.id, {
|
this.database.addAlert({
|
||||||
type: "klippyError",
|
type: "error",
|
||||||
message: klippyResult.state_message,
|
message: klippyResult.state_message,
|
||||||
priority: 9,
|
priority: 9,
|
||||||
timestamp: new Date()
|
timestamp: new Date(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (klippyResult.state === "startup" && klippyResult.state_message) {
|
if (klippyResult.state === "startup" && klippyResult.state_message) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Klippy startup message for ${this.name}: ${klippyResult.state_message}`,
|
`Klippy startup message for ${this.printer.name}: ${klippyResult.state_message}`,
|
||||||
this.database.addAlert(this.id, {
|
this.database.addAlert({
|
||||||
type: "klippyStartup",
|
type: "info",
|
||||||
message: klippyResult.state_message,
|
message: klippyResult.state_message,
|
||||||
priority: 8,
|
priority: 8,
|
||||||
timestamp: new Date()
|
timestamp: new Date(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error getting Klippy info (${this.name}):`, error);
|
logger.error(
|
||||||
|
`Error getting Klippy info for ${this.printer.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error getting server info (${this.name}):`, error);
|
logger.error(
|
||||||
|
`Error getting server info for ${this.printer.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getQueuedJobsInfo() {
|
async getQueuedJobsInfo() {
|
||||||
logger.info(`Getting queued jobs info for (${this.name})`);
|
logger.info(`Getting queued jobs info for (${this.printer.name})`);
|
||||||
const result = await this.sendPrinterCommand({
|
const result = await this.sendPrinterCommand({
|
||||||
method: "server.job_queue.status",
|
method: "server.job_queue.status",
|
||||||
});
|
});
|
||||||
@ -236,23 +243,23 @@ export class PrinterClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPrinterState() {
|
async getPrinterState() {
|
||||||
logger.info(`Getting state of (${this.name})`);
|
logger.info(`Getting state of (${this.printer.name})`);
|
||||||
if (!this.isOnline) {
|
if (!this.isOnline) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Cannot send command: Not connected to Moonraker (${this.name})`,
|
`Cannot send command: Not connected to Moonraker. (${this.printer.name})`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.klippyState.type === "error") {
|
if (this.klippyState.type === "error") {
|
||||||
logger.error(`Klippy is reporting error for ${this.name}`);
|
logger.error(`Klippy is reporting error for ${this.printer.name}`);
|
||||||
this.state = this.klippyState;
|
this.state = this.klippyState;
|
||||||
this.updatePrinterState();
|
this.updatePrinterState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.klippyState.type === "shutdown") {
|
if (this.klippyState.type === "shutdown") {
|
||||||
logger.error(`Klippy is reporting shutdown for ${this.name}`);
|
logger.error(`Klippy is reporting shutdown for ${this.printer.name}.`);
|
||||||
this.state = this.klippyState;
|
this.state = this.klippyState;
|
||||||
this.updatePrinterState();
|
this.updatePrinterState();
|
||||||
return;
|
return;
|
||||||
@ -261,21 +268,21 @@ export class PrinterClient {
|
|||||||
try {
|
try {
|
||||||
const result = await this.jsonRpc.callMethodWithKwargs(
|
const result = await this.jsonRpc.callMethodWithKwargs(
|
||||||
"printer.objects.query",
|
"printer.objects.query",
|
||||||
{ objects: this.baseSubscription },
|
{ objects: this.baseSubscription }
|
||||||
);
|
);
|
||||||
logger.debug(`Command sent to (${this.name})`);
|
logger.debug(`Command sent to ${this.printer.name}`);
|
||||||
if (result.status != undefined) {
|
if (result.status != undefined) {
|
||||||
this.handleStatusUpdate([result.status]);
|
this.handleStatusUpdate([result.status]);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error sending command to (${this.name}):`, error);
|
logger.error(`Error sending command to ${this.printer.name}:`, error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSubscriptions() {
|
async updateSubscriptions() {
|
||||||
logger.info(`Updating subscriptions for (${this.name})`);
|
logger.info(`Updating subscriptions for (${this.printer.name})`);
|
||||||
|
|
||||||
// Start with base subscription content
|
// Start with base subscription content
|
||||||
const allSubscriptions = { ...this.baseSubscription };
|
const allSubscriptions = { ...this.baseSubscription };
|
||||||
@ -285,11 +292,14 @@ export class PrinterClient {
|
|||||||
Object.assign(allSubscriptions, value);
|
Object.assign(allSubscriptions, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Combined subscriptions:", Object.keys(allSubscriptions).join(", "));
|
logger.debug(
|
||||||
|
"Combined subscriptions:",
|
||||||
|
Object.keys(allSubscriptions).join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
if (!this.isOnline) {
|
if (!this.isOnline) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Cannot send command: Not connected to Moonraker (${this.name})`,
|
`Cannot send command: Not connected to Moonraker (${this.printer.name})`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -298,22 +308,22 @@ export class PrinterClient {
|
|||||||
await this.jsonRpc.callMethodWithKwargs("printer.objects.subscribe", {
|
await this.jsonRpc.callMethodWithKwargs("printer.objects.subscribe", {
|
||||||
objects: allSubscriptions,
|
objects: allSubscriptions,
|
||||||
});
|
});
|
||||||
logger.debug(`Command sent to (${this.name})`);
|
logger.debug(`Command sent to ${this.printer.name}`);
|
||||||
logger.debug({
|
logger.debug({
|
||||||
objects: allSubscriptions,
|
objects: allSubscriptions,
|
||||||
})
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error sending command to (${this.name}):`, error);
|
logger.error(`Error sending command to (${this.printer.name}):`, error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendPrinterCommand(command) {
|
async sendPrinterCommand(command) {
|
||||||
logger.info(`Sending ${command.method} command to (${this.name})`);
|
logger.info(`Sending ${command.method} command to (${this.printer.name})`);
|
||||||
if (!this.isOnline) {
|
if (!this.isOnline) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Cannot send command: Not connected to Moonraker (${this.name})`,
|
`Cannot send command: Not connected to Moonraker (${this.printer.name})`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -321,9 +331,9 @@ export class PrinterClient {
|
|||||||
try {
|
try {
|
||||||
const result = await this.jsonRpc.callMethodWithKwargs(
|
const result = await this.jsonRpc.callMethodWithKwargs(
|
||||||
command.method,
|
command.method,
|
||||||
command.params,
|
command.params
|
||||||
);
|
);
|
||||||
logger.debug(`Command sent to (${this.name})`);
|
logger.debug(`Command sent to ${this.printer.name}`);
|
||||||
if (result.status != undefined) {
|
if (result.status != undefined) {
|
||||||
if (command.method == "printer.objects.query") {
|
if (command.method == "printer.objects.query") {
|
||||||
this.handleStatusUpdate([result.status]);
|
this.handleStatusUpdate([result.status]);
|
||||||
@ -331,7 +341,7 @@ export class PrinterClient {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error sending command to (${this.name}):`, error);
|
logger.error(`Error sending command to (${this.printer.name}):`, error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -339,7 +349,7 @@ export class PrinterClient {
|
|||||||
async handleStatusUpdate(status) {
|
async handleStatusUpdate(status) {
|
||||||
logger.trace("Status update:", status);
|
logger.trace("Status update:", status);
|
||||||
status = status[0];
|
status = status[0];
|
||||||
|
|
||||||
if (this.state.type === "deploying") {
|
if (this.state.type === "deploying") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -350,7 +360,9 @@ export class PrinterClient {
|
|||||||
if (status.print_stats?.state) {
|
if (status.print_stats?.state) {
|
||||||
const newState = status.print_stats.state;
|
const newState = status.print_stats.state;
|
||||||
if (newState !== this.state.type) {
|
if (newState !== this.state.type) {
|
||||||
logger.info(`Printer ${this.name} state changed from ${this.state.type} to ${newState}`);
|
logger.info(
|
||||||
|
`Printer ${this.printer.name} state changed from ${this.state.type} to ${newState}`
|
||||||
|
);
|
||||||
this.state.type = newState;
|
this.state.type = newState;
|
||||||
stateChanged = true;
|
stateChanged = true;
|
||||||
}
|
}
|
||||||
@ -361,86 +373,152 @@ export class PrinterClient {
|
|||||||
const filamentLengthCm = status.print_stats.filament_used / 10;
|
const filamentLengthCm = status.print_stats.filament_used / 10;
|
||||||
const filamentDiameterCm = 0.175; // 1.75mm in cm
|
const filamentDiameterCm = 0.175; // 1.75mm in cm
|
||||||
const filamentRadiusCm = filamentDiameterCm / 2;
|
const filamentRadiusCm = filamentDiameterCm / 2;
|
||||||
const filamentVolumeCm3 = Math.PI * Math.pow(filamentRadiusCm, 2) * filamentLengthCm;
|
const filamentVolumeCm3 =
|
||||||
|
Math.PI * Math.pow(filamentRadiusCm, 2) * filamentLengthCm;
|
||||||
|
|
||||||
// Calculate weight in grams
|
// Calculate weight in grams
|
||||||
const filamentWeightG = filamentVolumeCm3 * this.currentFilamentStockDensity;
|
const filamentWeightG =
|
||||||
|
filamentVolumeCm3 * this.currentFilamentStockDensity;
|
||||||
|
|
||||||
if (this.currentSubJobId != null && this.currentJobId != null && this.currentFilamentStockId != null) {
|
if (
|
||||||
this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filamentWeightG), this.currentSubJobId, this.currentJobId);
|
this.currentSubJobId != null &&
|
||||||
|
this.currentJobId != null &&
|
||||||
|
this.currentFilamentStockId != null
|
||||||
|
) {
|
||||||
|
this.database.updateFilamentStockWeight(
|
||||||
|
this.currentFilamentStockId,
|
||||||
|
-1 * filamentWeightG,
|
||||||
|
this.currentSubJobId,
|
||||||
|
this.currentJobId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.display_status?.progress !== undefined) {
|
if (status.display_status?.progress !== undefined) {
|
||||||
const newProgress = status.display_status.progress;
|
const newProgress = status.display_status.progress;
|
||||||
if (newProgress !== this.state.progress) {
|
if (newProgress !== this.state.progress) {
|
||||||
logger.info(`Printer ${this.name} progress changed from ${this.state.progress} to ${newProgress}`);
|
logger.info(
|
||||||
|
`Printer ${this.printer.name} progress changed from ${this.state.progress} to ${newProgress}`
|
||||||
|
);
|
||||||
this.state.progress = newProgress;
|
this.state.progress = newProgress;
|
||||||
progressChanged = true;
|
progressChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.display_status.message) {
|
if (status.display_status.message) {
|
||||||
await this.database.updateDisplayStatus(this.id, status.display_status.message);
|
await this.database.updateDisplayStatus(status.display_status.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const temperatureChanged =
|
||||||
|
status.extruder?.temperature !== undefined ||
|
||||||
|
status.hot_end?.temperature !== undefined;
|
||||||
|
|
||||||
|
if (status.extruder?.temperature !== undefined) {
|
||||||
|
_.merge(this.temperatureObject, {
|
||||||
|
extruder: { current: status.extruder.temperature },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.extruder?.target !== undefined) {
|
||||||
|
_.merge(this.temperatureObject, {
|
||||||
|
extruder: { target: status.extruder.target },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.heater_bed?.temperature !== undefined) {
|
||||||
|
_.merge(this.temperatureObject, {
|
||||||
|
bed: { current: status.heater_bed.temperature },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.heater_bed?.target !== undefined) {
|
||||||
|
_.merge(this.temperatureObject, {
|
||||||
|
bed: { target: status.heater_bed.target },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temperatureChanged) {
|
||||||
|
this.socketClient.objectEvent({
|
||||||
|
objectType: "printer",
|
||||||
|
_id: this.id,
|
||||||
|
eventType: "temperature",
|
||||||
|
eventData: this.temperatureObject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Handle filament switch sensor
|
// Handle filament switch sensor
|
||||||
if (status['filament_switch_sensor fsensor']?.filament_detected !== undefined) {
|
if (
|
||||||
const newFilamentDetected = status['filament_switch_sensor fsensor'].filament_detected;
|
status["filament_switch_sensor fsensor"]?.filament_detected !== undefined
|
||||||
|
) {
|
||||||
|
const newFilamentDetected =
|
||||||
|
status["filament_switch_sensor fsensor"].filament_detected;
|
||||||
if (newFilamentDetected !== this.filamentDetected) {
|
if (newFilamentDetected !== this.filamentDetected) {
|
||||||
logger.info(`Printer ${this.name} filament detection changed from ${this.filamentDetected} to ${newFilamentDetected} with no currentFilamentId`);
|
logger.info(
|
||||||
|
`Printer ${this.printer.name} filament detection changed from ${this.filamentDetected} to ${newFilamentDetected} with no currentFilamentId`
|
||||||
|
);
|
||||||
this.filamentDetected = newFilamentDetected;
|
this.filamentDetected = newFilamentDetected;
|
||||||
|
|
||||||
if (newFilamentDetected == true && this.currentFilamentStockId == null) {
|
if (
|
||||||
await this.database.addAlert(this.id, {
|
newFilamentDetected == true &&
|
||||||
type: "loadFilamentStock",
|
this.currentFilamentStockId == null
|
||||||
|
) {
|
||||||
|
await this.database.addAlert({
|
||||||
|
type: "info",
|
||||||
|
message:
|
||||||
|
"No filament loaded. Please load filament to continue printing.",
|
||||||
priority: 1,
|
priority: 1,
|
||||||
timestamp: new Date()
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
} else if (newFilamentDetected == false && this.currentFilamentStockId != null) {
|
} else if (
|
||||||
|
newFilamentDetected == false &&
|
||||||
this.currentFilamentStockId = null
|
this.currentFilamentStockId != null
|
||||||
await this.database.setCurrentFilamentStock(this.id, null)
|
) {
|
||||||
|
this.currentFilamentStockId = null;
|
||||||
|
await this.database.setCurrentFilamentStock(null);
|
||||||
// Remove filament select alert if it exists
|
// Remove filament select alert if it exists
|
||||||
await this.database.removeAlerts(this.id, {type:"loadFilamentStock"});
|
await this.database.removeAlerts({
|
||||||
|
type: "loadFilamentStock",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateChanged || progressChanged) {
|
if (stateChanged || progressChanged) {
|
||||||
// Update printer state first
|
// Update printer state first
|
||||||
await this.updatePrinterState()
|
await this.updatePrinterState();
|
||||||
// Set current job to null when not printing or paused
|
// Set current job to null when not printing or paused
|
||||||
if (!["printing", "paused"].includes(this.state.type)) {
|
if (!["printing", "paused"].includes(this.state.type)) {
|
||||||
this.currentJobId = null;
|
this.currentJobId = null;
|
||||||
this.currentSubJobId = null;
|
this.currentSubJobId = null;
|
||||||
await this.database.clearCurrentJob(this.id, `Printer is in ${this.state.type} state`);
|
await this.database.clearCurrentJob(
|
||||||
|
this.id,
|
||||||
|
`Printer is in ${this.state.type} state`
|
||||||
|
);
|
||||||
await this.getQueuedJobsInfo();
|
await this.getQueuedJobsInfo();
|
||||||
} else {
|
} else {
|
||||||
// If we have a current subjob, update its state
|
// If we have a current subjob, update its state
|
||||||
if (this.currentSubJobId) {
|
if (this.currentSubJobId) {
|
||||||
logger.debug(`Updating current subjob ${this.currentSubJobId} state:`, this.state);
|
logger.debug(
|
||||||
await this.database.updateSubJobState(this.currentSubJobId, this.state);
|
`Updating current subjob ${this.currentSubJobId} state:`,
|
||||||
|
this.state
|
||||||
|
);
|
||||||
|
await this.database.updateSubJobState(
|
||||||
|
this.currentSubJobId,
|
||||||
|
this.state
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// If no current subjob but we have queued jobs, check if we need to update printer subjobs
|
// If no current subjob but we have queued jobs, check if we need to update printer subjobs
|
||||||
await this.getQueuedJobsInfo();
|
await this.getQueuedJobsInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.socketManager.broadcastToSubscribers(this.id, {
|
|
||||||
method: "notify_status_update",
|
|
||||||
params: status,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePrinterState() {
|
async updatePrinterState() {
|
||||||
try {
|
try {
|
||||||
const state = this.klippyState.type !== 'ready' ? this.klippyState : this.state;
|
const state =
|
||||||
await this.database.updatePrinterState(this.id, state, this.isOnline);
|
this.klippyState.type !== "ready" ? this.klippyState : this.state;
|
||||||
|
this.database.updatePrinterState(state, this.isOnline);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to update printer state:`, error);
|
logger.error(`Failed to update printer state:`, error);
|
||||||
}
|
}
|
||||||
@ -448,7 +526,7 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
|
|
||||||
async removePrinterSubJob(subJobId) {
|
async removePrinterSubJob(subJobId) {
|
||||||
try {
|
try {
|
||||||
await this.database.removePrinterSubJob(this.id, subJobId);
|
await this.database.removePrinterSubJob(subJobId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to remove subjob:`, error);
|
logger.error(`Failed to remove subjob:`, error);
|
||||||
}
|
}
|
||||||
@ -460,18 +538,20 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
queuedJobIds: this.queuedJobIds,
|
queuedJobIds: this.queuedJobIds,
|
||||||
currentSubJobId: this.currentSubJobId,
|
currentSubJobId: this.currentSubJobId,
|
||||||
currentJobId: this.currentJobId,
|
currentJobId: this.currentJobId,
|
||||||
printerState: this.state.type
|
printerState: this.state.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
const printer = await printerModel.findById(this.id).populate('subJobs');
|
const printer = await this.database.getPrinter();
|
||||||
|
|
||||||
|
const subJobs = printer.subJobs;
|
||||||
|
|
||||||
const subJobs = printer.subJobs
|
|
||||||
|
|
||||||
// If printer is not printing or paused, clear current job/subjob
|
// If printer is not printing or paused, clear current job/subjob
|
||||||
if (!["printing", "paused"].includes(this.state.type)) {
|
if (!["printing", "paused"].includes(this.state.type)) {
|
||||||
if (this.currentSubJobId || this.currentJobId) {
|
if (this.currentSubJobId || this.currentJobId) {
|
||||||
logger.info(`Clearing current job/subjob for printer ${this.name} as state is ${this.state.type}`);
|
logger.info(
|
||||||
await this.database.clearCurrentJob(this.id);
|
`Clearing current job/subjob for printer ${this.printer.name} as state is ${this.state.type}`
|
||||||
|
);
|
||||||
|
await this.database.clearCurrentJob();
|
||||||
this.currentSubJobId = null;
|
this.currentSubJobId = null;
|
||||||
this.currentJobId = null;
|
this.currentJobId = null;
|
||||||
}
|
}
|
||||||
@ -479,34 +559,47 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
// Printer is printing or paused - find the current job
|
// Printer is printing or paused - find the current job
|
||||||
// Sort subjobs by number property
|
// Sort subjobs by number property
|
||||||
const sortedSubJobs = subJobs.sort((a, b) => a.number - b.number);
|
const sortedSubJobs = subJobs.sort((a, b) => a.number - b.number);
|
||||||
|
|
||||||
// Find subjobs that are in queued state
|
// Find subjobs that are in queued state
|
||||||
const queuedSubJobs = sortedSubJobs.filter(subJob =>
|
const queuedSubJobs = sortedSubJobs.filter(
|
||||||
subJob.state.type === "queued" &&
|
(subJob) =>
|
||||||
this.queuedJobIds.includes(subJob.subJobId)
|
subJob.state.type === "queued" &&
|
||||||
|
this.queuedJobIds.includes(subJob.subJobId)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Find subjobs that are not in queued state but should be
|
// Find subjobs that are not in queued state but should be
|
||||||
const missingQueuedSubJobs = sortedSubJobs.filter(subJob =>
|
const missingQueuedSubJobs = sortedSubJobs.filter(
|
||||||
subJob.state.type === "queued" &&
|
(subJob) =>
|
||||||
!this.queuedJobIds.includes(subJob.subJobId)
|
subJob.state.type === "queued" &&
|
||||||
|
!this.queuedJobIds.includes(subJob.subJobId)
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we have missing queued jobs and printer is in standby, mark them as failed
|
// If we have missing queued jobs and printer is in standby, mark them as failed
|
||||||
if (missingQueuedSubJobs.length > 0 && this.state.type === "standby") {
|
if (missingQueuedSubJobs.length > 0 && this.state.type === "standby") {
|
||||||
logger.warn(`Found ${missingQueuedSubJobs.length} missing queued jobs for printer ${this.name} in standby state`);
|
logger.warn(
|
||||||
|
`Found ${missingQueuedSubJobs.length} missing queued jobs for printer ${this.printer.name} in standby state`
|
||||||
|
);
|
||||||
for (const subJob of missingQueuedSubJobs) {
|
for (const subJob of missingQueuedSubJobs) {
|
||||||
logger.info(`Marking missing queued subjob ${subJob.id} as failed`);
|
logger.info(`Marking missing queued subjob ${subJob.id} as failed`);
|
||||||
await this.database.updateSubJobState(subJob.id, { type: "failed" });
|
await this.database.updateSubJobState(subJob.id, {
|
||||||
|
type: "failed",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a current subjob, verify it's still valid
|
// If we have a current subjob, verify it's still valid
|
||||||
if (this.currentSubJobId) {
|
if (this.currentSubJobId) {
|
||||||
const currentSubJob = sortedSubJobs.find(sj => sj.id === this.currentSubJobId);
|
const currentSubJob = sortedSubJobs.find(
|
||||||
if (!currentSubJob || !this.queuedJobIds.includes(currentSubJob.subJobId)) {
|
(sj) => sj.id === this.currentSubJobId
|
||||||
logger.info(`Current subjob ${this.currentSubJobId} is no longer valid, clearing it`);
|
);
|
||||||
await this.database.clearCurrentJob(this.id);
|
if (
|
||||||
|
!currentSubJob ||
|
||||||
|
!this.queuedJobIds.includes(currentSubJob.subJobId)
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
`Current subjob ${this.currentSubJobId} is no longer valid, clearing it`
|
||||||
|
);
|
||||||
|
await this.database.clearCurrentJob();
|
||||||
this.currentSubJobId = null;
|
this.currentSubJobId = null;
|
||||||
this.currentJobId = null;
|
this.currentJobId = null;
|
||||||
}
|
}
|
||||||
@ -514,32 +607,49 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
|
|
||||||
// If we don't have a current subjob but have queued jobs, find the first one
|
// If we don't have a current subjob but have queued jobs, find the first one
|
||||||
if (!this.currentSubJobId) {
|
if (!this.currentSubJobId) {
|
||||||
const result = await this.database.setCurrentJobForPrinting(this.id, this.queuedJobIds);
|
const result = await this.database.setCurrentJobForPrinting(
|
||||||
|
this.id,
|
||||||
|
this.queuedJobIds
|
||||||
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
logger.info(`Setting first queued subjob as current for printer ${this.name}: `, result.currentSubJob._id);
|
logger.info(
|
||||||
|
`Setting first queued subjob as current for printer ${this.printer.name}: `,
|
||||||
|
result.currentSubJob._id
|
||||||
|
);
|
||||||
this.currentSubJobId = result.currentSubJob._id;
|
this.currentSubJobId = result.currentSubJob._id;
|
||||||
this.currentJobId = result.currentJob._id;
|
this.currentJobId = result.currentJob._id;
|
||||||
await this.database.updateSubJobState(this.currentSubJobId, this.state);
|
await this.database.updateSubJobState(
|
||||||
|
this.currentSubJobId,
|
||||||
|
this.state
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update states for all subjobs
|
// Update states for all subjobs
|
||||||
for (const subJob of subJobs) {
|
for (const subJob of subJobs) {
|
||||||
if (!this.queuedJobIds.includes(subJob.subJobId)) {
|
if (!this.queuedJobIds.includes(subJob.subJobId)) {
|
||||||
if (subJob.subJobId === this.subJobCancelId) {
|
if (subJob.subJobId === this.subJobCancelId) {
|
||||||
logger.info(`Cancelling subjob ${subJob.id}`);
|
logger.info(`Cancelling subjob ${subJob.id}`);
|
||||||
await this.database.updateSubJobState(subJob.id, { type: "cancelled" });
|
await this.database.updateSubJobState(subJob.id, {
|
||||||
await this.database.removePrinterSubJob(this.id, subJob.id);
|
type: "cancelled",
|
||||||
} else if (!["failed", "complete", "draft", "cancelled"].includes(subJob.state.type)) {
|
});
|
||||||
|
await this.database.removePrinterSubJob(subJob.id);
|
||||||
|
} else if (
|
||||||
|
!["failed", "complete", "draft", "cancelled"].includes(
|
||||||
|
subJob.state.type
|
||||||
|
)
|
||||||
|
) {
|
||||||
// Update the subjob state to match printer state
|
// Update the subjob state to match printer state
|
||||||
await this.database.updateSubJobState(subJob.id, this.state);
|
await this.database.updateSubJobState(subJob.id, this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["failed", "complete", "cancelled"].includes(subJob.state.type)) {
|
if (["failed", "complete", "cancelled"].includes(subJob.state.type)) {
|
||||||
logger.info(`Removing completed/failed/cancelled subjob ${subJob.id} from printer ${this.name}`);
|
logger.info(
|
||||||
await this.database.removePrinterSubJob(this.id, subJob.id);
|
`Removing completed/failed/cancelled subjob ${subJob.id} from printer ${this.printer.name}`
|
||||||
|
);
|
||||||
|
await this.database.removePrinterSubJob(subJob.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -548,47 +658,52 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleKlippyDisconnected() {
|
async handleKlippyDisconnected() {
|
||||||
logger.info(`Klippy disconnected (${this.name})`);
|
logger.info(`Klippy disconnected (${this.printer.name})`);
|
||||||
this.state = { type: "offline" };
|
this.state = { type: "offline" };
|
||||||
this.klippyState = { type: 'offline'};
|
this.klippyState = { type: "offline" };
|
||||||
this.isOnline = false;
|
this.isOnline = false;
|
||||||
this.isPrinting = false;
|
this.isPrinting = false;
|
||||||
this.isError = false;
|
this.isError = false;
|
||||||
this.isReady = false;
|
this.isReady = false;
|
||||||
await this.database.clearAlerts(this.id);
|
await this.database.clearAlerts();
|
||||||
await this.updatePrinterState();
|
await this.updatePrinterState();
|
||||||
await this.updatePrinterSubJobs();
|
await this.updatePrinterSubJobs();
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleKlippyReady() {
|
async handleKlippyReady() {
|
||||||
logger.info(`Klippy ready (${this.name})`);
|
logger.info(`Klippy ready (${this.printer.name})`);
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFileListChanged(fileInfo) {
|
handleFileListChanged(fileInfo) {
|
||||||
logger.debug(`File list changed for ${this.name}:`, fileInfo);
|
logger.debug(`File list changed for ${this.printer.name}:`, fileInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMetadataUpdate(metadata) {
|
handleMetadataUpdate(metadata) {
|
||||||
logger.info(`Metadata updated for ${this.name}:`, metadata.filename);
|
logger.info(
|
||||||
|
`Metadata updated for ${this.printer.name}:`,
|
||||||
|
metadata.filename
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePowerChanged(powerStatus) {
|
handlePowerChanged(powerStatus) {
|
||||||
logger.info(`Power status changed for ${this.name}:`, powerStatus);
|
logger.info(`Power status changed for ${this.printer.name}:`, powerStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadGcodeFile(fileBlob, fileName) {
|
async uploadGcodeFile(fileBlob, fileName) {
|
||||||
logger.info(`Uploading G-code file ${fileName} to ${this.name}`);
|
logger.info(`Uploading G-code file ${fileName} to ${this.printer.name}`);
|
||||||
if (!this.isOnline) {
|
if (!this.isOnline) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Cannot upload file: Not connected to Moonraker (${this.name})`,
|
`Cannot upload file: Not connected to Moonraker (${this.printer.name})`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { protocol, host, port } = this.config;
|
const { protocol, host, port } = this.config;
|
||||||
const httpUrl = `${protocol === "ws" ? "http" : "https"}://${host}:${port}/server/files/upload`;
|
const httpUrl = `${
|
||||||
|
protocol === "ws" ? "http" : "https"
|
||||||
|
}://${host}:${port}/server/files/upload`;
|
||||||
|
|
||||||
// Convert Blob to Buffer
|
// Convert Blob to Buffer
|
||||||
const arrayBuffer = await fileBlob.arrayBuffer();
|
const arrayBuffer = await fileBlob.arrayBuffer();
|
||||||
@ -612,14 +727,14 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
headers,
|
headers,
|
||||||
onUploadProgress: (progressEvent) => {
|
onUploadProgress: (progressEvent) => {
|
||||||
const percentCompleted = Math.round(
|
const percentCompleted = Math.round(
|
||||||
(progressEvent.loaded * 100) / progressEvent.total,
|
(progressEvent.loaded * 100) / progressEvent.total
|
||||||
);
|
);
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Uploading file to ${this.name}: ` +
|
`Uploading file to ${this.printer.name}: ` +
|
||||||
fileName +
|
fileName +
|
||||||
" " +
|
" " +
|
||||||
percentCompleted +
|
percentCompleted +
|
||||||
"%",
|
"%"
|
||||||
);
|
);
|
||||||
this.socketManager.broadcast("notify_printer_update", {
|
this.socketManager.broadcast("notify_printer_update", {
|
||||||
printerId: this.id,
|
printerId: this.id,
|
||||||
@ -636,17 +751,19 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
throw new Error("Failed to upload G-code file to printer");
|
throw new Error("Failed to upload G-code file to printer");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Successfully uploaded file ${fileName} to ${this.name}`);
|
logger.info(
|
||||||
|
`Successfully uploaded file ${fileName} to ${this.printer.name}`
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error uploading file to ${this.name}:`, error);
|
logger.error(`Error uploading file to ${this.printer.name}:`, error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deploySubJobs(jobId) {
|
async deploySubJobs(jobId) {
|
||||||
try {
|
try {
|
||||||
const printSubJobs = await printSubJobModel
|
const printSubJobs = await subJobModel
|
||||||
.find({ printJob: jobId })
|
.find({ printJob: jobId })
|
||||||
.sort({ number: 1 });
|
.sort({ number: 1 });
|
||||||
|
|
||||||
@ -658,7 +775,7 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
"to printer:",
|
"to printer:",
|
||||||
this.id,
|
this.id,
|
||||||
"with files:",
|
"with files:",
|
||||||
`${jobId.id}.gcode`,
|
`${jobId.id}.gcode`
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await this.sendPrinterCommand({
|
const result = await this.sendPrinterCommand({
|
||||||
@ -674,7 +791,7 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the PrintSubJob model
|
// Update the PrintSubJob model
|
||||||
const updatedSubJob = await printSubJobModel.findByIdAndUpdate(
|
const updatedSubJob = await subJobModel.findByIdAndUpdate(
|
||||||
subJob.id,
|
subJob.id,
|
||||||
{
|
{
|
||||||
subJobId: result.queued_jobs[result.queued_jobs.length - 1].job_id,
|
subJobId: result.queued_jobs[result.queued_jobs.length - 1].job_id,
|
||||||
@ -689,7 +806,7 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the printer's subJobs array
|
// Update the printer's subJobs array
|
||||||
await this.database.addSubJobToPrinter(this.id, updatedSubJob._id);
|
await this.database.addSubJobToPrinter(updatedSubJob._id);
|
||||||
await this.database.updateSubJobState(subJob.id, { type: "queued" });
|
await this.database.updateSubJobState(subJob.id, { type: "queued" });
|
||||||
|
|
||||||
logger.info("Sub job deployed to printer:", this.id);
|
logger.info("Sub job deployed to printer:", this.id);
|
||||||
@ -724,8 +841,74 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
|
|||||||
|
|
||||||
async loadFilamentStock(filamentStockId) {
|
async loadFilamentStock(filamentStockId) {
|
||||||
this.currentFilamentStockId = filamentStockId;
|
this.currentFilamentStockId = filamentStockId;
|
||||||
const result = await this.database.setCurrentFilamentStock(this.id, this.currentFilamentStockId);
|
const result = await this.database.setCurrentFilamentStock(
|
||||||
this.currentFilamentStockDensity = result.filament.density
|
this.id,
|
||||||
await this.database.removeAlerts(this.id, {type: 'loadFilamentStock'})
|
this.currentFilamentStockId
|
||||||
|
);
|
||||||
|
this.currentFilamentStockDensity = result.filament.density;
|
||||||
|
await this.database.removeAlerts({ type: "loadFilamentStock" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTemperature(temperature) {
|
||||||
|
logger.info(`Setting temperature for ${this.printer.name}:`, temperature);
|
||||||
|
|
||||||
|
if (!this.isOnline) {
|
||||||
|
logger.error(
|
||||||
|
`Cannot set temperature: Not connected to Moonraker (${this.printer.name})`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let gcodeCommands = [];
|
||||||
|
|
||||||
|
// Handle extruder temperature
|
||||||
|
if (temperature.extruder?.target !== undefined) {
|
||||||
|
gcodeCommands.push(
|
||||||
|
`SET_HEATER_TEMPERATURE HEATER=extruder TARGET=${temperature.extruder.target}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle bed temperature
|
||||||
|
if (temperature.bed?.target !== undefined) {
|
||||||
|
gcodeCommands.push(
|
||||||
|
`SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=${temperature.bed.target}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gcodeCommands.length === 0) {
|
||||||
|
logger.warn(
|
||||||
|
`No valid temperature targets provided for ${this.printer.name}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Send each temperature command
|
||||||
|
for (const gcodeCommand of gcodeCommands) {
|
||||||
|
const result = await this.sendPrinterCommand({
|
||||||
|
method: "printer.gcode.script",
|
||||||
|
params: {
|
||||||
|
script: gcodeCommand,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to set temperature with command: ${gcodeCommand}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_.merge(this.temperatureObject, temperature);
|
||||||
|
|
||||||
|
logger.info(`Successfully set temperature for ${this.printer.name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error setting temperature for ${this.printer.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,107 +1,90 @@
|
|||||||
// printer-manager.js - Manages multiple printer connections through MongoDB
|
// printer-manager.js - Manages multiple printer connections through MongoDB
|
||||||
import { PrinterClient } from "./printerclient.js";
|
import { PrinterClient } from "./printerclient.js";
|
||||||
import { printerModel } from "../database/printer.schema.js"; // Import your printer model
|
|
||||||
import { printSubJobModel } from "../database/printsubjob.schema.js"; // Import your subjob model
|
|
||||||
import { loadConfig } from "../config.js";
|
import { loadConfig } from "../config.js";
|
||||||
import log4js from "log4js";
|
import log4js from "log4js";
|
||||||
import { printJobModel } from "../database/printjob.schema.js";
|
import { sendIPC } from "../electron/ipc.js";
|
||||||
// Load configuration
|
// Load configuration
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
const logger = log4js.getLogger("Printer Manager");
|
const logger = log4js.getLogger("Printer Manager");
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
export class PrinterManager {
|
export class PrinterManager {
|
||||||
constructor(config) {
|
constructor(socketClient) {
|
||||||
this.config = config;
|
this.socketClient = socketClient;
|
||||||
this.printerClientConnections = new Map();
|
this.printerClients = new Map();
|
||||||
this.statusCheckInterval = null;
|
this.printers = [];
|
||||||
this.initializePrinterConnections();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializePrinterConnections() {
|
async reloadPrinters() {
|
||||||
try {
|
try {
|
||||||
// Get all printers from the database
|
this.printers = await this.socketClient.listObjects({
|
||||||
const printers = await printerModel.find({}).populate({ path: "currentFilamentStock",
|
objectType: "printer",
|
||||||
populate: {
|
filter: { host: this.socketClient.id },
|
||||||
path: "filament",
|
});
|
||||||
},});
|
|
||||||
|
|
||||||
for (const printer of printers) {
|
sendIPC("setPrinters", this.printers);
|
||||||
await this.connectToPrinter(printer);
|
|
||||||
|
// Remove printer clients that are no longer in the printers list
|
||||||
|
const printerIds = this.printers.map((printer) => printer._id);
|
||||||
|
for (const [printerId, printerClient] of this.printerClients.entries()) {
|
||||||
|
if (!printerIds.includes(printerId)) {
|
||||||
|
// Close the connection before removing
|
||||||
|
if (printerClient.socket) {
|
||||||
|
printerClient.socket.close();
|
||||||
|
}
|
||||||
|
this.printerClients.delete(printerId);
|
||||||
|
logger.info(`Removed printer client for printer ID: ${printerId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Initialized connections to ${printers.length} printers`);
|
// Add new printer clients for printers not in the printerClients map
|
||||||
|
for (const printer of this.printers) {
|
||||||
|
const printerId = printer._id;
|
||||||
|
if (!this.printerClients.has(printerId)) {
|
||||||
|
const printerClient = new PrinterClient(printer, this);
|
||||||
|
await printerClient.connect();
|
||||||
|
this.printerClients.set(printerId, printerClient);
|
||||||
|
logger.info(`Added printer client for printer ID: ${printerId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error initializing printer connections: ${error.message}`);
|
logger.error("Failed to update printers:", error);
|
||||||
|
this.printers = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectToPrinter(printer) {
|
async setupPrintersListener() {}
|
||||||
// Create and store the connection
|
|
||||||
const printerClientConnection = new PrinterClient(
|
|
||||||
printer,
|
|
||||||
this,
|
|
||||||
this.socketManager,
|
|
||||||
);
|
|
||||||
this.printerClientConnections.set(printer.id, printerClientConnection);
|
|
||||||
|
|
||||||
// Connect to the printer
|
|
||||||
await printerClientConnection.connect();
|
|
||||||
|
|
||||||
logger.info(`Connected to printer: ${printer.name} (${printer.id})`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPrinterClient(printerId) {
|
getPrinterClient(printerId) {
|
||||||
return this.printerClientConnections.get(printerId);
|
return this.printerClients.get(printerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllPrinterClients() {
|
getAllPrinterClients() {
|
||||||
return this.printerClientConnections.values();
|
return this.printerClients.values();
|
||||||
}
|
|
||||||
|
|
||||||
// Process command for a specific printer
|
|
||||||
async processPrinterCommand(command) {
|
|
||||||
const printerId = command.params.printerId;
|
|
||||||
const printerClientConnection =
|
|
||||||
this.printerClientConnections.get(printerId);
|
|
||||||
if (!printerClientConnection) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Printer with ID ${printerId} not found`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return await printerClientConnection.sendPrinterCommand(command);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSubscription(printerId, socketId, mergedSubscription) {
|
async updateSubscription(printerId, socketId, mergedSubscription) {
|
||||||
const printerClientConnection =
|
const printerClient = this.printerClients.get(printerId);
|
||||||
this.printerClientConnections.get(printerId);
|
if (!printerClient) {
|
||||||
if (!printerClientConnection) {
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Printer with ID ${printerId} not found`,
|
error: `Printer with ID ${printerId} not found`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
printerClientConnection.subscriptions.set(socketId, mergedSubscription);
|
printerClient.subscriptions.set(socketId, mergedSubscription);
|
||||||
return await printerClientConnection.updateSubscriptions();
|
return await printerClient.updateSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close all printer connections
|
// Close all printer connections
|
||||||
closeAllConnections() {
|
closeAllConnections() {
|
||||||
for (const printerClientConnection of this.printerClientConnections.values()) {
|
for (const printerClient of this.printerClients.values()) {
|
||||||
if (printerClientConnection.socket) {
|
if (printerClient.socket) {
|
||||||
printerClientConnection.socket.close();
|
printerClient.socket.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSocketManager(socketManager) {
|
|
||||||
this.socketManager = socketManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadGCODE(gcodeFileId) {
|
async downloadGCODE(gcodeFileId) {
|
||||||
logger.info(`Downloading G-code file ${gcodeFileId}`);
|
logger.info(`Downloading G-code file ${gcodeFileId}`);
|
||||||
try {
|
try {
|
||||||
@ -109,12 +92,15 @@ export class PrinterManager {
|
|||||||
const url = `http://localhost:8080/gcodefiles/${gcodeFileId}/content/`;
|
const url = `http://localhost:8080/gcodefiles/${gcodeFileId}/content/`;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.socketManager.socketClientConnections.values().next().value.socket.handshake.auth.token}`,
|
Authorization: `Bearer ${
|
||||||
|
this.socketManager.socketClientConnections.values().next().value
|
||||||
|
.socket.handshake.auth.token
|
||||||
|
}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to download G-code file: ${response.statusText}`,
|
`Failed to download G-code file: ${response.statusText}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const gcodeContent = await response.blob();
|
const gcodeContent = await response.blob();
|
||||||
@ -133,7 +119,7 @@ export class PrinterManager {
|
|||||||
|
|
||||||
async deployPrintJob(printJobId) {
|
async deployPrintJob(printJobId) {
|
||||||
logger.info(`Deploying print job ${printJobId}`);
|
logger.info(`Deploying print job ${printJobId}`);
|
||||||
const printJob = await printJobModel
|
const printJob = await jobModel
|
||||||
.findById(printJobId)
|
.findById(printJobId)
|
||||||
.populate("printers")
|
.populate("printers")
|
||||||
.populate("subJobs");
|
.populate("subJobs");
|
||||||
@ -174,7 +160,7 @@ export class PrinterManager {
|
|||||||
|
|
||||||
async cancelSubJob(subJobId) {
|
async cancelSubJob(subJobId) {
|
||||||
logger.info(`Canceling sub job ${subJobId}`);
|
logger.info(`Canceling sub job ${subJobId}`);
|
||||||
const subJob = await printSubJobModel.findById(subJobId);
|
const subJob = await subJobModel.findById(subJobId);
|
||||||
if (!subJob) {
|
if (!subJob) {
|
||||||
throw new Error("Sub job not found");
|
throw new Error("Sub job not found");
|
||||||
}
|
}
|
||||||
|
|||||||