2026-08-30 23:41:54 +00:00
|
|
|
// Service worker — cache app shell, network-first for HTML + theme.js, cache-first for other static.
|
feat(pos): tiered catalog search — word order, plurals, glued SKUs
The till filtered the catalog with one case-insensitive includes() over
sku + description, so word order decided whether you got anything:
"nuts m20" returned 0 hits while "m20 nuts" returned 1. The filter also
checked sku_normalised, brand and search_text — fields that exist on
price_list but not on a stock_items row — so three of its five clauses
could never fire, and name was never searched at all.
Search moves into public/pos/pos-search.js, unit-tested from
src/lib/pos-search.test.ts. Every tier is a strict superset of the old
filter, so nothing that used to match stops matching.
exact whole query as a run
squish punctuation/spacing removed, so M20NUT finds M20 Hex Nut
tokens every word present in any order
plural "nuts" also tries "nut"
glued "M20NUT" -> M20 AND NUT on letter/digit boundaries
Measured on the live 3,591-row catalog: "bolts" 0 -> 884 (884 real bolt
rows, and no row literally says "bolts"), "nuts m20" 0 -> 12, "nut hex"
0 -> 13, "washers flat" 0 -> 14, M20NUT 1 -> 12, zero lost matches.
A plural widens to exactly the union with its singular, asserted per pair.
Widening is safe because both step-2 features are shaped as supersets:
plural forms are alternatives inside one clause, while split parts are
separate required clauses. Making split parts alternatives instead
reduces "M20NUT" to "has M20" — 242 rows. Edit-distance fuzzy matching
was measured and rejected: "nuts" 15 -> 172, "m-20-n-u-t" -> 2,481.
Also fixes three defects the live data caught, all reachable from a code
review alone:
- split kept the generic fragment, promoting "20" from "m20nut" to a
required clause; a leading 1-2 letter run now merges into its digits
- the split group dropped the other tokens, so "M20NUT WASHER" matched
rows containing no washer; other tokens are now carried along
- a punctuation-only query ("-", "---") tokenised to nothing and threw,
which would have blanked the catalog at the till
The index is built once per catalog load on both paths (IndexedDB and
network), keeping derived strings off the items so the cache stays lean.
0.21-2.98 ms per keystroke, so no debounce is needed. filteredProducts()
falls back to the old substring filter if PosSearch is missing, since a
skewed service-worker cache would otherwise blank a shop's catalog.
sw.js v28 -> v30: the new script must be in the app shell, and the second
bump is required because pos-search.js is served cache-first and step 2
edited it in place.
2026-10-06 21:36:12 +00:00
|
|
|
// v30 — step 2 of the catalog search work: pos-search.js gains plural
|
|
|
|
|
// handling ("nuts" also finds "nut", so "bolts" goes from 0 hits to the 884
|
|
|
|
|
// real bolt rows) and glued-token splitting ("M20NUT" finds "M20 Hex Nut").
|
|
|
|
|
// Bumped again even though the shell already contained pos-search.js: that
|
|
|
|
|
// file is served cache-first, so without a new CACHE name tills would keep
|
|
|
|
|
// running the step-1 build of it indefinitely.
|
|
|
|
|
// Measured on the live catalog: "nuts m20" 0 → 12, "washers flat" 0 → 14,
|
|
|
|
|
// with nothing that used to match ceasing to match.
|
feat(pos): refund a whole sale, put the goods back, print a credit slip
A customer who brings goods back needs the sale reversed: the items go
back into stock and the money comes back off the till. There was no way
to mark a POS sale as refunded, so nothing stopped the same sale being
refunded twice — which credits the customer twice and puts the same
stock back on the shelf twice.
Refunds are WHOLE-SALE only by operator decision; no partial refunds.
That is what lets `pos_sales.refunded` be a boolean instead of a
per-line refund table. Three columns: `refunded`, `refunded_at`,
`refund_amount`, stamped from the sale's post-discount total, which is
what the customer actually paid.
DOUBLE-REFUND GUARD, twice over, because this is the one failure that
costs real money:
- `refunded` is read before anything else, and a flagged sale returns
409 with no stock writes at all
- the flag is stamped BEFORE any stock write, so a second concurrent
tap sees it and bails. Writing stock first and flagging last would
let both taps through.
A stock write that fails part-way does NOT roll the flag back. The
customer has their money either way, so re-running would double-credit
them; the failed lines come back in `stockErrors` for a manual count
correction, which is strictly better than a second refund.
Stock needs no schema: `return` was already in STOCK_REASON_VALUES,
just never used. Rows are written with POSITIVE change_qty and
reference_type "pos_sale_refund", leaving the original
customer_fulfillment rows untouched so the sale's own audit trail still
shows what was originally rung up.
cashup.html had expected drawer = sum of cash sales, so a cash refund
would have shown as a false variance on every cash-up after it. It now
subtracts cash refunds, and refunded sales contribute nothing to
takings for any tender.
The tender is read off the sale, never off the request — an earlier
version accepted it in the body, which would have let a caller assert
that a cash sale was really an EFT and talk the drawer out of balance.
At the till: tap still reprints, hold a sale row for 600ms to refund —
moving money sits behind a gesture you cannot do by accident. Refunded
sales stay in the list, dimmed and badged, because deleting them would
destroy the audit trail. The sheet names the items and says plainly what
happens next ("Hand back R10.57 in cash. The drawer count for this day
will drop by that amount.").
The credit slip reuses the sale renderer and the whole printer path with
`refund: true`, so there is no second printer to configure — but note
there were THREE render paths to fix (on-screen, Bluetooth text, ePOS
XML), and missing the third would have printed "TOTAL" on a credit note.
Migration applied to live PB and verified idempotent; field shapes
round-tripped on a real sale and restored. 10 new tests, 148 POS tests
pass, tsc clean, build passes. sw.js v34 -> v35 (history, cashup,
receipt and epos-print are all cache-first in the shell).
2026-10-07 21:41:09 +00:00
|
|
|
// v35 — sale refunds. Hold a sale row in history.html to refund the whole
|
|
|
|
|
// sale: the items go back into stock (stock_ledger reason "return", the
|
|
|
|
|
// reason that already existed unused) and a credit slip prints reusing the
|
|
|
|
|
// sale printer path with refund:true. Refunds are whole-sale only, so
|
|
|
|
|
// pos_sales.refunded is a boolean and the server refuses a second one with
|
|
|
|
|
// 409. cashup.html subtracts cash refunds from the expected drawer —
|
|
|
|
|
// without that, every cash-up after a refund shows a false variance.
|
|
|
|
|
// Bumped because history.html, cashup.html, receipt.html and epos-print.js
|
|
|
|
|
// are all served cache-first out of the shell.
|
feat(pos): customer picker matches the company name, and stops hiding truncation
The account-sale picker filtered on three customer columns — name,
number, email. `company` on a customer row is a RELATION id, not a
name, so it could never be text-matched; the company name was only ever
resolved afterwards by `expand` for display. Typing "Sibanye" returned
nothing even though 52 people work there.
- the filter now traverses the relation: `company.company ~ q`.
Verified live: "Sibanye - Rustenburg" 0 → 52, "Adal" 0 → 2,
"Alano" 0 → 1, while "Botha" and "Smit" are unchanged at 6 and 4.
- each row carries `viaCompany`, and name matches sort above
company-only matches. PB's own sort is by name across the whole
result set, which would interleave the two bands.
- /customers returns { items, total, shown } and the till renders a
count header: "52 matches", or "Showing 100 of 122 — keep typing to
narrow". The old `slice(0, 8)` showed 8 of 52 people with nothing
to suggest 44 were missing; 15 companies have more staff than that.
The picker click handler bound to every `div` in the results box, so
adding a count header would have made it selectable and picked a
customer with an empty id. Now scoped to `.cust-row`.
Two bugs the tests caught rather than the code:
- the display cap was first set to 50 while its own comment claimed
it sat above the largest company — which has 52 staff. That
silently re-created the truncation this change removes. Now 100,
with a test asserting a 52-person company comes back whole.
- PocketBase has no escape for a literal backslash: `\'` is
accepted, but one, two or three backslashes all fail to parse and
500 the request, so typing `\` in the search box errored. No
escaping survives, so backslashes are stripped before the filter
(`M20\Nut` searches `M20Nut`) and a backslash-only query returns
nothing rather than matching every customer.
13 new tests cover the company clause, the 52-staff case, the cap,
ranking order, backslash stripping and the empty-after-strip case.
Verified live against PocketBase across 10 queries — the ranking
guarantee held on every one.
sw.js v33 -> v34: pos-app.js and pos.html are cache-first in the shell.
2026-10-07 13:53:23 +00:00
|
|
|
// v34 — escapePbValue now drops backslashes rather than escaping them:
|
|
|
|
|
// PB has no backslash escape in filters (1, 2 or 3 all throw), so a raw one
|
|
|
|
|
// 500s the request. v33 already removed the price field from the edit sheet.
|
|
|
|
|
// v34 — the account-sale customer picker now matches the COMPANY name as
|
|
|
|
|
// well as the person's name/number/email, so typing "Sibanye" returns that
|
|
|
|
|
// mine's staff instead of nothing. /api/v1/pos/customers also returns a
|
|
|
|
|
// real `total` now: the old `slice(0, 8)` silently showed 8 of 52 people
|
|
|
|
|
// at the largest company and read as the whole team, so the till renders
|
|
|
|
|
// "showing N of M" and says so when the list is cut. Name matches rank
|
|
|
|
|
// above company-only matches so an OR'd filter can't bury the person the
|
|
|
|
|
// cashier typed. escapePbValue also escapes backslashes now — a single
|
|
|
|
|
// "\" in the search box used to throw and 500 the request.
|
fix(pos): make cart quantity editable, and fix the sheet that hid it
Ringing up 800 of something means typing 800, and the only route to the
quantity field was a 600ms long-press with no affordance anywhere on
screen. The quantity itself — the thing a cashier reaches for — was a
plain <div class="v">, so it rendered inert.
- the quantity is now a real button that opens the Edit line sheet,
with an aria-label and a 44px touch target (it was 30px, too narrow
to even show "800")
- holding +/- no longer pops the sheet open mid-tap: the pointerdown
handler bails for .qty/.rm before starting the timer
- the sheet is quantity-only. The Unit price field is gone on request;
a mis-keyed price is expensive and easy to miss. The per-line
override it fed is removed with it — pos-service.ts only ever read
unit_price, the "price edited" label and the sale record's `edited`
flag were unreachable, and nothing else could set an override,
including a cart reload (persistCart saves only {id, qty, p})
The discount trigger moved out of its own 54px footer row to ride
inside the Subtotal row, so it costs no vertical height at all while
staying visible — a discount control that appears from nowhere is one
nobody finds under pressure. The applied-discount red totals row
remains the always-on indicator.
Fixes a pre-existing bug found while testing: long-pressing a cart row
showed a grey screen with no dialog. #editSheet is a SIBLING of
#editBackdrop, but the CSS revealed it with
`.edit-backdrop.show .edit-sheet` — a descendant selector that can
never match a sibling — so the sheet sat at opacity:0 while the
backdrop went opaque. Nothing threw and the build was green, which is
why it survived; it is also why manual quantity entry looked missing.
The sheet now has its own z-index above the backdrop and its own .show
rule. They stay siblings on purpose: nesting the sheet inside the
backdrop would make every tap on Save bubble to the backdrop's
onclick and close the dialog.
Verified in jsdom against the real markup: old CSS reproduces the grey
screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix
clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS
tests pass, including 23 new ones asserting the tap target exists, the
long-press guard fires before the timer, the sheet stacks above its
backdrop, and the popup stays quantity-only.
sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out
of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
|
|
|
// v33 — the Edit line sheet is quantity-only. The Unit price field is gone
|
|
|
|
|
// on request: the till sells at the catalogue shelf price, and a mis-keyed
|
|
|
|
|
// price is expensive and easy to miss. The per-line price override it fed is
|
|
|
|
|
// removed with it — the server only ever read `unit_price`, and the
|
|
|
|
|
// "price edited" label plus the sale record's `edited` flag are now
|
|
|
|
|
// unreachable. Nothing else could set an override, including a cart reload.
|
|
|
|
|
// v32 — fix the Edit line sheet, which never actually displayed. #editSheet
|
|
|
|
|
// is a sibling of #editBackdrop, but the CSS revealed it with
|
|
|
|
|
// `.edit-backdrop.show .edit-sheet`, a descendant selector that can never
|
|
|
|
|
// match a sibling — so the sheet sat at opacity:0 while the backdrop went
|
|
|
|
|
// opaque. Long-pressing a cart row produced a grey screen with no dialog.
|
|
|
|
|
// The sheet now carries its own z-index above the backdrop and its own
|
|
|
|
|
// .show rule. This is why manual quantity entry looked missing: the control
|
|
|
|
|
// existed and worked, it was just rendering underneath its own backdrop.
|
|
|
|
|
// Bumped because pos.html is served cache-first out of the shell.
|
|
|
|
|
// v31 — cart quantity is now directly editable: the number in each cart line
|
|
|
|
|
// is a real button that opens the existing Edit line sheet (previously only
|
|
|
|
|
// reachable by an undiscoverable 600ms long-press), and holding +/- no
|
|
|
|
|
// longer pops that sheet open mid-tap. The discount trigger moved out of its
|
|
|
|
|
// own 54px footer row to ride inside the Subtotal row, so it costs no
|
|
|
|
|
// vertical height while staying visible.
|
|
|
|
|
// Bumped because pos-app.js and pos.html are both served cache-first out of
|
|
|
|
|
// the shell; without a new CACHE name the tills keep the old ones.
|
fix(pos): dead discount toggle + stale discount figure, lift dark palette
Two reported symptoms that turned out to be one bug, plus the dark
mode the user described as "too black".
The `hidden` attribute does nothing on the discount controls
`hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`.
Any author rule that sets `display` outranks it — and `.totals` and
`.discount-body` both set `display:flex`. So `el.hidden = true` was a
no-op on both:
- the discount tier panel could not be collapsed, making "Add
discount" look like a dead control;
- the Discount totals row stayed on screen after the discount was
cleared, still showing the last figure (R 98.37) under a total
that no longer contained it. The JS only ever set `hidden` and
never cleared the text, so the operator had no way to tell stale
text from a live one.
`.cat-menu` and `.cat-backdrop` already carried per-element
`[hidden]{display:none}` overrides for exactly this reason; the two
discount elements were missed. Replaced with one global rule so the
whole class is covered rather than relying on each element to
remember. The clear path now also blanks the amount.
Verified statically rather than in a browser: jsdom does not model
the UA-vs-author cascade and reported the broken and fixed cases
identically, so the cascade was checked by resolving which rules
apply. Regression test fails without the fix.
Dark palette
The POS canvas was #0f0f0f (L* 4.3 — effectively black) with
surfaces only 3-4 L* above it, mixing a warm #1c1416 against a
neutral #181818 in the same UI. That near-black base plus two
competing surface tones is what read as muddy. Replaced with a
single cool-neutral ramp with four real steps, and menus/modals now
sit on a `--raised` layer instead of reusing the surface colour.
`#717171` was the secondary text colour in 25 places and measured
3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1.
Both the text greys and the surfaces were hardcoded per component
across seven POS pages; they are now tokens, with the dark block
rebinding them.
The shared `.dark` block in themes.css (drives all 24 themes) sat at
oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its
shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still
above AA. Cursor/Supabase, which override it darker, were at L* 1.3
and lifted too.
Service worker cache bumped to pos-v36 — pos.html and pos-app.js are
served cache-first, so without the bump the tills keep serving the
broken files.
3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
|
|
|
// v36 — the discount panel and the Discount totals row both carry a `hidden`
|
|
|
|
|
// attribute, but their classes (.discount-body, .totals) set `display:flex`,
|
|
|
|
|
// and an author display rule outranks the UA stylesheet's
|
|
|
|
|
// `[hidden]{display:none}`. So `hidden` did nothing: "Add discount" could not
|
|
|
|
|
// collapse the tier buttons, and a cleared discount left its old amount
|
|
|
|
|
// painted under an unchanged total. Fixed with a global `[hidden]` override
|
|
|
|
|
// plus blanking the figure when the row is hidden. Also carries the dark
|
|
|
|
|
// palette lift.
|
2026-10-30 21:33:11 +00:00
|
|
|
// v37: pos-app.js accepts a comma as the decimal point (custom item price, cash given).
|
2026-11-06 00:18:34 +00:00
|
|
|
// v38: the catalogue tile shows the stock quantity. current_qty was already
|
|
|
|
|
// coming down the wire — the till just never drew it. The number is shown
|
|
|
|
|
// as it is (negatives included) and the tile is tinted when the quantity is
|
|
|
|
|
// not positive. Nothing is blocked: the item still sells.
|
|
|
|
|
// Bumped because pos-app.js and pos.html are both served cache-first out of
|
|
|
|
|
// the shell; without a new CACHE name the tills keep the old ones.
|
fix(pos): put the stock quantity under the price, not in the sub line
The quantity went into the .sub line, which is nowrap + ellipsis: a
28- or 38-character SKU truncated it, so the number fell off the right
edge and pushed the in-cart count out with it. The sub line is back to
what it was — sku, brand, unit, N in cart — and the quantity now sits
under the price, right-aligned, where nothing truncates it.
The POS tests asserted the file's prose rather than its expressions, so
five mutations of the implementation all passed them: a min_qty cue, a
lost zero boundary, a clamp, a round and the segment deleted. They now
pin the expressions, the interpolation, the class wiring and the dark
rule's colour value. All seven mutations tried are killed.
A non-numeric current_qty rendered "NaN in stock" and was not badged —
NaN <= 0 is false. It now falls back to 0 and badges.
Bumped the service-worker cache: pos-app.js and pos.html are both
served cache-first out of the shell, so without a new CACHE name the
tills keep the old ones.
2026-11-06 02:53:37 +00:00
|
|
|
// v39: the quantity moved out of the .sub line and under the price —
|
|
|
|
|
// the sub line truncates on a long SKU, which pushed the number and the
|
|
|
|
|
// in-cart count off the right edge. The sub line is back to what it was.
|
|
|
|
|
// Bumped because pos-app.js and pos.html are both served cache-first out of
|
|
|
|
|
// the shell; without a new CACHE name the tills keep the old ones.
|
|
|
|
|
const CACHE = "pos-v39";
|
2026-08-30 23:41:54 +00:00
|
|
|
const APP_SHELL = [
|
|
|
|
|
'./',
|
|
|
|
|
'./index.html',
|
|
|
|
|
'./pos.html',
|
|
|
|
|
'./receipt.html',
|
|
|
|
|
'./history.html',
|
|
|
|
|
'./cashup.html',
|
2026-09-04 07:13:53 +00:00
|
|
|
'./settings.html',
|
2026-09-04 11:42:01 +00:00
|
|
|
'./slip-layout.html',
|
2026-08-30 23:48:49 +00:00
|
|
|
'./pos-config.js',
|
feat(pos): tiered catalog search — word order, plurals, glued SKUs
The till filtered the catalog with one case-insensitive includes() over
sku + description, so word order decided whether you got anything:
"nuts m20" returned 0 hits while "m20 nuts" returned 1. The filter also
checked sku_normalised, brand and search_text — fields that exist on
price_list but not on a stock_items row — so three of its five clauses
could never fire, and name was never searched at all.
Search moves into public/pos/pos-search.js, unit-tested from
src/lib/pos-search.test.ts. Every tier is a strict superset of the old
filter, so nothing that used to match stops matching.
exact whole query as a run
squish punctuation/spacing removed, so M20NUT finds M20 Hex Nut
tokens every word present in any order
plural "nuts" also tries "nut"
glued "M20NUT" -> M20 AND NUT on letter/digit boundaries
Measured on the live 3,591-row catalog: "bolts" 0 -> 884 (884 real bolt
rows, and no row literally says "bolts"), "nuts m20" 0 -> 12, "nut hex"
0 -> 13, "washers flat" 0 -> 14, M20NUT 1 -> 12, zero lost matches.
A plural widens to exactly the union with its singular, asserted per pair.
Widening is safe because both step-2 features are shaped as supersets:
plural forms are alternatives inside one clause, while split parts are
separate required clauses. Making split parts alternatives instead
reduces "M20NUT" to "has M20" — 242 rows. Edit-distance fuzzy matching
was measured and rejected: "nuts" 15 -> 172, "m-20-n-u-t" -> 2,481.
Also fixes three defects the live data caught, all reachable from a code
review alone:
- split kept the generic fragment, promoting "20" from "m20nut" to a
required clause; a leading 1-2 letter run now merges into its digits
- the split group dropped the other tokens, so "M20NUT WASHER" matched
rows containing no washer; other tokens are now carried along
- a punctuation-only query ("-", "---") tokenised to nothing and threw,
which would have blanked the catalog at the till
The index is built once per catalog load on both paths (IndexedDB and
network), keeping derived strings off the items so the cache stays lean.
0.21-2.98 ms per keystroke, so no debounce is needed. filteredProducts()
falls back to the old substring filter if PosSearch is missing, since a
skewed service-worker cache would otherwise blank a shop's catalog.
sw.js v28 -> v30: the new script must be in the app shell, and the second
bump is required because pos-search.js is served cache-first and step 2
edited it in place.
2026-10-06 21:36:12 +00:00
|
|
|
'./pos-search.js',
|
2026-08-30 23:41:54 +00:00
|
|
|
'./pos-app.js',
|
2026-09-04 07:13:53 +00:00
|
|
|
'./letterhead-config.js',
|
2026-09-04 11:25:31 +00:00
|
|
|
'./slip-layout.js',
|
2026-09-03 15:54:48 +00:00
|
|
|
'./epos-print.js',
|
2026-08-30 23:41:54 +00:00
|
|
|
'./theme.js',
|
2026-08-31 14:40:02 +00:00
|
|
|
'./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',
|
2026-08-30 23:41:54 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-08-30 23:48:49 +00:00
|
|
|
// 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;
|
2026-08-30 23:41:54 +00:00
|
|
|
// 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;
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
});
|