freeze cache after first install

Hunter
Wednesday, April 29th, 2026 at 11:54 PM

build.py+24-70

build.py
 76  	return "#080a0c"
 77  
 78  
    -def _next_cache_name(app_name, manifest_path):
    -	"""Determine the next cache name by incrementing the version in an existing manifest.
    -
    -	If manifest.json doesn't exist or has no versioned cache_name, returns "{app_name}-v1".
    -	If it already has e.g. "heaven-v1", returns "heaven-v1.1".
    -	If it already has e.g. "heaven-v1.3", returns "heaven-v1.4".
    -	"""
    -	if manifest_path.exists():
    -		try:
    -			with open(manifest_path, 'r', encoding='utf-8') as f:
    -				existing = json.load(f)
    -			old_cache = existing.get("cache_name", "")
    -			# Match pattern like "appname-v1" or "appname-v1.3"
    -			match = re.match(r'^(.+)-v(\d+)(?:\.(\d+))?$', old_cache)
    -			if match:
    -				prefix = match.group(1)
    -				major = int(match.group(2))
    -				minor = int(match.group(3)) if match.group(3) is not None else 0
    -				# If app was renamed, start fresh with new name
    -				if prefix != app_name:
    -					return f"{app_name}-v1"
    -				return f"{app_name}-v{major}.{minor + 1}"
    -		except (json.JSONDecodeError, KeyError):
    -			pass
    -	return f"{app_name}-v1"
    -
    -
 79  def build_pwa(app_name=None, base_path=None):
 80  	"""Generate manifest.json and service-worker.js based on tracks.json
 81  

 89  
 90  	# Derived values
 91  	short_name = app_name
    -	cache_name = _next_cache_name(app_name, SCRIPT_DIR / "manifest.json")
 92 +	cache_name = app_name
 93  	app_description = f"{app_name}"
 94  
 95  	print("Building PWA files...")

176  // Static files to cache on install
177  const STATIC_FILES = {static_files_js};
178  
    -// Install event - cache static resources
    -// Audio files will be cached by the main app's blob preloading system
179 +// Install event - cache static resources only if not already cached.
180 +// This makes installs immutable: once a file is in the cache, redeploys
181 +// will not overwrite it, so the app stays frozen at its first-installed version.
182 +// Audio files will be cached by the main app's blob preloading system.
183  self.addEventListener('install', (event) => {{
184  	console.log('Service Worker installing...', 'Base path:', basePath);
185  	event.waitUntil(

188  				if (url === './') return new URL(basePath, self.location.href).href;
189  				return new URL(url, new URL(basePath, self.location.href)).href;
190  			}});
    -			console.log('Caching', absoluteUrls.length, 'static resources');
191  
    -			// Cache files individually with better error handling
    -			// Using Promise.allSettled to continue even if some fail
192  			return Promise.allSettled(
193  				absoluteUrls.map(url =>
    -					fetch(url, {{ cache: 'no-cache' }})
    -						.then(response => {{
    -							if (!response.ok) {{
    -								throw new Error(`HTTP error! status: ${{response.status}}`);
    -							}}
    -							return cache.put(url, response);
    -						}})
    -						.then(() => console.log('✓ Cached:', url))
    -						.catch(err => {{
    -							console.error('✗ Failed to cache:', url, err);
    -							throw err;
    -						}})
194 +					cache.match(url).then(existing => {{
195 +						if (existing) {{
196 +							console.log('• Already cached, skipping:', url);
197 +							return;
198 +						}}
199 +						return fetch(url, {{ cache: 'no-cache' }})
200 +							.then(response => {{
201 +								if (!response.ok) {{
202 +									throw new Error(`HTTP error! status: ${{response.status}}`);
203 +								}}
204 +								return cache.put(url, response);
205 +							}})
206 +							.then(() => console.log('✓ Cached:', url))
207 +							.catch(err => {{
208 +								console.error('✗ Failed to cache:', url, err);
209 +								throw err;
210 +							}});
211 +					}})
212  				)
213  			).then(results => {{
214  				const failed = results.filter(r => r.status === 'rejected');
    -				const succeeded = results.filter(r => r.status === 'fulfilled');
    -				console.log(`Cached ${{succeeded.length}}/${{results.length}} static resources`);
    -				if (failed.length > 0) {{
    -					console.warn(`Failed to cache ${{failed.length}} resources`);
    -				}}
215 +				console.log(`Install complete: ${{results.length - failed.length}}/${{results.length}} ok`);
216  			}});
    -		}}).then(() => {{
    -			console.log('Service Worker installation complete');
    -			return self.skipWaiting();
217  		}}).catch(error => {{
218  			console.error('Service Worker installation failed:', error);
219  		}})
220  	);
221  }});
222  
    -// Activate event - clean up old caches
    -self.addEventListener('activate', (event) => {{
    -	console.log('Service Worker activating...');
    -	event.waitUntil(
    -		caches.keys().then((cacheNames) => {{
    -			return Promise.all(
    -				cacheNames.map((cacheName) => {{
    -					if (cacheName !== CACHE_NAME) {{
    -						console.log('Deleting old cache:', cacheName);
    -						return caches.delete(cacheName);
    -					}}
    -				}})
    -			);
    -		}}).then(() => self.clients.claim())
    -	);
    -}});
    -
223  // Fetch event - cache first, network fallback
224  self.addEventListener('fetch', (event) => {{
225  	// Ignore non-http(s) requests like blob: URLs, data: URLs, chrome-extension:, etc.