mirror of
https://github.com/Alvin-Zilverstand/narrow_casting_system.git
synced 2026-03-06 11:07:14 +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:
240
admin/js/websocket.js
Normal file
240
admin/js/websocket.js
Normal file
@@ -0,0 +1,240 @@
|
||||
// WebSocket Management for SnowWorld Admin Dashboard
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.reconnectDelay = 1000;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
try {
|
||||
this.socket = io('http://localhost:3000', {
|
||||
transports: ['websocket', 'polling'],
|
||||
timeout: 5000,
|
||||
forceNew: true
|
||||
});
|
||||
|
||||
this.setupEventListeners();
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleConnectionError();
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.socket.on('connect', () => {
|
||||
console.log('WebSocket connected');
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.updateConnectionStatus(true);
|
||||
|
||||
// Join admin room for global updates
|
||||
this.socket.emit('joinZone', 'admin');
|
||||
|
||||
this.showToast('Verbonden met server', 'success');
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
console.log('WebSocket disconnected');
|
||||
this.isConnected = false;
|
||||
this.updateConnectionStatus(false);
|
||||
|
||||
// Attempt reconnection
|
||||
this.attemptReconnect();
|
||||
});
|
||||
|
||||
this.socket.on('connect_error', (error) => {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleConnectionError();
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
handleContentUpdate(data) {
|
||||
// Clear content cache to force refresh
|
||||
if (window.ui) {
|
||||
window.ui.clearContentCache();
|
||||
}
|
||||
|
||||
// Show notification based on update type
|
||||
switch (data.type) {
|
||||
case 'content_added':
|
||||
this.showToast(`Nieuwe content toegevoegd: ${data.content.title}`, 'info');
|
||||
break;
|
||||
case 'content_deleted':
|
||||
this.showToast('Content verwijderd', 'warning');
|
||||
break;
|
||||
case 'content_updated':
|
||||
this.showToast('Content bijgewerkt', 'info');
|
||||
break;
|
||||
}
|
||||
|
||||
// Refresh current view if on content tab
|
||||
if (window.ui && window.ui.currentTab === 'content') {
|
||||
window.ui.loadContent();
|
||||
}
|
||||
}
|
||||
|
||||
handleScheduleUpdate(data) {
|
||||
// Show notification
|
||||
this.showToast(`Planning bijgewerkt voor zone: ${data.zone}`, 'info');
|
||||
|
||||
// Refresh schedule view if currently viewing this zone
|
||||
const currentZone = document.getElementById('scheduleZoneSelect')?.value;
|
||||
if (window.ui && window.ui.currentTab === 'schedule' && currentZone === data.zone) {
|
||||
window.ui.loadSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
handleZoneUpdate(data) {
|
||||
// Handle zone-specific updates
|
||||
this.showToast(`Zone ${data.zone} bijgewerkt`, 'info');
|
||||
|
||||
// Refresh relevant views
|
||||
if (window.ui) {
|
||||
if (window.ui.currentTab === 'zones') {
|
||||
window.ui.loadZonesOverview();
|
||||
} else if (window.ui.currentTab === 'content') {
|
||||
window.ui.loadContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSystemNotification(data) {
|
||||
// Handle system-level notifications
|
||||
const { message, type, duration } = data;
|
||||
this.showToast(message, type || 'info', duration);
|
||||
}
|
||||
|
||||
updateConnectionStatus(connected) {
|
||||
const statusDot = document.getElementById('connectionStatus');
|
||||
const statusText = document.getElementById('connectionText');
|
||||
|
||||
if (statusDot) {
|
||||
statusDot.className = connected ? 'status-dot' : 'status-dot disconnected';
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
statusText.textContent = connected ? 'Verbonden' : 'Verbinding verbroken';
|
||||
}
|
||||
}
|
||||
|
||||
attemptReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.error('Max reconnection attempts reached');
|
||||
this.showToast('Kan geen verbinding maken met de server', 'error');
|
||||
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);
|
||||
}
|
||||
|
||||
handleConnectionError() {
|
||||
this.isConnected = false;
|
||||
this.updateConnectionStatus(false);
|
||||
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.showToast('Verbinding verbroken. Probeert opnieuw...', 'warning');
|
||||
} else {
|
||||
this.showToast('Kan geen verbinding maken met de server', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Public methods
|
||||
joinZone(zone) {
|
||||
if (this.isConnected && this.socket) {
|
||||
this.socket.emit('joinZone', zone);
|
||||
console.log(`Joined zone: ${zone}`);
|
||||
}
|
||||
}
|
||||
|
||||
leaveZone(zone) {
|
||||
if (this.isConnected && this.socket) {
|
||||
this.socket.emit('leaveZone', zone);
|
||||
console.log(`Left zone: ${zone}`);
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(event, data) {
|
||||
if (this.isConnected && this.socket) {
|
||||
this.socket.emit(event, data);
|
||||
} else {
|
||||
console.warn('Cannot send message: not connected');
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
this.isConnected = false;
|
||||
this.updateConnectionStatus(false);
|
||||
}
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
this.disconnect();
|
||||
this.reconnectAttempts = 0;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
showToast(message, type = 'info', duration = 5000) {
|
||||
if (window.ui) {
|
||||
window.ui.showToast(message, type);
|
||||
} else {
|
||||
// Fallback to browser notification
|
||||
console.log(`Toast [${type}]: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get connection status
|
||||
getConnectionStatus() {
|
||||
return {
|
||||
connected: this.isConnected,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
socketId: this.socket?.id || null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create global WebSocket instance
|
||||
window.wsManager = new WebSocketManager();
|
||||
Reference in New Issue
Block a user