diff -u -r -x _headers deploy_v120b/site/app.js deploy_v121b/site/app.js --- deploy_v120b/site/app.js 2026-11-16 00:46:28.626737882 +0900 +++ deploy_v121b/site/app.js 2026-11-16 02:07:09.622600115 +0900 @@ -1,4 +1,4 @@ -import { APP_URL, MODES, EXAMPLES, LABELS, MIN_ITEMS, MAX_ITEMS, parseDecimal, compareMany, formatMoney, resultPrecision, percentLabel, headline, summaryText, isSampleRemaining } from './core.js'; +import { APP_URL, MODES, EXAMPLES, LABELS, MIN_ITEMS, MAX_ITEMS, parseDecimal, compareMany, formatMoney, resultPrecision, percentLabel, headline, summaryText, isSampleRemaining } from './core.js?v=1.2.1'; const $ = id => document.getElementById(id), form = $('compare-form'), box = $('products'), tpl = $('item-template'); const NAMES = ['ひとつめ', 'ふたつめ', 'みっつめ', 'よっつめ']; const FIELDS = ['price', 'amount', 'unit', 'packs']; diff -u -r -x _headers deploy_v120b/site/core.js deploy_v121b/site/core.js --- deploy_v120b/site/core.js 2026-11-16 00:46:28.628712265 +0900 +++ deploy_v121b/site/core.js 2026-11-16 02:07:09.622637609 +0900 @@ -1,4 +1,4 @@ -export const VERSION = '1.2.0'; +export const VERSION = '1.2.1'; export const APP_URL = 'https://ailab-otoku.pages.dev/'; export const LABELS = Object.freeze(['A', 'B', 'C', 'D']); export const MIN_ITEMS = 2; diff -u -r -x _headers deploy_v120b/site/index.html deploy_v121b/site/index.html --- deploy_v120b/site/index.html 2026-11-16 00:46:28.628741853 +0900 +++ deploy_v121b/site/index.html 2026-11-16 02:07:09.622674037 +0900 @@ -6,7 +6,7 @@ どっちがおトク?|お買い物の単価比較 - +
暮らしの小さな道具 端末内で計算
@@ -33,7 +33,7 @@
暮らしの「ちょっと困った」を、AIで。@ailab.omezaki 道具づくりの過程や、次のアプリはこちら。
- + - + diff -u -r -x _headers deploy_v120b/site/sw.js deploy_v121b/site/sw.js --- deploy_v120b/site/sw.js 2026-11-16 00:46:28.628827395 +0900 +++ deploy_v121b/site/sw.js 2026-11-16 02:07:09.622740948 +0900 @@ -1,5 +1,32 @@ -const CACHE='dochiga-otoku-v1.2.0'; -const ASSETS=['./','./index.html','./style.css','./style-v11.css','./core.js','./app.js','./manifest.webmanifest','./icon.svg']; -self.addEventListener('install',e=>{e.waitUntil(caches.open(CACHE).then(c=>c.addAll(ASSETS)).then(()=>self.skipWaiting()));}); +const VERSION='1.2.1'; +const CACHE='dochiga-otoku-v'+VERSION; +const V='?v='+VERSION; +// この版の各ファイルの中身(SHA-256)。インストール時に全部照合し、1つでも違えば失敗にする +const HASH={ +"./index.html": "0a571d0ce54a020e64f6c72e7465027313d7bfac2b1c9a69ee76a530fb853176", +"./style.css?v=1.2.1": "acf199e60d69b5a179d91d76f4985d9a431cf8e2d37ac342f0179ca04d52960c", +"./style-v11.css?v=1.2.1": "f5aa7974a72b8fbd01ec1e5c9581439d25b83ea291fcb44868a71a8e399efe2d", +"./core.js?v=1.2.1": "1ab71d41f37c2353cb82792429d8b4042f7c9d9105c3cfa95d9f15b0af3e21f4", +"./app.js?v=1.2.1": "7757f738b2c290661b14d213e686b08eb74e65e1c34104178f3dcff15b8c2cb2", +"./manifest.webmanifest": "7ca317d90fe72b5821a566ff7f21583440441ba690ba4c2aed7096a289a88fd4", +"./icon.svg": "12707f4ac1683992da697344519be0e76d30dc5a534572e18f97ea289e746d84" +}; +const ASSETS=Object.keys(HASH); +const hex=b=>[...new Uint8Array(b)].map(x=>x.toString(16).padStart(2,'0')).join(''); +const sha=res=>res.clone().arrayBuffer().then(b=>crypto.subtle.digest('SHA-256',b)).then(hex); +// インストール:全ファイルを取得→中身を照合→全部一致したときだけ保存して有効化。失敗したら作りかけのキャッシュを消す +self.addEventListener('install',e=>{e.waitUntil(Promise.all(ASSETS.map(u=>fetch(new Request(u,{cache:'reload'})).then(res=>{if(!res.ok)throw new Error('fetch '+u);return sha(res).then(h=>{if(h!==HASH[u])throw new Error('hash '+u);return [u,res];});}))).then(list=>caches.open(CACHE).then(c=>{const top=list.find(x=>x[0]==='./index.html')[1].clone();return Promise.all(list.map(([u,res])=>c.put(u,res)).concat(c.put('./',top)));})).then(()=>self.skipWaiting(),err=>caches.delete(CACHE).then(()=>{throw err;})));}); self.addEventListener('activate',e=>{e.waitUntil(caches.keys().then(ks=>Promise.all(ks.filter(k=>k.startsWith('dochiga-otoku-')&&k!==CACHE).map(k=>caches.delete(k)))).then(()=>self.clients.claim()));}); -self.addEventListener('fetch',e=>{const u=new URL(e.request.url);if(e.request.method!=='GET'||u.origin!==location.origin)return;e.respondWith(caches.match(e.request,{ignoreSearch:true}).then(r=>r||fetch(e.request).then(res=>{if(res.ok){const cp=res.clone();caches.open(CACHE).then(c=>c.put(e.request,cp));}return res;}).catch(()=>caches.match('./index.html'))));}); +const isPage=r=>r.mode==='navigate'||r.destination==='document'; +const saved=()=>caches.match('./index.html',{cacheName:CACHE}); +self.addEventListener('fetch',e=>{ + const r=e.request;const u=new URL(r.url); + if(r.method!=='GET'||u.origin!==location.origin)return; + if(isPage(r)){ + // 画面(HTML)はネット優先。キャッシュには書き込まない(インストール時に照合済みのものだけを使う)。エラー応答・通信失敗のときは照合済みの画面を返す + e.respondWith(fetch(r,{cache:'no-cache'}).then(res=>res.ok?res:saved().then(m=>m||res),()=>saved().then(m=>m||Response.error()))); + return; + } + // JS・CSSなどは、この版のキャッシュ(照合済み)から完全一致で探し、なければネットから返すだけ。実行中にキャッシュへは書き込まない + e.respondWith(caches.match(r,{cacheName:CACHE}).then(m=>m||fetch(r))); +});