Compare commits

...

3 Commits

Author SHA1 Message Date
1c72844c87 changed port to 80 2025-05-30 01:50:43 -04:00
7405cd22e8 added custom sound and sound test button in admin panel 2025-05-30 01:40:19 -04:00
965f349817 added timer dialogue after finish up 2025-05-30 01:17:23 -04:00
3 changed files with 259 additions and 14 deletions

View File

@@ -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

Binary file not shown.

View File

@@ -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;