The button has never worked. cashup.html wires it with an inline
onclick="printCashup()", but the page's whole script is IIFE-wrapped, so
printCashup was never a global — the click threw "ReferenceError:
printCashup is not defined" into the console and the button silently did
nothing. No alert, no dialog, no feedback.
Dead since 46d5d54 swapped the button from onclick="window.print()" (a
real global, so it worked) to the ePOS-Print path. receipt.html hit this
exact bug earlier and fixed it by exporting its handlers to window — with
a comment noting the buttons "all silently do nothing". cashup.html never
got the same line. It does now.
On the network transport the cash-up slip already mirrored the sale slip
(resolve 'cashup' blocks -> letterheadLogoRaster at 384 -> printViaNetwork),
so the export is the whole fix for a till on a network printer.
Also here:
- 20c and 10c added to the drawer count, with the total rounded to whole
cents. Neither is exact in binary floating point, so 3x10c would
otherwise have summed to 0.30000000000000004 and printed a variance
that reads as a rounding bug on the slip.
- The button restored its label with textContent, which dropped its
printer <svg> permanently on first use. Restores innerHTML instead.
- The Bluetooth branch quietly called window.print() (OS dialog, not the
thermal printer). getSettings() DEFAULTS to bluetooth, so any till with
cleared settings would land there and look dead all over again. It now
fails loudly, matching EposPrint.printTest()'s stance. No ESC/POS
cash-up renderer yet — only the sale slip speaks Bluetooth.
pos-app.js had the same defect in four more places: the edit-line modal's
Save / Cancel / Remove and the customer "Remove" chip are all inline
onclick handlers declared inside its IIFE, and only openEditLine was
exported — so the modal could be opened but not dismissed or committed.
Confirmed by parsing the file (one top-level statement, no top-level
function declarations), fixed with four additive export lines.
src/lib/pos-inline-handlers.test.ts guards the class across every POS
page: any identifier called from an inline on* attribute must be a
builtin, a namespaced call, or exported on window. It was red on exactly
these bugs and is green now.
SW cache bumped to pos-v26 so the tills pick the files up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
2.7 KiB
JavaScript
85 lines
2.7 KiB
JavaScript
// Service worker — cache app shell, network-first for HTML + theme.js, cache-first for other static.
|
|
const CACHE = "pos-v26";
|
|
const APP_SHELL = [
|
|
'./',
|
|
'./index.html',
|
|
'./pos.html',
|
|
'./receipt.html',
|
|
'./history.html',
|
|
'./cashup.html',
|
|
'./settings.html',
|
|
'./slip-layout.html',
|
|
'./pos-config.js',
|
|
'./pos-app.js',
|
|
'./letterhead-config.js',
|
|
'./slip-layout.js',
|
|
'./epos-print.js',
|
|
'./theme.js',
|
|
'./manifest.json',
|
|
// Font Awesome 6 Free (solid) — bundled so the till still renders icons offline.
|
|
'./fa/all.min.css',
|
|
'./fa/webfonts/fa-solid-900.woff2',
|
|
];
|
|
|
|
// Files that must always be fresh (network-first, fallback to cache)
|
|
const NETWORK_FIRST = new Set([
|
|
'theme.js'
|
|
]);
|
|
|
|
self.addEventListener('install', e => {
|
|
e.waitUntil(caches.open(CACHE).then(c => c.addAll(APP_SHELL)).then(()=>self.skipWaiting()));
|
|
});
|
|
|
|
self.addEventListener('activate', e => {
|
|
e.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))).then(()=>self.clients.claim()));
|
|
});
|
|
|
|
self.addEventListener('fetch', e => {
|
|
const url = new URL(e.request.url);
|
|
// Never cache API responses — only the static app shell. Serving a
|
|
// stale /catalog or, worse, a cached /checkout would be actively wrong.
|
|
if(url.pathname.startsWith('/api/')) return;
|
|
// HTML navigations: always network-first so the user gets the latest page
|
|
if(e.request.mode === 'navigate'){
|
|
e.respondWith(
|
|
fetch(e.request).then(res => {
|
|
if(res && res.ok) {
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then(c => c.put(e.request, clone));
|
|
}
|
|
return res;
|
|
}).catch(() => caches.match(e.request).then(r => r || new Response('<h1>Offline</h1>', {status:503, headers:{'Content-Type':'text/html'}})))
|
|
);
|
|
return;
|
|
}
|
|
if(e.request.method !== 'GET') return;
|
|
|
|
// theme.js (and other network-first assets) always go to network
|
|
const fname = url.pathname.split('/').pop();
|
|
if(NETWORK_FIRST.has(fname)){
|
|
e.respondWith(
|
|
fetch(e.request).then(res => {
|
|
if(res && res.ok){
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then(c => c.put(e.request, clone));
|
|
}
|
|
return res;
|
|
}).catch(() => caches.match(e.request).then(r => r || new Response('', {status:504})))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Default: cache-first for static assets
|
|
e.respondWith(
|
|
caches.match(e.request).then(cached => {
|
|
const fetchP = fetch(e.request).then(res => {
|
|
if(res && res.ok && (e.request.url.startsWith('http'))) {
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then(c => c.put(e.request, clone));
|
|
}
|
|
return res;
|
|
}).catch(() => cached);
|
|
return cached || fetchP;
|
|
})
|
|
);
|
|
});
|