From c06812db66cc5b6085c99b49a696326610e6e0a2 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 13 Mar 2026 12:59:52 +0000 Subject: [PATCH 01/10] fix(config-gen): fix writing of VAOC-related config files including crowsnest.conf - simplify the logic of writing files under ratos_generated: resetting these files is already handled by macros specifically created for that purpose, don't try to provide a reset story through the setup wizard. --- .../rat-rig-vaoc.ts | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts b/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts index a74ba82ac..f51ad7e0f 100644 --- a/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts +++ b/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts @@ -16,7 +16,7 @@ export const renderTemplate: RenderTemplateFn = (ctx) => { ctx.extrasGenerator.addFileToRender({ fileName: 'crowsnest.conf', content: updatedCrowsnestContent, - overwrite: false, + overwrite: true, }); return ` ########################### @@ -63,22 +63,20 @@ heater_temp: 50 // If overwrite is requested (or overwrite details are not available), reset dc-endstop.cfg. // Otherwise, use the existing content or the default content if the file does not exist. const dcEndstopCfgFileName = 'ratos_generated/dc-endstop.cfg'; - const forceDcEndstopDefault = ctx.extrasGenerator.isOverwriteRequestedForFile(dcEndstopCfgFileName); - const dcEndstopContent = getDcEndstopConfigurationFileContent(forceDcEndstopDefault, dcEndstopCfgFileName); + const dcEndstopContent = getDcEndstopConfigurationFileContent(dcEndstopCfgFileName); ctx.extrasGenerator.addFileToRender({ fileName: dcEndstopCfgFileName, content: dcEndstopContent, - overwrite: forceDcEndstopDefault, + overwrite: true, }); // Ditto, for adjust-y-max.cfg. const adjustYMaxCfgFileName = 'ratos_generated/adjust-y-max.cfg'; - const forceAdjustYMaxDefault = ctx.extrasGenerator.isOverwriteRequestedForFile(adjustYMaxCfgFileName); - const adjustYMaxContent = getAdjustYMaxConfigurationFileContent(forceAdjustYMaxDefault, adjustYMaxCfgFileName); + const adjustYMaxContent = getAdjustYMaxConfigurationFileContent(adjustYMaxCfgFileName); ctx.extrasGenerator.addFileToRender({ fileName: adjustYMaxCfgFileName, content: adjustYMaxContent, - overwrite: forceAdjustYMaxDefault, + overwrite: true, }); return ` @@ -124,16 +122,12 @@ max_fps: 30 ]); } -function getCommonBoilerplateConfigurationFileContent( - forceDefault: boolean, - fileName: string, - macroName: string, -): string { +function getCommonBoilerplateConfigurationFileContent(fileName: string, macroName: string): string { const environment = serverSchema.parse(process.env); const configPath = path.join(environment.KLIPPER_CONFIG_PATH, fileName); const exists = existsSync(configPath); - getLogger().debug(`getting content for ${fileName}, exists=${exists}, forceDefault=${forceDefault}`); - if (forceDefault || !exists) { + getLogger().debug(`getting content for ${fileName}, exists=${exists}`); + if (!exists) { return `# WARNING. THIS FILE IS GENERATED BY RATOS AND # WILL BE UPDATED BY THE ${macroName} MACRO. # DO NOT DELETE OR MODIFY THIS FILE. @@ -142,10 +136,10 @@ function getCommonBoilerplateConfigurationFileContent( return readFileSync(configPath, 'utf-8'); } -function getDcEndstopConfigurationFileContent(forceDefault: boolean, fileName: string): string { - return getCommonBoilerplateConfigurationFileContent(forceDefault, fileName, 'CONFIGURE_DC_ENDSTOP'); +function getDcEndstopConfigurationFileContent(fileName: string): string { + return getCommonBoilerplateConfigurationFileContent(fileName, 'CONFIGURE_DC_ENDSTOP'); } -function getAdjustYMaxConfigurationFileContent(forceDefault: boolean, fileName: string): string { - return getCommonBoilerplateConfigurationFileContent(forceDefault, fileName, 'INCREASE_Y_MAX'); +function getAdjustYMaxConfigurationFileContent(fileName: string): string { + return getCommonBoilerplateConfigurationFileContent(fileName, 'INCREASE_Y_MAX'); } From 171bb068db687fa04c30b11f4cf5043439b83b11 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 13 Mar 2026 13:00:38 +0000 Subject: [PATCH 02/10] fix(config-gen): don't assume `.cfg` extension when pruning config file backups --- src/server/routers/printer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/routers/printer.ts b/src/server/routers/printer.ts index a3ce7e02b..7021c1ef9 100644 --- a/src/server/routers/printer.ts +++ b/src/server/routers/printer.ts @@ -582,8 +582,8 @@ const generateKlipperConfiguration = async ( ); if (backups.length > 0) { const sortedBackups = backups.sort((a, b) => { - const aDate = new Date(a.split('-').slice(-1)[0].split('.cfg')[0]); - const bDate = new Date(b.split('-').slice(-1)[0].split('.cfg')[0]); + const aDate = new Date(a.split('-').slice(-1)[0].split(fileExt)[0]); + const bDate = new Date(b.split('-').slice(-1)[0].split(fileExt)[0]); return aDate.getTime() - bDate.getTime(); }); if (sortedBackups.length > BACKUPS_TO_KEEP) { From 2611436741b7ae33f949a2d456d104314930a194 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 13 Mar 2026 16:37:42 +0000 Subject: [PATCH 03/10] fix(config-gen): when replacing multiple ini file sections, preserve white space between neighbouring replaced sections --- src/__tests__/server.test.ts | 16 ++++++++++++++++ src/server/helpers/file-operations.ts | 11 ++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index adb692665..d4da82e69 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -1013,6 +1013,22 @@ describe('server', async () => { // and the updated value expect(out).toContain('val:2'); }); + test('preserves blank line between adjacent replaced sections', async () => { + const { replaceOrAddIniSections } = await import('@/server/helpers/file-operations'); + const content = '[a]\nval: 1\n\n[other]\nfoo: bar\n'; + const out = replaceOrAddIniSections(content, [ + { + section: 'a', + body: 'val: replaced\n', + }, + { + section: 'other', + body: 'foo: replaced\nbar: baz\n', + }, + ]); + + expect(out).toContain('val: replaced\n\n[other]'); + }); test('multiple updates for same name: last update wins', async () => { const { replaceOrAddIniSections } = await import('@/server/helpers/file-operations'); const content = ''; diff --git a/src/server/helpers/file-operations.ts b/src/server/helpers/file-operations.ts index aab796e09..4128967b3 100644 --- a/src/server/helpers/file-operations.ts +++ b/src/server/helpers/file-operations.ts @@ -330,9 +330,14 @@ export function replaceOrAddIniSections(content: string, updates: IniUpdate[]): if (newBodyTrimmed === originalBodyTrimmed) { out += originalBodyWithTrailing; } else { - // Body changed: use new body and ensure it ends with newline - out += newBody; - if (!out.endsWith('\n')) out += '\n'; + // Body changed: preserve trailing section spacing from original slice. + const originalTrailingNewlines = originalBodyWithTrailing.match(/\n+$/)?.[0].length ?? 0; + out += newBody.replace(/\n+$/, ''); + if (originalTrailingNewlines > 0) { + out += '\n'.repeat(originalTrailingNewlines); + } else if (!out.endsWith('\n')) { + out += '\n'; + } } } else { // copy original section slice exactly as-is From cde49544449e350636be0865fe0270c4c7308b54 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 13 Mar 2026 16:42:29 +0000 Subject: [PATCH 04/10] fix(config-gen): when replacing ini file sections, retain existing trailing inline comments for retained keys. --- src/__tests__/server.test.ts | 12 +++++++++++ src/server/helpers/file-operations.ts | 30 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index d4da82e69..2591520a5 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -1029,6 +1029,18 @@ describe('server', async () => { expect(out).toContain('val: replaced\n\n[other]'); }); + test('preserves trailing comments after retained values (including surrounding whitespace) ', async () => { + const { replaceOrAddIniSections } = await import('@/server/helpers/file-operations'); + const content = '[a]\nx: 1 # comment \ny: 2 # blah\n\n[other]\nq: 123\n'; + const out = replaceOrAddIniSections(content, [ + { + section: 'a', + body: 'x: 42\n', + }, + ]); + + expect(out).toContain('x: 42 # comment \n\n[other]'); + }); test('multiple updates for same name: last update wins', async () => { const { replaceOrAddIniSections } = await import('@/server/helpers/file-operations'); const content = ''; diff --git a/src/server/helpers/file-operations.ts b/src/server/helpers/file-operations.ts index 4128967b3..2a06ce75d 100644 --- a/src/server/helpers/file-operations.ts +++ b/src/server/helpers/file-operations.ts @@ -330,9 +330,37 @@ export function replaceOrAddIniSections(content: string, updates: IniUpdate[]): if (newBodyTrimmed === originalBodyTrimmed) { out += originalBodyWithTrailing; } else { + const preserveTrailingInlineComments = (originalBody: string, updatedBody: string) => { + const commentByKey = new Map(); + for (const line of originalBody.split('\n')) { + const match = line.match(/^\s*([^#;\s][^:]*?)\s*:\s*[^\n]*?(\s+[;#].*)$/); + if (!match) continue; + const key = match[1].trim(); + const commentSuffix = match[2]; + if (!commentByKey.has(key)) { + commentByKey.set(key, commentSuffix); + } + } + + return updatedBody + .split('\n') + .map((line) => { + const keyMatch = line.match(/^\s*([^#;\s][^:]*?)\s*:\s*[^\n]*?$/); + if (!keyMatch) return line; + if (line.match(/\s+[;#].*$/)) return line; + const key = keyMatch[1].trim(); + const commentSuffix = commentByKey.get(key); + if (!commentSuffix) return line; + return line + commentSuffix; + }) + .join('\n'); + }; + + const newBodyWithPreservedComments = preserveTrailingInlineComments(originalBodyWithTrailing, newBody); + // Body changed: preserve trailing section spacing from original slice. const originalTrailingNewlines = originalBodyWithTrailing.match(/\n+$/)?.[0].length ?? 0; - out += newBody.replace(/\n+$/, ''); + out += newBodyWithPreservedComments.replace(/\n+$/, ''); if (originalTrailingNewlines > 0) { out += '\n'.repeat(originalTrailingNewlines); } else if (!out.endsWith('\n')) { From a9eb2757c8b4c3533920aaaa4e38a19e4cfc02c5 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 13 Mar 2026 16:56:23 +0000 Subject: [PATCH 05/10] fix(config-gen): ensure that "last saved" files are kept up-to-date --- src/server/routers/printer.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/server/routers/printer.ts b/src/server/routers/printer.ts index 7021c1ef9..7760411aa 100644 --- a/src/server/routers/printer.ts +++ b/src/server/routers/printer.ts @@ -567,6 +567,19 @@ const generateKlipperConfiguration = async ( // At this point we know the file exists. if (file.overwrite) { if (file.exists && file.diskContent === file.content) { + // Ensure that last saved file is up to date. This can happen if the desired content has + // intentionally been read from the current disk content (eg. some of the + // VAOC files do this to achieve "create if it doesn't exist, otherwise keep unchanged" behaviour ). + // If we don't do this, the last saved content can get out of sync with the actual content on disk, + // which can cause confusion later when we compare desired content with last saved content to + // determine if a file has been changed on disk. + if (file.lastSavedContent !== file.content) { + const lastSavedDir = path.dirname(lastSavedPath); + if (!existsSync(lastSavedDir)) { + mkdirSync(lastSavedDir, { recursive: true }); + } + await writeFile(lastSavedPath, file.content); + } return { fileName: file.fileName, action: 'unchanged' }; } // Make a back up. From 49d712ff396773b3f4dbf0221d8c39dc972589a1 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 13 Mar 2026 17:24:57 +0000 Subject: [PATCH 06/10] fix(config-gen): when replacing ini file sections, ensure that all trailing decoration is preserved --- src/__tests__/server.test.ts | 22 ++++++++++++++++++++-- src/server/helpers/file-operations.ts | 8 ++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 2591520a5..1cc682b6d 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -1058,8 +1058,26 @@ describe('server', async () => { const out = replaceOrAddIniSections(content, [{ section: 'section1', body: 'v: new\n' }]); // new body should be present expect(out).toContain('v: new'); - // section2 should still be present - expect(out).toContain('[section2]'); + // comment before section2 should be preserved + expect(out).toContain('# Another comment\n[section2]'); + }); + test('retains trailing comments after replaced sections at end of content', async () => { + const { replaceOrAddIniSections } = await import('@/server/helpers/file-operations'); + const content = '# Start comment\n[section1]\nval: 1\n\n# Another comment\n[section2]\nval: 2\n\n# End comment'; + const out = replaceOrAddIniSections(content, [{ section: 'section2', body: 'v: new\n' }]); + // new body should be present + expect(out).toContain('v: new'); + // comment after section2 should be preserved + expect(out).toContain('v: new\n\n# End comment'); + }); + test('retains trailing comments after unreplaced sections at end of content', async () => { + const { replaceOrAddIniSections } = await import('@/server/helpers/file-operations'); + const content = '# Start comment\n[section1]\nval: 1\n\n# Another comment\n[section2]\nval: 2\n\n# End comment'; + const out = replaceOrAddIniSections(content, [{ section: 'section1', body: 'v: new\n' }]); + // new body should be present + expect(out).toContain('v: new'); + // comment after section 2 should be preserved + expect(out).toContain('val: 2\n\n# End comment'); }); test('idempotent: replacing section with identical content returns identical output', async () => { const { replaceOrAddIniSections } = await import('@/server/helpers/file-operations'); diff --git a/src/server/helpers/file-operations.ts b/src/server/helpers/file-operations.ts index 2a06ce75d..1541f9232 100644 --- a/src/server/helpers/file-operations.ts +++ b/src/server/helpers/file-operations.ts @@ -357,12 +357,12 @@ export function replaceOrAddIniSections(content: string, updates: IniUpdate[]): }; const newBodyWithPreservedComments = preserveTrailingInlineComments(originalBodyWithTrailing, newBody); + const trailingDecoration = + originalBodyWithTrailing.match(/(\n(?:(?:[ \t]*(?:[#;].*)?)\n)*(?:[ \t]*(?:[#;].*)?)?)$/)?.[1] ?? ''; - // Body changed: preserve trailing section spacing from original slice. - const originalTrailingNewlines = originalBodyWithTrailing.match(/\n+$/)?.[0].length ?? 0; out += newBodyWithPreservedComments.replace(/\n+$/, ''); - if (originalTrailingNewlines > 0) { - out += '\n'.repeat(originalTrailingNewlines); + if (trailingDecoration.length > 0) { + out += trailingDecoration; } else if (!out.endsWith('\n')) { out += '\n'; } From 2a03b1cae334925d61c65671ccf76c97e9f5daca Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 13 Mar 2026 18:07:48 +0000 Subject: [PATCH 07/10] fix(config-gen): for VAOC crowsnest.conf updates, only set the `[cam 1]` section - don't set the `[crowsnest]` section as this too easily stomps on user changes, and the `[crowsnest]` section is not strictly a VAOC-specific concern anyhow --- src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts b/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts index f51ad7e0f..4ad0e84dc 100644 --- a/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts +++ b/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts @@ -99,14 +99,6 @@ export function getUpdatedCrowsnestConfigurationForVaoc() { const environment = serverSchema.parse(process.env); const crowsnestPath = path.join(environment.KLIPPER_CONFIG_PATH, 'crowsnest.conf'); return replaceOrAddIniSectionsFromFileSync(crowsnestPath, [ - { - section: 'crowsnest', - body: `log_path: /home/pi/printer_data/logs/crowsnest.log -log_level: verbose -delete_log: false -no_proxy: false -`.trim(), - }, { section: 'cam 1', body: `# Required for Rat Rig VAOC camera integration, DO NOT MODIFY THIS SECTION. From 99a30081198b2b1a942a0804435fa6fd20dcaea2 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 13 Mar 2026 18:20:51 +0000 Subject: [PATCH 08/10] fix(config-gen): use the hardware-specific device link for the VAOC camera - use /dev/RatOS/rr-vaoc-camera instead of /dev/video0. This aims to pick the right camera even if the user has an additional camera installed. --- src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts b/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts index 4ad0e84dc..dcb313d8b 100644 --- a/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts +++ b/src/templates/toolhead-alignment-systems/rat-rig-vaoc.ts @@ -106,7 +106,7 @@ mode: camera-streamer enable_rtsp: false rtsp_port: 8554 port: 8080 -device: /dev/video0 +device: /dev/RatOS/rr-vaoc-camera resolution: 1920x1080 max_fps: 30 `.trim(), From ec6081288d42191245d49aecf718817bbac08cfe Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 14 Mar 2026 15:22:06 +0000 Subject: [PATCH 09/10] fix(config-gen): if crowsnest.conf is modified by config generation, also restart crowsnest before restarting klipper --- src/instrumentation.ts | 9 +++- src/server/helpers/klipper.ts | 85 +++++++++++++++++++++++++++++++++-- src/server/routers/printer.ts | 18 ++++++-- 3 files changed, 103 insertions(+), 9 deletions(-) diff --git a/src/instrumentation.ts b/src/instrumentation.ts index a1fb3f1a6..f8bc94f9f 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -1,3 +1,5 @@ +import type { PermittedServices } from '@/server/helpers/klipper'; + export const register = async () => { if (process.env.NEXT_RUNTIME === 'nodejs') { const { getLogger } = await import('./server/helpers/logger'); @@ -44,7 +46,12 @@ export const register = async () => { }); logger.info('Configuration changed, restarting klipper..'); try { - const restarted = await klipperRestart(); + const servicesToRestart: PermittedServices[] | undefined = res.some( + (r) => r.fileName === 'crowsnest.conf' && (r.action === 'created' || r.action === 'overwritten'), + ) + ? ['crowsnest'] + : undefined; + const restarted = await klipperRestart({ servicesToRestart }); if (restarted) { logger.info('Klipper restarted!'); } else { diff --git a/src/server/helpers/klipper.ts b/src/server/helpers/klipper.ts index b5ec62c52..b488549d7 100644 --- a/src/server/helpers/klipper.ts +++ b/src/server/helpers/klipper.ts @@ -1,9 +1,17 @@ import { getLogger } from '@/app/_helpers/logger'; import { getErrorMessage } from '@/utils/exception-handling'; import { MoonrakerPrinterState, MoonrakerPrinterStateErrorEnum, parseMoonrakerHTTPResponse } from '@/zods/moonraker'; -import { get } from 'http'; import { ZodError } from 'zod'; +/** + * Query Moonraker for the printer's current print state. + * + * This function hits Moonraker's `printer/objects/query?print_stats` endpoint and + * returns the `state` field from `print_stats`. + * + * If Moonraker is offline, failing, or returns unexpected data, this function returns + * `'error'`. + */ export const queryPrinterState = async (): Promise< Zod.output['status']['print_stats']['state'] > => { @@ -27,13 +35,34 @@ export const queryPrinterState = async (): Promise< return 'error'; }; -export const klipperRestart = async (force = false) => { - if (force) { +/** + * Restart Klipper (and optionally other services) via Moonraker. + * + * This helper will first check the printer state (unless `force` is true) and + * will only send a restart request if the printer is in an idle/finished/error + * state. When the printer is actively printing, restart will be skipped. + * + * @param opts.force - If true, skip querying printer state and always attempt restarts. + * @param opts.servicesToRestart - Optional list of permitted services to restart before + * triggering the Klipper restart. Service restarts are only attempted if + * the printer state allows for a restart or if `force` is true. + * @param opts.abortOnServiceRestartFailure - If true, abort the whole restart process + * when any service restart request fails. + * @returns `true` when the klipper restart command was successfully sent, `false` otherwise. + * Note that service restart failures do not affect the return value unless + * `abortOnServiceRestartFailure` is set to true. + */ +export const klipperRestart = async (opts?: { + force?: boolean; + servicesToRestart?: PermittedServices[]; + abortOnServiceRestartFailure?: boolean; +}) => { + if (opts?.force === true) { getLogger().info('Restarting Klipper without checking printer state...'); } else { let state: string | undefined; try { - const state = await queryPrinterState(); + state = await queryPrinterState(); } catch (e) { getLogger().error(`Failed to query printer state before Klipper restart: ${getErrorMessage(e)}`); return false; @@ -45,6 +74,16 @@ export const klipperRestart = async (force = false) => { getLogger().info(`Restarting Klipper, printer is currently in '${state}' state...`); } + if (opts?.servicesToRestart && opts.servicesToRestart.length > 0) { + for (const service of opts.servicesToRestart) { + const restarted = await serviceRestart(service); + if (!restarted && opts?.abortOnServiceRestartFailure === true) { + getLogger().error(`Failed to restart service ${service}, aborting Klipper restart.`); + return false; + } + } + } + try { await fetch('http://localhost:7125/printer/restart', { method: 'POST' }); getLogger().info('Klipper restart command sent successfully.'); @@ -55,3 +94,41 @@ export const klipperRestart = async (force = false) => { return false; }; + +// Keep in sync with the expected content of moonraker.asvc (see ratos-common.sh) +export type PermittedServices = + | 'klipper_mcu' + | 'webcamd' + | 'MoonCord' + | 'KlipperScreen' + | 'moonraker-telegram-bot' + | 'moonraker-obico' + | 'sonar' + | 'crowsnest' + | 'octoeverywhere' + | 'ratos-configurator'; + +/** + * Restart a permitted service through Moonraker. + * + * This simply sends a restart request to Moonraker's service API. + * + * @param service - One of the allowed services defined by `PermittedServices`. + * @returns `true` when the restart request was sent successfully, `false` on error. + */ +export const serviceRestart = async (service: PermittedServices) => { + getLogger().info(`Attempting to restart service ${service} via Moonraker...`); + try { + await fetch('http://localhost:7125/machine/services/restart', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ service }), + }); + getLogger().info(`${service} restart command sent successfully.`); + return true; + } catch (e) { + getLogger().error(`Failed to send ${service} restart command: ${getErrorMessage(e)}`); + } + + return false; +}; diff --git a/src/server/routers/printer.ts b/src/server/routers/printer.ts index 7760411aa..5e0a8f5d6 100644 --- a/src/server/routers/printer.ts +++ b/src/server/routers/printer.ts @@ -76,7 +76,7 @@ import { ToolheadHelper, ToolheadSuffix } from '@/helpers/toolhead'; import { getLastPrinterSettings, hasLastPrinterSettings, savePrinterSettings } from '@/server/helpers/printer-settings'; import { PrinterAxis } from '@/zods/motion'; import { ServerCache, cacheAsyncDirectoryFn } from '@/server/helpers/cache'; -import { klipperRestart } from '@/server/helpers/klipper'; +import { klipperRestart, PermittedServices, serviceRestart } from '@/server/helpers/klipper'; import { access, copyFile, readFile, unlink, writeFile } from 'fs/promises'; import { exec } from 'child_process'; import objectHash from 'object-hash'; @@ -1171,7 +1171,12 @@ export const printerRouter = router({ .mutation(async ({ input }) => { const res = await regenerateKlipperConfiguration(undefined, input.overwriteFiles, input.skipFiles); if (res.some((r) => r.action === 'created' || r.action === 'overwritten')) { - klipperRestart(); + const servicesToRestart: PermittedServices[] | undefined = res.some( + (r) => r.fileName === 'crowsnest.conf' && (r.action === 'created' || r.action === 'overwritten'), + ) + ? ['crowsnest'] + : undefined; + klipperRestart({ servicesToRestart }); } return res; }), @@ -1197,8 +1202,13 @@ export const printerRouter = router({ .mutation(async (ctx) => { const { config: serializedConfig, overwriteFiles, skipFiles } = ctx.input; const config = await deserializePrinterConfiguration(serializedConfig); - const configResult = await generateKlipperConfiguration(config, overwriteFiles, skipFiles); - klipperRestart(); + const configResult = await generateKlipperConfiguration(config, overwriteFiles, skipFiles); + const servicesToRestart: PermittedServices[] | undefined = configResult.some( + (r) => r.fileName === 'crowsnest.conf' && (r.action === 'created' || r.action === 'overwritten'), + ) + ? ['crowsnest'] + : undefined; + klipperRestart({ servicesToRestart }); return configResult; }), flashBeacon: publicProcedure.mutation(async () => { From a882af7bbe4e6fe629b3b030501b22d098fb10ba Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 14 Mar 2026 15:45:19 +0000 Subject: [PATCH 10/10] fix(config-gen): import the correct getLogger in src/server/helpers/klipper.ts --- src/server/helpers/klipper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/helpers/klipper.ts b/src/server/helpers/klipper.ts index b488549d7..32e184e1b 100644 --- a/src/server/helpers/klipper.ts +++ b/src/server/helpers/klipper.ts @@ -1,4 +1,4 @@ -import { getLogger } from '@/app/_helpers/logger'; +import { getLogger } from '@/server/helpers/logger'; import { getErrorMessage } from '@/utils/exception-handling'; import { MoonrakerPrinterState, MoonrakerPrinterStateErrorEnum, parseMoonrakerHTTPResponse } from '@/zods/moonraker'; import { ZodError } from 'zod';