From cb7bcff93e6088ae1ef253a3a23ccf4818b943bc Mon Sep 17 00:00:00 2001 From: jessikitty Date: Mon, 7 Sep 2026 14:50:34 +1000 Subject: [PATCH] Add patch script to wire printer.js to the native QL module Replaces the brother_ql subprocess call in printBadge with printPng, and swaps available() for a real reachability check. Uses brace matching rather than literal body matching so it survives formatting differences, refuses to run if the anchors are missing, is idempotent, and writes a .bak first. The badge layout is untouched. --- scripts/wire-native-printing.mjs | 171 +++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 scripts/wire-native-printing.mjs diff --git a/scripts/wire-native-printing.mjs b/scripts/wire-native-printing.mjs new file mode 100644 index 0000000..6da3318 --- /dev/null +++ b/scripts/wire-native-printing.mjs @@ -0,0 +1,171 @@ +#!/usr/bin/env node +/** + * One-shot patch: point printer.js at the native QL protocol module instead of + * shelling out to the Python brother_ql CLI. + * + * node scripts/wire-native-printing.mjs # patch + * node scripts/wire-native-printing.mjs --check # report only, change nothing + * + * Safe to run twice — it detects an already-patched file and stops. + * Writes printer.js.bak next to the original before touching anything. + * + * Only the imports and the two functions at the bottom change. The badge + * layout (drawBadge / renderBadgePng) is left exactly as it is. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const target = path.resolve(here, '..', 'src', 'printer.js'); +const checkOnly = process.argv.includes('--check'); + +function fail(message) { + console.error(`\n FAILED: ${message}\n`); + console.error(' Nothing was changed. Patch printer.js by hand instead.\n'); + process.exit(1); +} + +/** + * Find a top-level function and return its full text by matching braces. + * Formatting-independent, unlike matching the whole body literally. + */ +function extractFunction(source, signature) { + const start = source.indexOf(signature); + if (start === -1) return null; + + let depth = 0; + let i = source.indexOf('{', start); + if (i === -1) return null; + + for (; i < source.length; i++) { + if (source[i] === '{') depth++; + else if (source[i] === '}') { + depth--; + if (depth === 0) { + return { start, end: i + 1, text: source.slice(start, i + 1) }; + } + } + } + return null; +} + +if (!fs.existsSync(target)) fail(`${target} does not exist. Run this from the repo root.`); + +let source = fs.readFileSync(target, 'utf8'); +const original = source; + +if (source.includes("from './printing/ql-print.js'")) { + console.log('\n Already patched — printer.js imports the native module. Nothing to do.\n'); + process.exit(0); +} + +const changes = []; + +/* -- 1. Import the native module ---------------------------------- */ + +const importAnchor = "import { photoAbsolutePath } from './photos.js';"; +if (!source.includes(importAnchor)) fail('could not find the photos.js import'); + +source = source.replace( + importAnchor, + `${importAnchor}\nimport { printPng, printerHealth } from './printing/ql-print.js';` +); +changes.push('added the ql-print import'); + +/* -- 2. Replace printBadge ---------------------------------------- */ + +const printBadge = extractFunction(source, 'export async function printBadge(visit, site)'); +if (!printBadge) fail('could not locate printBadge'); +if (!printBadge.text.includes('runBrotherQl')) { + fail('printBadge does not call runBrotherQl — it may already have been changed'); +} + +const newPrintBadge = `export async function printBadge(visit, site) { + if (!isConfigured(site)) throw new Error('Server printing is not turned on for this site.'); + + const png = await renderBadgePng(visit, site); + const port = Number(site.printer_port) || 9100; + const target = \`tcp://\${site.printer_host}:\${port}\`; + + // The roll decides whether the job is two-colour, not the accent setting. + // A DK-22251 roll refuses a monochrome job even when the badge has no red + // on it, so labelFor() is passed straight through and ql-print maps it. + const result = await printPng(png, { + host: site.printer_host, + port, + label: labelFor(site), + }); + + note(site.id, result.ok, result.message); + if (!result.ok) throw new Error(result.message); + + // confirmed is false when the printer accepted the bytes but never said the + // label came out. Over the network that is the normal case, not a fault. + return { ok: true, confirmed: Boolean(result.confirmed), target }; +}`; + +source = + source.slice(0, printBadge.start) + newPrintBadge + source.slice(printBadge.end); +changes.push('printBadge now uses printPng'); + +/* -- 3. Replace available ----------------------------------------- */ + +const available = extractFunction(source, 'export function available()'); +if (available) { + const newAvailable = `export function available() { + // Kept for callers that only ask "can this server print at all?". The + // external brother_ql binary is no longer involved, so the answer is always + // yes; use health(site) to ask about a specific printer. + return Promise.resolve(true); +} + +/** + * Reachability and, where the printer will say, roll state. + * + * ready is tri-state: true, false, or null meaning "reachable but it won't + * tell us". Null is the normal answer over the network — show it as unknown + * in the admin console rather than green or red. + */ +export function health(site) { + return printerHealth({ + host: site?.printer_host, + port: Number(site?.printer_port) || 9100, + label: labelFor(site), + }); +}`; + + source = + source.slice(0, available.start) + newAvailable + source.slice(available.end); + changes.push('available() no longer probes for a binary; added health(site)'); +} else { + console.warn(' note: available() not found, skipping that step'); +} + +/* -- 4. Flag the now-dead subprocess code ------------------------- */ + +for (const name of ['function explainPrintError', 'function runBrotherQl']) { + if (source.includes(name)) { + changes.push(`${name.split(' ')[1]}() is now unused and can be deleted`); + } +} + +/* -- Report / write ----------------------------------------------- */ + +console.log('\n Changes to src/printer.js:'); +for (const c of changes) console.log(` - ${c}`); + +if (checkOnly) { + console.log('\n --check given, nothing written.\n'); + process.exit(0); +} + +if (source === original) fail('nothing actually changed'); + +fs.writeFileSync(`${target}.bak`, original); +fs.writeFileSync(target, source); + +console.log(`\n Wrote ${target}`); +console.log(` Backup at ${target}.bak`); +console.log('\n Next: docker compose build && docker compose up -d\n');