Compare commits
3 Commits
1a364391d1
...
1c72844c87
Author | SHA1 | Date | |
---|---|---|---|
1c72844c87 | |||
7405cd22e8 | |||
965f349817 |
@@ -338,6 +338,10 @@
|
|||||||
background-color: rgba(0,0,0,0.8);
|
background-color: rgba(0,0,0,0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal.no-close {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: #2a2a2a;
|
background-color: #2a2a2a;
|
||||||
margin: 15% auto;
|
margin: 15% auto;
|
||||||
@@ -346,6 +350,11 @@
|
|||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
border: 2px solid #404040;
|
border: 2px solid #404040;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.no-close .modal-content {
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal h2 {
|
.modal h2 {
|
||||||
@@ -517,6 +526,7 @@
|
|||||||
<button class="btn btn-warning btn-small" onclick="clearLog()">Clear Log</button>
|
<button class="btn btn-warning btn-small" onclick="clearLog()">Clear Log</button>
|
||||||
<button class="btn btn-warning btn-small" onclick="resetDay()">Reset Daily Limit</button>
|
<button class="btn btn-warning btn-small" onclick="resetDay()">Reset Daily Limit</button>
|
||||||
<button class="btn btn-danger btn-small" onclick="resetAllPoints()">Reset All</button>
|
<button class="btn btn-danger btn-small" onclick="resetAllPoints()">Reset All</button>
|
||||||
|
<button class="btn btn-warning btn-small" onclick="testAudio()">Test Audio</button>
|
||||||
<button class="btn btn-primary btn-small" onclick="exitAdminMode()">Exit Admin</button>
|
<button class="btn btn-primary btn-small" onclick="exitAdminMode()">Exit Admin</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -676,10 +686,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Finish-Up Modal -->
|
<!-- Finish-Up Modal -->
|
||||||
<div id="finishUpModal" class="modal">
|
<div id="finishUpModal" class="modal no-close">
|
||||||
<div class="modal-content" style="max-width: 400px;">
|
<div class="modal-content" style="max-width: 400px;">
|
||||||
<h2>🔔 Timer Completed!</h2>
|
<h2>🔔 Timer Completed!</h2>
|
||||||
|
|
||||||
<p id="finishUpMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
|
<p id="finishUpMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
|
||||||
<p style="color: #e0e0e0;">Would you like 2 minutes of finish-up time?</p>
|
<p style="color: #e0e0e0;">Would you like 2 minutes of finish-up time?</p>
|
||||||
<div class="timer-controls">
|
<div class="timer-controls">
|
||||||
@@ -689,10 +698,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Timer Complete Modal -->
|
||||||
|
<div id="timerCompleteModal" class="modal no-close">
|
||||||
|
<div class="modal-content" style="max-width: 400px;">
|
||||||
|
<h2>🔔 Timer Completed!</h2>
|
||||||
|
<p id="timerCompleteMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
|
||||||
|
<div class="timer-controls">
|
||||||
|
<button class="btn btn-primary" onclick="dismissTimerComplete()">Dismiss</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Audio element for timer sounds -->
|
<!-- Audio element for timer sounds -->
|
||||||
<audio id="timerAudio" preload="auto">
|
<audio id="timerAudio" preload="auto" loop>
|
||||||
<source src="/timer-complete.mp3" type="audio/mpeg">
|
<source src="timer-complete.mp3" type="audio/mpeg">
|
||||||
Your browser does not support the audio element.
|
<source src="timer-complete.wav" type="audio/wav">
|
||||||
|
<source src="timer-complete.ogg" type="audio/ogg">
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -757,11 +778,10 @@
|
|||||||
socket.on('timerComplete', (data) => {
|
socket.on('timerComplete', (data) => {
|
||||||
const { pointId, type, offerFinishUp } = data;
|
const { pointId, type, offerFinishUp } = data;
|
||||||
|
|
||||||
// Play completion sound
|
|
||||||
playCompletionSound();
|
|
||||||
|
|
||||||
if (offerFinishUp) {
|
if (offerFinishUp) {
|
||||||
// Show custom finish-up modal
|
// Play sound and show finish-up modal
|
||||||
|
startAlarmSound();
|
||||||
|
|
||||||
const point = findPoint(pointId);
|
const point = findPoint(pointId);
|
||||||
const pointName = point ? (point.customName || point.type) : 'Timer';
|
const pointName = point ? (point.customName || point.type) : 'Timer';
|
||||||
document.getElementById('finishUpMessage').textContent = `${pointName} point completed!`;
|
document.getElementById('finishUpMessage').textContent = `${pointName} point completed!`;
|
||||||
@@ -769,14 +789,24 @@
|
|||||||
|
|
||||||
// Set up button handlers
|
// Set up button handlers
|
||||||
document.getElementById('finishUpYes').onclick = () => {
|
document.getElementById('finishUpYes').onclick = () => {
|
||||||
|
stopAlarmSound();
|
||||||
socket.emit('finishUpResponse', { pointId, accepted: true });
|
socket.emit('finishUpResponse', { pointId, accepted: true });
|
||||||
closeModal('finishUpModal');
|
closeModal('finishUpModal');
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('finishUpNo').onclick = () => {
|
document.getElementById('finishUpNo').onclick = () => {
|
||||||
|
stopAlarmSound();
|
||||||
socket.emit('finishUpResponse', { pointId, accepted: false });
|
socket.emit('finishUpResponse', { pointId, accepted: false });
|
||||||
closeModal('finishUpModal');
|
closeModal('finishUpModal');
|
||||||
};
|
};
|
||||||
|
} else if (type === 'finishUp') {
|
||||||
|
// Finish-up completed - show completion dialog
|
||||||
|
startAlarmSound();
|
||||||
|
|
||||||
|
const point = findPoint(pointId);
|
||||||
|
const pointName = point ? (point.customName || point.type) : 'Timer';
|
||||||
|
document.getElementById('timerCompleteMessage').textContent = `${pointName} point finish-up time completed!`;
|
||||||
|
document.getElementById('timerCompleteModal').style.display = 'block';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1281,16 +1311,154 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Audio functions
|
// Audio functions
|
||||||
function playCompletionSound() {
|
let audioPlaying = false;
|
||||||
|
|
||||||
|
function startAlarmSound() {
|
||||||
|
console.log('Starting alarm sound...');
|
||||||
const audioElement = document.getElementById('timerAudio');
|
const audioElement = document.getElementById('timerAudio');
|
||||||
|
|
||||||
|
// Force load the audio
|
||||||
|
audioElement.load();
|
||||||
|
|
||||||
|
// Set volume to max
|
||||||
|
audioElement.volume = 1.0;
|
||||||
|
audioElement.loop = true;
|
||||||
|
|
||||||
|
// Try to play
|
||||||
|
const playPromise = audioElement.play();
|
||||||
|
|
||||||
|
if (playPromise !== undefined) {
|
||||||
|
playPromise
|
||||||
|
.then(() => {
|
||||||
|
console.log('Audio playing successfully');
|
||||||
|
audioPlaying = true;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Audio playback failed:', error);
|
||||||
|
console.log('Audio src:', audioElement.src);
|
||||||
|
console.log('Audio readyState:', audioElement.readyState);
|
||||||
|
console.log('Audio error:', audioElement.error);
|
||||||
|
|
||||||
|
// Try fallback beep
|
||||||
|
createBeepSound();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAlarmSound() {
|
||||||
|
console.log('Stopping alarm sound...');
|
||||||
|
const audioElement = document.getElementById('timerAudio');
|
||||||
|
audioElement.pause();
|
||||||
|
audioElement.currentTime = 0;
|
||||||
|
audioPlaying = false;
|
||||||
|
|
||||||
|
// Stop any fallback beep
|
||||||
|
if (window.audioContext) {
|
||||||
|
window.audioContext.close();
|
||||||
|
window.audioContext = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playCompletionSound() {
|
||||||
|
console.log('Playing completion sound (single)...');
|
||||||
|
const audioElement = document.getElementById('timerAudio');
|
||||||
|
audioElement.loop = false;
|
||||||
|
audioElement.volume = 1.0;
|
||||||
audioElement.play().catch(error => {
|
audioElement.play().catch(error => {
|
||||||
console.log('Audio playback failed:', error);
|
console.error('Single play failed:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modals when clicking outside
|
function createBeepSound() {
|
||||||
|
console.log('Creating fallback beep sound...');
|
||||||
|
try {
|
||||||
|
window.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const oscillator = window.audioContext.createOscillator();
|
||||||
|
const gainNode = window.audioContext.createGain();
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(window.audioContext.destination);
|
||||||
|
|
||||||
|
oscillator.frequency.value = 800; // Frequency in Hz
|
||||||
|
oscillator.type = 'sine';
|
||||||
|
gainNode.gain.value = 0.3; // Volume
|
||||||
|
|
||||||
|
oscillator.start();
|
||||||
|
audioPlaying = true;
|
||||||
|
|
||||||
|
// Create beeping pattern
|
||||||
|
const beepPattern = () => {
|
||||||
|
if (!audioPlaying) {
|
||||||
|
oscillator.stop();
|
||||||
|
window.audioContext.close();
|
||||||
|
window.audioContext = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gainNode.gain.value = gainNode.gain.value > 0 ? 0 : 0.3;
|
||||||
|
setTimeout(beepPattern, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
beepPattern();
|
||||||
|
console.log('Beep sound started');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Web Audio API error:', e);
|
||||||
|
// Last resort - use notification API if available
|
||||||
|
if ('Notification' in window && Notification.permission === 'granted') {
|
||||||
|
new Notification('Timer Complete!', {
|
||||||
|
body: 'Your timer has finished.',
|
||||||
|
requireInteraction: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test audio on page load
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const audioElement = document.getElementById('timerAudio');
|
||||||
|
console.log('Audio element found:', audioElement);
|
||||||
|
console.log('Audio sources:', audioElement.getElementsByTagName('source').length);
|
||||||
|
|
||||||
|
// Check if audio can play
|
||||||
|
audioElement.addEventListener('canplay', () => {
|
||||||
|
console.log('Audio can play');
|
||||||
|
});
|
||||||
|
|
||||||
|
audioElement.addEventListener('error', (e) => {
|
||||||
|
console.error('Audio error event:', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request notification permission for fallback
|
||||||
|
if ('Notification' in window && Notification.permission === 'default') {
|
||||||
|
Notification.requestPermission();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function dismissTimerComplete() {
|
||||||
|
stopAlarmSound();
|
||||||
|
closeModal('timerCompleteModal');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test audio function
|
||||||
|
function testAudio() {
|
||||||
|
console.log('Testing audio...');
|
||||||
|
startAlarmSound();
|
||||||
|
|
||||||
|
// Show test modal
|
||||||
|
showAlert('Testing audio alarm. Click OK to stop.', 'Audio Test');
|
||||||
|
|
||||||
|
// Override OK button to stop sound
|
||||||
|
setTimeout(() => {
|
||||||
|
const okButton = document.querySelector('#alertModal button');
|
||||||
|
okButton.onclick = () => {
|
||||||
|
stopAlarmSound();
|
||||||
|
closeModal('alertModal');
|
||||||
|
};
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modals when clicking outside (except for no-close modals)
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
const modals = document.querySelectorAll('.modal');
|
const modals = document.querySelectorAll('.modal:not(.no-close)');
|
||||||
modals.forEach(modal => {
|
modals.forEach(modal => {
|
||||||
if (event.target === modal) {
|
if (event.target === modal) {
|
||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
|
BIN
public/timer-complete.mp3
Normal file
BIN
public/timer-complete.mp3
Normal file
Binary file not shown.
79
server.js
79
server.js
@@ -37,7 +37,7 @@ const CONFIG = {
|
|||||||
together: 60 * 60, // 1 hour in seconds
|
together: 60 * 60, // 1 hour in seconds
|
||||||
finishUp: 2 * 60 // 2 minutes in seconds
|
finishUp: 2 * 60 // 2 minutes in seconds
|
||||||
},
|
},
|
||||||
port: 3000
|
port: 80
|
||||||
};
|
};
|
||||||
|
|
||||||
// Data file path
|
// Data file path
|
||||||
@@ -53,6 +53,7 @@ const initialState = {
|
|||||||
activityLog: [],
|
activityLog: [],
|
||||||
togetherUsedToday: 0,
|
togetherUsedToday: 0,
|
||||||
lastResetDate: new Date().toDateString(),
|
lastResetDate: new Date().toDateString(),
|
||||||
|
lastWeeklyReset: '',
|
||||||
connectedClients: 0
|
connectedClients: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -579,6 +580,79 @@ app.get('/api/config', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Scheduled tasks
|
||||||
|
function scheduleResets() {
|
||||||
|
// Check every minute for reset times
|
||||||
|
setInterval(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const hours = now.getHours();
|
||||||
|
const minutes = now.getMinutes();
|
||||||
|
const day = now.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
||||||
|
|
||||||
|
// Daily reset at 00:01
|
||||||
|
if (hours === 0 && minutes === 1) {
|
||||||
|
// Reset daily limit
|
||||||
|
const today = new Date().toDateString();
|
||||||
|
if (gameState.lastResetDate !== today) {
|
||||||
|
gameState.togetherUsedToday = 0;
|
||||||
|
gameState.lastResetDate = today;
|
||||||
|
addLogEntry('Daily together limit reset automatically');
|
||||||
|
io.emit('stateUpdate', gameState);
|
||||||
|
saveGameState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weekly reset on Monday at 00:01
|
||||||
|
if (day === 1 && hours === 0 && minutes === 1) {
|
||||||
|
// Check if we haven't already reset this week
|
||||||
|
const lastWeeklyReset = gameState.lastWeeklyReset || '';
|
||||||
|
const thisWeek = `${now.getFullYear()}-W${getWeekNumber(now)}`;
|
||||||
|
|
||||||
|
if (lastWeeklyReset !== thisWeek) {
|
||||||
|
// Reset to 5 solo and 4 together
|
||||||
|
gameState.soloPoints = Array(5).fill(null).map((_, i) => ({
|
||||||
|
id: `solo_${Date.now()}_${i}`,
|
||||||
|
type: 'solo',
|
||||||
|
status: 'available',
|
||||||
|
duration: CONFIG.timers.solo,
|
||||||
|
label: '15m'
|
||||||
|
}));
|
||||||
|
|
||||||
|
gameState.togetherPoints = Array(4).fill(null).map((_, i) => ({
|
||||||
|
id: `together_${Date.now()}_${i}`,
|
||||||
|
type: 'together',
|
||||||
|
status: 'available',
|
||||||
|
duration: CONFIG.timers.together,
|
||||||
|
label: '1h'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Clear custom points and timers
|
||||||
|
gameState.customPoints = [];
|
||||||
|
gameState.activeTimers = {};
|
||||||
|
gameState.pausedTimers = {};
|
||||||
|
gameState.lastWeeklyReset = thisWeek;
|
||||||
|
|
||||||
|
// Clear all timer intervals
|
||||||
|
timerIntervals.forEach((interval) => clearInterval(interval));
|
||||||
|
timerIntervals.clear();
|
||||||
|
|
||||||
|
addLogEntry('Weekly points reset automatically (Monday 00:01)');
|
||||||
|
io.emit('stateUpdate', gameState);
|
||||||
|
saveGameState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 60000); // Check every minute
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get week number
|
||||||
|
function getWeekNumber(date) {
|
||||||
|
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||||
|
const dayNum = d.getUTCDay() || 7;
|
||||||
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||||
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||||
|
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||||||
|
}
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
server.listen(CONFIG.port, () => {
|
server.listen(CONFIG.port, () => {
|
||||||
console.log(`Point Tracker Server running on port ${CONFIG.port}`);
|
console.log(`Point Tracker Server running on port ${CONFIG.port}`);
|
||||||
@@ -587,6 +661,9 @@ server.listen(CONFIG.port, () => {
|
|||||||
// Check day reset on startup
|
// Check day reset on startup
|
||||||
checkDayReset();
|
checkDayReset();
|
||||||
|
|
||||||
|
// Start scheduled resets
|
||||||
|
scheduleResets();
|
||||||
|
|
||||||
// Restore any active timers
|
// Restore any active timers
|
||||||
Object.entries(gameState.activeTimers).forEach(([pointId, timer]) => {
|
Object.entries(gameState.activeTimers).forEach(([pointId, timer]) => {
|
||||||
const elapsed = Date.now() - timer.startTime;
|
const elapsed = Date.now() - timer.startTime;
|
||||||
|
Reference in New Issue
Block a user