Add formatTraceData utility and integrate into logging
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good

- Introduced a new utility function `formatTraceData` to standardize the formatting of trace data for logging purposes.
- Updated various modules including auth, database, and stats manager to utilize `formatTraceData` for improved log clarity and consistency.
- Added new schemas for printer profiles and filament profiles to enhance the data model.
- Implemented tests for session authentication to ensure robust functionality and error handling.
This commit is contained in:
Tom Butcher 2026-07-20 02:11:56 +01:00
parent 15132fefa0
commit aad49bfcfd
14 changed files with 572 additions and 72 deletions

View File

@ -0,0 +1,91 @@
import { jest } from '@jest/globals';
const redisGetKeyMock = jest.fn();
jest.unstable_mockModule('../../config.js', () => ({
loadConfig: jest.fn(() => ({
server: { logLevel: 'info' },
auth: { requiredRoles: [] },
})),
}));
jest.unstable_mockModule('../../database/redis.js', () => ({
redisServer: { getKey: redisGetKeyMock },
}));
jest.unstable_mockModule('../../database/database.js', () => ({
editObject: jest.fn(),
getObject: jest.fn(),
listObjects: jest.fn(),
}));
jest.unstable_mockModule('../../database/schemas/management/host.schema.js', () => ({
hostModel: {},
}));
jest.unstable_mockModule('../../database/schemas/management/user.schema.js', () => ({
userModel: {},
}));
jest.unstable_mockModule('../../utils.js', () => ({
generateAuthCode: jest.fn(),
formatTraceData: (value) => (typeof value === 'string' ? value : JSON.stringify(value)),
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
info: jest.fn(),
error: jest.fn(),
trace: jest.fn(),
}),
},
}));
const { SessionAuth } = await import('../auth.js');
describe('SessionAuth', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('accepts the API shared Redis session shape', async () => {
const user = { _id: 'user-1', username: 'slicer-user' };
redisGetKeyMock.mockResolvedValue({
sessionToken: 'slicer-token',
user,
expiresAt: Date.now() + 10000,
source: 'slicer',
});
await expect(new SessionAuth().verifyToken('slicer-token')).resolves.toEqual({
valid: true,
user,
});
expect(redisGetKeyMock).toHaveBeenCalledWith('session:slicer-token');
});
it('does not cache a token deleted by slicer-session replacement', async () => {
const user = { _id: 'user-1', username: 'slicer-user' };
redisGetKeyMock
.mockResolvedValueOnce({ user, expiresAt: Date.now() + 10000 })
.mockResolvedValueOnce(null);
const auth = new SessionAuth();
await expect(auth.verifyToken('old-token')).resolves.toEqual({ valid: true, user });
await expect(auth.verifyToken('old-token')).resolves.toEqual({ valid: false });
expect(redisGetKeyMock).toHaveBeenCalledTimes(2);
});
it('rejects expired sessions', async () => {
redisGetKeyMock.mockResolvedValue({
user: { _id: 'user-1' },
expiresAt: Date.now() - 1,
});
await expect(new SessionAuth().verifyToken('expired-token')).resolves.toEqual({
valid: false,
});
});
});

View File

@ -5,7 +5,7 @@ import { redisServer } from '../database/redis.js';
import { editObject, getObject, listObjects } from '../database/database.js';
import { hostModel } from '../database/schemas/management/host.schema.js';
import { userModel } from '../database/schemas/management/user.schema.js';
import { generateAuthCode } from '../utils.js';
import { formatTraceData, generateAuthCode } from '../utils.js';
const config = loadConfig();
@ -21,20 +21,11 @@ const SESSION_KEY_PREFIX = 'session:';
export class SessionAuth {
constructor() {
this.config = config.auth;
this.tokenCache = new Map();
}
async verifyToken(token) {
if (!token) return { valid: false };
if (this.tokenCache.has(token)) {
const cached = this.tokenCache.get(token);
if (cached.expiresAt > Date.now()) {
return { valid: true, user: cached.user };
}
this.tokenCache.delete(token);
}
try {
const key = SESSION_KEY_PREFIX + token;
const session = await redisServer.getKey(key);
@ -58,11 +49,6 @@ export class SessionAuth {
}
}
this.tokenCache.set(token, {
expiresAt: session.expiresAt,
user: session.user
});
return { valid: true, user: session.user };
} catch (error) {
logger.error('Session verification error:', error.message);
@ -77,9 +63,9 @@ export class KeycloakAuth extends SessionAuth {}
export class CodeAuth {
async verifyCode(id, authCode) {
try {
logger.trace('Verifying code:', { id, authCode });
logger.trace(`Verifying code: ${formatTraceData({ id, authCode })}`);
const host = await getObject({ model: hostModel, id, cached: true });
logger.trace('Host retrieved:', host);
logger.trace(`Host retrieved: ${formatTraceData(host)}`);
if (host == undefined) {
const error = 'Host not found.';
logger.warn(error, 'Host:', id);
@ -100,7 +86,7 @@ export class CodeAuth {
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
logger.trace('Code verification successful:', host);
logger.trace(`Code verification successful: ${formatTraceData(host)}`);
return { valid: true, host: host };
} catch (error) {
logger.error('Code verification error:', error.message);

View File

@ -10,7 +10,7 @@ import {
} from './utils.js';
import log4js from 'log4js';
import { loadConfig } from '../config.js';
import { getQueryToCacheKey } from '../utils.js';
import { formatTraceData, getQueryToCacheKey } from '../utils.js';
import { redisServer } from './redis.js';
import { auditLogModel } from './schemas/management/auditlog.schema.js';
@ -61,7 +61,7 @@ export const updateObjectCache = async ({
// Get all keys matching the filter pattern
const matchingKeys = await redisServer.getKeysByPattern(cacheKeyFilter);
logger.trace('Matching keys:', matchingKeys);
logger.trace(`Matching keys: ${formatTraceData(matchingKeys)}`);
// Merge the object with each cached object and update
const mergedObjects = [];
@ -124,15 +124,17 @@ export const listObjects = async ({
cached = false
}) => {
try {
logger.trace('Listing objects:', {
model,
populate,
filter,
sort,
order,
project,
cached
});
logger.trace(
`Listing objects: ${formatTraceData({
model,
populate,
filter,
sort,
order,
project,
cached,
})}`
);
// Fix: descend should be -1, ascend should be 1
const sortOrder = order === 'descend' ? -1 : 1;
@ -177,16 +179,18 @@ export const listObjects = async ({
const finalResult = expandObjectIds(queryResult);
logger.trace('Retreived from database:', {
model,
populate,
filter,
sort,
order,
project,
cached,
length: finalResult.length
});
logger.trace(
`Retreived from database: ${formatTraceData({
model,
populate,
filter,
sort,
order,
project,
cached,
length: finalResult.length,
})}`
);
return finalResult;
} catch (error) {
logger.error('Object list error:', error);
@ -202,11 +206,13 @@ export const getObject = async ({
cached = false
}) => {
try {
logger.trace('Getting object:', {
model,
id,
populate
});
logger.trace(
`Getting object: ${formatTraceData({
model,
id,
populate,
})}`
);
if (cached == true) {
const cachedObject = await retrieveObjectCache({ model, id, populate });
@ -238,13 +244,15 @@ export const getObject = async ({
return undefined;
}
logger.trace('Retreived object from database:', {
model,
id,
populate
});
logger.trace(
`Retreived object from database: ${formatTraceData({
model,
id,
populate,
})}`
);
logger.trace(finalResult);
logger.trace(formatTraceData(finalResult));
await updateObjectCache({
model: model,

View File

@ -1,6 +1,7 @@
import { connect } from '@nats-io/transport-node';
import log4js from 'log4js';
import { loadConfig } from '../config.js';
import { formatTraceData } from '../utils.js';
const config = loadConfig();
const logger = log4js.getLogger('Nats');
@ -72,7 +73,7 @@ class NatsServer {
try {
client.publish(subject, this.textEncoder.encode(payload));
logger.trace(`Published to subject: ${subject}, data: ${payload}`);
logger.trace(`Published to subject: ${subject}, data: ${formatTraceData(payload)}`);
return { success: true };
} catch (error) {
logger.error(`Failed to publish to subject ${subject}:`, error);
@ -94,7 +95,9 @@ class NatsServer {
);
const responseData = this.textDecoder.decode(response.data);
logger.trace(`Request to subject: ${subject}, response: ${responseData}`);
logger.trace(
`Request to subject: ${subject}, response: ${formatTraceData(responseData)}`
);
// Try to parse as JSON, fallback to string
try {

View File

@ -1,6 +1,7 @@
import { createClient } from 'redis';
import log4js from 'log4js';
import { loadConfig } from '../config.js';
import { formatTraceData } from '../utils.js';
const config = loadConfig();
const logger = log4js.getLogger('Redis');
@ -80,7 +81,7 @@ class RedisServer {
COUNT: 100
});
cursor = result.cursor;
logger.trace('Result:', result);
logger.trace(`Result: ${formatTraceData(result)}`);
keys.push(...result.keys);
} while (cursor !== '0');
return keys;

View File

@ -8,28 +8,28 @@ const userSettingsSchema = new mongoose.Schema({
type: Schema.Types.ObjectId,
ref: 'user',
required: true,
unique: true
unique: true,
},
defaults: {
viewMode: { type: Schema.Types.Mixed, default: () => ({}) },
filterSidebarVisibility: {
type: Schema.Types.Mixed,
default: () => ({})
default: () => ({}),
},
columnVisibility: { type: Schema.Types.Mixed, default: () => ({}) },
collapseState: { type: Schema.Types.Mixed, default: () => ({}) }
collapseState: { type: Schema.Types.Mixed, default: () => ({}) },
},
createdAt: {
type: Date,
required: true,
default: Date.now
default: Date.now,
},
updatedAt: {
type: Date,
required: true,
default: Date.now
}
default: Date.now,
},
});
userSettingsSchema.virtual('id').get(function () {
@ -38,7 +38,4 @@ userSettingsSchema.virtual('id').get(function () {
userSettingsSchema.set('toJSON', { virtuals: true });
export const userSettingsModel = mongoose.model(
'userSettings',
userSettingsSchema
);
export const userSettingsModel = mongoose.model('userSettings', userSettingsSchema);

View File

@ -1,6 +1,8 @@
import { jobModel } from './production/job.schema.js';
import { subJobModel } from './production/subjob.schema.js';
import { printerModel } from './production/printer.schema.js';
import { printerProfileModel } from './production/printerprofile.schema.js';
import { filamentProfileModel } from './production/filamentprofile.schema.js';
import { filamentModel } from './management/filament.schema.js';
import { filamentSkuModel } from './management/filamentsku.schema.js';
import { gcodeFileModel } from './production/gcodefile.schema.js';
@ -55,6 +57,20 @@ export const models = {
referenceField: '_reference',
label: 'Printer',
},
PPF: {
model: printerProfileModel,
idField: '_id',
type: 'printerProfile',
referenceField: '_reference',
label: 'Printer Profile',
},
FPF: {
model: filamentProfileModel,
idField: '_id',
type: 'filamentProfile',
referenceField: '_reference',
label: 'Filament Profile',
},
FIL: {
model: filamentModel,
idField: '_id',

View File

@ -0,0 +1,171 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const filamentProfileSchema = new mongoose.Schema(
{
_reference: { type: String, default: () => generateId()() },
name: { type: String, required: true },
filamentType: { type: String, required: true },
filament: {
type: mongoose.Schema.Types.ObjectId,
refPath: 'filamentType',
required: true,
},
filamentIsSupport: { type: Boolean },
filamentSoluble: { type: Boolean },
filamentPrintable: { type: Number },
filamentAdhesivenessCategory: { type: Number },
temperatureVitrification: { type: Number },
idleTemperature: { type: Number },
pelletFlowCoefficient: { type: Number },
requiredNozzleHrc: { type: Number },
filamentFlowRatio: { type: Number },
enablePressureAdvance: { type: Boolean },
pressureAdvance: { type: Number },
adaptivePressureAdvance: { type: Boolean },
adaptivePressureAdvanceBridges: { type: Boolean },
adaptivePressureAdvanceOverhangs: { type: Boolean },
adaptivePressureAdvanceModel: { type: String },
activateChamberTempControl: { type: Boolean },
chamberTemperature: { type: Number },
chamberMinimalTemperature: { type: Number },
nozzleTemperatureInitialLayer: { type: Number },
nozzleTemperature: { type: Number },
nozzleTemperatureRangeLow: { type: Number },
nozzleTemperatureRangeHigh: { type: Number },
hotPlateTempInitialLayer: { type: Number },
hotPlateTemp: { type: Number },
coolPlateTempInitialLayer: { type: Number },
coolPlateTemp: { type: Number },
engPlateTempInitialLayer: { type: Number },
engPlateTemp: { type: Number },
texturedPlateTempInitialLayer: { type: Number },
texturedPlateTemp: { type: Number },
texturedCoolPlateTempInitialLayer: { type: Number },
texturedCoolPlateTemp: { type: Number },
supertackPlateTempInitialLayer: { type: Number },
supertackPlateTemp: { type: Number },
filamentAdaptiveVolumetricSpeed: { type: Boolean },
filamentMaxVolumetricSpeed: { type: Number },
volumetricSpeedCoefficients: { type: String },
closeFanTheFirstXLayers: { type: Number },
fullFanSpeedLayer: { type: Number },
fanMinSpeed: { type: Number },
fanMaxSpeed: { type: Number },
reduceFanStopStartFreq: { type: Boolean },
slowDownForLayerCooling: { type: Boolean },
dontSlowDownOuterWall: { type: Boolean },
slowDownMinSpeed: { type: Number },
slowDownLayerTime: { type: Number },
fanCoolingLayerTime: { type: Number },
enableOverhangBridgeFan: { type: Boolean },
overhangFanThreshold: { type: String },
overhangFanSpeed: { type: Number },
internalBridgeFanSpeed: { type: Number },
supportMaterialInterfaceFanSpeed: { type: Number },
ironingFanSpeed: { type: Number },
initialLayerFanSpeed: { type: Number },
firstXLayerFanSpeed: { type: Number },
additionalCoolingFanSpeed: { type: Number },
additionalFanFullSpeedLayer: { type: Number },
closeAdditionalFanFirstXLayers: { type: Boolean },
activateAirFiltration: { type: Boolean },
activateAirFiltrationDuringPrint: { type: Boolean },
activateAirFiltrationOnCompletion: { type: Boolean },
duringPrintExhaustFanSpeed: { type: Number },
completePrintExhaustFanSpeed: { type: Number },
filamentRetractionLength: { type: Number },
filamentRetractionSpeed: { type: Number },
filamentDeretractionSpeed: { type: Number },
filamentRetractionMinimumTravel: { type: Number },
filamentRetractWhenChangingLayer: { type: Boolean },
filamentRetractBeforeWipe: { type: Number },
filamentRetractAfterWipe: { type: Number },
filamentRetractRestartExtra: { type: Number },
filamentRetractLiftAbove: { type: Number },
filamentRetractLiftBelow: { type: Number },
filamentRetractLiftEnforce: { type: String },
filamentWipe: { type: Boolean },
filamentWipeDistance: { type: Number },
filamentZHop: { type: Number },
filamentZHopTypes: { type: String },
filamentLongRetractionsWhenCut: { type: Boolean },
filamentRetractionDistancesWhenCut: { type: Number },
longRetractionsWhenEc: { type: Boolean },
retractionDistancesWhenEc: { type: Number },
filamentLoadingSpeed: { type: Number },
filamentLoadingSpeedStart: { type: Number },
filamentUnloadingSpeed: { type: Number },
filamentUnloadingSpeedStart: { type: Number },
filamentChangeLength: { type: Number },
filamentChangeLengthNc: { type: Number },
filamentToolchangeDelay: { type: Number },
filamentExtruderCompatibility: { type: Number },
filamentExtruderVariant: { type: String },
filamentMultitoolRamming: { type: Boolean },
filamentMultitoolRammingFlow: { type: Number },
filamentMultitoolRammingVolume: { type: Number },
filamentRammingParameters: { type: String },
filamentRammingTravelTime: { type: Number },
filamentRammingTravelTimeNc: { type: Number },
filamentRammingVolumetricSpeed: { type: Number },
filamentRammingVolumetricSpeedNc: { type: Number },
filamentMinimalPurgeOnWipeTower: { type: Number },
filamentTowerInterfacePreExtrusionDist: { type: Number },
filamentTowerInterfacePreExtrusionLength: { type: Number },
filamentTowerInterfacePrintTemp: { type: Number },
filamentTowerInterfacePurgeVolume: { type: Number },
filamentTowerIroningArea: { type: Number },
filamentCoolingBeforeTower: { type: Number },
filamentCoolingInitialSpeed: { type: Number },
filamentCoolingFinalSpeed: { type: Number },
filamentCoolingMoves: { type: Number },
filamentFlushTemp: { type: Number },
filamentFlushTempFast: { type: Number },
filamentFlushVolumetricSpeed: { type: Number },
filamentPreCoolingTemperature: { type: Number },
filamentPreCoolingTemperatureNc: { type: Number },
filamentPreheatTemperatureDelta: { type: Number },
filamentPrimeVolumeNc: { type: Number },
filamentRetractLengthNc: { type: Number },
filamentStampingDistance: { type: Number },
filamentStampingLoadingSpeed: { type: Number },
filamentStartGcode: { type: String },
filamentEndGcode: { type: String },
filamentChangeExtrusionRoleGcode: { type: String },
filamentShrink: { type: String },
filamentShrinkageCompensationZ: { type: String },
filamentDevAmsDryingTemperature: { type: Number },
filamentDevAmsDryingTime: { type: Number },
filamentDevAmsDryingHeatDistortionTemperature: { type: Number },
filamentDevAmsDryingAmsLimitations: { type: String },
filamentDevChamberDryingBedTemperature: { type: Number },
filamentDevChamberDryingTime: { type: Number },
filamentDevDryingCoolingTemperature: { type: Number },
filamentDevDryingSofteningTemperature: { type: Number },
compatiblePrinters: [
{ type: mongoose.Schema.Types.ObjectId, ref: 'printerProfile' },
],
compatiblePrintersCondition: { type: String },
compatiblePrints: [{ type: String }],
compatiblePrintsCondition: { type: String },
filamentNotes: { type: String },
},
{ timestamps: true }
);
filamentProfileSchema.index({
name: 'text',
filamentNotes: 'text',
});
filamentProfileSchema.virtual('id').get(function () {
return this._id;
});
filamentProfileSchema.set('toJSON', { virtuals: true });
export const filamentProfileModel = mongoose.model(
'filamentProfile',
filamentProfileSchema
);

View File

@ -1,7 +1,15 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
import {
aggregateRollups,
aggregateRollupsHistory,
listObjects,
editObject,
newObject,
deleteObject,
} from '../../database.js';
import { subJobModel } from './subjob.schema.js';
const jobSchema = new mongoose.Schema(
{
@ -84,6 +92,95 @@ jobSchema.statics.history = async function (from, to) {
return results;
};
const normalizeId = (value) => value?._id ?? value;
jobSchema.statics.recalculate = async function (job, user) {
const jobId = job._id || job;
if (!jobId || job?.state?.type !== 'draft') {
return;
}
const quantity = job.quantity || 1;
const printers = (job.printers || []).map((printer) => normalizeId(printer));
const gcodeFile = normalizeId(job.gcodeFile);
const existingSubJobs = await listObjects({
model: subJobModel,
filter: { job: jobId },
sort: 'number',
order: 'ascend',
pagination: false,
});
if (existingSubJobs?.error) {
throw existingSubJobs;
}
const existingSubJobsByNumber = new Map(
existingSubJobs.map((subJob) => [subJob.number, subJob])
);
var printerCount = 0;
for (let i = 0; i < quantity; i++) {
const subJobNumber = i + 1;
const printer = printers[printerCount];
const subJobUpdateData = {
updatedAt: new Date(),
printer,
gcodeFile,
number: subJobNumber,
};
const existingSubJob = existingSubJobsByNumber.get(subJobNumber);
if (existingSubJob) {
const subJobResult = await editObject({
model: subJobModel,
id: existingSubJob._id,
updateData: subJobUpdateData,
user,
});
if (subJobResult.error) {
throw subJobResult;
}
} else {
const subJobResult = await newObject({
model: subJobModel,
newData: {
createdAt: new Date(),
...subJobUpdateData,
job: jobId,
state: { type: 'draft' },
},
user,
});
if (subJobResult.error) {
throw subJobResult;
}
}
if (printerCount >= printers.length - 1) {
printerCount = 0;
} else {
printerCount += 1;
}
}
for (const existingSubJob of existingSubJobs) {
if (existingSubJob.number > quantity) {
const deleteResult = await deleteObject({
model: subJobModel,
id: existingSubJob._id,
user,
});
if (deleteResult.error) {
throw deleteResult;
}
}
}
};
jobSchema.virtual('id').get(function () {
return this._id;
});

View File

@ -25,6 +25,18 @@ const alertSchema = new Schema(
{ timestamps: true, _id: false }
);
const pendingSlicerUploadSchema = new Schema(
{
file: { type: Schema.Types.ObjectId, ref: 'file', required: true },
gcodeFile: { type: Schema.Types.ObjectId, ref: 'gcodeFile', default: null },
job: { type: Schema.Types.ObjectId, ref: 'job', default: null },
shouldPrint: { type: Boolean, required: true, default: false },
new: { type: Boolean, required: true, default: true },
properties: { type: Object, required: true, default: () => ({}) },
},
{ timestamps: true }
);
// Define the main printer schema
const printerSchema = new Schema(
{
@ -53,6 +65,7 @@ const printerSchema = new Schema(
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', default: null },
host: { type: Schema.Types.ObjectId, ref: 'host', default: null },
alerts: [alertSchema],
pendingSlicerUploads: { type: [pendingSlicerUploadSchema], default: [] },
},
{ timestamps: true }
);

View File

@ -0,0 +1,107 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const minMaxNumberSchema = {
min: { type: Number },
max: { type: Number }
};
const thumbnailSchema = new mongoose.Schema(
{
width: { type: Number },
height: { type: Number }
},
{ _id: true }
);
const bedExcludeAreaSchema = new mongoose.Schema(
{
x: { type: Number },
y: { type: Number }
},
{ _id: true }
);
const extruderSchema = new mongoose.Schema(
{
nozzleDiameter: { type: Number },
nozzleVolume: { type: Number },
minLayerHeight: { type: Number },
maxLayerHeight: { type: Number },
positionOffsetX: { type: Number },
positionOffsetY: { type: Number },
retractionLength: { type: Number },
retractionExtraLengthOnRestart: { type: Number },
retractionSpeed: { type: Number },
deretractionSpeed: { type: Number },
retractionTravelDistanceThreshold: { type: Number },
retractOnLayerChange: { type: Boolean },
wipeWhileRetracting: { type: Boolean },
wipeDistance: { type: Number },
retractAmountBeforeWipe: { type: Number },
retractAmountAfterWipe: { type: Number },
zHopOnSurfaces: { type: String },
zHopType: { type: String },
zHopHeight: { type: Number },
zHopTravelingAngle: { type: Number },
zHopOnlyLiftZAbove: { type: Number },
zHopOnlyLiftZBelow: { type: Number },
materialSwitchRetractionLength: { type: Number },
materialSwitchExtraLengthOnRestart: { type: Number },
longRetractionWhenCut: { type: Boolean }
},
{ _id: true }
);
const printerProfileSchema = new mongoose.Schema(
{
_reference: { type: String, default: () => generateId()() },
name: { type: String, required: true },
printer: { type: mongoose.Schema.Types.ObjectId, ref: 'printer', default: null },
extruders: [extruderSchema],
printBedWidth: { type: Number },
printBedHeight: { type: Number },
originX: { type: Number },
originY: { type: Number },
bedExcludeArea: [bedExcludeAreaSchema],
printableHeight: { type: Number },
supportMultiBedTypes: { type: Boolean },
bestObjectPositionX: { type: Number },
bestObjectPositionY: { type: Number },
zOffset: { type: Number },
preferredOrientation: { type: Number },
machineMaxAccelerationRetracting: minMaxNumberSchema,
machineMaxSpeedE: minMaxNumberSchema,
machineMaxSpeedX: minMaxNumberSchema,
machineMaxSpeedY: minMaxNumberSchema,
machinePauseGcode: { type: String },
machineStartGcode: { type: String },
machineEndGcode: { type: String },
layerChangeGcode: { type: String },
beforeLayerChangeGcode: { type: String },
printerNotes: { type: String },
scanFirstLayer: { type: Boolean },
machineLoadFilamentTime: { type: Number },
machineUnloadFilamentTime: { type: Number },
thumbnails: [thumbnailSchema],
auxiliaryFan: { type: Boolean },
machineMaxJunctionDeviation: minMaxNumberSchema
},
{ timestamps: true }
);
printerProfileSchema.index({
name: 'text',
printerNotes: 'text'
});
printerProfileSchema.virtual('id').get(function () {
return this._id;
});
printerProfileSchema.set('toJSON', { virtuals: true });
export const printerProfileModel = mongoose.model(
'printerProfile',
printerProfileSchema
);

View File

@ -1,5 +1,6 @@
import log4js from 'log4js';
import { loadConfig } from '../config.js';
import { formatTraceData } from '../utils.js';
import { natsServer } from '../database/nats.js';
const config = loadConfig();
@ -53,7 +54,7 @@ export class StatsManager {
async sendStats(type, stats) {
try {
logger.trace(`Publishing stats: type=${type}, stats:`, stats);
logger.trace(`Publishing stats: type=${type}, stats: ${formatTraceData(stats)}`);
await natsServer.publish(`${type}s.stats`, stats);
return { success: true };
} catch (error) {

View File

@ -3,6 +3,7 @@ import _ from 'lodash';
import { loadConfig } from '../config.js';
import { natsServer } from '../database/nats.js';
import { expandObjectIds } from '../database/utils.js';
import { formatTraceData } from '../utils.js';
const config = loadConfig();
// Setup logger
@ -102,11 +103,13 @@ export class UpdateManager {
);
if (!matches) {
logger.trace(`Filtered ${eventName} event:`, {
objectType,
filter: normalizedFilter,
value
});
logger.trace(
`Filtered ${eventName} event: ${formatTraceData({
objectType,
filter: normalizedFilter,
value,
})}`
);
return;
}
@ -126,7 +129,7 @@ export class UpdateManager {
);
await natsServer.subscribe(subject, owner, async (key, value) => {
logger.trace('Object new event:', value);
logger.trace(`Object new event: ${formatTraceData(value)}`);
this.emitObjectTypeEvent(
'objectNew',
objectType,
@ -148,7 +151,7 @@ export class UpdateManager {
);
await natsServer.subscribe(subject, owner, async (key, value) => {
logger.trace('Object delete event:', value);
logger.trace(`Object delete event: ${formatTraceData(value)}`);
this.emitObjectTypeEvent(
'objectDelete',
objectType,

View File

@ -40,6 +40,12 @@ export function generateEtcId() {
return nanoid(24);
}
export function formatTraceData(value, maxLength = 128) {
if (value === undefined) return 'undefined';
const text = typeof value === 'string' ? value : JSON.stringify(value);
return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
}
export function getChangedValues(oldObj, newObj, old = false) {
const changes = {};