Compare commits
3 Commits
1a364391d1
...
1c72844c87
Author | SHA1 | Date | |
---|---|---|---|
1c72844c87 | |||
7405cd22e8 | |||
965f349817 |
@@ -338,6 +338,10 @@
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
.modal.no-close {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #2a2a2a;
|
||||
margin: 15% auto;
|
||||
@@ -346,6 +350,11 @@
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
border: 2px solid #404040;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal.no-close .modal-content {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.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="resetDay()">Reset Daily Limit</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -676,10 +686,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Finish-Up Modal -->
|
||||
<div id="finishUpModal" class="modal">
|
||||
<div id="finishUpModal" class="modal no-close">
|
||||
<div class="modal-content" style="max-width: 400px;">
|
||||
<h2>🔔 Timer Completed!</h2>
|
||||
|
||||
<p id="finishUpMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
|
||||
<p style="color: #e0e0e0;">Would you like 2 minutes of finish-up time?</p>
|
||||
<div class="timer-controls">
|
||||
@@ -689,10 +698,22 @@
|
||||
</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 id="timerAudio" preload="auto">
|
||||
<source src="/timer-complete.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
<audio id="timerAudio" preload="auto" loop>
|
||||
<source src="timer-complete.mp3" type="audio/mpeg">
|
||||
<source src="timer-complete.wav" type="audio/wav">
|
||||
<source src="timer-complete.ogg" type="audio/ogg">
|
||||
</audio>
|
||||
|
||||
<script>
|
||||
@@ -757,11 +778,10 @@
|
||||
socket.on('timerComplete', (data) => {
|
||||
const { pointId, type, offerFinishUp } = data;
|
||||
|
||||
// Play completion sound
|
||||
playCompletionSound();
|
||||
|
||||
if (offerFinishUp) {
|
||||
// Show custom finish-up modal
|
||||
// Play sound and show finish-up modal
|
||||
startAlarmSound();
|
||||
|
||||
const point = findPoint(pointId);
|
||||
const pointName = point ? (point.customName || point.type) : 'Timer';
|
||||
document.getElementById('finishUpMessage').textContent = `${pointName} point completed!`;
|
||||
@@ -769,14 +789,24 @@
|
||||
|
||||
// Set up button handlers
|
||||
document.getElementById('finishUpYes').onclick = () => {
|
||||
stopAlarmSound();
|
||||
socket.emit('finishUpResponse', { pointId, accepted: true });
|
||||
closeModal('finishUpModal');
|
||||
};
|
||||
|
||||
document.getElementById('finishUpNo').onclick = () => {
|
||||
stopAlarmSound();
|
||||
socket.emit('finishUpResponse', { pointId, accepted: false });
|
||||
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
|
||||
function playCompletionSound() {
|
||||
let audioPlaying = false;
|
||||
|
||||
function startAlarmSound() {
|
||||
console.log('Starting alarm sound...');
|
||||
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 => {
|
||||
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) {
|
||||
const modals = document.querySelectorAll('.modal');
|
||||
const modals = document.querySelectorAll('.modal:not(.no-close)');
|
||||
modals.forEach(modal => {
|
||||
if (event.target === modal) {
|
||||
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
|
||||
finishUp: 2 * 60 // 2 minutes in seconds
|
||||
},
|
||||
port: 3000
|
||||
port: 80
|
||||
};
|
||||
|
||||
// Data file path
|
||||
@@ -53,6 +53,7 @@ const initialState = {
|
||||
activityLog: [],
|
||||
togetherUsedToday: 0,
|
||||
lastResetDate: new Date().toDateString(),
|
||||
lastWeeklyReset: '',
|
||||
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
|
||||
server.listen(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
|
||||
checkDayReset();
|
||||
|
||||
// Start scheduled resets
|
||||
scheduleResets();
|
||||
|
||||
// Restore any active timers
|
||||
Object.entries(gameState.activeTimers).forEach(([pointId, timer]) => {
|
||||
const elapsed = Date.now() - timer.startTime;
|
||||
|
Reference in New Issue
Block a user