Created a beautiful landing page with random room/passphrase generation and updated security model for read-only access. New Files: - server/php/index.html: Landing page with URL generator Features: - Random room name generation (e.g., "swift-phoenix-1234") - Random passphrase generation (16 chars, URL-safe) - Copy-to-clipboard functionality - Responsive design with gradient header - Step-by-step usage instructions - FAQ section Security Model Changes: - WRITE (send transcriptions): Requires room + passphrase - READ (view display): Only requires room name Updated Files: - server.php: * handleStream(): Passphrase optional (read-only) * handleList(): Passphrase optional (read-only) * Added roomExists() helper function - display.php: * Removed passphrase from URL parameters * Removed passphrase from SSE connection * Removed passphrase from list endpoint Benefits: - Display URL is safer (no passphrase in OBS browser source) - Simpler setup (only room name needed for viewing) - Better security model (write-protected, read-open) - Anyone with room name can watch, only authorized can send Example URLs: - Client: server.php (with room + passphrase in app settings) - Display: display.php?room=swift-phoenix-1234&fade=10×tamps=true 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
184 lines
5.9 KiB
PHP
184 lines
5.9 KiB
PHP
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Multi-User Transcription Display</title>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 20px;
|
|
background: transparent;
|
|
font-family: Arial, sans-serif;
|
|
color: white;
|
|
}
|
|
#transcriptions {
|
|
max-height: 100vh;
|
|
overflow-y: auto;
|
|
}
|
|
.transcription {
|
|
margin: 10px 0;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
border-radius: 5px;
|
|
animation: slideIn 0.3s ease-out;
|
|
transition: opacity 1s ease-out;
|
|
}
|
|
.transcription.fading {
|
|
opacity: 0;
|
|
}
|
|
.timestamp {
|
|
color: #888;
|
|
font-size: 0.9em;
|
|
margin-right: 10px;
|
|
}
|
|
.user {
|
|
font-weight: bold;
|
|
margin-right: 10px;
|
|
/* Color set dynamically via inline style */
|
|
}
|
|
.text {
|
|
color: white;
|
|
}
|
|
#status {
|
|
position: fixed;
|
|
top: 10px;
|
|
right: 10px;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
border-radius: 5px;
|
|
font-size: 0.9em;
|
|
}
|
|
#status.connected { color: #4CAF50; }
|
|
#status.disconnected { color: #f44336; }
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="status" class="disconnected">⚫ Connecting...</div>
|
|
<div id="transcriptions"></div>
|
|
|
|
<script>
|
|
// Get URL parameters
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const room = urlParams.get('room') || 'default';
|
|
const fadeAfter = parseInt(urlParams.get('fade') || '10');
|
|
const showTimestamps = urlParams.get('timestamps') !== 'false';
|
|
|
|
const container = document.getElementById('transcriptions');
|
|
const statusEl = document.getElementById('status');
|
|
const userColors = new Map(); // Map user names to HSL colors
|
|
let colorIndex = 0;
|
|
|
|
// Generate distinct color for each user using golden ratio
|
|
function getUserColor(userName) {
|
|
if (!userColors.has(userName)) {
|
|
// Use golden ratio for evenly distributed hues
|
|
const goldenRatio = 0.618033988749895;
|
|
const hue = (colorIndex * goldenRatio * 360) % 360;
|
|
// High saturation and medium lightness for vibrant, readable colors
|
|
const color = `hsl(${hue}, 85%, 65%)`;
|
|
userColors.set(userName, color);
|
|
colorIndex++;
|
|
}
|
|
return userColors.get(userName);
|
|
}
|
|
|
|
// Connect to Server-Sent Events
|
|
function connect() {
|
|
const url = `server.php?action=stream&room=${encodeURIComponent(room)}`;
|
|
const eventSource = new EventSource(url);
|
|
|
|
eventSource.onopen = () => {
|
|
statusEl.textContent = '🟢 Connected';
|
|
statusEl.className = 'connected';
|
|
};
|
|
|
|
eventSource.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
addTranscription(data);
|
|
};
|
|
|
|
eventSource.onerror = () => {
|
|
statusEl.textContent = '🔴 Disconnected';
|
|
statusEl.className = 'disconnected';
|
|
eventSource.close();
|
|
|
|
// Reconnect after 3 seconds
|
|
setTimeout(connect, 3000);
|
|
};
|
|
}
|
|
|
|
function addTranscription(data) {
|
|
const div = document.createElement('div');
|
|
div.className = 'transcription';
|
|
|
|
// Get user color (generates new color if first time)
|
|
const userColor = getUserColor(data.user_name);
|
|
|
|
let html = '';
|
|
if (showTimestamps && data.timestamp) {
|
|
html += `<span class="timestamp">[${data.timestamp}]</span>`;
|
|
}
|
|
if (data.user_name) {
|
|
html += `<span class="user" style="color: ${userColor}">${data.user_name}:</span>`;
|
|
}
|
|
html += `<span class="text">${data.text}</span>`;
|
|
|
|
div.innerHTML = html;
|
|
container.appendChild(div);
|
|
|
|
// Auto-scroll to bottom
|
|
container.scrollTop = container.scrollHeight;
|
|
|
|
// Set up fade-out if enabled
|
|
if (fadeAfter > 0) {
|
|
setTimeout(() => {
|
|
div.classList.add('fading');
|
|
setTimeout(() => {
|
|
if (div.parentNode === container) {
|
|
container.removeChild(div);
|
|
}
|
|
}, 1000);
|
|
}, fadeAfter * 1000);
|
|
}
|
|
|
|
// Limit to 100 transcriptions
|
|
while (container.children.length > 100) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
}
|
|
|
|
// Load recent transcriptions on startup
|
|
async function loadRecent() {
|
|
try {
|
|
const url = `server.php?action=list&room=${encodeURIComponent(room)}`;
|
|
const response = await fetch(url);
|
|
const data = await response.json();
|
|
|
|
if (data.transcriptions) {
|
|
// Show last 20 transcriptions
|
|
data.transcriptions.slice(-20).forEach(addTranscription);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading recent transcriptions:', error);
|
|
}
|
|
}
|
|
|
|
// Start
|
|
loadRecent().then(() => {
|
|
connect();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|