Files
narrow_casting_system/client/js/connection.js
Alvin-Zilverstand 8e446a1339 🎿 Complete SnowWorld Narrowcasting System - MBO Challenge 18
 Full-stack narrowcasting platform implementation
 Real-time WebSocket communication for instant updates
 Zone-specific content distribution (reception, restaurant, skislope, lockers, shop)
 Professional admin dashboard with content management interface
 Beautiful client display with winter/snow theme matching SnowWorld branding
 Comprehensive technical documentation and test suite
 Docker deployment support with CI/CD pipeline
 All system tests passing successfully

🏗️ Technical Implementation:
- Backend: Node.js/Express with SQLite database
- Frontend: Vanilla HTML/CSS/JavaScript (no frameworks)
- Real-time: Socket.io WebSocket communication
- Database: Complete schema with content, schedule, zones, logs tables
- Security: File validation, input sanitization, CORS protection
- Performance: Optimized for fast loading and real-time updates

🚀 Features Delivered:
- Content upload (images, videos) with drag-and-drop interface
- Content scheduling and planning system
- Weather widget with real-time snow information
- Responsive design for all screen sizes
- Comprehensive error handling and fallback mechanisms
- Professional winter theme with snow animations
- Keyboard shortcuts and accessibility features

📁 Project Structure:
- /backend: Complete Node.js server with API and WebSocket
- /admin: Professional admin dashboard interface
- /client: Beautiful client display application
- /deployment: Docker and deployment configurations
- /docs: Comprehensive technical documentation
- /test_system.js: Complete test suite (all tests passing)

🧪 Testing Results:
- Server health:  Online and responsive
- API endpoints:  All endpoints functional
- Database operations:  All operations successful
- WebSocket communication:  Real-time updates working
- Zone distribution:  6 zones correctly loaded
- Weather integration:  Weather data available

Ready for production deployment at SnowWorld! 🎿❄️
2026-01-19 10:02:11 +01:00

388 lines
12 KiB
JavaScript

// Connection Management for SnowWorld Client
class ConnectionManager {
constructor() {
this.socket = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectDelay = 1000;
this.heartbeatInterval = null;
this.lastPingTime = null;
this.serverUrl = 'http://localhost:3000';
this.zone = this.getZoneFromURL() || 'reception';
this.contentUpdateInterval = null;
this.lastContentUpdate = null;
this.init();
}
init() {
this.connect();
this.setupHeartbeat();
}
connect() {
try {
console.log('Connecting to server...');
this.updateConnectionStatus('connecting');
this.socket = io(this.serverUrl, {
transports: ['websocket', 'polling'],
timeout: 5000,
forceNew: true,
reconnection: false // We handle reconnection manually
});
this.setupEventListeners();
} catch (error) {
console.error('Connection error:', error);
this.handleConnectionError(error);
}
}
setupEventListeners() {
this.socket.on('connect', () => {
console.log('Connected to server');
this.isConnected = true;
this.reconnectAttempts = 0;
this.updateConnectionStatus('connected');
// Join zone-specific room
this.joinZone(this.zone);
// Request initial content
this.requestContentForZone(this.zone);
// Hide error overlay if shown
this.hideErrorOverlay();
console.log(`Joined zone: ${this.zone}`);
});
this.socket.on('disconnect', (reason) => {
console.log('Disconnected from server:', reason);
this.isConnected = false;
this.updateConnectionStatus('disconnected');
// Attempt reconnection
this.attemptReconnect();
});
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error);
this.handleConnectionError(error);
});
this.socket.on('reconnect_failed', () => {
console.error('Reconnection failed');
this.handleConnectionError(new Error('Reconnection failed'));
});
// Content updates
this.socket.on('contentUpdated', (data) => {
console.log('Content update received:', data);
this.handleContentUpdate(data);
});
// Schedule updates
this.socket.on('scheduleUpdated', (data) => {
console.log('Schedule update received:', data);
this.handleScheduleUpdate(data);
});
// Zone-specific updates
this.socket.on('zoneUpdate', (data) => {
console.log('Zone update received:', data);
this.handleZoneUpdate(data);
});
// System notifications
this.socket.on('systemNotification', (data) => {
console.log('System notification:', data);
this.handleSystemNotification(data);
});
// Ping/pong for latency monitoring
this.socket.on('pong', (data) => {
if (this.lastPingTime) {
const latency = Date.now() - this.lastPingTime;
console.log(`Latency: ${latency}ms`);
this.updateLatencyDisplay(latency);
}
});
}
joinZone(zone) {
if (this.isConnected && this.socket) {
this.socket.emit('joinZone', zone);
console.log(`Requested to join zone: ${zone}`);
}
}
leaveZone(zone) {
if (this.isConnected && this.socket) {
this.socket.emit('leaveZone', zone);
console.log(`Requested to leave zone: ${zone}`);
}
}
requestContentForZone(zone) {
console.log(`Requesting content for zone: ${zone}`);
// Use HTTP API as fallback if WebSocket is not available
if (this.isConnected) {
// Request via WebSocket
this.socket.emit('requestContent', { zone: zone });
} else {
// Fallback to HTTP
this.fetchContentViaHTTP(zone);
}
}
async fetchContentViaHTTP(zone) {
try {
const response = await fetch(`http://localhost:3000/api/schedule/${zone}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const content = await response.json();
this.handleContentUpdate({
type: 'zone_content',
zone: zone,
content: content
});
} catch (error) {
console.error('HTTP content fetch error:', error);
this.handleContentError(error);
}
}
handleContentUpdate(data) {
if (data.type === 'zone_content' && data.zone === this.zone) {
console.log(`Updating content for zone ${this.zone}:`, data.content);
if (window.displayManager) {
window.displayManager.updateContent(data.content);
}
this.lastContentUpdate = new Date().toISOString();
} else if (data.type === 'content_added' || data.type === 'content_deleted') {
// Refresh content for current zone
this.requestContentForZone(this.zone);
}
}
handleScheduleUpdate(data) {
if (data.zone === this.zone) {
console.log(`Schedule updated for zone ${this.zone}`);
// Refresh content for current zone
this.requestContentForZone(this.zone);
}
}
handleZoneUpdate(data) {
if (data.zone === this.zone) {
console.log(`Zone ${this.zone} updated`);
// Refresh content for current zone
this.requestContentForZone(this.zone);
}
}
handleSystemNotification(data) {
const { message, type } = data;
// Show notification on display
if (window.displayManager) {
// Could implement a notification overlay in the display manager
console.log(`System notification: ${message} (${type})`);
}
}
handleConnectionError(error) {
console.error('Connection error:', error);
this.isConnected = false;
this.updateConnectionStatus('error');
// Show error overlay
this.showErrorOverlay('Verbindingsfout', 'Kan geen verbinding maken met de server');
// Attempt reconnection
this.attemptReconnect();
}
handleContentError(error) {
console.error('Content error:', error);
if (window.displayManager) {
window.displayManager.handleError(error);
}
}
attemptReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
this.showErrorOverlay(
'Verbinding verbroken',
'Kan geen verbinding maken. Controleer de server en netwerk.'
);
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
setTimeout(() => {
if (!this.isConnected) {
this.connect();
}
}, delay);
}
setupHeartbeat() {
// Send ping every 30 seconds
this.heartbeatInterval = setInterval(() => {
this.sendPing();
}, 30000);
// Initial ping
this.sendPing();
}
sendPing() {
if (this.isConnected && this.socket) {
this.lastPingTime = Date.now();
this.socket.emit('ping');
}
}
updateConnectionStatus(status) {
const statusElement = document.getElementById('connectionStatus');
if (!statusElement) return;
const statusDot = statusElement.querySelector('.status-dot');
const statusText = statusElement.querySelector('.status-text');
switch (status) {
case 'connected':
statusDot.className = 'status-dot';
statusText.textContent = 'Verbonden';
break;
case 'connecting':
statusDot.className = 'status-dot connecting';
statusText.textContent = 'Verbinden...';
break;
case 'disconnected':
statusDot.className = 'status-dot disconnected';
statusText.textContent = 'Verbroken';
break;
case 'error':
statusDot.className = 'status-dot disconnected';
statusText.textContent = 'Fout';
break;
}
}
updateLatencyDisplay(latency) {
// Could add latency display to UI if needed
console.log(`Connection latency: ${latency}ms`);
// Show warning if latency is high
if (latency > 1000) {
console.warn('High latency detected:', latency + 'ms');
}
}
showErrorOverlay(title, message) {
const overlay = document.getElementById('errorOverlay');
if (!overlay) return;
document.getElementById('errorMessage').textContent = message;
overlay.classList.add('active');
// Add retry button functionality
const retryButton = document.getElementById('retryButton');
if (retryButton) {
retryButton.onclick = () => {
this.hideErrorOverlay();
this.reconnect();
};
}
}
hideErrorOverlay() {
const overlay = document.getElementById('errorOverlay');
if (overlay) {
overlay.classList.remove('active');
}
}
getZoneFromURL() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('zone');
}
setZone(zone) {
if (this.zone !== zone) {
console.log(`Zone changed from ${this.zone} to ${zone}`);
// Leave current zone
this.leaveZone(this.zone);
// Update zone
this.zone = zone;
// Join new zone
this.joinZone(zone);
// Request content for new zone
this.requestContentForZone(zone);
// Update display
if (window.displayManager) {
window.displayManager.setZone(zone);
}
}
}
reconnect() {
console.log('Manually reconnecting...');
this.disconnect();
this.reconnectAttempts = 0;
this.connect();
}
disconnect() {
console.log('Disconnecting from server...');
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
if (this.socket) {
this.socket.disconnect();
}
this.isConnected = false;
this.updateConnectionStatus('disconnected');
}
// Get connection status
getStatus() {
return {
connected: this.isConnected,
zone: this.zone,
reconnectAttempts: this.reconnectAttempts,
lastContentUpdate: this.lastContentUpdate,
socketId: this.socket?.id || null
};
}
}
// Create global connection manager instance
window.connectionManager = new ConnectionManager();