diff --git a/AGENTS.md b/AGENTS.md index 98b6bc9..67a84ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,13 +118,13 @@ npm run docker:run # docker run -p 3000:3000 ... - **⚠️ אילוץ קריטי — רינדור מחדש של טאב לא-פעיל**: `updUI()` (שרץ מ-`schedUI()`/`procSA`/`visibilitychange` וכו') קורא ל-`refreshCurrentTab()` ולא ל-`renderSB()` ישירות — `refreshCurrentTab()` בודק את `currentTab` ומעדכן רק את `#sbC` של הטאב הפעיל בפועל. **לעולם אל תקראו ל-`renderSB()`/`renderHistoryTab()` בלי בדיקת `currentTab` קודם** — קריאה לא-מותנית כותבת על `#sbC` את תוכן האזעקות/היסטוריה גם כשמשתמש צופה בטאב אחר (זה בדיוק הבאג שתוקן: מעבר טאב בדפדפן החזיר את התצוגה לאזעקות במקום להישאר בטאב האחרון). - **⚠️ אילוץ קריטי — שדות חיפוש חיים (`#fQ`, `#hQ`)**: `renderSB()`/`renderHistoryTab()` מחליפים את כל ה-`innerHTML` של `#sbC`, כולל את שדה החיפוש עצמו. כל שינוי בפונקציות האלה **חייב** לשמר `document.activeElement`/`selectionStart`/`selectionEnd` לפני ה-render ולשחזר אותם (`focus()`+`setSelectionRange()`) אחריו, אחרת כל הקשה במקלדת מוציאה את הפוקוס מהשדה (המשתמש לא יכול להקליד ברצף). `renderHistoryTab()` גם מדלג על ה-skeleton loader כשהטאב כבר מרונדר (רק בטעינה ראשונה), ומגן מפני race condition עם `historySeq`. - **⚠️ אילוץ קריטי — `sDB()` לא שומר `type`**: רשומות ב-IndexedDB (`sDB()`) שומרות רק `typeKey` (מחרוזת), לא את אובייקט `type` המלא (icon/css/color) ש-`rItem()`/`addMrk()` דורשים. `renderHistoryTab()` משחזר אותו ידנית (`a.type=TM[a.typeKey]||TM.rockets`) אחרי `gDB()` — **זה היה באג אמיתי וארוך-טווח**: לפני התיקון, כל ביקור בטאב היסטוריה עם נתוני IndexedDB אמיתיים קרס על `Cannot read properties of undefined (reading 'icon')` ותקוע על ה-skeleton loader לנצח. אם מוסיפים עוד מקום שקורא רשומות גולמיות מ-`gDB()`/IndexedDB — לבדוק את זה שוב. יש טסט E2E ל-regression הזה (`test/e2e.js`, זורע רשומה ידנית ב-IndexedDB בפורמט המדויק של `sDB()`). -- **מפה**: Leaflet, כל שכבות הבסיס דרך Esri (`server.arcgisonline.com`, ללא מפתח API בשום מצב — hostname אחד ל-CSP `img-src`/SW tile-cache). מצב רגיל (בהיר/כהה): `Canvas/World_{Dark,Light}_Gray_Base` + שכבת שמות/גבולות נפרדת `Canvas/World_{Dark,Light}_Gray_Reference` (`basemapRef`, `pane:'labelsPane'`, תמיד דלוקה — לא ניתנת לכיבוי) — **עד 2026-08 היה זה CartoDB** (`basemaps.cartocdn.com`); הוחלף כי CARTO דרשו מפתח API והתחילו למרוח "API KEY REQUIRED" על כל אריח. שכבת לוויין חינמית — Esri World Imagery — דרך כפתור 🛰️ ב-`.mc` (`tglSat()`); מעליה אפשר להציג/להסתיר שכבת שמות מקומות נפרדת (`Reference/World_Boundaries_and_Places`, גם היא ב-`labelsPane`) דרך כפתור 🏷️ (`satLblB`/`tglSatLabels()`, מוצג רק כש-`satOn` — זו כן ניתנת לכיבוי, בשונה מ-`basemapRef`). -- **PWA**: SW מקודד בתוך `server.js` (משתנה `SW`), מטמון `red-alert-v12` + מטמון אריחים נפרד `red-alert-tiles-v1` (Leaflet basemap כולל לוויין ושכבת שמות, cache-first). שינוי ל-SW דורש bump של `CN` ב-server.js. +- **מפה**: Leaflet, כל שכבות הבסיס דרך Esri (`server.arcgisonline.com`, ללא מפתח API בשום מצב — hostname אחד ל-CSP `img-src`/SW tile-cache). מצב רגיל (בהיר/כהה): `Canvas/World_{Dark,Light}_Gray_Base` + שכבת שמות/גבולות נפרדת `Canvas/World_{Dark,Light}_Gray_Reference` (`basemapRef`, `pane:'labelsPane'`, תמיד דלוקה — לא ניתנת לכיבוי) — **עד 2026-08 היה זה CartoDB** (`basemaps.cartocdn.com`); הוחלף כי CARTO דרשו מפתח API והתחילו למרוח "API KEY REQUIRED" על כל אריח. **חשוב**: ל-Canvas layers (base+reference) אין כיסוי אמיתי מעבר לזום 16 ברוב האזורים (בניגוד ל-World_Imagery) — `tileOpts()` קובע `maxNativeZoom:16` כדי ש-Leaflet יגדיל (upscale) את האריח האחרון במקום לבקש אריחים לא-קיימים/ריקים. שכבת לוויין חינמית — Esri World Imagery — דרך כפתור 🛰️ ב-`.mc` (`tglSat()`); מעליה אפשר להציג/להסתיר שכבת שמות מקומות נפרדת (`Reference/World_Boundaries_and_Places`, גם היא ב-`labelsPane`) דרך כפתור 🏷️ (`satLblB`/`tglSatLabels()`, מוצג רק כש-`satOn` — זו כן ניתנת לכיבוי, בשונה מ-`basemapRef`). +- **PWA**: SW מקודד בתוך `server.js` (משתנה `SW`), מטמון `red-alert-v17` + מטמון אריחים נפרד `red-alert-tiles-v2` (Leaflet basemap כולל לוויין ושכבת שמות, cache-first). שינוי ל-SW דורש bump של `CN` ב-server.js. **שים לב**: `TILE` הוא מטמון נפרד שה-`activate` handler *לעולם לא* מנקה (כדי ששטחים שכבר נצפו יעבדו אופליין) — אם משנים URL/ספק של אריחים, חובה ל-bump גם את `TILE`, אחרת אריחים ישנים/פגומים נשארים במטמון של משתמשים קיימים לצמיתות. v1.6.1 (מעבר CARTO→Esri) פספס את זה; תוקן ב-v1.6.2. ## איך להוסיף תכונה / לשנות קוד 1. **שינויים בלוגיקת השרת** — `server.js` ערוך ישירות. אין hot reload — `node server.js` מחדש. -2. **שינויים בקליינט** — `index.html` ערוך ישירות. השרת מזהה את שינוי ה-mtime ומגיש את הגרסה החדשה (refresh בדפדפן). זכור ש-Service Worker עלול להגיש cached גרסה — חשוב ל-bump את `CN` (כרגע `red-alert-v12`) ב-`server.js` (משתנה `SW`) כדי להפעיל invalidate, או לפתוח DevTools → Application → Service Workers → Unregister. +2. **שינויים בקליינט** — `index.html` ערוך ישירות. השרת מזהה את שינוי ה-mtime ומגיש את הגרסה החדשה (refresh בדפדפן). זכור ש-Service Worker עלול להגיש cached גרסה — חשוב ל-bump את `CN` (כרגע `red-alert-v17`) ב-`server.js` (משתנה `SW`) כדי להפעיל invalidate, או לפתוח DevTools → Application → Service Workers → Unregister. אם שינית URL/ספק של אריחי מפה — bump גם את `TILE` (כרגע `red-alert-tiles-v2`), אחרת אריחים ישנים נשארים במטמון הנפרד לצמיתות (ה-`activate` handler לא מנקה אותו). 3. **הוספת עיר** — ערוך את `CITIES` ב-[lib.js](lib.js). פורמט: `"שם":{lat:X,lng:Y,r:"אזור",s:זמן_מיגון}`. 4. **הוספת שפה** — הוסף ערך ל-`LN`, ל-`LANG_META` (שם native + דגל) ול-`TTS_LOCALE` (קוד BCP-47) ב-[lib.js](lib.js) — כולל **כל** המפתחות שקיימים ב-`LN.en` (test/unit.js/הקוד לא בודקים זאת אוטומטית, אבל חוסר מפתח נופל חזרה ל-Hebrew בשקט). `` ב-index.html נבנה אוטומטית מ-`LANG_META` דרך `initLangSelect()` — אין לערוך אותו ידנית. 5. **endpoint חדש** — הוסף `if (p === '/api/...')` ב-`server.js` ל-pipeline הקיים בתוך `http.createServer`. תזכור `track(p, code)` ו-`gz(req, res, body, ct)`. @@ -210,6 +210,8 @@ npm run docker:run # docker run -p 3000:3000 ... - **בדיקת `node test/unit.js` כושלת על `health fetch error`** — הפורט תפוס, או `server.js` לא קיים. הבדיקה משתמשת בפורט 3001-4000 רנדומלי. - **PWA לא מתעדכן** — DevTools → Application → Service Workers → Unregister, או bump של `CN` ב-`SW` בתוך server.js. - **אזעקות כפולות** — הדפדפן פתוח ב-2 טאבים; כל אחד מקבל SSE עצמאי. תקין. +- **המפה מטושטשת בזום עמוק מאוד (light/dark)** — תקין; `maxNativeZoom:16` על שכבות ה-Esri Canvas גורם ל-Leaflet להגדיל (upscale) את האריח האחרון מעבר לזום 16 במקום לבקש אריחים לא-קיימים. מרקרים/מקלטים לא מושפעים (הם overlay וקטורי, לא חלק מהאריח). +- **אריחי מפה ישנים/שבורים ממשיכים להופיע אצל משתמש ספציפי אחרי תיקון בקוד** — כנראה שכחת לבמפ את `TILE` (לא רק `CN`) ב-server.js; `TILE` הוא מטמון SW נפרד שלעולם לא מתנקה אוטומטית. ## אזהרות בטיחות diff --git a/index.html b/index.html index c69145a..0b9991e 100644 --- a/index.html +++ b/index.html @@ -284,7 +284,7 @@ function reportClientError(message,stack){if(clientErrSent>=5)return;clientErrSent++;try{fetch(`${API}/api/client-error`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:String(message||'').slice(0,500),stack:String(stack||'').slice(0,2000),url:location.href})}).catch(()=>{})}catch{}} window.addEventListener('error',e=>reportClientError(e.message,e.error?.stack)); window.addEventListener('unhandledrejection',e=>reportClientError('unhandledrejection: '+String(e.reason?.message||e.reason),e.reason?.stack)); -const APP_VERSION='1.6.1'; // bump alongside package.json; drives the "what's new" popup in checkWhatsNew() +const APP_VERSION='1.6.2'; // bump alongside package.json; drives the "what's new" popup in checkWhatsNew() // Donation links — rendered as a row of buttons everywhere the donate card/mini appears. Empty array hides them. const DONATE_LINKS=[ {n:'Ko-fi',i:'☕',url:'https://ko-fi.com/idanlights',cls:'kofi'}, @@ -387,13 +387,17 @@ let satLabelsOn=JSON.parse(localStorage.getItem('alertmap-sat-labels')||'true'); let tileLabels=null,basemapRef=null; function curTileUrl(){return satOn?SAT_URL:(theme==='light'?LIGHT_BASE_URL:DARK_BASE_URL)} +// maxNativeZoom caps the Canvas base+reference layers' actual tile requests at their real coverage +// depth (~16 in most regions, unlike World_Imagery) — beyond that Leaflet upscales the last tile +// instead of requesting nonexistent/blank ones. +function tileOpts(){return satOn?{maxZoom:19,attribution:'Esri'}:{maxZoom:19,maxNativeZoom:16,attribution:'Esri'}} // Adds/removes the labels overlay + shows/hides its toggle button, matching current satOn/satLabelsOn. function syncSatLabels(){const btn=document.getElementById('satLblB');if(btn)btn.style.display=satOn?'':'none';if(!map)return;if(satOn&&satLabelsOn){if(!tileLabels){tileLabels=L.tileLayer(SAT_LABELS_URL,{maxZoom:19,pane:'labelsPane'})}if(!map.hasLayer(tileLabels))tileLabels.addTo(map)}else if(tileLabels&&map.hasLayer(tileLabels)){map.removeLayer(tileLabels)}} function tglSatLabels(){satLabelsOn=!satLabelsOn;localStorage.setItem('alertmap-sat-labels',JSON.stringify(satLabelsOn));document.getElementById('satLblB')?.classList.toggle('active',satLabelsOn);syncSatLabels()} // Light/dark basemap's place-name/road/border overlay — always on (not user-toggleable, unlike the // optional satellite labels) since it replaces what used to be baked directly into the CartoDB tiles. -function syncBasemapRef(){if(!map)return;if(basemapRef){map.removeLayer(basemapRef);basemapRef=null}if(!satOn)basemapRef=L.tileLayer(theme==='light'?LIGHT_REF_URL:DARK_REF_URL,{maxZoom:19,pane:'labelsPane'}).addTo(map)} -function tglSat(){satOn=!satOn;localStorage.setItem('alertmap-sat',JSON.stringify(satOn));document.getElementById('satB')?.classList.toggle('active',satOn);if(!map||!tileL)return;map.removeLayer(tileL);tileL=L.tileLayer(curTileUrl(),{maxZoom:19,attribution:'Esri'}).addTo(map);syncSatLabels();syncBasemapRef()} +function syncBasemapRef(){if(!map)return;if(basemapRef){map.removeLayer(basemapRef);basemapRef=null}if(!satOn)basemapRef=L.tileLayer(theme==='light'?LIGHT_REF_URL:DARK_REF_URL,{maxZoom:19,maxNativeZoom:16,pane:'labelsPane'}).addTo(map)} +function tglSat(){satOn=!satOn;localStorage.setItem('alertmap-sat',JSON.stringify(satOn));document.getElementById('satB')?.classList.toggle('active',satOn);if(!map||!tileL)return;map.removeLayer(tileL);tileL=L.tileLayer(curTileUrl(),tileOpts()).addTo(map);syncSatLabels();syncBasemapRef()} let flt={r:'',t:'',time:'all',q:''},lastFly=0,actx=null,lastS=0,uiP=false,tlP=false; let uLoc=null,uMark=null,favs=JSON.parse(localStorage.getItem('alertmap-favs')||'[]'); let dndOn=JSON.parse(localStorage.getItem('alertmap-dnd')||'false'); @@ -421,11 +425,11 @@ let theme=URL_PARAMS.get('theme')||localStorage.getItem('alertmap-theme'); if(theme!=='light'&&theme!=='dark')theme=null; if(!theme){const mq=window.matchMedia&&window.matchMedia('(prefers-color-scheme:dark)').matches;const h=new Date().getHours();theme=(mq||(h>=21||h<6))?'dark':'light'} -function applyTh(th){theme=th;document.documentElement.classList.toggle('light',th==='light');document.getElementById('themeB').textContent=th==='light'?'☀️':'🌙';localStorage.setItem('alertmap-theme',th);if(tileL&&map&&!satOn){map.removeLayer(tileL);tileL=L.tileLayer(curTileUrl(),{maxZoom:19,attribution:'Esri'}).addTo(map);syncBasemapRef()}schedTL();syncURL()} +function applyTh(th){theme=th;document.documentElement.classList.toggle('light',th==='light');document.getElementById('themeB').textContent=th==='light'?'☀️':'🌙';localStorage.setItem('alertmap-theme',th);if(tileL&&map&&!satOn){map.removeLayer(tileL);tileL=L.tileLayer(curTileUrl(),tileOpts()).addTo(map);syncBasemapRef()}schedTL();syncURL()} function tglTheme(){const next=theme==='dark'?'light':'dark';if(document.startViewTransition){document.startViewTransition(()=>applyTh(next))}else{applyTh(next)}} function tglHC(){hcOn=!hcOn;localStorage.setItem('alertmap-hc',JSON.stringify(hcOn));document.documentElement.classList.toggle('high-contrast',hcOn);document.getElementById('hcT').checked=hcOn} -function initMap(){map=L.map('map',{center:[31.5,34.8],zoom:8,zoomControl:false,attributionControl:false});map.createPane('labelsPane');const lp=map.getPane('labelsPane');lp.style.zIndex=450;lp.style.pointerEvents='none';tileL=L.tileLayer(curTileUrl(),{maxZoom:19,attribution:'Esri'}).addTo(map);syncBasemapRef();if(satOn){document.getElementById('satB')?.classList.add('active');document.getElementById('satLblB')?.classList.toggle('active',satLabelsOn);syncSatLabels()}L.control.zoom({position:'bottomleft'}).addTo(map);clsG=L.markerClusterGroup({chunkedLoading:true,maxClusterRadius:50,disableClusteringAtZoom:12});if(clsOn)map.addLayer(clsG);updCls()} +function initMap(){map=L.map('map',{center:[31.5,34.8],zoom:8,zoomControl:false,attributionControl:false});map.createPane('labelsPane');const lp=map.getPane('labelsPane');lp.style.zIndex=450;lp.style.pointerEvents='none';tileL=L.tileLayer(curTileUrl(),tileOpts()).addTo(map);syncBasemapRef();if(satOn){document.getElementById('satB')?.classList.add('active');document.getElementById('satLblB')?.classList.toggle('active',satLabelsOn);syncSatLabels()}L.control.zoom({position:'bottomleft'}).addTo(map);clsG=L.markerClusterGroup({chunkedLoading:true,maxClusterRadius:50,disableClusteringAtZoom:12});if(clsOn)map.addLayer(clsG);updCls()} function tglCls(){clsOn=!clsOn;localStorage.setItem('alertmap-cls',JSON.stringify(clsOn));updCls();if(!clsG)return;if(clsOn){map.addLayer(clsG);mrk.forEach((m,id)=>{if(!act.has(id)){if(map.hasLayer(m))map.removeLayer(m);clsG.addLayer(m)}})}else{clsG.eachLayer(l=>map.addLayer(l));clsG.clearLayers();map.removeLayer(clsG)}} function updCls(){document.getElementById('clsB').classList.toggle('active',clsOn)} @@ -632,6 +636,7 @@ // Updates tab — mirrors CHANGELOG.md's newest entries (update this array by hand on release, // same as the CHANGELOG.md file; kept short on purpose, full history lives in the repo). const CHANGES=[ + {v:'1.6.2',d:'2026-09-02',items:['🐛 תוקן: v1.6.1 החליף את ספק אריחי המפה (CARTO ← Esri) אבל לא ניקה את מטמון האריחים הנפרד ב-Service Worker — מבקרים שראו את ה-watermark "API KEY REQUIRED" לפני התיקון המשיכו לקבל אותם אריחים ישנים מהמטמון גם אחריו. גם נוקה קוד מת (allowlist ישן ל-CARTO ב-CSP/SW) ונוסף גבול זום לשכבות ה-Canvas כדי שזום עמוק מאוד לא יבקש אריחים לא-קיימים']}, {v:'1.6.1',d:'2026-08-30',items:['🐛 קריטי: המפה הרגילה (לא-לוויין) הראתה "API KEY REQUIRED" על כל אריח — הספק הקודם (CartoDB) התחיל לדרוש מפתח API; הוחלף בשכבת Esri חינמית וללא מפתח (בהיר וכהה)','🐛 תוקן: רק לטאב "אזעקות" הייתה תווית טקסט גלויה — שאר 4 הטאבים היו אייקון בלבד; עכשיו לכולם יש תווית גלויה ומתורגמת מתחת לאייקון','🖼️ תוקן: הבאנרים הממותגים של v1.5.0/v1.6.0 עדיין הציגו את מספרי הגרסה הישנים (v3.5.0/v3.6.0) בתמונה עצמה']}, {v:'1.6.0',d:'2026-08-30',items:['🐛 קריטי: תוקן קריסה ארוכת-טווח בטאב היסטוריה — רשומות מ-IndexedDB חסרו שדה שהתצוגה דרשה, והטאב נתקע על טעינה לכל משתמש עם היסטוריה אמיתית שמורה','🗺️ מקלטים אמיתיים נוספו לירושלים ולחיפה (בנוסף לתל אביב), עם קישורי ניווט Waze/Google Maps מכל מקלט על המפה','▶ Replay בטאב היסטוריה — צפייה כרונולוגית באזעקות ישירות על המפה','🔐 2FA אופציונלי לאדמין (קוד TOTP) + יומן ביקורת התחברויות','🌍 ייצוא GeoJSON/KML להיסטוריה, פאג\'ינציה אמיתית ל-API, מדיניות שמירת היסטוריה ניתנת להגדרה','🌐 זיהוי שפה אוטומטי מהדפדפן בביקור ראשון (במקום נפילה ישירה לעברית)','🔴 באדג׳ מספר אזעקות פעילות על אייקון האפליקציה, הודעת התקנה ל-iPhone/iPad','🧪 בדיקות E2E בדפדפן אמיתי (Playwright) בנוסף ליחידה ואינטגרציה','📉 דיווח שגיאות קליינט לשרת, ותרשים ארכיטקטורה חדש ב-README']}, {v:'1.5.0',d:'2026-08-29',items:['💛 3 אפשרויות תרומה במקום אחת: Ko-fi, Buy Me a Coffee ו-Patreon (חשבון חדש) — מוצגות כשורת כפתורים בכל מקום שהיה כפתור תרומה יחיד','🖼️ צילום מסך של האפליקציה נוסף ל-README ולכל release חדש ב-GitHub']}, diff --git a/package.json b/package.json index 674b79e..79e4362 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tzafir", - "version": "1.6.1", + "version": "1.6.2", "description": "צפיר (Tzafir) — Real-time alert monitoring based on IDF Home Front Command (Pikud HaOref). Formerly israel-alert-map.", "main": "server.js", "scripts": { diff --git a/server.js b/server.js index b42eea3..3de53c5 100644 --- a/server.js +++ b/server.js @@ -65,8 +65,10 @@ function secHeaders(res, html, allowFrame) { if (!allowFrame) res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); // CSP frame-ancestors *: required for /embed.js use cases; X-Frame-Options is the old-school equivalent - // img-src also allows Esri World Imagery (free, no API key) — the satellite basemap layer - if (html) res.setHeader('Content-Security-Policy', `default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://server.arcgisonline.com; connect-src 'self'; frame-ancestors ${allowFrame ? '*' : "'self'"}`); + // img-src allows server.arcgisonline.com — Esri's free, no-API-key tile services back every basemap + // layer (light/dark Canvas base+reference, World Imagery satellite, boundary labels). CARTO's + // basemaps.cartocdn.com was dropped in v1.6.1 (started requiring a paid API key); removed here too. + if (html) res.setHeader('Content-Security-Policy', `default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data: blob: https://server.arcgisonline.com; connect-src 'self'; frame-ancestors ${allowFrame ? '*' : "'self'"}`); } // ── Metrics ────────────────────────────────────────────────── @@ -315,10 +317,15 @@ function getLib() { const p = path.join(__dirname, 'lib.js'); try { const s = fs // ── PWA ───────────────────────────────────────────────────── const MANIFEST = JSON.stringify({ name: 'צפיר', short_name: 'צפיר', description: 'ניטור התרעות פיקוד העורף בזמן אמת', start_url: '/', display: 'standalone', background_color: '#0a0e17', theme_color: '#ef4444', orientation: 'any', lang: 'he', dir: 'rtl', icons: [{ src: '/icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any maskable' }] }, null, 2); -// NOTE: bump CN whenever the client (index.html) or SW logic changes, otherwise users keep cached version +// NOTE: bump CN whenever the client (index.html) or SW logic changes, otherwise users keep cached version. +// TILE is a *separate* cache the 'activate' handler deliberately never clears (so previously-viewed +// map areas keep working offline) — bump TILE too whenever the tile provider/URLs change, otherwise +// stale/bad cached tile images linger forever. v1.6.1 swapped CARTO for Esri but missed this: TILE +// was left at v1, so anyone who'd visited before that fix kept their watermarked CARTO tiles cached +// indefinitely. Bumping it now actually clears them. const SW = ` -const CN='red-alert-v16'; -const TILE='red-alert-tiles-v1'; +const CN='red-alert-v17'; +const TILE='red-alert-tiles-v2'; const AS=['/','/index.html','/lib.js','/manifest.json','/icon.svg']; const CDN=['https://unpkg.com/leaflet@1.9.4/dist/leaflet.css','https://unpkg.com/leaflet@1.9.4/dist/leaflet.js','https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css','https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css','https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js']; self.addEventListener('install',e=>{e.waitUntil((async()=>{const c=await caches.open(CN);await Promise.allSettled(AS.map(u=>c.add(u)));Promise.allSettled(CDN.map(u=>c.add(new Request(u,{mode:'no-cors'}))))})());self.skipWaiting()}); @@ -326,8 +333,8 @@ self.addEventListener('activate',e=>{e.waitUntil(caches.keys().then(ks=>Promise. self.addEventListener('fetch',e=>{ const u=new URL(e.request.url); if(e.request.method!=='GET'||u.pathname.startsWith('/api/'))return; - // Map basemap tiles (incl. satellite) → cache-first so previously-viewed areas work offline (capped ~500 tiles) - if(u.hostname.endsWith('basemaps.cartocdn.com')||u.hostname==='server.arcgisonline.com'){ + // Map basemap tiles (light/dark base+reference/satellite/labels, all Esri) → cache-first so previously-viewed areas work offline (capped ~500 tiles) + if(u.hostname==='server.arcgisonline.com'){ e.respondWith((async()=>{ const c=await caches.open(TILE); const hit=await c.match(e.request);