WFDASH-WEBAPP/public/pos/sw.js
Dave 6bedf27617 fix(pos): print the letterhead's own logo, and match delivery note to the sale slip
Logo — three separate faults, all of which had to be fixed for the
letterhead logo to print at all:

1. receipt.html hardcoded '/wfdash-logo.png' for the sale slip, so it
   printed the app's bundled mark (the solid black W block) and never
   looked at the letterhead at all.
2. The paths that DID use the letterhead (printTest, cashup) passed
   PocketBase's fully-qualified cross-origin URL into imageToRaster(),
   which draws to a <canvas> — getImageData() taints and throws on a
   cross-origin image, so the .catch(() => null) silently dropped the
   logo. pos-letterhead-service now returns a same-origin
   /api/v1/files/... URL (per AGENTS.md's file-URL rule).
3. letterhead_settings wasn't whitelisted in that file proxy, so the
   same-origin URL would have 403'd.

Verified against the live WESTSCOTT letterhead: its SVG rasterizes to
384x137 at ~15% ink coverage (line art), no tainted canvas.

Also fixes the company name never printing: every builder read
`letterhead.name`, but the POS letterhead record exposes
`display_name` — so the name line silently rendered empty on every
slip, on screen and on paper.

Delivery note now matches the sale slip, per request: gains the logo
and business_info blocks and the same 2x-size title, instead of being
a stripped-down variant. This also reconciles the transport
divergence flagged earlier (Bluetooth baked business info into its
header; the network builder omitted it entirely) — it is now one
reorderable business_info block on both.

resolveBlocks() now slots a newly-shipped block in at its intended
position rather than appending it to the end. Without this, any
letterhead with an already-saved layout would have printed the new
logo/business_info blocks BELOW the signature line.
2026-09-06 23:03:24 +02:00

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-v25";
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;
})
);
});