mirror of
https://github.com/Alvin-Zilverstand/narrow_casting_system.git
synced 2026-03-06 21:29:47 +01:00
🎿 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! 🎿❄️
This commit is contained in:
388
client/js/connection.js
Normal file
388
client/js/connection.js
Normal file
@@ -0,0 +1,388 @@
|
||||
// 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();
|
||||
Reference in New Issue
Block a user