added build-time information
This commit is contained in:
@@ -1 +0,0 @@
|
||||
../scripts/git-hooks/pre-commit.cjs
|
||||
151
README.md
151
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.
|
||||
* **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
|
||||
|
||||
58
docs/architecture.md
Normal file
58
docs/architecture.md
Normal file
@@ -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.
|
||||
68
docs/deployment.md
Normal file
68
docs/deployment.md
Normal file
@@ -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
|
||||
22
docs/development.md
Normal file
22
docs/development.md
Normal file
@@ -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
|
||||
```
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
150
src/sw.js
Normal file
150
src/sw.js
Normal file
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -57,16 +57,23 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const buildTime = ref("2025-05-09 20:58:36");
|
||||
// --- Build Time from Vite Environment Variable ---
|
||||
const buildTime = ref('N/A'); // Default value
|
||||
|
||||
onMounted(() => {
|
||||
// Access the Vite-defined environment variable
|
||||
// It's available after the module is imported and setup runs
|
||||
buildTime.value = import.meta.env.VITE_APP_BUILD_TIME || 'Unknown';
|
||||
});
|
||||
// --- End Build Time ---
|
||||
|
||||
// --- PWA Update Check Logic ---
|
||||
const canCheckForUpdate = ref('serviceWorker' in navigator);
|
||||
const checkingForUpdate = ref(false);
|
||||
const updateStatusMessage = ref('');
|
||||
@@ -78,55 +85,39 @@ const checkForUpdates = async () => {
|
||||
updateError.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
checkingForUpdate.value = true;
|
||||
updateStatusMessage.value = 'Checking for updates...';
|
||||
updateError.value = false;
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.getRegistration();
|
||||
if (!registration) {
|
||||
updateStatusMessage.value = 'No active service worker found. Try reloading.';
|
||||
updateStatusMessage.value = 'No active service worker. Try reloading.';
|
||||
updateError.value = true;
|
||||
checkingForUpdate.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// This attempts to update the service worker.
|
||||
// If a new version is found, it will be installed in the background.
|
||||
// The update bar logic in App.vue will handle prompting the user to refresh.
|
||||
await registration.update();
|
||||
|
||||
// Check if an update was found and is now waiting
|
||||
// This needs a small delay for the update process to potentially complete
|
||||
setTimeout(() => {
|
||||
const newWorker = registration.waiting;
|
||||
if (newWorker) {
|
||||
updateStatusMessage.value = 'A new version has been downloaded! Refresh prompt will appear soon or on next load.';
|
||||
updateStatusMessage.value = 'New version downloaded! Refresh prompt may appear.';
|
||||
updateError.value = false;
|
||||
// You could also trigger the App.vue update bar logic directly if needed
|
||||
// e.g., by emitting an event or calling a global function/store action.
|
||||
// For now, relying on App.vue's existing SW listeners.
|
||||
} else if (registration.active && registration.installing) {
|
||||
updateStatusMessage.value = 'New version is installing...';
|
||||
updateStatusMessage.value = 'New version installing...';
|
||||
updateError.value = false;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
updateStatusMessage.value = 'You are on the latest version.';
|
||||
updateError.value = false;
|
||||
}
|
||||
checkingForUpdate.value = false;
|
||||
}, 2000); // Give some time for SW update process
|
||||
|
||||
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Error checking for PWA updates:', error);
|
||||
updateStatusMessage.value = 'Error checking for updates. See console.';
|
||||
updateStatusMessage.value = 'Error checking updates. See console.';
|
||||
updateError.value = true;
|
||||
checkingForUpdate.value = false;
|
||||
}
|
||||
};
|
||||
// --- End PWA Update Check Logic ---
|
||||
|
||||
const goBack = () => {
|
||||
if (store.getters.players && store.getters.players.length >= 2) {
|
||||
|
||||
@@ -1,10 +1,52 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import fs from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
const appVersion = packageJson.version;
|
||||
|
||||
// Options for date formatting
|
||||
const dateTimeFormatOptions = {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hour12: false // Use 24-hour format
|
||||
};
|
||||
// Generate build time string using Slovak locale
|
||||
const appBuildTime = new Date().toLocaleString('sk-SK', dateTimeFormatOptions);
|
||||
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 8080
|
||||
},
|
||||
define: {
|
||||
'import.meta.env.VITE_APP_BUILD_TIME': JSON.stringify(appBuildTime),
|
||||
'__APP_CACHE_VERSION__': JSON.stringify(`nexus-timer-cache-v${appVersion}-${Date.now()}`)
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, 'index.html'),
|
||||
sw: resolve(__dirname, 'src/sw.js') // Assuming sw.js is now in src
|
||||
},
|
||||
output: {
|
||||
entryFileNames: assetInfo => {
|
||||
// Output service-worker.js to the root of the dist directory
|
||||
if (assetInfo.name === 'sw') {
|
||||
return 'service-worker.js'; // Ensure consistent name
|
||||
}
|
||||
})
|
||||
// Default naming for other entry points/chunks
|
||||
return 'assets/[name]-[hash].js';
|
||||
},
|
||||
// For chunks, if any are generated from sw.js (unlikely for simple SW)
|
||||
chunkFileNames: 'assets/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||
}
|
||||
},
|
||||
// Set to false if you don't want to empty the dist dir on each build
|
||||
// but usually true is good for clean builds.
|
||||
emptyOutDir: true,
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user