Compare commits

..

24 Commits

Author SHA1 Message Date
cpu
832f19235f refactor 2025-03-29 03:45:26 +01:00
cpu
5763407edc clean up 2025-03-28 22:36:08 +01:00
cpu
08632ee711 button battery level warning 2025-03-28 15:31:28 +01:00
cpu
451a61d357 added auth so subscribe request 2025-03-28 15:06:02 +01:00
cpu
0414bdb217 fix flic handler functions 2025-03-28 06:26:06 +01:00
cpu
e14c12ce66 clean up 2025-03-28 05:58:58 +01:00
cpu
2e47461f34 clean up 2025-03-28 05:50:28 +01:00
cpu
d1ad962ec3 refactoring 2025-03-28 05:04:25 +01:00
cpu
96aeb22210 added more button actions 2025-03-28 03:54:36 +01:00
cpu
75b8dc5d15 labels example 2025-03-27 00:35:51 +01:00
cpu
98ffee7764 handle all button types 2025-03-26 22:53:07 +01:00
cpu
429c1dd557 handle all button types 2025-03-26 22:41:22 +01:00
cpu
80989a248e remote button triggers next player 2025-03-26 21:57:45 +01:00
cpu
bade9c0a15 added unsubscribe 2025-03-26 21:15:47 +01:00
cpu
3561db616c updated public key 2025-03-26 21:01:18 +01:00
cpu
fc278ed256 push notifications 2025-03-26 05:04:50 +01:00
cpu
a0f3489656 externalise deeplinks 2025-03-24 01:52:53 +01:00
cpu
d959f4929d handling deeplinks 2025-03-24 01:21:53 +01:00
cpu
df1e316930 clean up 2025-03-24 01:02:06 +01:00
cpu
2838df5e05 added URL scheme/deep linking 2025-03-24 00:43:55 +01:00
cpu
8a6947f4ea bigger picture, footer 2025-03-23 20:14:50 +01:00
cpu
1cfcd628d4 added dockerfile and updated Readme 2025-03-23 12:32:48 +01:00
cpu
21cb105cd0 added dockerfile and updated Readme 2025-03-22 23:17:40 +01:00
cpu
fdb6e5e618 Initial commit 2025-03-22 22:35:45 +01:00
32 changed files with 397 additions and 1391 deletions

View File

@@ -49,12 +49,9 @@ build/
# We need .env for our application # We need .env for our application
#.env #.env
.env.* .env.*
# Don't ignore config.env.js
!config.env.js
# Project specific files # Project specific files
dev-start.sh
generate-config.sh
labels.example labels.example
virt-game-timer.service virt-game-timer.service
package.json package.json

1
.gitignore vendored
View File

@@ -19,7 +19,6 @@ yarn-error.log*
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
config.env.js
# Editor directories and files # Editor directories and files
.idea/ .idea/

View File

@@ -1,38 +1,14 @@
# Use a lightweight server # Use a lightweight server
FROM nginx:alpine FROM nginx:alpine
# Install bash for the script execution
RUN apk add --no-cache bash
# Set working directory # Set working directory
WORKDIR /usr/share/nginx/html WORKDIR /usr/share/nginx/html
# Copy all the application files # Copy all the application files
COPY . . COPY . .
# Create a simple script to generate config.env.js # Copy the .env file
RUN echo '#!/bin/sh' > /usr/share/nginx/html/docker-generate-config.sh && \ COPY .env .
echo 'echo "// config.env.js - Generated from .env" > config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
echo 'echo "// This file contains environment variables for the PWA" >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
echo 'echo "// Generated on $(date)" >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
echo 'echo "" >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
echo 'echo "window.ENV_CONFIG = {" >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
echo 'grep -v "^#" .env | grep "=" | while read line; do' >> /usr/share/nginx/html/docker-generate-config.sh && \
echo ' key=$(echo $line | cut -d= -f1)' >> /usr/share/nginx/html/docker-generate-config.sh && \
echo ' value=$(echo $line | cut -d= -f2-)' >> /usr/share/nginx/html/docker-generate-config.sh && \
echo ' echo " $key: \"$value\"," >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
echo 'done' >> /usr/share/nginx/html/docker-generate-config.sh && \
echo 'echo "};" >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
chmod +x /usr/share/nginx/html/docker-generate-config.sh
# Generate config.env.js from .env
RUN /usr/share/nginx/html/docker-generate-config.sh
# Uninstall bash
RUN apk del bash
# Remove the .env file and the generation script for security
RUN rm .env docker-generate-config.sh
# Expose port 80 # Expose port 80
EXPOSE 80 EXPOSE 80

View File

@@ -27,7 +27,7 @@ game-timer/
## Environment Variables ## Environment Variables
The application uses environment variables for configuration. These are loaded from a `.env` file and converted to a `config.env.js` file that is served by the web server. The application uses environment variables for configuration. These are loaded from a `.env` file at runtime.
### Setting Up Environment Variables ### Setting Up Environment Variables
@@ -45,12 +45,7 @@ The application uses environment variables for configuration. These are loaded f
BACKEND_URL=https://your-push-server.example.com BACKEND_URL=https://your-push-server.example.com
``` ```
3. Generate the `config.env.js` file using the provided script: 3. For security, never commit your `.env` file to version control. It's already included in `.gitignore`.
```bash
./generate-config.sh
```
4. For security, never commit your `.env` file to version control. It's already included in `.gitignore`.
### Generating VAPID Keys ### Generating VAPID Keys

View File

@@ -353,124 +353,18 @@ input[type="file"] {
cursor: pointer; cursor: pointer;
} }
/* Push notification controls */ .app-footer {
.push-notification-controls { background-color: #2c3e50;
margin-right: 10px;
}
.notification-status-container {
margin: 1rem 0;
padding: 1rem;
background-color: #f8f9fa;
border-radius: 4px;
}
.notification-status p {
margin-bottom: 0.5rem;
}
.advanced-options {
margin-top: 1rem;
display: flex;
justify-content: space-between;
border-top: 1px solid #eee;
padding-top: 1rem;
}
.advanced-options button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
flex: 1;
margin: 0 0.5rem;
}
.advanced-options button:first-child {
margin-left: 0;
}
.advanced-options button:last-child {
margin-right: 0;
}
/* Status indicators */
.status-granted {
color: #28a745;
font-weight: bold;
}
.status-denied {
color: #dc3545;
font-weight: bold;
}
.status-default {
color: #ffc107;
font-weight: bold;
}
.status-active {
color: #28a745;
font-weight: bold;
}
.status-inactive {
color: #6c757d;
font-weight: bold;
}
/* Service Worker Message Monitor Styles */
.message-monitor-section {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #eee;
}
.message-monitor-section h3 {
margin-bottom: 0.5rem;
}
.monitor-controls {
display: flex;
justify-content: space-between;
margin: 0.5rem 0;
}
.action-button {
background-color: #3498db;
color: white; color: white;
border: none; padding: 1rem;
padding: 0.5rem 1rem; text-align: center;
border-radius: 4px; font-size: 0.9rem;
cursor: pointer; margin-top: auto; /* This pushes the footer to the bottom when possible */
width: 100%;
} }
.action-button:hover { .author-info {
background-color: #2980b9; display: flex;
} flex-direction: column;
align-items: center;
button:disabled { gap: 0.3rem;
background-color: #cccccc !important; /* Gray background */
color: #888888 !important; /* Darker gray text */
cursor: not-allowed !important; /* Change cursor */
opacity: 0.7 !important; /* Reduce opacity */
}
.cancel-button:disabled {
background-color: #e0e0e0 !important; /* Light gray */
color: #999999 !important;
border: 1px solid #cccccc !important;
}
.message-output {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 0.75rem;
margin-top: 0.5rem;
height: 150px;
overflow-y: auto;
font-size: 0.85rem;
white-space: pre-wrap;
} }

View File

@@ -1,30 +0,0 @@
#!/bin/bash
# Script to start a local development server with environment variables
# Check if .env file exists
if [ ! -f .env ]; then
echo "Error: .env file not found!"
echo "Please create a .env file based on .env.example"
exit 1
fi
# Generate config.env.js from .env
echo "Generating config.env.js from .env..."
./generate-config.sh
# Start a local development server
echo "Starting development server..."
if command -v python3 &> /dev/null; then
echo "Using Python3 HTTP server on port 8000..."
python3 -m http.server 8000
elif command -v python &> /dev/null; then
echo "Using Python HTTP server on port 8000..."
python -m SimpleHTTPServer 8000
elif command -v npx &> /dev/null; then
echo "Using npx serve on port 8000..."
npx serve -l 8000
else
echo "Error: Could not find a suitable static file server."
echo "Please install Python or Node.js, or manually start a server."
exit 1
fi

View File

@@ -1,39 +0,0 @@
#!/bin/bash
# Script to generate config.env.js from .env file
# Usage: ./generate-config.sh
# Check if .env file exists
if [ ! -f .env ]; then
echo "Error: .env file not found!"
echo "Please create a .env file based on .env.example"
exit 1
fi
echo "Generating config.env.js from .env..."
# Create config.env.js file
echo "// config.env.js - Generated from .env" > config.env.js
echo "// This file contains environment variables for the PWA" >> config.env.js
echo "// Generated on $(date)" >> config.env.js
echo "" >> config.env.js
echo "window.ENV_CONFIG = {" >> config.env.js
# Read .env file line by line
while IFS="=" read -r key value || [ -n "$key" ]; do
# Skip comments and empty lines
[[ $key =~ ^#.*$ ]] && continue
[[ -z $key ]] && continue
# Remove quotes if present
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
# Add the key-value pair to config.env.js
echo " $key: \"$value\"," >> config.env.js
done < .env
echo "};" >> config.env.js
echo "config.env.js generated successfully!"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 340 KiB

View File

@@ -24,11 +24,6 @@
<body> <body>
<div class="app-container"> <div class="app-container">
<header class="header"> <header class="header">
<div class="push-notification-controls">
<button id="pushSettingsButton" class="header-button" title="Push Notification Settings">
<i class="fas fa-bell"></i>
</button>
</div>
<div class="game-controls"> <div class="game-controls">
<button id="gameButton" class="game-button">Start Game</button> <button id="gameButton" class="game-button">Start Game</button>
</div> </div>
@@ -117,46 +112,6 @@
</div> </div>
</div> </div>
<!-- Push Notification Settings Modal -->
<div id="pushSettingsModal" class="modal">
<div class="modal-content">
<h2>Push Notification Settings</h2>
<div class="notification-status-container">
<div class="notification-status">
<p><strong>Notification Permission: </strong><span id="notificationPermissionStatus">Unknown</span></p>
<p><strong>Subscription Status: </strong><span id="subscriptionStatus">Unknown</span></p>
</div>
</div>
<div class="form-group">
<label for="pushUsername">Username for Push Service</label>
<input type="text" id="pushUsername" placeholder="Enter username">
</div>
<div class="form-group">
<label for="pushPassword">Password</label>
<input type="password" id="pushPassword" placeholder="Enter password">
</div>
<div class="advanced-options">
<button type="button" id="pushUnsubscribeButton" class="cancel-button">Unsubscribe</button>
<button type="button" id="pushResubscribeButton" class="save-button">Subscribe</button>
</div>
<div class="form-buttons">
<button type="button" id="pushCancelButton" class="cancel-button">Cancel</button>
<button type="button" id="pushSaveButton" class="save-button">Save</button>
</div>
<!-- Message Monitor Section - No visible title -->
<div class="message-monitor-section">
<div class="monitor-controls">
<button type="button" id="simulateClickButton" class="action-button" style="margin-right: 10px;">Simulate Remote Button Click</button>
</div>
<pre id="swMessagesOutput" class="message-output">Monitoring for service worker messages...</pre>
</div>
</div>
</div>
<!-- Load environment configuration first -->
<script src="/config.env.js"></script>
<!-- Main application script --> <!-- Main application script -->
<script type="module" src="/js/app.js"></script> <script type="module" src="/js/app.js"></script>
<script> <script>
@@ -174,107 +129,13 @@
console.log('Notification permission:', permission); console.log('Notification permission:', permission);
}); });
} }
// Helper function to get stored credentials
async function getStoredCredentials() {
const storedAuth = localStorage.getItem('basicAuthCredentials');
if (!storedAuth) return null;
try {
const credentials = JSON.parse(storedAuth);
if (!credentials.username || !credentials.password) return null;
return credentials;
} catch (error) {
console.error('Failed to parse stored credentials:', error);
return null;
}
}
// Function to simulate a button click with default values
async function simulateButtonClick() {
const action = 'SingleClick'; // Default action
const buttonName = 'Game-button'; // Default button name
const batteryLevel = 100; // Default battery level
const output = document.getElementById('swMessagesOutput');
// Don't show the simulating text
try {
// Get credentials
const credentials = await getStoredCredentials();
if (!credentials) {
output.textContent = 'No credentials found. Please set up credentials first.\n';
return;
}
// Create basic auth header
const authHeader = 'Basic ' + btoa(`${credentials.username}:${credentials.password}`);
// Create timestamp (current time)
const timestamp = new Date().toISOString();
// Prepare request to backend webhook
let backendUrl;
try {
const configModule = await import('/js/config.js');
backendUrl = configModule.getBackendUrl();
} catch (error) {
output.textContent = 'Failed to load backend URL. Please check configuration.\n';
return;
}
const webhookUrl = `${backendUrl}/webhook/${action}`;
output.textContent = `Sending request to: ${webhookUrl}\n`;
output.textContent += `Authorization: Basic ****\n`;
output.textContent += `Button-Name: ${buttonName}\n`;
output.textContent += `Timestamp: ${timestamp}\n`;
output.textContent += `Button-Battery-Level: ${batteryLevel}\n\n`;
// Headers similar to the curl command
const headers = {
'Authorization': authHeader,
'Button-Name': buttonName,
'Timestamp': timestamp,
'Button-Battery-Level': batteryLevel.toString()
};
// Send GET request to webhook
const response = await fetch(webhookUrl, {
method: 'GET',
headers: headers,
credentials: 'include'
});
if (response.ok) {
let result;
try {
result = await response.json();
output.textContent += `Success! Response: ${JSON.stringify(result, null, 2)}\n`;
} catch (e) {
// Text response
result = await response.text();
output.textContent += `Success! Response: ${result}\n`;
}
} else {
let errorText;
try {
errorText = await response.text();
} catch (e) {
errorText = `Status ${response.status}`;
}
output.textContent += `Error: ${errorText}\n`;
}
} catch (error) {
output.textContent += `Error: ${error.message}\n`;
}
}
// Attach click event to the new button
const simulateClickButton = document.getElementById('simulateClickButton');
if (simulateClickButton) {
simulateClickButton.addEventListener('click', simulateButtonClick);
}
}); });
</script> </script>
<footer class="app-footer">
<div class="author-info">
<p>Vibe coded by Martin</p>
<p>Version 0.0.1</p>
</div>
</footer>
</body> </body>
</html> </html>

1
js Symbolic link
View File

@@ -0,0 +1 @@
src/js

View File

@@ -1,87 +0,0 @@
// env-loader.js
// This module is responsible for loading environment variables for the PWA
// Store environment variables in a global object
window.ENV_CONFIG = {
// Default values that will be overridden when config.env.js loads
PUBLIC_VAPID_KEY: 'your_public_vapid_key_here',
BACKEND_URL: 'https://your-push-server.example.com'
};
// Function to load environment variables from config.env.js
async function loadEnvVariables() {
try {
// Try to fetch the config.env.js file which will be generated at build time or deployment
const response = await fetch('/config.env.js');
if (!response.ok) {
console.warn('Could not load config.env.js file. Using default values.');
return;
}
const configText = await response.text();
// Extract the configuration object from the JavaScript file
// The file should be in format: window.ENV_CONFIG = { key: "value" };
try {
// Create a safe way to evaluate the config file
const configScript = document.createElement('script');
configScript.textContent = configText;
document.head.appendChild(configScript);
document.head.removeChild(configScript);
console.log('Environment variables loaded successfully from config.env.js');
// Dispatch an event to notify that environment variables have been loaded
window.dispatchEvent(new CustomEvent('env-config-loaded', {
detail: { config: window.ENV_CONFIG }
}));
} catch (parseError) {
console.error('Error parsing config.env.js:', parseError);
}
} catch (error) {
console.error('Error loading environment variables:', error);
}
}
// Export function to initialize environment variables
export async function initEnv() {
await loadEnvVariables();
return window.ENV_CONFIG;
}
// Start loading environment variables immediately
initEnv();
// Export access functions for environment variables
export function getEnv(key, defaultValue = '') {
return window.ENV_CONFIG[key] || defaultValue;
}
// Export a function to wait for environment variables to be loaded
export function waitForEnv() {
return new Promise((resolve) => {
// If we already have non-default values, resolve immediately
if (window.ENV_CONFIG.BACKEND_URL !== 'https://your-push-server.example.com') {
resolve(window.ENV_CONFIG);
return;
}
// Otherwise, wait for the env-config-loaded event
window.addEventListener('env-config-loaded', (event) => {
resolve(event.detail.config);
}, { once: true });
// Set a timeout to resolve with current values if loading takes too long
setTimeout(() => {
console.warn('Environment loading timed out, using current values');
resolve(window.ENV_CONFIG);
}, 3000);
});
}
export default {
initEnv,
getEnv,
waitForEnv
};

View File

@@ -1,128 +0,0 @@
// screenLockManager.js - Manages screen wake lock to prevent screen from turning off
// Uses the Screen Wake Lock API: https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API
let wakeLock = null;
let isLockEnabled = false;
/**
* Requests a screen wake lock to prevent the screen from turning off
* @returns {Promise<boolean>} - True if wake lock was acquired successfully
*/
export async function acquireWakeLock() {
if (!isScreenWakeLockSupported()) {
console.warn('[ScreenLockManager] Screen Wake Lock API not supported in this browser');
return false;
}
try {
// Release any existing wake lock first
await releaseWakeLock();
// Request a new wake lock
wakeLock = await navigator.wakeLock.request('screen');
isLockEnabled = true;
console.log('[ScreenLockManager] Screen Wake Lock acquired');
// Add event listeners to reacquire the lock when needed
setupWakeLockListeners();
return true;
} catch (error) {
console.error('[ScreenLockManager] Error acquiring wake lock:', error);
isLockEnabled = false;
return false;
}
}
/**
* Releases the screen wake lock if one is active
* @returns {Promise<boolean>} - True if wake lock was released successfully
*/
export async function releaseWakeLock() {
if (!wakeLock) {
return true; // No wake lock to release
}
try {
await wakeLock.release();
wakeLock = null;
isLockEnabled = false;
console.log('[ScreenLockManager] Screen Wake Lock released');
return true;
} catch (error) {
console.error('[ScreenLockManager] Error releasing wake lock:', error);
return false;
}
}
/**
* Checks if the Screen Wake Lock API is supported in this browser
* @returns {boolean} - True if supported
*/
export function isScreenWakeLockSupported() {
return 'wakeLock' in navigator && 'request' in navigator.wakeLock;
}
/**
* Returns the current status of the wake lock
* @returns {boolean} - True if wake lock is currently active
*/
export function isWakeLockActive() {
return isLockEnabled && wakeLock !== null;
}
/**
* Sets up event listeners to reacquire the wake lock when needed
* (e.g., when the page becomes visible again after being hidden)
*/
function setupWakeLockListeners() {
// When the page becomes visible again, reacquire the wake lock
document.addEventListener('visibilitychange', handleVisibilityChange);
// When the screen orientation changes, reacquire the wake lock
if ('screen' in window && 'orientation' in window.screen) {
window.screen.orientation.addEventListener('change', handleOrientationChange);
}
}
/**
* Handles visibility change events to reacquire wake lock when page becomes visible
*/
async function handleVisibilityChange() {
if (isLockEnabled && document.visibilityState === 'visible') {
// Only try to reacquire if we previously had a lock
await acquireWakeLock();
}
}
/**
* Handles orientation change events to reacquire wake lock
*/
async function handleOrientationChange() {
if (isLockEnabled) {
// Some devices may release the wake lock on orientation change
await acquireWakeLock();
}
}
/**
* Initializes the screen lock manager
* @param {Object} options - Configuration options
* @param {boolean} options.autoAcquire - Whether to automatically acquire wake lock on init
* @returns {Promise<boolean>} - True if initialization was successful
*/
export async function initScreenLockManager(options = {}) {
const { autoAcquire = true } = options; // Default to true - automatically acquire on init
// Check for support
const isSupported = isScreenWakeLockSupported();
console.log(`[ScreenLockManager] Screen Wake Lock API ${isSupported ? 'is' : 'is not'} supported`);
// Automatically acquire wake lock if supported (now default behavior)
if (autoAcquire && isSupported) {
return await acquireWakeLock();
}
return isSupported;
}

View File

@@ -1,116 +0,0 @@
// serviceWorkerManager.js - Service worker registration and Flic integration
import * as pushFlic from './pushFlicIntegration.js';
// Store the action handlers passed from app.js
let flicActionHandlers = {};
export function setFlicActionHandlers(handlers) {
if (handlers && Object.keys(handlers).length > 0) {
flicActionHandlers = handlers;
console.log('[ServiceWorkerManager] Stored action handlers:', Object.keys(flicActionHandlers));
// Always pass handlers to pushFlic, regardless of service worker state
pushFlic.initPushFlic(flicActionHandlers);
} else {
console.warn('[ServiceWorkerManager] No action handlers provided to setFlicActionHandlers!');
}
}
// --- Flic Integration Setup ---
export function initFlic() {
// Make sure we have handlers before initializing
if (Object.keys(flicActionHandlers).length === 0) {
console.warn('[ServiceWorkerManager] No Flic handlers registered before initFlic! Actions may not work.');
}
// This function is used by setupServiceWorker and relies on
// flicActionHandlers being set before this is called
console.log('[ServiceWorkerManager] Initializing PushFlic with handlers:', Object.keys(flicActionHandlers));
pushFlic.initPushFlic(flicActionHandlers);
}
// Export functions for manually triggering push notifications setup
export function setupPushNotifications() {
pushFlic.setupPushNotifications();
}
// --- Handle Messages from Service Worker ---
export function flicMessageHandler(event) {
// This function is passed to setupServiceWorker and called when a message arrives from the service worker
console.log('[App] Message received from Service Worker:', event.data);
// Check if this is a Flic action message
if (event.data && event.data.type === 'flic-action') {
const { action, button, timestamp, batteryLevel } = event.data;
try {
// Pass to push-flic service to handle
pushFlic.handleFlicAction(action, button, timestamp, batteryLevel);
} catch (error) {
console.error('[App] Error handling flic action:', error);
}
}
}
// Global message handler function to ensure we catch all service worker messages
function handleServiceWorkerMessage(event) {
// Check if the message might be from our service worker
if (event.data && typeof event.data === 'object') {
console.log('[App] Potential window message received:', event.data);
// If it looks like a flic action message, handle it
if (event.data.type === 'flic-action') {
try {
// Process the message with our flicMessageHandler
flicMessageHandler(event);
} catch (error) {
console.error('[App] Error handling window message:', error);
}
}
}
}
// --- Service Worker and PWA Setup ---
export function setupServiceWorker(messageHandler) {
if ('serviceWorker' in navigator) {
console.log('[ServiceWorkerManager] Setting up service worker...');
// Set up global message event listener on window object
window.addEventListener('message', handleServiceWorkerMessage);
// Listen for messages FROM the Service Worker
// This is the main way messages from the service worker are received
navigator.serviceWorker.addEventListener('message', event => {
console.log('[ServiceWorkerManager] Service worker message received:', event.data);
messageHandler(event);
});
window.addEventListener('load', () => {
console.log('[ServiceWorkerManager] Window loaded, registering service worker...');
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('[ServiceWorkerManager] ServiceWorker registered successfully.');
// Add an event listener that will work with service worker controlled clients
if (navigator.serviceWorker.controller) {
console.log('[ServiceWorkerManager] Service worker already controlling the page.');
}
// Initialize Flic integration
initFlic();
})
.catch(error => {
console.error('[ServiceWorkerManager] ServiceWorker registration failed:', error);
});
});
// Listen for SW controller changes
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('[ServiceWorkerManager] Service Worker controller changed, potentially updated.');
});
} else {
console.warn('[ServiceWorkerManager] ServiceWorker not supported.');
}
}

View File

@@ -1,435 +0,0 @@
// pushSettingsUI.js - UI handling for push notification settings
import { setupPushNotifications } from '../services/serviceWorkerManager.js';
import { FLIC_BUTTON_ID, getBackendUrl} from '../config.js';
// --- DOM Elements ---
const elements = {
pushSettingsButton: null,
pushSettingsModal: null,
notificationPermissionStatus: null,
subscriptionStatus: null,
pushUsername: null,
pushPassword: null,
pushSaveButton: null,
pushCancelButton: null,
pushUnsubscribeButton: null,
pushResubscribeButton: null,
// Message monitor elements
swMessagesOutput: null,
simulateClickButton: null
};
// --- State ---
let currentSubscription = null;
let messageListener = null;
let isMonitoring = false;
// --- Initialization ---
export function initPushSettingsUI() {
// Cache DOM elements
elements.pushSettingsButton = document.getElementById('pushSettingsButton');
elements.pushSettingsModal = document.getElementById('pushSettingsModal');
elements.notificationPermissionStatus = document.getElementById('notificationPermissionStatus');
elements.subscriptionStatus = document.getElementById('subscriptionStatus');
elements.pushUsername = document.getElementById('pushUsername');
elements.pushPassword = document.getElementById('pushPassword');
elements.pushSaveButton = document.getElementById('pushSaveButton');
elements.pushCancelButton = document.getElementById('pushCancelButton');
elements.pushUnsubscribeButton = document.getElementById('pushUnsubscribeButton');
elements.pushResubscribeButton = document.getElementById('pushResubscribeButton');
// Message Monitor elements
elements.swMessagesOutput = document.getElementById('swMessagesOutput');
elements.simulateClickButton = document.getElementById('simulateClickButton');
// Set up event listeners
elements.pushSettingsButton.addEventListener('click', openPushSettingsModal);
elements.pushCancelButton.addEventListener('click', closePushSettingsModal);
elements.pushSaveButton.addEventListener('click', saveCredentialsAndSubscribe);
elements.pushUnsubscribeButton.addEventListener('click', unsubscribeFromPush);
elements.pushResubscribeButton.addEventListener('click', resubscribeToPush);
// Initial status check
updateNotificationStatus();
}
// --- UI Functions ---
// Open the push settings modal and update statuses
function openPushSettingsModal() {
// Update status displays
updateNotificationStatus();
updateSubscriptionStatus();
// Load saved credentials if available
loadSavedCredentials();
// Start monitoring automatically when modal opens
startMessageMonitoring();
// Show the modal
elements.pushSettingsModal.classList.add('active');
}
// Close the push settings modal
function closePushSettingsModal() {
// Stop monitoring when the modal is closed to avoid unnecessary processing
stopMessageMonitoring();
elements.pushSettingsModal.classList.remove('active');
}
// --- Message Monitor Functions ---
// Start monitoring service worker messages
function startMessageMonitoring() {
// If already monitoring, don't set up a new listener
if (isMonitoring) {
return;
}
if (!('serviceWorker' in navigator)) {
elements.swMessagesOutput.textContent = 'Service Worker not supported in this browser.';
return;
}
// Reset the output area
elements.swMessagesOutput.textContent = 'Monitoring for service worker messages...';
// Create and register the message listener
messageListener = function(event) {
const now = new Date().toISOString();
const formattedMessage = `[${now}] Message received: \n${JSON.stringify(event.data, null, 2)}\n\n`;
elements.swMessagesOutput.textContent += formattedMessage;
// Auto-scroll to the bottom
elements.swMessagesOutput.scrollTop = elements.swMessagesOutput.scrollHeight;
};
// Add the listener
navigator.serviceWorker.addEventListener('message', messageListener);
isMonitoring = true;
}
// Stop monitoring service worker messages
function stopMessageMonitoring() {
if (messageListener) {
navigator.serviceWorker.removeEventListener('message', messageListener);
messageListener = null;
isMonitoring = false;
}
}
// Update the notification permission status display
function updateNotificationStatus() {
if (!('Notification' in window)) {
elements.notificationPermissionStatus.textContent = 'Not Supported';
elements.notificationPermissionStatus.className = 'status-denied';
// Disable subscribe button when notifications are not supported
elements.pushResubscribeButton.disabled = true;
elements.pushResubscribeButton.classList.add('disabled');
return;
}
const permission = Notification.permission;
elements.notificationPermissionStatus.textContent = permission;
switch (permission) {
case 'granted':
elements.notificationPermissionStatus.className = 'status-granted';
// Enable subscribe button when permission is granted
elements.pushResubscribeButton.disabled = false;
elements.pushResubscribeButton.classList.remove('disabled');
break;
case 'denied':
elements.notificationPermissionStatus.className = 'status-denied';
// Disable subscribe button when permission is denied
elements.pushResubscribeButton.disabled = true;
elements.pushResubscribeButton.classList.add('disabled');
break;
default:
elements.notificationPermissionStatus.className = 'status-default';
// Enable subscribe button for default state (prompt)
elements.pushResubscribeButton.disabled = false;
elements.pushResubscribeButton.classList.remove('disabled');
}
}
// Update the subscription status display
async function updateSubscriptionStatus() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
elements.subscriptionStatus.textContent = 'Not Supported';
elements.subscriptionStatus.className = 'status-denied';
// Disable unsubscribe button when not supported
elements.pushUnsubscribeButton.disabled = true;
// Set subscribe button text
elements.pushResubscribeButton.textContent = 'Subscribe';
// Disable simulate button when not supported
if (elements.simulateClickButton) {
elements.simulateClickButton.disabled = true;
}
return;
}
try {
const registration = await navigator.serviceWorker.ready;
currentSubscription = await registration.pushManager.getSubscription();
if (currentSubscription) {
elements.subscriptionStatus.textContent = 'active';
elements.subscriptionStatus.className = 'status-active';
// Enable unsubscribe button when subscription is active
elements.pushUnsubscribeButton.disabled = false;
// Change subscribe button text to "Re-subscribe"
elements.pushResubscribeButton.textContent = 'Re-subscribe';
// Enable simulate button when subscription is active
if (elements.simulateClickButton) {
elements.simulateClickButton.disabled = false;
}
} else {
elements.subscriptionStatus.textContent = 'Not Subscribed';
elements.subscriptionStatus.className = 'status-inactive';
// Disable unsubscribe button when not subscribed
elements.pushUnsubscribeButton.disabled = true;
// Set subscribe button text
elements.pushResubscribeButton.textContent = 'Subscribe';
// Disable simulate button when not subscribed
if (elements.simulateClickButton) {
elements.simulateClickButton.disabled = true;
}
}
} catch (error) {
console.error('Error checking subscription status:', error);
elements.subscriptionStatus.textContent = 'Error';
elements.subscriptionStatus.className = 'status-denied';
// Disable unsubscribe button on error
elements.pushUnsubscribeButton.disabled = true;
// Set subscribe button text
elements.pushResubscribeButton.textContent = 'Subscribe';
// Disable simulate button on error
if (elements.simulateClickButton) {
elements.simulateClickButton.disabled = true;
}
}
}
// Load saved credentials from localStorage
function loadSavedCredentials() {
try {
const storedAuth = localStorage.getItem('basicAuthCredentials');
if (storedAuth) {
const credentials = JSON.parse(storedAuth);
if (credentials.username && credentials.password) {
elements.pushUsername.value = credentials.username;
elements.pushPassword.value = credentials.password;
}
}
} catch (error) {
console.error('Error loading saved credentials:', error);
}
}
// --- Action Functions ---
// Save credentials and close the modal
async function saveCredentialsAndSubscribe() {
const username = elements.pushUsername.value.trim();
const password = elements.pushPassword.value.trim();
if (!username || !password) {
alert('Please enter both username and password');
return;
}
// Save credentials to localStorage
const credentials = { username, password };
localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials));
// Close the modal
closePushSettingsModal();
}
// Unsubscribe from push notifications
async function unsubscribeFromPush() {
if (!currentSubscription) {
alert('No active subscription to unsubscribe from');
return;
}
try {
await currentSubscription.unsubscribe();
await updateSubscriptionStatus();
// No success alert
} catch (error) {
console.error('Error unsubscribing:', error);
alert(`Error unsubscribing: ${error.message}`);
}
}
// Subscribe to push notifications
async function resubscribeToPush() {
try {
let username = elements.pushUsername.value.trim();
let password = elements.pushPassword.value.trim();
// If fields are empty, try to use stored credentials
if (!username || !password) {
try {
const storedAuth = localStorage.getItem('basicAuthCredentials');
if (storedAuth) {
const credentials = JSON.parse(storedAuth);
if (credentials.username && credentials.password) {
username = credentials.username;
password = credentials.password;
// Update the form fields with stored values
elements.pushUsername.value = username;
elements.pushPassword.value = password;
}
}
} catch (error) {
console.error('Error loading stored credentials:', error);
}
}
// Check if we have credentials, show alert if missing
if (!username || !password) {
console.log('No credentials available. Showing alert.');
alert('Please enter your username and password to subscribe.');
return;
}
// Save the credentials to localStorage
const credentials = { username, password };
localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials));
console.log('Saved credentials to localStorage');
// Use the credentials to subscribe
await setupPushNotifications();
// Wait a moment for the subscription to complete
await new Promise(resolve => setTimeout(resolve, 500));
// Get the updated subscription
const registration = await navigator.serviceWorker.ready;
currentSubscription = await registration.pushManager.getSubscription();
// Update the UI directly
if (currentSubscription && elements.subscriptionStatus) {
elements.subscriptionStatus.textContent = 'active';
elements.subscriptionStatus.className = 'status-active';
// Enable unsubscribe button when subscription is active
elements.pushUnsubscribeButton.disabled = false;
// Change subscribe button text to "Re-subscribe"
elements.pushResubscribeButton.textContent = 'Re-subscribe';
// Enable simulate button when subscription is active
if (elements.simulateClickButton) {
elements.simulateClickButton.disabled = false;
}
} else {
// Disable unsubscribe button when not subscribed
elements.pushUnsubscribeButton.disabled = true;
// Set subscribe button text
elements.pushResubscribeButton.textContent = 'Subscribe';
// Disable simulate button when not subscribed
if (elements.simulateClickButton) {
elements.simulateClickButton.disabled = true;
}
// Fall back to the standard update function
await updateSubscriptionStatus();
}
} catch (error) {
console.error('Error subscribing:', error);
alert(`Error subscribing: ${error.message}`);
}
}
// Manually trigger sendSubscriptionToServer with the current subscription
export async function sendSubscriptionToServer() {
if (!currentSubscription) {
await updateSubscriptionStatus();
if (!currentSubscription) {
// No alert, just return silently
return;
}
}
// Get stored credentials
let credentials;
try {
const storedAuth = localStorage.getItem('basicAuthCredentials');
if (storedAuth) {
credentials = JSON.parse(storedAuth);
if (!credentials.username || !credentials.password) {
throw new Error('Invalid credentials');
}
} else {
throw new Error('No stored credentials');
}
} catch (error) {
// No alert, just open the modal to let the user set credentials
openPushSettingsModal();
return;
}
// Create Basic Auth header
const createBasicAuthHeader = (creds) => {
return 'Basic ' + btoa(`${creds.username}:${creds.password}`);
};
const headers = {
'Content-Type': 'application/json',
'Authorization': createBasicAuthHeader(credentials)
};
try {
// Make the request to the server
const response = await fetch(`${getBackendUrl()}/subscribe`, {
method: 'POST',
body: JSON.stringify({
button_id: FLIC_BUTTON_ID,
subscription: currentSubscription
}),
headers: headers,
credentials: 'include'
});
if (response.ok) {
const result = await response.json();
// No success alert
// Update the currentSubscription variable
const registration = await navigator.serviceWorker.ready;
currentSubscription = await registration.pushManager.getSubscription();
// Directly update the subscription status element in the DOM
if (currentSubscription && elements.subscriptionStatus) {
elements.subscriptionStatus.textContent = 'active';
elements.subscriptionStatus.className = 'status-active';
// Enable unsubscribe button when subscription is active
elements.pushUnsubscribeButton.disabled = false;
// Change subscribe button text to "Re-subscribe"
elements.pushResubscribeButton.textContent = 'Re-subscribe';
// Enable simulate button when subscription is active
if (elements.simulateClickButton) {
elements.simulateClickButton.disabled = false;
}
}
} else {
let errorMsg = `Server error: ${response.status}`;
if (response.status === 401 || response.status === 403) {
localStorage.removeItem('basicAuthCredentials');
errorMsg = 'Authentication failed. Credentials cleared.';
} else {
try {
const errorData = await response.json();
errorMsg = errorData.message || errorMsg;
} catch (e) { /* use default */ }
}
// No error alert, just log the error
console.error(`Failed to send subscription: ${errorMsg}`);
}
} catch (error) {
// No error alert, just log the error
console.error(`Network error: ${error.message}`);
}
}

View File

@@ -1,16 +1,13 @@
{ {
"name": "Game Timer", "name": "Game Timer PWA",
"short_name": "Timer", "short_name": "Game Timer",
"author": "Martin",
"version": "0.0.1",
"version_name": "0.0.1",
"description": "Multi-player chess-like timer with carousel navigation", "description": "Multi-player chess-like timer with carousel navigation",
"start_url": "/", "start_url": "/index.html",
"id": "/index.html", "id": "/index.html",
"display": "standalone", "display": "standalone",
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"], "display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
"background_color": "#ffffff", "background_color": "#f5f5f5",
"theme_color": "#000000", "theme_color": "#2c3e50",
"icons": [ "icons": [
{ {
"src": "/icons/android-chrome-192x192.png", "src": "/icons/android-chrome-192x192.png",
@@ -41,13 +38,13 @@
"screenshots": [ "screenshots": [
{ {
"src": "/images/screenshot1.png", "src": "/images/screenshot1.png",
"sizes": "2212×1614", "sizes": "2560x1860",
"type": "image/png", "type": "image/png",
"form_factor": "wide" "form_factor": "wide"
}, },
{ {
"src": "/images/screenshot2.png", "src": "/images/screenshot2.png",
"sizes": "828×1912", "sizes": "750x1594",
"type": "image/png" "type": "image/png"
} }
] ]

View File

@@ -7,9 +7,7 @@
"docker:build": "docker build -t 'game-timer:latest' .", "docker:build": "docker build -t 'game-timer:latest' .",
"start": "docker run -d -p 80:80 --name game-timer game-timer:latest", "start": "docker run -d -p 80:80 --name game-timer game-timer:latest",
"stop": "docker stop game-timer && docker rm game-timer", "stop": "docker stop game-timer && docker rm game-timer",
"rebuild": "npm run stop || true && npm run docker:build && npm run start", "rebuild": "npm run stop || true && npm run docker:build && npm run start"
"generate-config": "./generate-config.sh",
"dev": "./dev-start.sh"
}, },
"keywords": [ "keywords": [
"timer", "timer",

View File

@@ -4,14 +4,15 @@ import * as state from './core/state.js';
import * as ui from './ui/ui.js'; import * as ui from './ui/ui.js';
import * as timer from './core/timer.js'; import * as timer from './core/timer.js';
import camera from './ui/camera.js'; // Default export import camera from './ui/camera.js'; // Default export
import * as pushSettingsUI from './ui/pushSettingsUI.js'; // Import the new push settings UI module import audioManager from './ui/audio.js';
import * as pushFlic from './services/pushFlicIntegration.js';
import { initEnv } from './env-loader.js';
// Import externalized modules // Import externalized modules
import * as gameActions from './core/gameActions.js'; import * as gameActions from './core/gameActions.js';
import * as playerManager from './core/playerManager.js'; import * as playerManager from './core/playerManager.js';
import * as eventHandlers from './core/eventHandlers.js'; import * as eventHandlers from './core/eventHandlers.js';
import * as serviceWorkerManager from './services/serviceWorkerManager.js'; import * as serviceWorkerManager from './services/serviceWorkerManager.js';
import * as screenLockManager from './services/screenLockManager.js'; // Import the screen lock manager
// --- Initialization --- // --- Initialization ---
@@ -20,9 +21,8 @@ async function initialize() {
// 0. Wait for environment variables to load // 0. Wait for environment variables to load
try { try {
// Use the ensureEnvLoaded function from config.js to make sure environment variables are loaded await initEnv();
await config.ensureEnvLoaded(); console.log("Environment variables loaded");
console.log("Environment variables loaded and verified");
} catch (error) { } catch (error) {
console.warn("Failed to load environment variables, using defaults:", error); console.warn("Failed to load environment variables, using defaults:", error);
} }
@@ -30,23 +30,6 @@ async function initialize() {
// 1. Load saved state or defaults // 1. Load saved state or defaults
state.loadData(); state.loadData();
// Setup Flic action handlers early in the initialization process
// to ensure they're available when the service worker initializes
const flicActionHandlers = {
[config.FLIC_ACTIONS.SINGLE_CLICK]: playerManager.nextPlayer,
[config.FLIC_ACTIONS.DOUBLE_CLICK]: playerManager.previousPlayer,
[config.FLIC_ACTIONS.HOLD]: gameActions.togglePauseResume,
};
// Log the registered handlers for debugging
console.log("Registering Flic action handlers:", {
"SingleClick": config.FLIC_ACTIONS.SINGLE_CLICK,
"DoubleClick": config.FLIC_ACTIONS.DOUBLE_CLICK,
"Hold": config.FLIC_ACTIONS.HOLD
});
serviceWorkerManager.setFlicActionHandlers(flicActionHandlers);
// 2. Initialize UI (pass carousel swipe handler) // 2. Initialize UI (pass carousel swipe handler)
ui.initUI({ ui.initUI({
onCarouselSwipe: (direction) => { onCarouselSwipe: (direction) => {
@@ -87,21 +70,22 @@ async function initialize() {
ui.elements.resetCancelButton.addEventListener('click', eventHandlers.handleResetCancel); ui.elements.resetCancelButton.addEventListener('click', eventHandlers.handleResetCancel);
ui.elements.cameraButton.addEventListener('click', eventHandlers.handleCameraButtonClick); ui.elements.cameraButton.addEventListener('click', eventHandlers.handleCameraButtonClick);
// 6. Initialize Push Notification Settings UI // 6. Setup Flic action handlers
pushSettingsUI.initPushSettingsUI(); const flicActionHandlers = {
[config.FLIC_ACTIONS.SINGLE_CLICK]: playerManager.nextPlayer,
[config.FLIC_ACTIONS.DOUBLE_CLICK]: playerManager.previousPlayer,
[config.FLIC_ACTIONS.HOLD]: gameActions.togglePauseResume,
};
serviceWorkerManager.setFlicActionHandlers(flicActionHandlers);
// 7. Setup Service Worker (which also initializes Flic) // 7. Setup Service Worker (which also initializes Flic)
serviceWorkerManager.setupServiceWorker(serviceWorkerManager.flicMessageHandler); serviceWorkerManager.setupServiceWorker(serviceWorkerManager.handleServiceWorkerMessage);
// 8. Initialize Screen Lock Manager (automatically acquires wake lock) // 8. Initial UI Update based on loaded state
const screenLockSupported = await screenLockManager.initScreenLockManager();
console.log(`Screen Wake Lock API ${screenLockSupported ? 'is' : 'is not'} supported`);
// 9. Initial UI Update based on loaded state
ui.renderPlayers(); ui.renderPlayers();
ui.updateGameButton(); ui.updateGameButton();
// 10. Reset running state to paused on load // 9. Reset running state to paused on load
if (state.getGameState() === config.GAME_STATES.RUNNING) { if (state.getGameState() === config.GAME_STATES.RUNNING) {
console.log("Game was running on load, setting to paused."); console.log("Game was running on load, setting to paused.");
state.setGameState(config.GAME_STATES.PAUSED); state.setGameState(config.GAME_STATES.PAUSED);

View File

@@ -1,38 +1,20 @@
// config.js // config.js
import { getEnv, waitForEnv } from './env-loader.js'; import { getEnv } from './env-loader.js';
// Initialize environment variables
let envInitialized = false;
let initPromise = null;
// Function to ensure environment variables are loaded
export async function ensureEnvLoaded() {
if (envInitialized) return;
if (!initPromise) {
initPromise = waitForEnv().then(() => {
envInitialized = true;
console.log('Environment variables loaded in config.js');
});
}
return initPromise;
}
// Initialize immediately
ensureEnvLoaded();
// Direct access to environment variables (synchronous, may return default values if called too early)
export function getPublicVapidKey() { export function getPublicVapidKey() {
return getEnv('PUBLIC_VAPID_KEY'); // Get the VAPID key from environment variables through the env-loader
return getEnv('PUBLIC_VAPID_KEY', 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E');
} }
export function getBackendUrl() { // The VAPID key should not be exposed directly in the source code
return getEnv('BACKEND_URL'); // Use the getter function instead: getPublicVapidKey()
} // export const PUBLIC_VAPID_KEY = 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E';
// Get backend URL from environment variables
export const BACKEND_URL = getEnv('BACKEND_URL', 'https://webpush.virtonline.eu');
export const FLIC_BUTTON_ID = 'game-button'; // Example ID, might need configuration export const FLIC_BUTTON_ID = 'game-button'; // Example ID, might need configuration
export const LOCAL_STORAGE_KEY = 'gameTimerData'; export const LOCAL_STORAGE_KEY = 'gameTimerData';
export const FLIC_BATTERY_THRESHOLD = 50; // Battery percentage threshold for low battery warning
// Default player settings // Default player settings
export const DEFAULT_PLAYER_TIME_SECONDS = 300; // 5 minutes export const DEFAULT_PLAYER_TIME_SECONDS = 300; // 5 minutes

View File

@@ -4,7 +4,6 @@ import * as state from './state.js';
import * as ui from '../ui/ui.js'; import * as ui from '../ui/ui.js';
import * as timer from './timer.js'; import * as timer from './timer.js';
import audioManager from '../ui/audio.js'; import audioManager from '../ui/audio.js';
import * as screenLockManager from '../services/screenLockManager.js'; // Import screen lock manager
// --- Core Game Actions --- // --- Core Game Actions ---
@@ -13,14 +12,6 @@ export function handleGameOver() {
state.setGameState(config.GAME_STATES.OVER); state.setGameState(config.GAME_STATES.OVER);
audioManager.play('gameOver'); audioManager.play('gameOver');
timer.stopTimer(); // Ensure timer is stopped timer.stopTimer(); // Ensure timer is stopped
// Release screen wake lock when game is over
screenLockManager.releaseWakeLock().then(success => {
if (success) {
console.log('Screen wake lock released on game over');
}
});
ui.updateGameButton(); ui.updateGameButton();
ui.renderPlayers(); // Update to show final state ui.renderPlayers(); // Update to show final state
} }
@@ -34,16 +25,6 @@ export function startGame() {
state.setGameState(config.GAME_STATES.RUNNING); state.setGameState(config.GAME_STATES.RUNNING);
audioManager.play('gameStart'); audioManager.play('gameStart');
timer.startTimer(); timer.startTimer();
// Acquire screen wake lock when game starts
screenLockManager.acquireWakeLock().then(success => {
if (success) {
console.log('Screen wake lock acquired for game');
} else {
console.warn('Failed to acquire screen wake lock');
}
});
ui.updateGameButton(); ui.updateGameButton();
ui.renderPlayers(); // Ensure active timer styling is applied ui.renderPlayers(); // Ensure active timer styling is applied
} }
@@ -54,14 +35,6 @@ export function pauseGame() {
state.setGameState(config.GAME_STATES.PAUSED); state.setGameState(config.GAME_STATES.PAUSED);
audioManager.play('gamePause'); audioManager.play('gamePause');
timer.stopTimer(); timer.stopTimer();
// Release screen wake lock when game is paused
screenLockManager.releaseWakeLock().then(success => {
if (success) {
console.log('Screen wake lock released on pause');
}
});
ui.updateGameButton(); ui.updateGameButton();
ui.renderPlayers(); // Ensure active timer styling is removed ui.renderPlayers(); // Ensure active timer styling is removed
} }
@@ -103,14 +76,6 @@ export function resetGame() {
state.resetPlayersTime(); state.resetPlayersTime();
state.setGameState(config.GAME_STATES.SETUP); state.setGameState(config.GAME_STATES.SETUP);
state.setCurrentPlayerIndex(0); // Go back to first player state.setCurrentPlayerIndex(0); // Go back to first player
// Release screen wake lock when game is reset
screenLockManager.releaseWakeLock().then(success => {
if (success) {
console.log('Screen wake lock released on reset');
}
});
audioManager.play('buttonClick'); // Or a specific reset sound? audioManager.play('buttonClick'); // Or a specific reset sound?
ui.updateGameButton(); ui.updateGameButton();
ui.renderPlayers(); ui.renderPlayers();
@@ -119,14 +84,6 @@ export function resetGame() {
export function fullResetApp() { export function fullResetApp() {
timer.stopTimer(); timer.stopTimer();
state.resetToDefaults(); state.resetToDefaults();
// Release screen wake lock on full reset
screenLockManager.releaseWakeLock().then(success => {
if (success) {
console.log('Screen wake lock released on full reset');
}
});
audioManager.play('gameOver'); // Use game over sound for full reset audioManager.play('gameOver'); // Use game over sound for full reset
ui.hideResetModal(); ui.hideResetModal();
ui.updateGameButton(); ui.updateGameButton();

88
src/js/env-loader.js Normal file
View File

@@ -0,0 +1,88 @@
// env-loader.js
// This module is responsible for loading environment variables from .env file
// Store environment variables in a global object
window.ENV_CONFIG = {};
// Function to load environment variables from .env file
async function loadEnvVariables() {
try {
// Fetch the .env file as text
const response = await fetch('/.env');
if (!response.ok) {
console.warn('Could not load .env file. Using default values.');
setDefaultEnvValues();
return;
}
const envText = await response.text();
// Parse the .env file content
const envVars = parseEnvFile(envText);
// Store in the global ENV_CONFIG object
window.ENV_CONFIG = { ...window.ENV_CONFIG, ...envVars };
console.log('Environment variables loaded successfully');
} catch (error) {
console.error('Error loading environment variables:', error);
setDefaultEnvValues();
}
}
// Parse .env file content into key-value pairs
function parseEnvFile(envText) {
const envVars = {};
// Split by lines and process each line
envText.split('\n').forEach(line => {
// Skip empty lines and comments
if (!line || line.trim().startsWith('#')) return;
// Extract key-value pairs
const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
if (match) {
const key = match[1];
let value = match[2] || '';
// Remove quotes if present
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
envVars[key] = value;
}
});
return envVars;
}
// Set default values for required environment variables
function setDefaultEnvValues() {
window.ENV_CONFIG = {
...window.ENV_CONFIG,
PUBLIC_VAPID_KEY: 'your_public_vapid_key_here',
BACKEND_URL: 'https://your-push-server.example.com'
};
console.log('Using default environment values');
}
// Export function to initialize environment variables
export async function initEnv() {
await loadEnvVariables();
return window.ENV_CONFIG;
}
// Auto-initialize when imported
initEnv();
// Export access functions for environment variables
export function getEnv(key, defaultValue = '') {
return window.ENV_CONFIG[key] || defaultValue;
}
export default {
initEnv,
getEnv
};

View File

@@ -1,8 +1,9 @@
// pushFlicIntegration.js // pushFlicIntegration.js
import { getPublicVapidKey, getBackendUrl, FLIC_BUTTON_ID} from '../config.js'; import { getPublicVapidKey, BACKEND_URL, FLIC_BUTTON_ID, FLIC_ACTIONS, FLIC_BATTERY_THRESHOLD } from '../config.js';
let pushSubscription = null; // Keep track locally if needed let pushSubscription = null; // Keep track locally if needed
let actionHandlers = {}; // Store handlers for different Flic actions let actionHandlers = {}; // Store handlers for different Flic actions
let lastBatteryWarningTimestamp = 0; // Track when last battery warning was shown
// --- Helper Functions --- // --- Helper Functions ---
@@ -28,6 +29,19 @@ function getBasicAuthCredentials() {
return null; return null;
} }
// Prompt the user for credentials after permissions are granted
function promptForCredentials() {
console.log('Prompting user for auth credentials.');
const username = prompt('Please enter your username for backend authentication:');
if (!username) return null;
const password = prompt('Please enter your password:');
if (!password) return null;
const credentials = { username, password };
localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials));
return credentials;
}
// Create Basic Auth header string // Create Basic Auth header string
function createBasicAuthHeader(credentials) { function createBasicAuthHeader(credentials) {
if (!credentials?.username || !credentials.password) return null; if (!credentials?.username || !credentials.password) return null;
@@ -54,6 +68,31 @@ function arrayBufferToBase64(buffer) {
return window.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); return window.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
} }
// Show a popup notification to the user
function showBatteryWarning(batteryLevel) {
// Only show warning once every 4 hours (to avoid annoying users)
const now = Date.now();
const fourHoursInMs = 4 * 60 * 60 * 1000;
if (now - lastBatteryWarningTimestamp < fourHoursInMs) {
console.log(`[PushFlic] Battery warning suppressed (shown recently): ${batteryLevel}%`);
return;
}
lastBatteryWarningTimestamp = now;
// Show the notification
const message = `Flic button battery is low (${batteryLevel}%). Please replace the battery soon.`;
// Show browser notification if permission granted
if (Notification.permission === 'granted') {
new Notification('Flic Button Low Battery', {
body: message,
icon: '/public/favicon.ico'
});
}
}
// --- Push Subscription Logic --- // --- Push Subscription Logic ---
async function subscribeToPush() { async function subscribeToPush() {
@@ -77,24 +116,27 @@ async function subscribeToPush() {
console.log('Notification permission granted.'); console.log('Notification permission granted.');
// Get stored credentials but don't prompt // After permission is granted, check for stored credentials or prompt user
let credentials = getBasicAuthCredentials(); let credentials = getBasicAuthCredentials();
const hasExistingCreds = !!credentials;
console.log('Has existing credentials:', hasExistingCreds);
// No prompting for credentials - user must enter them manually in the UI
if (!credentials) { if (!credentials) {
console.log('No credentials found. User needs to enter them manually.'); const confirmAuth = confirm('Do you want to set up credentials for push notifications now?');
// Just return if no credentials are available if (!confirmAuth) {
return; console.log('User declined to provide auth credentials.');
return;
}
credentials = promptForCredentials();
if (!credentials) {
console.log('User canceled credential input.');
alert('Authentication required to set up push notifications.');
return;
}
} }
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
let existingSubscription = await registration.pushManager.getSubscription(); let existingSubscription = await registration.pushManager.getSubscription();
let needsResubscribe = !existingSubscription; let needsResubscribe = !existingSubscription;
console.log('Existing subscription found:', !!existingSubscription);
if (existingSubscription) { if (existingSubscription) {
const existingKey = existingSubscription.options?.applicationServerKey; const existingKey = existingSubscription.options?.applicationServerKey;
if (!existingKey || arrayBufferToBase64(existingKey) !== getPublicVapidKey()) { if (!existingKey || arrayBufferToBase64(existingKey) !== getPublicVapidKey()) {
@@ -112,18 +154,12 @@ async function subscribeToPush() {
if (needsResubscribe) { if (needsResubscribe) {
console.log('Subscribing for push notifications...'); console.log('Subscribing for push notifications...');
const applicationServerKey = urlBase64ToUint8Array(getPublicVapidKey()); const applicationServerKey = urlBase64ToUint8Array(getPublicVapidKey());
try { finalSubscription = await registration.pushManager.subscribe({
finalSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true,
userVisibleOnly: true, applicationServerKey: applicationServerKey
applicationServerKey: applicationServerKey });
}); console.log('New push subscription obtained:', finalSubscription);
console.log('New push subscription obtained:', finalSubscription); pushSubscription = finalSubscription; // Store it
pushSubscription = finalSubscription; // Store it
} catch (subscribeError) {
console.error('Error subscribing to push:', subscribeError);
alert(`Failed to subscribe: ${subscribeError.message}`);
return;
}
} }
if (!finalSubscription) { if (!finalSubscription) {
@@ -144,8 +180,20 @@ async function sendSubscriptionToServer(subscription, buttonId) {
console.log(`Sending subscription for button "${buttonId}" to backend...`); console.log(`Sending subscription for button "${buttonId}" to backend...`);
const credentials = getBasicAuthCredentials(); const credentials = getBasicAuthCredentials();
if (!credentials) { if (!credentials) {
console.log('No credentials found. User needs to enter them manually.'); // One more chance to enter credentials if needed
return; const confirmAuth = confirm('Authentication required to complete setup. Provide credentials now?');
if (!confirmAuth) {
alert('Authentication required to save button link.');
return;
}
const newCredentials = promptForCredentials();
if (!newCredentials) {
alert('Authentication required to save button link.');
return;
}
credentials = newCredentials;
} }
const headers = { 'Content-Type': 'application/json' }; const headers = { 'Content-Type': 'application/json' };
@@ -154,7 +202,7 @@ async function sendSubscriptionToServer(subscription, buttonId) {
try { try {
// Add support for handling CORS preflight with credentials // Add support for handling CORS preflight with credentials
const response = await fetch(`${getBackendUrl()}/subscribe`, { const response = await fetch(`${BACKEND_URL}/subscribe`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ button_id: buttonId, subscription: subscription }), body: JSON.stringify({ button_id: buttonId, subscription: subscription }),
headers: headers, headers: headers,
@@ -164,27 +212,7 @@ async function sendSubscriptionToServer(subscription, buttonId) {
if (response.ok) { if (response.ok) {
const result = await response.json(); const result = await response.json();
console.log('Subscription sent successfully:', result.message); console.log('Subscription sent successfully:', result.message);
alert('Push notification setup completed successfully!');
// Update the UI to show subscription status as active
const subscriptionStatusElement = document.getElementById('subscriptionStatus');
if (subscriptionStatusElement) {
subscriptionStatusElement.textContent = 'active';
subscriptionStatusElement.className = 'status-active';
// Enable unsubscribe button when subscription is active
const unsubscribeButton = document.getElementById('pushUnsubscribeButton');
if (unsubscribeButton) unsubscribeButton.disabled = false;
// Change subscribe button text to "Re-subscribe"
const resubscribeButton = document.getElementById('pushResubscribeButton');
if (resubscribeButton) resubscribeButton.textContent = 'Re-subscribe';
// Enable simulate button when subscription is active
const simulateButton = document.getElementById('simulateClickButton');
if (simulateButton) simulateButton.disabled = false;
}
// Success alert removed as requested
} else { } else {
let errorMsg = `Server error: ${response.status}`; let errorMsg = `Server error: ${response.status}`;
if (response.status === 401 || response.status === 403) { if (response.status === 401 || response.status === 403) {
@@ -208,6 +236,11 @@ async function sendSubscriptionToServer(subscription, buttonId) {
export function handleFlicAction(action, buttonId, timestamp, batteryLevel) { export function handleFlicAction(action, buttonId, timestamp, batteryLevel) {
console.log(`[PushFlic] Received Action: ${action} from Button: ${buttonId} battery: ${batteryLevel}% at ${timestamp}`); console.log(`[PushFlic] Received Action: ${action} from Button: ${buttonId} battery: ${batteryLevel}% at ${timestamp}`);
// Check if battery is below threshold and show warning if needed
if (batteryLevel < FLIC_BATTERY_THRESHOLD) {
showBatteryWarning(batteryLevel);
}
// Ignore actions from buttons other than the configured one // Ignore actions from buttons other than the configured one
if (buttonId !== FLIC_BUTTON_ID) { if (buttonId !== FLIC_BUTTON_ID) {
console.warn(`[PushFlic] Ignoring action from unknown button: ${buttonId}`); console.warn(`[PushFlic] Ignoring action from unknown button: ${buttonId}`);
@@ -216,36 +249,35 @@ export function handleFlicAction(action, buttonId, timestamp, batteryLevel) {
// Find the registered handler for this action // Find the registered handler for this action
const handler = actionHandlers[action]; const handler = actionHandlers[action];
if (handler && typeof handler === 'function') { if (handler && typeof handler === 'function') {
console.log(`[PushFlic] Executing handler for ${action}`); console.log(`[PushFlic] Executing handler for ${action}`);
try { // Execute the handler registered in app.js
// Execute the handler registered in app.js handler(); // Use the handler function directly instead of hardcoded function calls
handler();
// Log success
console.log(`[PushFlic] Successfully executed handler for ${action}`);
} catch (error) {
console.error(`[PushFlic] Error executing handler for ${action}:`, error);
}
} else { } else {
console.warn(`[PushFlic] No handler registered for action: ${action}. Available handlers:`, Object.keys(actionHandlers)); console.warn(`[PushFlic] No handler registered for action: ${action}`);
} }
} }
// --- Initialization --- // --- Initialization ---
// Initialize PushFlic with action handlers
export function initPushFlic(handlers) { export function initPushFlic(handlers) {
if (handlers && Object.keys(handlers).length > 0) { actionHandlers = handlers; // Store the handlers passed from app.js
actionHandlers = handlers; // Example: handlers = { SingleClick: handleNextPlayer, Hold: handleTogglePause }
console.log('[PushFlic] Stored action handlers:', Object.keys(actionHandlers));
} else { // Attempt to subscribe immediately if permission might already be granted
console.warn('[PushFlic] No action handlers provided to initPushFlic!'); // Or trigger subscription on a user action (e.g., a "Link Flic Button" button)
// For simplicity, let's try subscribing if SW is ready and permission allows
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
console.log('[PushFlic] Permission granted, attempting subscription.');
subscribeToPush();
} else {
console.log('[PushFlic] Notification permission not granted.');
// Optionally provide a button for the user to trigger subscription later
}
});
});
} }
} }
// New function to manually trigger the subscription process
export function setupPushNotifications() {
console.log('[PushFlic] Manually triggering push notification setup');
subscribeToPush();
}

View File

@@ -0,0 +1,58 @@
// serviceWorkerManager.js - Service worker registration and Flic integration
import * as config from '../config.js';
import * as pushFlic from './pushFlicIntegration.js';
// Store the action handlers passed from app.js
let flicActionHandlers = {};
export function setFlicActionHandlers(handlers) {
flicActionHandlers = handlers;
}
// --- Flic Integration Setup ---
export function initFlic() {
// This function is used by setupServiceWorker and relies on
// flicActionHandlers being set before this is called
pushFlic.initPushFlic(flicActionHandlers);
}
export function handleServiceWorkerMessage(event) {
console.log('[App] Message received from Service Worker:', event.data);
if (event.data?.type === 'flic-action') {
const { action, button, timestamp, batteryLevel } = event.data;
if (flicActionHandlers[action]) {
flicActionHandlers[action]();
} else {
pushFlic.handleFlicAction(action, button, timestamp, batteryLevel);
}
}
}
// --- Service Worker and PWA Setup ---
export function setupServiceWorker(messageHandler) {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('ServiceWorker registered successfully.');
// Listen for messages FROM the Service Worker (e.g., Flic actions)
navigator.serviceWorker.addEventListener('message', messageHandler);
// Initialize Flic integration (which will try to subscribe)
initFlic();
})
.catch(error => {
console.error('ServiceWorker registration failed:', error);
});
});
// Listen for SW controller changes
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('Service Worker controller changed, potentially updated.');
// window.location.reload(); // Consider prompting user to reload
});
} else {
console.warn('ServiceWorker not supported.');
}
}

124
sw.js
View File

@@ -5,34 +5,29 @@ const CACHE_NAME = `game-timer-${CACHE_VERSION}`;
// Files to cache // Files to cache
const CACHE_FILES = [ const CACHE_FILES = [
'/', '/',
'/sw.js',
'/index.html', '/index.html',
'/manifest.json', '/manifest.json',
'/sw.js',
'/favicon.ico',
'/config.env.js',
'/css/styles.css', '/css/styles.css',
'/favicon.ico',
'/icons/android-chrome-192x192.png', '/icons/android-chrome-192x192.png',
'/icons/android-chrome-512x512.png', '/icons/android-chrome-512x512.png',
'/icons/apple-touch-icon.png', '/icons/apple-touch-icon.png',
'/icons/favicon-32x32.png', '/icons/favicon-32x32.png',
'/icons/favicon-16x16.png', '/icons/favicon-16x16.png',
'/images/screenshot1.png',
'/images/screenshot2.png',
'/js/app.js', '/js/app.js',
'/js/config.js', '/js/config.js',
'/js/env-loader.js', '/js/env-loader.js',
'/js/core/eventHandlers.js',
'/js/core/gameActions.js',
'/js/core/playerManager.js',
'/js/core/state.js',
'/js/core/timer.js',
'/js/services/pushFlicIntegration.js',
'/js/services/screenLockManager.js',
'/js/services/serviceWorkerManager.js',
'/js/ui/audio.js', '/js/ui/audio.js',
'/js/ui/camera.js', '/js/ui/camera.js',
'/js/ui/pushSettingsUI.js', '/js/ui/ui.js',
'/js/ui/ui.js' '/js/core/state.js',
'/js/core/timer.js',
'/js/core/gameActions.js',
'/js/core/playerManager.js',
'/js/core/eventHandlers.js',
'/js/services/pushFlicIntegration.js',
'/js/services/serviceWorkerManager.js'
]; ];
// Install event - Cache files // Install event - Cache files
@@ -70,15 +65,6 @@ self.addEventListener('activate', event => {
); );
}); });
self.addEventListener('fetch', (event) => {
console.log('[ServiceWorker] Fetch');
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
return cachedResponse || fetch(event.request);
})
);
});
// Helper function to determine if a response should be cached // Helper function to determine if a response should be cached
function shouldCacheResponse(request, response) { function shouldCacheResponse(request, response) {
// Only cache GET requests // Only cache GET requests
@@ -105,8 +91,7 @@ self.addEventListener('push', event => {
data: { data: {
action: 'Unknown', action: 'Unknown',
button: 'Unknown', button: 'Unknown',
batteryLevel: undefined, batteryLevel: undefined
timestamp: new Date().toISOString()
} }
}; };
@@ -120,15 +105,9 @@ self.addEventListener('push', event => {
pushData = { pushData = {
title: parsedData.title || pushData.title, title: parsedData.title || pushData.title,
body: parsedData.body || pushData.body, body: parsedData.body || pushData.body,
data: parsedData.data || pushData.data data: parsedData.data || pushData.data // Expecting { action: 'SingleClick', button: 'game-button', batteryLevel: 75 }
}; };
// Ensure all required fields are present in data
pushData.data = pushData.data || {};
if (!pushData.data.timestamp) {
pushData.data.timestamp = new Date().toISOString();
}
} catch (e) { } catch (e) {
console.error('[ServiceWorker] Error parsing push data:', e); console.error('[ServiceWorker] Error parsing push data:', e);
// Use default notification if parsing fails // Use default notification if parsing fails
@@ -141,58 +120,65 @@ self.addEventListener('push', event => {
// --- Send message to client(s) --- // --- Send message to client(s) ---
const messagePayload = { const messagePayload = {
type: 'flic-action', // Custom message type type: 'flic-action', // Custom message type
action: pushData.data.action || 'Unknown', // e.g., 'SingleClick', 'DoubleClick', 'Hold' action: pushData.data.action, // e.g., 'SingleClick', 'DoubleClick', 'Hold'
button: pushData.data.button || 'Unknown', // e.g., the button name button: pushData.data.button, // e.g., the button name
timestamp: pushData.data.timestamp || new Date().toISOString(), // e.g., the timestamp of the action timestamp: pushData.data.timestamp, // e.g., the timestamp of the action
batteryLevel: pushData.data.batteryLevel // e.g., the battery level percentage batteryLevel: pushData.data.batteryLevel // e.g., the battery level percentage
}; };
console.log('[ServiceWorker] Preparing message payload:', messagePayload); // Send message to all open PWA windows controlled by this SW
event.waitUntil( event.waitUntil(
self.clients.matchAll({ self.clients.matchAll({
type: 'window', type: 'window', // Only target window clients
includeUncontrolled: true includeUncontrolled: true // Include clients that might not be fully controlled yet
}).then(clientList => { }).then(clientList => {
if (!clientList || clientList.length === 0) { if (!clientList || clientList.length === 0) {
// No clients available, just resolve console.log('[ServiceWorker] No client windows found to send message to.');
return Promise.resolve(); // If no window is open, we MUST show a notification
return self.registration.showNotification(pushData.title, {
body: pushData.body,
icon: '/icons/android-chrome-192x192.png', // Updated path
data: pushData.data // Pass data if needed when notification is clicked
});
} }
// Post message to each client with improved reliability // Post message to each client
let messageSent = false; let messageSent = false;
const sendPromises = clientList.map(client => { clientList.forEach(client => {
console.log(`[ServiceWorker] Posting message to client: ${client.id}`, messagePayload); console.log(`[ServiceWorker] Posting message to client: ${client.id}`, messagePayload);
try { client.postMessage(messagePayload);
// Try to send the message and mark it as sent messageSent = true; // Mark that we at least tried to send a message
client.postMessage(messagePayload);
messageSent = true;
// Just return true to indicate message was sent
return Promise.resolve(true);
} catch (error) {
console.error('[ServiceWorker] Error posting message to client:', error);
return Promise.resolve(false);
}
}); });
return Promise.all(sendPromises).then(() => { // Decide whether to still show a notification even if a window is open.
// No notifications will be shown for any action, including low battery // Generally good practice unless you are SURE the app will handle it visibly.
return Promise.resolve(); // You might choose *not* to show a notification if a client was found and focused.
}); // For simplicity here, we'll still show one. Adjust as needed.
if (!messageSent) { // Only show notification if no message was sent? Or always show?
return self.registration.showNotification(pushData.title, {
body: pushData.body,
icon: '/icons/android-chrome-192x192.png',
data: pushData.data
});
}
}) })
); );
});
// Listen for messages from client // --- Show a notification (Important!) ---
self.addEventListener('message', event => { // Push notifications generally REQUIRE showing a notification to the user
const message = event.data; // unless the PWA is already in the foreground AND handles the event visually.
// It's safer to always show one unless you have complex foreground detection.
if (!message || typeof message !== 'object') { /* This part is now handled inside the clients.matchAll promise */
return; /*
} const notificationOptions = {
body: pushData.body,
// No battery-related handling icon: './icons/android-chrome-192x192.png', // Optional: path to an icon
data: pushData.data // Attach data if needed when notification is clicked
};
event.waitUntil(
self.registration.showNotification(pushData.title, notificationOptions)
);
*/
}); });
// This helps with navigation after app is installed // This helps with navigation after app is installed

36
test.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Worker Test</title>
</head>
<body>
<h1>Service Worker Test</h1>
<p>This page tests if the service worker is registered correctly.</p>
<div id="status">Checking service worker registration...</div>
<script>
const statusDiv = document.getElementById('status');
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
statusDiv.innerHTML = 'Service worker registered successfully!<br>' +
'Scope: ' + registration.scope;
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(error => {
statusDiv.innerHTML = 'Service worker registration failed: ' + error;
console.error('ServiceWorker registration failed: ', error);
});
navigator.serviceWorker.ready.then(registration => {
statusDiv.innerHTML += '<br><br>Service worker is ready!';
});
} else {
statusDiv.innerHTML = 'Service workers are not supported in this browser.';
}
</script>
</body>
</html>