<!DOCTYPE html>
<html>
<head>
<title>OSCE Station</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-database.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
text-align: center;
background: #111;
color: white;
padding-top: 10vh;
margin: 0;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
}
#phase { font-size: 2.5rem; color: #aaa; text-transform: uppercase; margin-bottom: 20px; letter-spacing: 2px; }
#timer { font-size: 25vw; font-weight: bold; margin-bottom: 40px; font-variant-numeric: tabular-nums; line-height: 1; }
button { padding: 20px 40px; font-size: 1.2rem; background: #dc3545; color: white; border: none; border-radius: 12px; cursor: pointer; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: 0.2s;}
button:active { transform: scale(0.95); }
@media (min-width: 768px) {
#timer { font-size: 12rem; }
}
/* Full-screen overlay to unlock audio */
#join-screen {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: #007bff; display: flex; flex-direction: column;
align-items: center; justify-content: center; z-index: 100;
}
#join-screen h2 { margin-bottom: 30px; font-size: 2rem;}
#join-screen button { background: white; color: #007bff; font-weight: bold; font-size: 1.5rem; padding: 25px 50px;}
</style>
</head>
<body>
<!-- Audio Unlock Overlay -->
<div id="join-screen">
<h2>OSCE Timer Portal</h2>
<button onclick="joinAndUnlockAudio()">Tap to Join Station</button>
</div>
<div id="phase">Waiting for Master</div>
<div id="timer">00:00</div>
<button onclick="requestExtraTime()">Request Extra Time</button>
<script>
// --- 1. FIREBASE SETUP ---
const firebaseConfig = {
apiKey: "AIzaSyC1YUuGjJtPfKoUvjg3oZ9KWg2qiMCg5Ec",
authDomain: "osce-timer-75bc3.firebaseapp.com",
databaseURL: "https://osce-timer-75bc3-default-rtdb.asia-southeast1.firebasedatabase.app",
projectId: "osce-timer-75bc3",
storageBucket: "osce-timer-75bc3.firebasestorage.app",
messagingSenderId: "1074792422880",
appId: "1:1074792422880:web:893bd949b3e610b021df85",
measurementId: "G-00NQP07LE2"
};
firebase.initializeApp(firebaseConfig);
const db = firebase.database().ref('osce_timer');
let serverOffset = 0;
firebase.database().ref('.info/serverTimeOffset').on('value', snap => serverOffset = snap.val());
// --- 2. AUDIO SYNTHESIS ENGINE ---
let audioCtx;
let lastAlertedEndTime = 0; // Tracks if we've already beeped for this phase
function joinAndUnlockAudio() {
// Create audio context on user click
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// Play a silent note immediately to securely unlock iOS Safari
const osc = audioCtx.createOscillator();
osc.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.01);
// Hide overlay
document.getElementById('join-screen').style.display = 'none';
}
function playChime() {
if (!audioCtx) return;
// Synthesize a clean, professional "ding" chime
const osc = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, audioCtx.currentTime); // High pitch start
osc.frequency.exponentialRampToValueAtTime(300, audioCtx.currentTime + 0.5); // Drop in pitch
gainNode.gain.setValueAtTime(1, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 1); // Fade out over 1 sec
osc.connect(gainNode);
gainNode.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 1);
}
// --- 3. STATE AND TIMER RENDER LOOP ---
let state = {};
db.on('value', snap => {
state = snap.val() || {};
if(!state.isRunning) {
document.getElementById('timer').innerText = "00:00";
document.getElementById('phase').innerText = "Stopped";
}
});
setInterval(() => {
if (state.isRunning && state.endTime) {
const trueTime = Date.now() + serverOffset;
const timeLeftMs = state.endTime - trueTime;
if (timeLeftMs > 0) {
document.getElementById('phase').innerText = state.currentPhase;
const minutes = Math.floor(timeLeftMs / 60000);
const seconds = Math.floor((timeLeftMs % 60000) / 1000);
document.getElementById('timer').innerText =
String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
} else {
// TIME IS UP
document.getElementById('timer').innerText = "00:00";
document.getElementById('phase').innerText = "Time Up";
// Check if we already played the chime for this specific phase end-time
if (state.endTime !== lastAlertedEndTime) {
playChime();
lastAlertedEndTime = state.endTime; // Prevent infinite looping
}
}
}
}, 100);
function requestExtraTime() {
const stationNum = prompt("Enter your Station Number:");
if(stationNum) {
db.update({ extraTimeRequestedBy: `Station ${stationNum}` });
alert("Request sent to Master.");
}
}
</script>
</body>
</html>