diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 120000 index ade65c5..0000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1 +0,0 @@ -../scripts/git-hooks/pre-commit.cjs \ No newline at end of file diff --git a/README.md b/README.md index 940e8f6..85e7796 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,6 @@ Nexus Timer visualizes players in a circular sequence. The **Current Player** is Game enthusiasts who play turn-based games (board games, tabletop RPGs, card games) and need a visually clear and customizable timer solution. -## Tech Stack - -* **HTML5:** For structuring the user interface. -* **CSS3:** For styling and visual presentation, including animations. Consider a CSS framework like Tailwind CSS for rapid prototyping. -* **JavaScript:** For application logic, timer functionality, and event handling. -* **Web Audio API:** For audio feedback (ticking sounds, alerts). -* **Browser API:** For capturing Players' photo. -* **Screen Wake Lock API:** For preventing of the screen lock in a PWA. -* **Local Storage/IndexedDB:** For persistent storage of player data, timer states, and settings. -* **Service Worker:** Essential for PWA functionality (offline access, push notifications - potential future feature). -* **Manifest File:** Defines the PWA's metadata (name, icons, theme color). -* **Web Framework:** Use Vue.js -* **(Optional) Tailwind CSS:** Utility-first CSS framework for rapid UI development. - ## Hardware Recommendations (Optional Enhancement) For an enhanced tactile experience, Nexus Timer supports Smart Buttons based on Bluetooth-connected microcontroller (e.g., XIAO nRF52840) implementing HID (Human Interface Device) protocol. @@ -32,10 +18,10 @@ For an enhanced tactile experience, Nexus Timer supports Smart Buttons based on * **Configuration:** * **Player 1's Button:** Single Click: Emulates a key press (e.g., 'a'). Configure this as Player 1's "Pass Turn / My Pause" hotkey in the app. * **Player 2's Button:** Single Click: Emulates a key press (e.g., 'b'). Configure as Player 2's "Pass Turn / My Pause" hotkey. - * If Player 3 is Game Admin: * **Player 3's Button:** Single Click: Emulates a key press (e.g., 'c'). Configure as Player 3's "Pass Turn / My Pause" hotkey. - * **Player 3's Button:** Double Click: Emulates a key press (e.g., 'x'). Configure as the "Global Run All Timers" hotkey in the app. - * **Player 3's Button:** Long Press: Emulates a key press (e.g., 's'). Configure as the "Global Stop/Pause All" hotkey in the app. + * **If Player 3 is Game Admin:** + * **Player 3's Button:** Long Press: Emulates a key press (e.g., 's'). Configure as the "Global Stop/Pause All" hotkey in the app. + * **Player 3's Button:** Double Click: Emulates a key press (e.g., 'x'). Configure as the "Global Run All Timers" hotkey in the app. ## Key Features @@ -85,7 +71,7 @@ For an enhanced tactile experience, Nexus Timer supports Smart Buttons based on * **Persistence:** Player setups, timer states, and settings are saved locally using browser Local Storage. * **Global Reset:** "Reset Game" button restores all timers to initial values and resets game state. -## UI/UX Considerations (For AI Generation) +## UI/UX Considerations * **Minimalist Design:** Focus on clarity and ease of use. Avoid clutter. * **Large, Clear Timers:** Timers should be easily readable at a glance. @@ -93,134 +79,9 @@ For an enhanced tactile experience, Nexus Timer supports Smart Buttons based on * **Responsive Layout:** The UI should adapt to different (mobile phone) screen sizes. * **Touch-Friendly:** Buttons and interactive elements should be large enough for easy tapping. -## Data Model (For AI Generation) +### For Developer Setup, see [Developer Setup Guide](docs/development.md). +### For Deployment Setup, see [Deployment Setup Guide](docs/deployment.md). +### For Architecture Docs, see [Architecture](docs/architecture.md). -```json -{ - "players": [ - { - "id": "1", - "name": "Player 1", - "avatar": null, - "initialTimerSec": 3600, - "currentTimerSec": 3600, - "hotkey": "a", - "isSkipped": false - }, - { - "id": "2", - "name": "Player 2", - "avatar": null, - "initialTimerSec": 3600, - "currentTimerSec": 3600, - "hotkey": "b", - "isSkipped": false - } - ], - "globalHotkeyStopPause": "s", - "globalHotkeyRunAll": "x", - "currentPlayerIndex": 0, - "gameMode": "normal", // "normal" or "allTimers" - "isMuted": false, - "theme": "dark" -} -``` -## Developer Setup -### Clone the repository -```bash -git clone https://gitea.virtonline.eu/2HoursProject/nexus-timer.git -cd nexus-timer -``` -Run the live update server locally -```bash -npm run dev -``` -### Modify the app -Make code changes... -### Test the PWA locally -Open it in your browser: -[http://localhost:8080/](http://localhost:8080/) -### Git Pre-Commit Hook -This project uses a Git pre-commit hook to automatically updates the build timestamp placeholder in `src/views/InfoView.vue` and increments the `CACHE_VERSION` in `public/service-worker.js` (it is the indicator for the installed PWA that the new version is available). This ensures that each commit intended for a build/deployment reflects the correct information. -#### Configure Git to use the local hooks directory -Tell the Git to use the hooks located in the `.githooks` directory: -```bash -git config core.hooksPath .githooks -``` -This step needs to be done once per local clone of the repository. The script `scripts/git-hooks/pre-commit.cjs` will be executed before every commit. -### Commit & Push -Stage changes, commit and push -```bash -git add . -git commit -m 'fixed visuals' -git push -``` -## Building for the production -### On the Server -Navigate to the service directory on the server -```bash -cd /virt -``` -Clone the repository -```bash -git clone --depth 1 https://gitea.virtonline.eu/2HoursProject/nexus-timer.git -cd nexus-timer -``` -Build the docker image -```bash -docker build -t virt-nexus-timer . -``` -## Exposing the App Behind Traefik (Reverse Proxy) -### Review the provided docker labels and systemd service file -Copy the example label file to its destination -```bash -cp docker/traefik.labels labels -``` -View the example service definition: -```bash -cat systemd/virt-nexus-timer.service -``` -### Create the systemd service -Use the editor to create or overwrite the service: -```bash -sudo systemctl edit --force --full virt-nexus-timer.service -``` -Paste the content from `systemd/virt-nexus-timer.service`, then save and exit. - -Enable on system boot and start the service -```bash -sudo systemctl enable --now virt-nexus-timer.service -``` -Check the service status -```bash -systemctl status virt-nexus-timer.service -``` -View real-time logs -```bash -journalctl -fu virt-nexus-timer.service -``` -### Test the web application -Verify that the application is accessible via HTTPS: -```bash -curl https://nexus-timer.virtonline.eu -``` -Or open it in your browser: -[https://nexus-timer.virtonline.eu](https://nexus-timer.virtonline.eu) - -## Release the update -### On the Server -Navigate to the app directory on your server -```bash -cd /virt/nexus-timer -``` -Pull the changes, build the docker image and restart the service -```bash -git pull && docker build -t virt-nexus-timer . && systemctl restart virt-nexus-timer.service -``` -View real-time logs -```bash -journalctl -fu virt-nexus-timer.service -``` -The previously installed PWA should update automatically or offer an upgrade diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..bb7d7ce --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,58 @@ +## Tech Stack +* **HTML5:** For structuring the user interface. +* **CSS3:** For styling and visual presentation, including animations. Consider a CSS framework like Tailwind CSS for rapid prototyping. +* **JavaScript:** For application logic, timer functionality, and event handling. +* **Web Audio API:** For audio feedback (ticking sounds, alerts). +* **Browser API:** For capturing Players' photo. +* **Screen Wake Lock API:** For preventing of the screen lock in a PWA. +* **Local Storage/IndexedDB:** For persistent storage of player data, timer states, and settings. +* **Service Worker:** Essential for PWA functionality (offline access, push notifications - potential future feature). +* **Manifest File:** Defines the PWA's metadata (name, icons, theme color). +* **Web Framework:** Use Vue.js +* **Tailwind CSS:** Utility-first CSS framework for rapid UI development. + +## Data Model (For AI Generation) +```json +{ + "players": [ + { + "id": "1", + "name": "Player 1", + "avatar": null, + "initialTimerSec": 3600, + "currentTimerSec": 3600, + "hotkey": "a", + "isSkipped": false + }, + { + "id": "2", + "name": "Player 2", + "avatar": null, + "initialTimerSec": 3600, + "currentTimerSec": 3600, + "hotkey": "b", + "isSkipped": false + } + ], + "globalHotkeyStopPause": "s", + "globalHotkeyRunAll": "x", + "currentPlayerIndex": 0, + "gameMode": "normal", // "normal" or "allTimers" + "isMuted": false, + "theme": "dark" +} +``` + +## Build-time Information & Service Worker Versioning +The application incorporates build-time information and a mechanism for service worker updates: + +1. **Build Timestamp:** + * The build date and time are automatically injected into the application during the Vite build process. + * This is configured in `vite.config.js` using Vite's `define` feature, making `import.meta.env.VITE_APP_BUILD_TIME` available in the Vue components. + * The timestamp (formatted for the `sk-SK` locale) is displayed on the "About" screen (`src/views/InfoView.vue`). + +2. **Service Worker Cache Versioning:** + * The `CACHE_VERSION` constant within the service worker (`src/sw.js`) is also dynamically generated during the Vite build. + * `vite.config.js` uses the `define` feature to replace a placeholder (`__APP_CACHE_VERSION__`) in `src/sw.js` with a unique version string. This string typically incorporates the application's version from `package.json` and a build timestamp (`Date.now()`) to ensure uniqueness. + * The `src/sw.js` file is configured as a separate Rollup entry point in `vite.config.js` so that Vite processes it and performs this replacement, outputting the final `service-worker.js` to the `dist` directory root. + * When a new version of the app is deployed with a changed `service-worker.js` (due to this new `CACHE_VERSION`), the browser detects the difference. The updated service worker installs, and upon activation, it clears out old caches associated with previous versions. This mechanism is key to how the PWA updates and provides users with the latest assets. The "Check for Update" feature on the "About" screen manually triggers the browser to check for a new `service-worker.js` file. \ No newline at end of file diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..7903c8a --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,68 @@ +## Deployment Setup +### On the Server +Navigate to the service directory on the server +```bash +cd /virt +``` +Clone the repository +```bash +git clone --depth 1 https://gitea.virtonline.eu/2HoursProject/nexus-timer.git +cd nexus-timer +``` +Build the docker image +```bash +docker build -t virt-nexus-timer . +``` +## Exposing the App Behind Traefik (Reverse Proxy) +### Review the provided docker labels and systemd service file + +Copy the example label file to its destination +```bash +cp docker/traefik.labels labels +``` +View the example service definition: +```bash +cat systemd/virt-nexus-timer.service +``` +### Create the systemd service +Use the editor to create or overwrite the service: +```bash +sudo systemctl edit --force --full virt-nexus-timer.service +``` +Paste the content from `systemd/virt-nexus-timer.service`, then save and exit. + +Enable on system boot and start the service +```bash +sudo systemctl enable --now virt-nexus-timer.service +``` +Check the service status +```bash +systemctl status virt-nexus-timer.service +``` +View real-time logs +```bash +journalctl -fu virt-nexus-timer.service +``` +### Test the web application +Verify that the application is accessible via HTTPS: +```bash +curl https://nexus-timer.virtonline.eu +``` +Or open it in your browser: +[https://nexus-timer.virtonline.eu](https://nexus-timer.virtonline.eu) + +## Release the update +### On the Server +Navigate to the app directory on your server +```bash +cd /virt/nexus-timer +``` +Pull the changes, build the docker image and restart the service +```bash +git pull && docker build -t virt-nexus-timer . && systemctl restart virt-nexus-timer.service +``` +View real-time logs +```bash +journalctl -fu virt-nexus-timer.service +``` +The previously installed PWA should update automatically or offer an upgrade \ No newline at end of file diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..076b3e6 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,22 @@ +## Developer Setup +Clone the repository +```bash +git clone https://gitea.virtonline.eu/2HoursProject/nexus-timer.git +cd nexus-timer +``` +Run the live update server locally +```bash +npm run dev +``` +### Modify the app +Make code changes... +### Test the PWA locally +Open it in your browser: +[http://localhost:8080/](http://localhost:8080/) +### Commit & Push +Stage changes, commit and push +```bash +git add . +git commit -m 'My cool feature' +git push +``` \ No newline at end of file diff --git a/public/service-worker.js b/public/service-worker.js deleted file mode 100644 index e2a257b..0000000 --- a/public/service-worker.js +++ /dev/null @@ -1,154 +0,0 @@ -const CACHE_VERSION = 'nexus-timer-cache-v10'; -const APP_SHELL_URLS = [ - // '/', // Let NetworkFirst handle '/' - '/manifest.json', - '/favicon.ico', - '/icons/icon-192x192.png', - '/icons/icon-512x512.png', - '/icons/maskable-icon-192x192.png', - '/icons/maskable-icon-512x512.png', - '/icons/shortcut-setup-96x96.png', - '/icons/shortcut-info-96x96.png', - // Add Vite output paths? Usually hard due to hashing. Rely on dynamic caching. -]; - -self.addEventListener('install', event => { - console.log(`[SW ${CACHE_VERSION}] Install`); - event.waitUntil( - caches.open(CACHE_VERSION) - .then(cache => { - console.log(`[SW ${CACHE_VERSION}] Caching app shell essentials`); - return cache.addAll(APP_SHELL_URLS); // Cache core static assets - }) - .then(() => self.skipWaiting()) - ); -}); - -self.addEventListener('activate', event => { - console.log(`[SW ${CACHE_VERSION}] Activate`); - event.waitUntil( - caches.keys().then(cacheNames => { - return Promise.all( - cacheNames.map(cacheName => { - if (cacheName !== CACHE_VERSION) { - console.log(`[SW ${CACHE_VERSION}] Deleting old cache: ${cacheName}`); - return caches.delete(cacheName); - } - }) - ); - }).then(() => self.clients.claim()) - ); -}); - -self.addEventListener('fetch', event => { - const { request } = event; - const url = new URL(request.url); - - // Ignore non-GET requests & non-http protocols - if (request.method !== 'GET' || !url.protocol.startsWith('http')) { - return; - } - - // --- Strategy 1: Network First for HTML --- - if (request.mode === 'navigate' || (request.destination === 'document' || url.pathname === '/')) { - event.respondWith( - fetch(request) - .then(response => { - // If fetch is successful, cache it and return it - if (response.ok) { - const responseClone = response.clone(); - caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); - } - return response; - }) - .catch(async () => { - // If fetch fails, try cache - const cachedResponse = await caches.match(request); - if (cachedResponse) { - return cachedResponse; - } - // If specific request not in cache, try root '/' as SPA fallback - const rootCache = await caches.match('/'); - if (rootCache) { - return rootCache; - } - // Optional: return a proper offline fallback page if available - // return caches.match('/offline.html'); - // Or just let the browser show its offline error - return new Response('Network error and no cache found.', { status: 404, statusText: 'Not Found' }); - }) - ); - return; - } - - // --- Strategy 2: Stale-While-Revalidate for assets --- - if (request.destination === 'style' || - request.destination === 'script' || - request.destination === 'worker' || - request.destination === 'image' || - request.destination === 'font') { - event.respondWith( - caches.open(CACHE_VERSION).then(cache => { // Open cache first - return cache.match(request).then(cachedResponse => { - // Fetch in parallel (don't wait for it here) - const fetchPromise = fetch(request).then(networkResponse => { - // Check if fetch was successful - if (networkResponse.ok) { - // Clone before caching - const responseToCache = networkResponse.clone(); - // Update the cache with the network response - cache.put(request, responseToCache); - } else { - console.warn(`[SW ${CACHE_VERSION}] Fetch for ${request.url} failed with status ${networkResponse.status}`); - } - // Return the original network response for the fetch promise resolution - // This isn't directly used for the *initial* response unless cache misses. - return networkResponse; - }).catch(err => { - console.warn(`[SW ${CACHE_VERSION}] Fetch error for ${request.url}:`, err); - // If fetch fails, we just rely on the cache if it existed. - // Re-throw error so if cache also missed, browser knows resource failed. - throw err; - }); - - // Return the cached response immediately if available. - // If not cached, this strategy requires waiting for the fetch. - // The common implementation returns cache THEN fetches, but if cache misses, - // the user waits for the network. Let's return fetchPromise if no cache. - return cachedResponse || fetchPromise; - - }).catch(err => { - // Error during cache match or subsequent fetch - console.error(`[SW ${CACHE_VERSION}] Error handling fetch for ${request.url}:`, err); - // Fallback to network just in case cache interaction failed? Or let browser handle. - // Let's try network as a last resort if cache fails. - return fetch(request); - }); - }) - ); - return; - } - - // --- Strategy 3: Cache First for others --- - // (e.g., manifest, potentially icons not pre-cached) - event.respondWith( - caches.match(request) - .then(response => { - return response || fetch(request).then(networkResponse => { - if(networkResponse.ok) { - const responseClone = networkResponse.clone(); - caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); - } - return networkResponse; - }); - }) - ); -}); - -// Listener for skipWaiting message -self.addEventListener('message', event => { - if (event.data && event.data.action === 'skipWaiting') { - console.log('[SW] Received skipWaiting message, activating new SW.'); - self.skipWaiting(); - } -}); \ No newline at end of file diff --git a/scripts/git-hooks/pre-commit.cjs b/scripts/git-hooks/pre-commit.cjs deleted file mode 100755 index c3d4e30..0000000 --- a/scripts/git-hooks/pre-commit.cjs +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); - -const projectRoot = path.resolve(__dirname, '../..'); -const infoViewFile = path.join(projectRoot, 'src/views/InfoView.vue'); -const serviceWorkerFile = path.join(projectRoot, 'public/service-worker.js'); - -console.log('Running pre-commit hook...'); - -// --- 1. Update Build Time in InfoView.vue --- -try { - let infoViewContent = fs.readFileSync(infoViewFile, 'utf8'); - - // Regex to find the line assigning to buildTime.value or ref("...") - // It looks for ref("...") containing either __BUILD_TIME__ or a date-like string. - // This regex captures the part inside ref("..."). - const buildTimeAssignmentRegex = /const buildTime = ref\s*\(\s*["']([^"']*)["']\s*\);/; - const matchBuildTime = infoViewContent.match(buildTimeAssignmentRegex); - - const now = new Date(); - const newTimestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; - - if (matchBuildTime && matchBuildTime[0]) { // If the line `const buildTime = ref(...)` is found - // Replace the entire matched line with the new timestamp - const oldLine = matchBuildTime[0]; - const newLine = `const buildTime = ref("${newTimestamp}");`; - - infoViewContent = infoViewContent.replace(oldLine, newLine); - - fs.writeFileSync(infoViewFile, infoViewContent, 'utf8'); - console.log(`Updated build time in ${path.basename(infoViewFile)} to: ${newTimestamp}`); - execSync(`git add "${infoViewFile}"`, { stdio: 'inherit' }); - } else { - console.warn(`Could not find the buildTime ref assignment line in ${path.basename(infoViewFile)}. Skipping build time update.`); - // You might want to make this an error if the line should always exist after the first run. - // For now, just a warning. - } -} catch (error) { - console.error(`Error updating build time in ${infoViewFile}:`, error); - process.exit(1); -} - -// --- 2. Increment Service Worker Cache Version --- -try { - let swContent = fs.readFileSync(serviceWorkerFile, 'utf8'); - const cacheVersionRegex = /const CACHE_VERSION = ['"](nexus-timer-cache-v)(\d+)['"];/; - const matchCache = swContent.match(cacheVersionRegex); - - if (matchCache && matchCache[1] && matchCache[2]) { - const prefix = matchCache[1]; - const currentVersion = parseInt(matchCache[2], 10); - const newVersion = currentVersion + 1; - const newCacheVersionLine = `const CACHE_VERSION = '${prefix}${newVersion}';`; - - swContent = swContent.replace(cacheVersionRegex, newCacheVersionLine); - fs.writeFileSync(serviceWorkerFile, swContent, 'utf8'); - console.log(`Updated Service Worker cache version in ${path.basename(serviceWorkerFile)} to: v${newVersion}`); - execSync(`git add "${serviceWorkerFile}"`, { stdio: 'inherit' }); - } else { - console.warn(`Could not find or parse CACHE_VERSION in ${path.basename(serviceWorkerFile)}. Skipping version increment.`); - } -} catch (error) { - console.error(`Error updating Service Worker cache version in ${serviceWorkerFile}:`, error); - process.exit(1); -} - -console.log('Pre-commit hook finished successfully.'); -process.exit(0); \ No newline at end of file diff --git a/src/sw.js b/src/sw.js new file mode 100644 index 0000000..0cc7aac --- /dev/null +++ b/src/sw.js @@ -0,0 +1,150 @@ +// This global constant __APP_CACHE_VERSION__ will be replaced by Vite +// during the build process due to the `define` config in vite.config.js. +const CACHE_VERSION = typeof __APP_CACHE_VERSION__ !== 'undefined' + ? __APP_CACHE_VERSION__ + : 'nexus-timer-cache-fallback-dev-vManual'; // Fallback for dev or if define fails + +const APP_SHELL_URLS = [ + // Note: '/' (index.html) is handled by NetworkFirst strategy, no need to precache explicitly here. + '/manifest.json', // Will be served from public, copied to dist root + '/favicon.ico', // Will be served from public, copied to dist root + // Icons from public/icons, will be copied to dist/icons + '/icons/icon-192x192.png', + '/icons/icon-512x512.png', + '/icons/maskable-icon-192x192.png', + '/icons/maskable-icon-512x512.png', + '/icons/shortcut-setup-96x96.png', + '/icons/shortcut-info-96x96.png', + // Any other critical static assets from the public folder that should be part of the app shell +]; + +self.addEventListener('install', event => { + console.log(`[SW ${CACHE_VERSION}] Install`); + event.waitUntil( + caches.open(CACHE_VERSION) + .then(cache => { + console.log(`[SW ${CACHE_VERSION}] Caching app shell essentials`); + return cache.addAll(APP_SHELL_URLS); + }) + .then(() => { + console.log(`[SW ${CACHE_VERSION}] Skip waiting on install.`); + return self.skipWaiting(); + }) + ); +}); + +self.addEventListener('activate', event => { + console.log(`[SW ${CACHE_VERSION}] Activate`); + event.waitUntil( + caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + if (cacheName !== CACHE_VERSION) { + console.log(`[SW ${CACHE_VERSION}] Deleting old cache: ${cacheName}`); + return caches.delete(cacheName); + } + }) + ); + }).then(() => { + console.log(`[SW ${CACHE_VERSION}] Clients claimed.`); + return self.clients.claim(); + }) + ); +}); + +self.addEventListener('fetch', event => { + const { request } = event; + const url = new URL(request.url); + + if (request.method !== 'GET' || !url.protocol.startsWith('http')) { + // console.log(`[SW ${CACHE_VERSION}] Ignoring non-GET or non-http(s) request: ${request.url}`); + return; + } + + // Strategy 1: Network First for HTML (navigations or direct / request) + if (request.mode === 'navigate' || (request.destination === 'document' || url.pathname === '/')) { + // console.log(`[SW ${CACHE_VERSION}] NetworkFirst for: ${request.url}`); + event.respondWith( + fetch(request) + .then(response => { + if (response.ok) { + const responseClone = response.clone(); + caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); + } + return response; + }) + .catch(async () => { + // console.warn(`[SW ${CACHE_VERSION}] Network fetch failed for ${request.url}, trying cache.`); + const cachedResponse = await caches.match(request); + if (cachedResponse) return cachedResponse; + // Fallback to root /index.html from cache if specific page not found offline + const rootCache = await caches.match('/'); + if (rootCache) return rootCache; + // console.error(`[SW ${CACHE_VERSION}] Network and cache miss for navigation: ${request.url}`); + return new Response('Network error: You are offline and this page is not cached.', { + status: 404, + statusText: 'Not Found', + headers: { 'Content-Type': 'text/html' } // Important for SPA offline fallback + }); + }) + ); + return; + } + + // Strategy 2: Stale-While-Revalidate for assets (CSS, JS, images, fonts) + if (request.destination === 'style' || + request.destination === 'script' || + request.destination === 'worker' || + request.destination === 'image' || + request.destination === 'font') { + // console.log(`[SW ${CACHE_VERSION}] StaleWhileRevalidate for: ${request.url}`); + event.respondWith( + caches.open(CACHE_VERSION).then(cache => { + return cache.match(request).then(cachedResponse => { + const fetchPromise = fetch(request).then(networkResponse => { + if (networkResponse.ok) { + const responseToCache = networkResponse.clone(); + cache.put(request, responseToCache); + } else { + // console.warn(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Network fetch for ${request.url} failed with status ${networkResponse.status}`); + } + return networkResponse; + }).catch(err => { + // console.warn(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Network fetch error for ${request.url}:`, err); + // If fetch fails, and we already served from cache, that's okay. + // If cache also missed (i.e., cachedResponse was null), then this error will propagate. + throw err; + }); + return cachedResponse || fetchPromise; + }).catch(err => { + // This catch block handles errors from cache.match() or if fetchPromise was returned and rejected + // console.error(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Error for ${request.url}. Trying network fallback.`, err); + return fetch(request); // Final fallback to network if cache interactions fail + }); + }) + ); + return; + } + + // Strategy 3: Cache First for other types of requests (e.g., manifest.json if not in APP_SHELL_URLS) + // console.log(`[SW ${CACHE_VERSION}] CacheFirst for: ${request.url}`); + event.respondWith( + caches.match(request) + .then(response => { + return response || fetch(request).then(networkResponse => { + if(networkResponse.ok) { + const responseClone = networkResponse.clone(); + caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); + } + return networkResponse; + }); + }) + ); +}); + +self.addEventListener('message', event => { + if (event.data && event.data.action === 'skipWaiting') { + console.log(`[SW ${CACHE_VERSION}] Received skipWaiting message, activating new SW.`); + self.skipWaiting(); + } +}); \ No newline at end of file diff --git a/src/views/InfoView.vue b/src/views/InfoView.vue index f4b092f..6503e89 100644 --- a/src/views/InfoView.vue +++ b/src/views/InfoView.vue @@ -57,16 +57,23 @@