Files
narrow_casting_system/backend/services/ScheduleManager.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

259 lines
6.9 KiB
JavaScript

class ScheduleManager {
constructor(databaseManager, socketIO) {
this.db = databaseManager;
this.io = socketIO;
this.activeSchedules = new Map();
}
async addSchedule(scheduleData) {
try {
// Validate content exists
const content = await this.db.getContentById(scheduleData.contentId);
if (!content) {
throw new Error('Content not found');
}
// Validate time range
const startTime = new Date(scheduleData.startTime);
const endTime = new Date(scheduleData.endTime);
if (startTime >= endTime) {
throw new Error('End time must be after start time');
}
if (startTime < new Date()) {
throw new Error('Start time cannot be in the past');
}
// Check for overlapping schedules with higher priority
const overlapping = await this.checkOverlappingSchedules(
scheduleData.zone,
scheduleData.startTime,
scheduleData.endTime,
scheduleData.priority
);
if (overlapping.length > 0) {
console.warn('Schedule overlaps with higher priority content:', overlapping);
}
const schedule = await this.db.addSchedule(scheduleData);
await this.db.addLog('schedule', 'Schedule created', {
scheduleId: schedule.id,
zone: schedule.zone,
contentId: schedule.contentId
});
// Update active schedules cache
this.updateActiveSchedules(scheduleData.zone);
return schedule;
} catch (error) {
console.error('Error adding schedule:', error);
throw error;
}
}
async checkOverlappingSchedules(zone, startTime, endTime, priority) {
return new Promise((resolve, reject) => {
const query = `
SELECT s.*, c.title, c.type FROM schedule s
JOIN content c ON s.contentId = c.id
WHERE s.zone = ?
AND s.startTime < ?
AND s.endTime > ?
AND s.priority > ?
AND s.isActive = 1
`;
this.db.db.all(query, [zone, endTime, startTime, priority], (err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
});
});
}
async getActiveSchedule(zone) {
try {
const now = new Date().toISOString();
// Check cache first
if (this.activeSchedules.has(zone)) {
const cached = this.activeSchedules.get(zone);
if (cached.timestamp > now) {
return cached.schedule;
}
}
// Get from database
const schedule = await this.db.getActiveSchedule(zone);
// Cache result for 1 minute
this.activeSchedules.set(zone, {
schedule: schedule,
timestamp: new Date(Date.now() + 60000).toISOString()
});
return schedule;
} catch (error) {
console.error('Error getting active schedule:', error);
throw error;
}
}
async updateActiveSchedules(zone) {
try {
const schedule = await this.getActiveSchedule(zone);
// Emit update to clients in this zone
this.io.to(zone).emit('scheduleUpdate', {
zone: zone,
schedule: schedule,
timestamp: new Date().toISOString()
});
// Also emit to admin clients
this.io.to('admin').emit('scheduleUpdate', {
zone: zone,
schedule: schedule,
timestamp: new Date().toISOString()
});
await this.db.addLog('schedule', 'Active schedule updated', { zone, count: schedule.length });
} catch (error) {
console.error('Error updating active schedules:', error);
}
}
async deleteSchedule(scheduleId) {
try {
// Get schedule info before deletion for logging
const schedule = await this.getScheduleById(scheduleId);
await this.db.db.run('DELETE FROM schedule WHERE id = ?', [scheduleId]);
if (schedule) {
await this.db.addLog('schedule', 'Schedule deleted', {
scheduleId,
zone: schedule.zone,
contentId: schedule.contentId
});
// Update active schedules for the zone
this.updateActiveSchedules(schedule.zone);
}
return true;
} catch (error) {
console.error('Error deleting schedule:', error);
throw error;
}
}
async getScheduleById(scheduleId) {
return new Promise((resolve, reject) => {
this.db.db.get('SELECT * FROM schedule WHERE id = ?', [scheduleId], (err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
});
});
}
async getUpcomingSchedules(zone, limit = 10) {
return new Promise((resolve, reject) => {
const now = new Date().toISOString();
const query = `
SELECT s.*, c.title, c.type FROM schedule s
JOIN content c ON s.contentId = c.id
WHERE s.zone = ?
AND s.startTime > ?
AND s.isActive = 1
ORDER BY s.startTime ASC
LIMIT ?
`;
this.db.db.all(query, [zone, now, limit], (err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
});
});
}
async getScheduleStats() {
try {
const totalSchedules = await new Promise((resolve, reject) => {
this.db.db.get('SELECT COUNT(*) as count FROM schedule WHERE isActive = 1', (err, row) => {
if (err) reject(err);
else resolve(row.count);
});
});
const activeSchedules = await new Promise((resolve, reject) => {
const now = new Date().toISOString();
this.db.db.get(
'SELECT COUNT(*) as count FROM schedule WHERE startTime <= ? AND endTime >= ? AND isActive = 1',
[now, now],
(err, row) => {
if (err) reject(err);
else resolve(row.count);
}
);
});
const upcomingSchedules = await new Promise((resolve, reject) => {
const now = new Date().toISOString();
this.db.db.get(
'SELECT COUNT(*) as count FROM schedule WHERE startTime > ? AND isActive = 1',
[now],
(err, row) => {
if (err) reject(err);
else resolve(row.count);
}
);
});
return {
total: totalSchedules,
active: activeSchedules,
upcoming: upcomingSchedules
};
} catch (error) {
console.error('Error getting schedule stats:', error);
throw error;
}
}
// Start schedule monitoring
startScheduleMonitoring() {
// Check every minute for schedule updates
setInterval(() => {
this.checkScheduleUpdates();
}, 60000);
// Initial check
this.checkScheduleUpdates();
}
async checkScheduleUpdates() {
try {
const zones = await this.db.getZones();
for (const zone of zones) {
await this.updateActiveSchedules(zone.id);
}
} catch (error) {
console.error('Error in schedule monitoring:', error);
}
}
}
module.exports = ScheduleManager;