13 changed files with 834 additions and 48 deletions
@ -1,11 +1,326 @@
@@ -1,11 +1,326 @@
|
||||
import { Controller } from '@hotwired/stimulus' |
||||
|
||||
export default class extends Controller { |
||||
static targets = ['status'] |
||||
static values = { |
||||
staticUrls: Array, |
||||
cacheName: String |
||||
} |
||||
|
||||
connect() { |
||||
// Check if we've shown an update prompt recently (within 1 hour)
|
||||
const lastPrompt = localStorage.getItem('sw-update-prompt-shown'); |
||||
const oneHourAgo = Date.now() - (60 * 60 * 1000); |
||||
|
||||
if (lastPrompt && parseInt(lastPrompt) > oneHourAgo) { |
||||
this.updatePromptShown = true; |
||||
} else { |
||||
this.updatePromptShown = false; |
||||
} |
||||
|
||||
if ('serviceWorker' in navigator) { |
||||
navigator.serviceWorker.register('/service-worker.js') |
||||
.then(reg => console.log('SW registered:', reg)) |
||||
.catch(err => console.error('SW failed:', err)); |
||||
this.loadStaticRoutes().then(() => { |
||||
this.registerServiceWorker(); |
||||
this.setupServiceWorkerEventListeners(); |
||||
}); |
||||
} else { |
||||
this.updateStatus('Service Worker not supported'); |
||||
} |
||||
} |
||||
|
||||
async loadStaticRoutes() { |
||||
try { |
||||
const response = await fetch('/api/static-routes'); |
||||
if (response.ok) { |
||||
const data = await response.json(); |
||||
this.staticUrlsValue = data.routes; |
||||
this.cacheNameValue = data.cacheName; |
||||
console.log('Loaded static routes for controller:', this.staticUrlsValue); |
||||
} else { |
||||
console.warn('Failed to load static routes from API'); |
||||
} |
||||
} catch (error) { |
||||
console.error('Error loading static routes:', error); |
||||
} |
||||
} |
||||
|
||||
async registerServiceWorker() { |
||||
try { |
||||
const registration = await navigator.serviceWorker.register('/service-worker.js'); |
||||
console.log('SW registered:', registration); |
||||
|
||||
// Check if there's actually a waiting service worker with new content
|
||||
if (registration.waiting && !this.updatePromptShown) { |
||||
this.showUpdateAvailable(registration); |
||||
} |
||||
|
||||
// Listen for service worker updates, but be more selective
|
||||
registration.addEventListener('updatefound', () => { |
||||
const newWorker = registration.installing; |
||||
|
||||
newWorker.addEventListener('statechange', () => { |
||||
// Only show update if:
|
||||
// 1. The worker is installed
|
||||
// 2. We have an active controller (not first install)
|
||||
// 3. Haven't shown prompt recently
|
||||
if (newWorker.state === 'installed' && |
||||
navigator.serviceWorker.controller && |
||||
!this.updatePromptShown) { |
||||
|
||||
// Double-check we haven't prompted recently
|
||||
const lastPrompt = localStorage.getItem('sw-update-prompt-shown'); |
||||
const thirtyMinutesAgo = Date.now() - (30 * 60 * 1000); |
||||
|
||||
if (!lastPrompt || parseInt(lastPrompt) < thirtyMinutesAgo) { |
||||
// Add a delay to avoid immediate prompts during development
|
||||
setTimeout(() => { |
||||
if (!this.updatePromptShown && registration.waiting) { |
||||
this.showUpdateAvailable(registration); |
||||
} |
||||
}, 3000); |
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
this.updateStatus('Service Worker registered successfully'); |
||||
await this.checkCacheStatus(); |
||||
} catch (error) { |
||||
console.error('SW failed:', error); |
||||
this.updateStatus('Service Worker registration failed'); |
||||
} |
||||
} |
||||
|
||||
setupServiceWorkerEventListeners() { |
||||
// Listen for messages from the service worker
|
||||
navigator.serviceWorker.addEventListener('message', (event) => { |
||||
if (event.data && event.data.type) { |
||||
switch (event.data.type) { |
||||
case 'CACHE_UPDATED': |
||||
this.updateStatus('Cache updated successfully'); |
||||
break; |
||||
case 'CACHE_ERROR': |
||||
this.updateStatus('Cache update failed'); |
||||
break; |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
async checkCacheStatus() { |
||||
if ('caches' in window) { |
||||
try { |
||||
const cacheNames = await caches.keys(); |
||||
const cacheName = this.cacheNameValue || 'newsroom-static'; |
||||
const hasStaticCache = cacheNames.some(name => name.includes(cacheName.split('-')[0] + '-' + cacheName.split('-')[1])); |
||||
|
||||
if (hasStaticCache) { |
||||
const cache = await caches.open(cacheName); |
||||
const cachedRequests = await cache.keys(); |
||||
this.updateStatus(`${cachedRequests.length} static pages cached`); |
||||
} else { |
||||
this.updateStatus('Static pages not yet cached'); |
||||
} |
||||
} catch (error) { |
||||
console.error('Cache status check failed:', error); |
||||
} |
||||
} |
||||
} |
||||
|
||||
showUpdateAvailable(registration) { |
||||
// Prevent multiple prompts
|
||||
if (this.updatePromptShown) { |
||||
return; |
||||
} |
||||
|
||||
this.updatePromptShown = true; |
||||
|
||||
// Store the timestamp to avoid showing again too soon
|
||||
localStorage.setItem('sw-update-prompt-shown', Date.now().toString()); |
||||
|
||||
// Use a more user-friendly notification
|
||||
const shouldUpdate = confirm('A new version of the app is available. Would you like to update now?'); |
||||
|
||||
if (shouldUpdate) { |
||||
if (registration.waiting) { |
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' }); |
||||
registration.waiting.addEventListener('statechange', (e) => { |
||||
if (e.target.state === 'activated') { |
||||
window.location.reload(); |
||||
} |
||||
}); |
||||
} |
||||
} else { |
||||
// If user declines, don't ask again for 2 hours
|
||||
localStorage.setItem('sw-update-prompt-shown', (Date.now() + (2 * 60 * 60 * 1000)).toString()); |
||||
|
||||
// Reset the flag after 2 hours
|
||||
setTimeout(() => { |
||||
this.updatePromptShown = false; |
||||
}, 2 * 60 * 60 * 1000); |
||||
} |
||||
} |
||||
|
||||
async clearCache() { |
||||
try { |
||||
const cacheNames = await caches.keys(); |
||||
await Promise.all( |
||||
cacheNames.map(cacheName => caches.delete(cacheName)) |
||||
); |
||||
this.updateStatus('All caches cleared'); |
||||
console.log('All caches cleared'); |
||||
} catch (error) { |
||||
console.error('Failed to clear caches:', error); |
||||
this.updateStatus('Failed to clear caches'); |
||||
} |
||||
} |
||||
|
||||
async refreshCache() { |
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { |
||||
navigator.serviceWorker.controller.postMessage({ |
||||
type: 'REFRESH_CACHE' |
||||
}); |
||||
this.updateStatus('Cache refresh requested'); |
||||
} |
||||
} |
||||
|
||||
async getCacheStatus() { |
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { |
||||
return new Promise((resolve) => { |
||||
const messageChannel = new MessageChannel(); |
||||
messageChannel.port1.onmessage = (event) => { |
||||
if (event.data.type === 'CACHE_STATUS') { |
||||
resolve(event.data.status); |
||||
} else { |
||||
resolve(null); |
||||
} |
||||
}; |
||||
|
||||
navigator.serviceWorker.controller.postMessage( |
||||
{ type: 'GET_CACHE_STATUS' }, |
||||
[messageChannel.port2] |
||||
); |
||||
}); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
async displayCacheInfo() { |
||||
const status = await this.getCacheStatus(); |
||||
if (status) { |
||||
console.log('Cache Status:', status); |
||||
let message = 'Cache Status:\n'; |
||||
for (const [cacheName, count] of Object.entries(status)) { |
||||
message += `${cacheName}: ${count} items\n`; |
||||
} |
||||
this.updateStatus(message.trim()); |
||||
} |
||||
} |
||||
|
||||
async preloadCriticalAssets() { |
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { |
||||
// Get the manifest to find critical assets
|
||||
try { |
||||
const manifestResponse = await fetch('/assets/manifest.json'); |
||||
if (manifestResponse.ok) { |
||||
const manifest = await manifestResponse.json(); |
||||
|
||||
// Critical assets that should be preloaded
|
||||
const criticalAssets = [ |
||||
'app.js', |
||||
'bootstrap.js', |
||||
'styles/app.css', |
||||
'styles/theme.css', |
||||
'styles/layout.css', |
||||
'styles/fonts.css' |
||||
]; |
||||
|
||||
const assetsToPreload = criticalAssets |
||||
.map(asset => manifest[asset]) |
||||
.filter(Boolean); |
||||
|
||||
// Trigger preloading by making requests
|
||||
const preloadPromises = assetsToPreload.map(assetUrl => |
||||
fetch(assetUrl, { mode: 'no-cors' }).catch(() => { |
||||
console.warn('Failed to preload:', assetUrl); |
||||
}) |
||||
); |
||||
|
||||
await Promise.all(preloadPromises); |
||||
this.updateStatus(`Preloaded ${assetsToPreload.length} critical assets`); |
||||
console.log('Preloaded assets:', assetsToPreload); |
||||
} |
||||
} catch (error) { |
||||
console.error('Failed to preload critical assets:', error); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Enhanced cache management methods
|
||||
async clearAssetsCache() { |
||||
if ('caches' in window) { |
||||
try { |
||||
const deleted = await caches.delete('newsroom-assets-v1'); |
||||
if (deleted) { |
||||
this.updateStatus('Assets cache cleared'); |
||||
console.log('Assets cache cleared'); |
||||
} else { |
||||
this.updateStatus('Assets cache not found'); |
||||
} |
||||
} catch (error) { |
||||
console.error('Failed to clear assets cache:', error); |
||||
this.updateStatus('Failed to clear assets cache'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async clearStaticCache() { |
||||
if ('caches' in window) { |
||||
try { |
||||
const deleted = await caches.delete('newsroom-static-v1'); |
||||
if (deleted) { |
||||
this.updateStatus('Static cache cleared'); |
||||
console.log('Static cache cleared'); |
||||
} else { |
||||
this.updateStatus('Static cache not found'); |
||||
} |
||||
} catch (error) { |
||||
console.error('Failed to clear static cache:', error); |
||||
this.updateStatus('Failed to clear static cache'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
updateStatus(message) { |
||||
if (this.hasStatusTarget) { |
||||
this.statusTarget.textContent = message; |
||||
console.log('SW Status:', message); |
||||
} |
||||
} |
||||
|
||||
// Action methods that can be called from the template
|
||||
clearCacheAction() { |
||||
this.clearCache(); |
||||
} |
||||
|
||||
refreshCacheAction() { |
||||
this.refreshCache(); |
||||
} |
||||
|
||||
// Enhanced action methods
|
||||
clearAssetsCacheAction() { |
||||
this.clearAssetsCache(); |
||||
} |
||||
|
||||
clearStaticCacheAction() { |
||||
this.clearStaticCache(); |
||||
} |
||||
|
||||
displayCacheInfoAction() { |
||||
this.displayCacheInfo(); |
||||
} |
||||
|
||||
preloadCriticalAssetsAction() { |
||||
this.preloadCriticalAssets(); |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
# Asset caching configuration for better browser caching |
||||
framework: |
||||
assets: |
||||
# AssetMapper already handles versioning via manifest.json |
||||
# No need to set version_strategy as it conflicts with json_manifest_path |
||||
json_manifest_path: '%kernel.project_dir%/public/assets/manifest.json' |
||||
|
||||
# Configure HTTP cache headers for static assets |
||||
when@prod: |
||||
framework: |
||||
http_cache: |
||||
# Cache static assets for longer periods |
||||
default_ttl: 3600 # 1 hour default |
||||
private_headers: ['authorization', 'cookie'] |
||||
allow_reload: false |
||||
allow_revalidate: false |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
# Robots.txt |
||||
|
||||
# Disallow all by default |
||||
User-agent: * |
||||
Disallow: / |
||||
|
||||
# Allow only specific public content |
||||
Allow: /assets/ |
||||
Allow: /icons/ |
||||
Allow: /$ |
||||
Allow: /unfold |
||||
Allow: /about |
||||
Allow: /mag/ |
||||
Allow: /p/ |
||||
Allow: /list/ |
||||
|
||||
# Crawl delay |
||||
Crawl-delay: 1 |
||||
@ -1,11 +1,309 @@
@@ -1,11 +1,309 @@
|
||||
// Define cache names and versions
|
||||
const CACHE_NAME = 'newsroom-v1'; |
||||
const STATIC_CACHE = 'newsroom-static-v1'; |
||||
const ASSETS_CACHE = 'newsroom-assets-v1'; |
||||
const RUNTIME_CACHE = 'newsroom-runtime-v1'; |
||||
|
||||
// Assets to cache immediately on install
|
||||
const PRECACHE_ASSETS = [ |
||||
'/', |
||||
'/assets/app.js', |
||||
'/assets/bootstrap.js', |
||||
'/assets/styles/app.css', |
||||
'/assets/styles/theme.css', |
||||
'/assets/styles/layout.css', |
||||
'/assets/styles/fonts.css', |
||||
'/assets/icons/favicon.ico', |
||||
'/assets/icons/web-app-manifest-192x192.png', |
||||
'/assets/icons/web-app-manifest-512x512.png' |
||||
]; |
||||
|
||||
// Define what should be cached with different strategies
|
||||
const CACHE_STRATEGIES = { |
||||
// CSS files - cache first (they change infrequently)
|
||||
css: { |
||||
pattern: /\.css$/, |
||||
strategy: 'cacheFirst', |
||||
cacheName: ASSETS_CACHE, |
||||
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||
}, |
||||
// JS files - cache first with network fallback
|
||||
js: { |
||||
pattern: /\.js$/, |
||||
strategy: 'cacheFirst', |
||||
cacheName: ASSETS_CACHE, |
||||
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||
}, |
||||
// Fonts - cache first (rarely change)
|
||||
fonts: { |
||||
pattern: /\.(woff2?|ttf|eot)$/, |
||||
strategy: 'cacheFirst', |
||||
cacheName: ASSETS_CACHE, |
||||
maxAge: 365 * 24 * 60 * 60 * 1000 // 1 year
|
||||
}, |
||||
// Images and icons - cache first
|
||||
images: { |
||||
pattern: /\.(png|jpg|jpeg|gif|svg|ico)$/, |
||||
strategy: 'cacheFirst', |
||||
cacheName: ASSETS_CACHE, |
||||
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||
}, |
||||
// Static pages - network first with cache fallback
|
||||
pages: { |
||||
pattern: /^https?.*\/(about|roadmap|tos|landing|unfold)$/, |
||||
strategy: 'networkFirst', |
||||
cacheName: STATIC_CACHE, |
||||
maxAge: 24 * 60 * 60 * 1000 // 1 day
|
||||
}, |
||||
// API calls - network first
|
||||
api: { |
||||
pattern: /\/api\//, |
||||
strategy: 'networkFirst', |
||||
cacheName: RUNTIME_CACHE, |
||||
maxAge: 5 * 60 * 1000 // 5 minutes
|
||||
} |
||||
}; |
||||
|
||||
self.addEventListener('install', async (event) => { |
||||
// Activate the service worker immediately after install
|
||||
await self.skipWaiting(); |
||||
console.log('Service Worker installing...'); |
||||
|
||||
event.waitUntil( |
||||
(async () => { |
||||
try { |
||||
// Cache core assets immediately
|
||||
const cache = await caches.open(ASSETS_CACHE); |
||||
console.log('Precaching core assets...'); |
||||
|
||||
// Get the current asset manifest to cache the actual versioned files
|
||||
const manifestResponse = await fetch('/assets/manifest.json'); |
||||
if (manifestResponse.ok) { |
||||
const manifest = await manifestResponse.json(); |
||||
const versionedAssets = PRECACHE_ASSETS.map(asset => { |
||||
const logicalPath = asset.replace('/assets/', ''); |
||||
return manifest[logicalPath] || asset; |
||||
}); |
||||
|
||||
await cache.addAll(versionedAssets); |
||||
console.log('Precached assets:', versionedAssets); |
||||
} else { |
||||
// Fallback to original assets if manifest not available
|
||||
await cache.addAll(PRECACHE_ASSETS); |
||||
} |
||||
|
||||
// Activate immediately
|
||||
await self.skipWaiting(); |
||||
} catch (error) { |
||||
console.error('Precaching failed:', error); |
||||
} |
||||
})() |
||||
); |
||||
}); |
||||
|
||||
self.addEventListener('activate', (event) => { |
||||
// Take control of all clients immediately
|
||||
event.waitUntil(self.clients.claim()); |
||||
console.log('Service Worker activating...'); |
||||
|
||||
event.waitUntil( |
||||
(async () => { |
||||
// Clean up old caches
|
||||
const cacheNames = await caches.keys(); |
||||
const oldCaches = cacheNames.filter(name => |
||||
name.startsWith('newsroom-') && |
||||
![CACHE_NAME, STATIC_CACHE, ASSETS_CACHE, RUNTIME_CACHE].includes(name) |
||||
); |
||||
|
||||
await Promise.all(oldCaches.map(name => caches.delete(name))); |
||||
console.log('Cleaned up old caches:', oldCaches); |
||||
|
||||
// Take control of all clients
|
||||
await self.clients.claim(); |
||||
})() |
||||
); |
||||
}); |
||||
|
||||
self.addEventListener('fetch', (event) => { |
||||
const request = event.request; |
||||
const url = new URL(request.url); |
||||
|
||||
// Skip non-GET requests
|
||||
if (request.method !== 'GET') return; |
||||
|
||||
// Skip chrome-extension and other non-http requests
|
||||
if (!url.protocol.startsWith('http')) return; |
||||
|
||||
// Find matching cache strategy
|
||||
const strategy = findCacheStrategy(request.url); |
||||
|
||||
if (strategy) { |
||||
event.respondWith(handleRequest(request, strategy)); |
||||
} |
||||
}); |
||||
|
||||
// No fetch or cache handlers — fully fall-through
|
||||
self.addEventListener('message', (event) => { |
||||
const { type, data } = event.data || {}; |
||||
|
||||
switch (type) { |
||||
case 'SKIP_WAITING': |
||||
self.skipWaiting(); |
||||
break; |
||||
|
||||
case 'REFRESH_CACHE': |
||||
handleCacheRefresh(); |
||||
break; |
||||
|
||||
case 'GET_CACHE_STATUS': |
||||
handleCacheStatus(event); |
||||
break; |
||||
} |
||||
}); |
||||
|
||||
function findCacheStrategy(url) { |
||||
for (const [name, config] of Object.entries(CACHE_STRATEGIES)) { |
||||
if (config.pattern.test(url)) { |
||||
return config; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
async function handleRequest(request, strategy) { |
||||
const cache = await caches.open(strategy.cacheName); |
||||
|
||||
switch (strategy.strategy) { |
||||
case 'cacheFirst': |
||||
return cacheFirst(request, cache, strategy); |
||||
case 'networkFirst': |
||||
return networkFirst(request, cache, strategy); |
||||
case 'staleWhileRevalidate': |
||||
return staleWhileRevalidate(request, cache, strategy); |
||||
default: |
||||
return fetch(request); |
||||
} |
||||
} |
||||
|
||||
async function cacheFirst(request, cache, strategy) { |
||||
try { |
||||
// Check cache first
|
||||
const cachedResponse = await cache.match(request); |
||||
if (cachedResponse && !isExpired(cachedResponse, strategy.maxAge)) { |
||||
return cachedResponse; |
||||
} |
||||
|
||||
// Fetch from network
|
||||
const networkResponse = await fetch(request); |
||||
|
||||
if (networkResponse.ok) { |
||||
// Clone and cache the response
|
||||
const responseToCache = networkResponse.clone(); |
||||
await cache.put(request, responseToCache); |
||||
} |
||||
|
||||
return networkResponse; |
||||
} catch (error) { |
||||
console.error('Cache first strategy failed:', error); |
||||
// Return cached version even if expired as fallback
|
||||
const cachedResponse = await cache.match(request); |
||||
if (cachedResponse) { |
||||
return cachedResponse; |
||||
} |
||||
throw error; |
||||
} |
||||
} |
||||
|
||||
async function networkFirst(request, cache, strategy) { |
||||
try { |
||||
// Try network first
|
||||
const networkResponse = await fetch(request); |
||||
|
||||
if (networkResponse.ok) { |
||||
// Cache successful responses
|
||||
const responseToCache = networkResponse.clone(); |
||||
await cache.put(request, responseToCache); |
||||
} |
||||
|
||||
return networkResponse; |
||||
} catch (error) { |
||||
console.log('Network failed, trying cache:', error.message); |
||||
// Fallback to cache
|
||||
const cachedResponse = await cache.match(request); |
||||
if (cachedResponse) { |
||||
return cachedResponse; |
||||
} |
||||
throw error; |
||||
} |
||||
} |
||||
|
||||
async function staleWhileRevalidate(request, cache, strategy) { |
||||
const cachedResponse = await cache.match(request); |
||||
|
||||
// Always fetch in background to update cache
|
||||
const fetchPromise = fetch(request).then(networkResponse => { |
||||
if (networkResponse.ok) { |
||||
cache.put(request, networkResponse.clone()); |
||||
} |
||||
return networkResponse; |
||||
}).catch(() => { |
||||
// Ignore background fetch failures
|
||||
}); |
||||
|
||||
// Return cached version immediately if available
|
||||
if (cachedResponse && !isExpired(cachedResponse, strategy.maxAge)) { |
||||
return cachedResponse; |
||||
} |
||||
|
||||
// Wait for network if no cache or expired
|
||||
return fetchPromise; |
||||
} |
||||
|
||||
function isExpired(response, maxAge) { |
||||
if (!maxAge) return false; |
||||
|
||||
const dateHeader = response.headers.get('date'); |
||||
if (!dateHeader) return false; |
||||
|
||||
const responseTime = new Date(dateHeader).getTime(); |
||||
return (Date.now() - responseTime) > maxAge; |
||||
} |
||||
|
||||
async function handleCacheRefresh() { |
||||
try { |
||||
// Clear all caches
|
||||
const cacheNames = await caches.keys(); |
||||
await Promise.all(cacheNames.map(name => caches.delete(name))); |
||||
|
||||
// Reinstall with fresh cache
|
||||
const cache = await caches.open(ASSETS_CACHE); |
||||
await cache.addAll(PRECACHE_ASSETS); |
||||
|
||||
// Notify all clients
|
||||
const clients = await self.clients.matchAll(); |
||||
clients.forEach(client => { |
||||
client.postMessage({ type: 'CACHE_UPDATED' }); |
||||
}); |
||||
|
||||
console.log('Cache refreshed successfully'); |
||||
} catch (error) { |
||||
console.error('Cache refresh failed:', error); |
||||
// Notify clients of error
|
||||
const clients = await self.clients.matchAll(); |
||||
clients.forEach(client => { |
||||
client.postMessage({ type: 'CACHE_ERROR', error: error.message }); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
async function handleCacheStatus(event) { |
||||
try { |
||||
const cacheNames = await caches.keys(); |
||||
const status = {}; |
||||
|
||||
for (const cacheName of cacheNames) { |
||||
const cache = await caches.open(cacheName); |
||||
const keys = await cache.keys(); |
||||
status[cacheName] = keys.length; |
||||
} |
||||
|
||||
event.ports[0].postMessage({ type: 'CACHE_STATUS', status }); |
||||
} catch (error) { |
||||
event.ports[0].postMessage({ type: 'CACHE_STATUS_ERROR', error: error.message }); |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,154 @@
@@ -0,0 +1,154 @@
|
||||
{% extends 'base.html.twig' %} |
||||
|
||||
{% block title %}Cache Management - {{ parent() }}{% endblock %} |
||||
|
||||
{% block stylesheets %} |
||||
{{ parent() }} |
||||
<style> |
||||
.cache-management { |
||||
max-width: 800px; |
||||
margin: 2rem auto; |
||||
padding: 1rem; |
||||
} |
||||
.cache-status { |
||||
background: #f8f9fa; |
||||
border: 1px solid #dee2e6; |
||||
border-radius: 0.5rem; |
||||
padding: 1rem; |
||||
margin: 1rem 0; |
||||
font-family: monospace; |
||||
white-space: pre-wrap; |
||||
} |
||||
.cache-actions { |
||||
display: grid; |
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
||||
gap: 1rem; |
||||
margin: 2rem 0; |
||||
} |
||||
.cache-action-btn { |
||||
padding: 0.75rem 1rem; |
||||
border: none; |
||||
border-radius: 0.5rem; |
||||
background: #007bff; |
||||
color: white; |
||||
cursor: pointer; |
||||
transition: background-color 0.2s; |
||||
} |
||||
.cache-action-btn:hover { |
||||
background: #0056b3; |
||||
} |
||||
.cache-action-btn.danger { |
||||
background: #dc3545; |
||||
} |
||||
.cache-action-btn.danger:hover { |
||||
background: #c82333; |
||||
} |
||||
.cache-info { |
||||
background: #d4edda; |
||||
border: 1px solid #c3e6cb; |
||||
border-radius: 0.5rem; |
||||
padding: 1rem; |
||||
margin: 1rem 0; |
||||
} |
||||
</style> |
||||
{% endblock %} |
||||
|
||||
{% block layout %} |
||||
<div class="cache-management" data-controller="service-worker"> |
||||
<h1>Cache Management</h1> |
||||
|
||||
<div class="cache-info"> |
||||
<h3>Caching Strategy Overview</h3> |
||||
<p>Your newsroom application uses a multi-layered caching strategy:</p> |
||||
<ul> |
||||
<li><strong>Service Worker Cache</strong> - Caches JS, CSS, fonts, and images for offline access</li> |
||||
<li><strong>Browser Cache</strong> - HTTP cache headers for optimal browser caching</li> |
||||
<li><strong>Asset Versioning</strong> - Content-hashed filenames for cache busting</li> |
||||
</ul> |
||||
</div> |
||||
|
||||
<div class="cache-status" data-service-worker-target="status"> |
||||
Loading cache status... |
||||
</div> |
||||
|
||||
<div class="cache-actions"> |
||||
<button |
||||
class="cache-action-btn" |
||||
data-action="click->service-worker#displayCacheInfoAction" |
||||
> |
||||
Show Cache Status |
||||
</button> |
||||
|
||||
<button |
||||
class="cache-action-btn" |
||||
data-action="click->service-worker#preloadCriticalAssetsAction" |
||||
> |
||||
Preload Critical Assets |
||||
</button> |
||||
|
||||
<button |
||||
class="cache-action-btn" |
||||
data-action="click->service-worker#refreshCacheAction" |
||||
> |
||||
Refresh All Caches |
||||
</button> |
||||
|
||||
<button |
||||
class="cache-action-btn" |
||||
data-action="click->service-worker#clearAssetsCacheAction" |
||||
> |
||||
Clear Assets Cache |
||||
</button> |
||||
|
||||
<button |
||||
class="cache-action-btn" |
||||
data-action="click->service-worker#clearStaticCacheAction" |
||||
> |
||||
Clear Static Cache |
||||
</button> |
||||
|
||||
<button |
||||
class="cache-action-btn danger" |
||||
data-action="click->service-worker#clearCacheAction" |
||||
> |
||||
Clear All Caches |
||||
</button> |
||||
</div> |
||||
|
||||
<div class="cache-info"> |
||||
<h3>Cache Types Explained</h3> |
||||
<dl> |
||||
<dt><strong>Assets Cache (newsroom-assets-v1)</strong></dt> |
||||
<dd>Stores JS files, CSS files, fonts, and images. Uses "cache-first" strategy for fast loading.</dd> |
||||
|
||||
<dt><strong>Static Cache (newsroom-static-v1)</strong></dt> |
||||
<dd>Stores static pages like About, Roadmap, etc. Uses "network-first" strategy for fresh content.</dd> |
||||
|
||||
<dt><strong>Runtime Cache (newsroom-runtime-v1)</strong></dt> |
||||
<dd>Stores API responses and dynamic content with short expiration times.</dd> |
||||
</dl> |
||||
</div> |
||||
|
||||
<div class="cache-info"> |
||||
<h3>Asset Caching Details</h3> |
||||
<p>The following asset types are automatically cached:</p> |
||||
<ul> |
||||
<li><strong>JavaScript files (.js)</strong> - Cached for 30 days</li> |
||||
<li><strong>CSS files (.css)</strong> - Cached for 30 days</li> |
||||
<li><strong>Font files (.woff2, .woff, .ttf)</strong> - Cached for 1 year</li> |
||||
<li><strong>Images (.png, .jpg, .svg, .ico)</strong> - Cached for 30 days</li> |
||||
<li><strong>Static pages</strong> - Cached for 1 day with network-first strategy</li> |
||||
</ul> |
||||
</div> |
||||
|
||||
<div class="cache-info"> |
||||
<h3>Performance Benefits</h3> |
||||
<ul> |
||||
<li>Faster page loads after first visit</li> |
||||
<li>Reduced bandwidth usage</li> |
||||
<li>Better offline experience</li> |
||||
<li>Automatic cache invalidation when assets change</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
{% endblock %} |
||||
Loading…
Reference in new issue