diff --git a/frontend/static/script/js/core/receipt-designer.js b/frontend/static/script/js/core/receipt-designer.js
index 561c2e31c..22ed87f83 100644
--- a/frontend/static/script/js/core/receipt-designer.js
+++ b/frontend/static/script/js/core/receipt-designer.js
@@ -415,7 +415,7 @@
if (boundary) text += '\n';
}
walk(receipt);
- text = text.replace(/₹/g, 'Rs. ').replace(/×/g, 'x').replace(/−/g, '-').replace(/…/g, '...').replace(/\u00a0/g, ' ').replace(/\u00b7/g, '-');
+ text = text.replace(/\u00a3/g, 'GBP ').replace(/\u20ac/g, 'EUR ').replace(/₹/g, 'Rs. ').replace(/×/g, 'x').replace(/−/g, '-').replace(/…/g, '...').replace(/\u00a0/g, ' ').replace(/\u00b7/g, '-');
if (/[^\x09\x0a\x0d\x20-\x7e]/.test(text)) return null;
var lines = text.split(/\r?\n/).map(function (line) { return line.replace(/\s+/g, ' ').trim().replace(/ \|$/, ''); }).filter(Boolean);
return lines.length ? lines : null;
diff --git a/src/windows-print-queue.js b/src/windows-print-queue.js
index e93ef3e82..e4234eb01 100644
--- a/src/windows-print-queue.js
+++ b/src/windows-print-queue.js
@@ -262,13 +262,13 @@ class WindowsPrintQueue {
if (job.accepted && this.live.has(job.id) && !job.lastJobError && !reason) return this.transition(job, 'sent');
return this.transition(job, 'failed', 'Print outcome unknown; check the printer before requesting a duplicate');
}
- // An idle initialization must finish before a receipt is submitted.
+ // Optional idle traffic must not hold a ready printer for 30 seconds.
+ // The same per-printer writer and Windows queue preserve submission order.
+ // Keep failed/offline jobs blocked by readiness; suppress further idle writes
+ // when an old pulse is still pending rather than adding more reset jobs.
const pulse = this.keepAlive[job.printer.toLowerCase()];
if (pulse?.pending && this.pulsePending(pulse, health)) {
- if (this.now() - pulse.at < 30000) return this.transition(job, 'waiting', pulse.status);
- // Do not bypass an actual failed Windows job: readiness still applies.
- // Optional ESC @ traffic alone cannot permanently starve real receipts.
- pulse.pending = false; pulse.suppressed = true;
+ pulse.suppressed = true;
this.write('keep-alive.json', this.keepAlive);
}
if (job.state === 'failed') {
@@ -276,7 +276,10 @@ class WindowsPrintQueue {
job.reconnect = false; job.retries = 0; job.nextAt = 0;
}
if (job.nextAt > this.now()) return;
- const canWake = !reason || (health.present === true && reason === 'Printer offline');
+ // Ready printers receive the receipt immediately: enqueue already prefixes ESC @.
+ // A separate wake job can remain visible in Windows for 30 seconds.
+ // Only an actually offline device needs the recovery initialization.
+ const canWake = health.present === true && reason === 'Printer offline';
if (canWake && job.binding.initialize !== false && this.transport.initialize) {
if (!job.wakeDocument) {
job.wakeDocument = 'Posnic-initialize-' + job.id;
@@ -300,11 +303,7 @@ class WindowsPrintQueue {
reason = readiness(health, job.binding);
if (reason) return this.defer(job, reason);
}
- if (health.jobs.some(item => item.document === job.wakeDocument)) {
- if (job.wakeStartedAt == null) { job.wakeStartedAt = this.now(); this.write(job.id + '.json', job); }
- if (this.now() - job.wakeStartedAt < 30000) return this.transition(job, 'waiting', 'Waiting for printer initialization');
- // Receipt already contains ESC @ in the same job; no new wake is sent.
- }
+
}
if (reason) return this.defer(job, reason, /offline|disconnected|Printer error|failed job|USB discovery failed|could not be identified|Multiple connected USB|Saved USB device is absent|Printer status unknown/i.test(reason));
// Persist BEFORE crossing the spooler boundary. A crash in the following
diff --git a/tests/receipt-designer.test.js b/tests/receipt-designer.test.js
index 5064d2071..083832c22 100644
--- a/tests/receipt-designer.test.js
+++ b/tests/receipt-designer.test.js
@@ -1502,3 +1502,14 @@ test('hiding payment method removes cash and card without hiding totals or balan
}
dom.window.close();
});
+
+test('GBP and EUR receipts stay on Fast receipt without raster fallback', () => {
+ const {dom, engine} = setup();
+ try {
+ const lines = engine.fastLines('Total \u00a31.50
Cash \u00a32.00
Change \u00a30.50
Total \u20ac3.00
');
+ assert.ok(lines);
+ const bytes = require('../src/receipt-fast').renderFastReceipt(lines, '80').toString('ascii');
+ assert.match(bytes, /Total GBP 1.50/); assert.match(bytes, /Cash GBP 2.00/);
+ assert.match(bytes, /Change GBP 0.50/); assert.match(bytes, /Total EUR 3.00/);
+ } finally { dom.window.close(); }
+});
diff --git a/tests/tools/receipt-cold-paint-proof.cjs b/tests/tools/receipt-cold-paint-proof.cjs
index 739184d9e..9b0297630 100644
--- a/tests/tools/receipt-cold-paint-proof.cjs
+++ b/tests/tools/receipt-cold-paint-proof.cjs
@@ -15,6 +15,7 @@ app.whenReady().then(async () => {
const body = Array.from({ length: 12 }, (_, i) => `
`).join('');
const document = `Synthetic receipt total${body}`;
for (let attempt = 0; attempt < 3; attempt++) {
+ const started = performance.now();
const strips = await rasterize({ document, paper, width });
assert.equal(strips.length, 12);
for (let index = 0; index < strips.length; index++) {
@@ -23,7 +24,7 @@ app.whenReady().then(async () => {
for (let x = 0; x < width / 8; x++) assert.equal(data[row * width / 8 + x], x < (index + 1) * 2 ? 255 : 0, `paper=${paper},attempt=${attempt},strip=${index},row=${row},byte=${x}`);
}
}
- results.push({ paper, attempt, strips: strips.length });
+ results.push({ paper, attempt, strips: strips.length, preparationMs: Math.round(performance.now() - started) });
}
}
fs.writeFileSync(output, JSON.stringify({ passed: results.length, results }, null, 2));
diff --git a/tests/windows-print-queue.test.js b/tests/windows-print-queue.test.js
index 209490275..9c3730b6b 100644
--- a/tests/windows-print-queue.test.js
+++ b/tests/windows-print-queue.test.js
@@ -37,10 +37,10 @@ function rig(t) {
tick: async (ms = 15000, q = queue) => { now += ms; await q.tick(); }, hold: () => { drain = false; } };
}
-test('online: initialization has no paper feed and receipt bytes/cut are preserved', async t => {
+test('online: receipt initializes in the same job without a separate wake delay', async t => {
const r = rig(t); const result = await r.send();
assert.equal(result.status, 'Sent to printer'); assert.equal(result.success, true);
- assert.equal(r.initialized.length, 1);
+ assert.equal(r.initialized.length, 0);
assert.deepEqual([...r.calls[0].bytes], [27, 64, 65, 10, 29, 86, 0]);
assert.equal(result.spoolerJobId, 1);
assert.ok(r.events.some(e => e.status === 'Queued'));
@@ -157,12 +157,16 @@ test('unknown USB presence and permission denied fail visibly without elevation'
assert.equal(r.calls.length, 0);
});
-test('retained initialization job blocks receipt and initialization does not repeat', async t => {
- const r = rig(t);
- r.transport.initialize = async job => { r.initialized.push(job); r.health.jobs.push({ id: 5, document: job.document, error: false, status: 'Spooling' }); return { success: true }; };
- assert.equal((await r.send()).status, 'Waiting'); await r.tick();
- assert.equal(r.initialized.length, 1); assert.equal(r.calls.length, 0);
- r.health.jobs = []; await r.tick(); assert.equal(r.calls.length, 1);
+test('retained wake job does not delay a ready receipt or create another wake', async t => {
+ const r = rig(t); r.health.printerStatus = 7;
+ r.transport.initialize = async job => {
+ r.initialized.push(job); r.health.printerStatus = 3;
+ r.health.jobs.push({ id: 5, document: job.document, error: false, status: 'Spooling' });
+ return { success: true };
+ };
+ assert.equal((await r.send()).status, 'Sent to printer');
+ assert.equal(r.initialized.length, 1); assert.equal(r.calls.length, 1);
+ await r.tick(); assert.equal(r.calls.length, 1);
});
test('offline status with a present device can be initialized before readiness is rechecked', async t => {
@@ -400,7 +404,7 @@ test('missing legacy payload is not automatically recreated or replayed', async
});
test('startup failure is diagnosed accurately and pre-submission retries are bounded', async t => {
- const r=rig(t);let attempts=0;
+ const r=rig(t);r.health.printerStatus=7;let attempts=0;
r.transport.initialize=async()=>{attempts++;return {success:false,unavailable:true,submission:'not-submitted',error:'Print helper startup failed: READY deadline exceeded'};};
await r.send();for(let i=0;i<6;i++)await r.tick(60000);
assert.equal(attempts,4);assert.equal(r.calls.length,0);
@@ -411,13 +415,14 @@ test('startup failure is diagnosed accurately and pre-submission retries are bou
test('controlled legacy recovery preserves a successful Reception copy and Kitchen identity', async t => {
const r=rig(t);await r.send('reception','Reception');
const submit=r.transport.submit;
+ r.health.printerStatus=7;
r.transport.initialize=async()=>({success:false,unavailable:true,error:'Print helper startup failed'});
await r.send('kitchen','Kitchen_New');
const job=r.queue.jobs.get(idFor('kitchen'));
job.state='failed';job.reason='Printer initialization failed; check Windows queue';delete job.nonSubmissionVerified;
r.queue.write(job.id+'.json',job);
r.queue.stop();const restored=r.make();
- r.transport.initialize=async()=>({success:true});r.transport.submit=submit;
+ r.health.printerStatus=3; r.transport.initialize=async()=>({success:true});r.transport.submit=submit;
const before={...job.binding};
const result=await restored.recoverUnsubmitted(job.id);
assert.equal(result.success,true);assert.equal(result.jobId,job.id);
@@ -443,13 +448,13 @@ test('failure before receipt submission is retried once helper recovers; failure
});
test('optional retained initialization cannot starve healthy receipts indefinitely',async t=>{
const r=rig(t);r.transport.initialize=async job=>{r.initialized.push(job);r.health.jobs.push({id:99,document:job.document,status:'Spooling',error:false});return {success:true};};
- await r.send();assert.equal(r.calls.length,0);await r.tick(30001);
- assert.equal(r.calls.length,1);assert.equal(r.initialized.length,1);
+ await r.send();assert.equal(r.calls.length,1);assert.equal(r.initialized.length,0);
+ await r.tick(0);assert.equal(r.calls.length,1);
});
test('unknown idle pulse suppresses further optional traffic but not healthy receipts',async t=>{
const r=rig(t);r.queue.keepAlive.kitchen_printer={at:0,pending:true,document:'Posnic-idle-old',status:'unknown'};
- await r.send();assert.equal(r.calls.length,0);await r.tick(30001);
- assert.equal(r.calls.length,1);assert.equal(r.queue.keepAlive.kitchen_printer.suppressed,true);
+ await r.send();assert.equal(r.calls.length,1);
+ assert.equal(r.queue.keepAlive.kitchen_printer.suppressed,true);
});
test('a failed Windows queue job still blocks receipts after optional wake deadline',async t=>{
const r=rig(t);r.queue.keepAlive.kitchen_printer={at:0,pending:true,document:'Posnic-idle-old',status:'unknown'};