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:
308
backend/database/DatabaseManager.js
Normal file
308
backend/database/DatabaseManager.js
Normal file
@@ -0,0 +1,308 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
class DatabaseManager {
|
||||
constructor() {
|
||||
this.dbPath = path.join(__dirname, '../database/snowworld.db');
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Ensure database directory exists
|
||||
const fs = require('fs-extra');
|
||||
fs.ensureDirSync(path.dirname(this.dbPath));
|
||||
|
||||
this.db = new sqlite3.Database(this.dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database:', err);
|
||||
return;
|
||||
}
|
||||
console.log('Connected to SQLite database');
|
||||
this.createTables();
|
||||
});
|
||||
}
|
||||
|
||||
createTables() {
|
||||
const contentTable = `
|
||||
CREATE TABLE IF NOT EXISTS content (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
originalName TEXT NOT NULL,
|
||||
mimeType TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
zone TEXT DEFAULT 'all',
|
||||
duration INTEGER DEFAULT 10,
|
||||
isActive INTEGER DEFAULT 1,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT
|
||||
)
|
||||
`;
|
||||
|
||||
const scheduleTable = `
|
||||
CREATE TABLE IF NOT EXISTS schedule (
|
||||
id TEXT PRIMARY KEY,
|
||||
contentId TEXT NOT NULL,
|
||||
zone TEXT NOT NULL,
|
||||
startTime TEXT NOT NULL,
|
||||
endTime TEXT NOT NULL,
|
||||
priority INTEGER DEFAULT 1,
|
||||
isActive INTEGER DEFAULT 1,
|
||||
createdAt TEXT NOT NULL,
|
||||
FOREIGN KEY (contentId) REFERENCES content (id) ON DELETE CASCADE
|
||||
)
|
||||
`;
|
||||
|
||||
const zonesTable = `
|
||||
CREATE TABLE IF NOT EXISTS zones (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
displayOrder INTEGER DEFAULT 0,
|
||||
isActive INTEGER DEFAULT 1
|
||||
)
|
||||
`;
|
||||
|
||||
const logsTable = `
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
data TEXT,
|
||||
timestamp TEXT NOT NULL
|
||||
)
|
||||
`;
|
||||
|
||||
this.db.serialize(() => {
|
||||
this.db.run(contentTable);
|
||||
this.db.run(scheduleTable);
|
||||
this.db.run(zonesTable);
|
||||
this.db.run(logsTable);
|
||||
|
||||
// Insert default zones
|
||||
const defaultZones = [
|
||||
{ id: 'reception', name: 'Receptie', description: 'Hoofdingang en receptie', displayOrder: 1 },
|
||||
{ id: 'restaurant', name: 'Restaurant', description: 'Eetgelegenheid', displayOrder: 2 },
|
||||
{ id: 'skislope', name: 'Skibaan', description: 'Hoofdskibaan', displayOrder: 3 },
|
||||
{ id: 'lockers', name: 'Kluisjes', description: 'Kleedkamers en kluisjes', displayOrder: 4 },
|
||||
{ id: 'shop', name: 'Winkel', description: 'Ski-uitrusting winkel', displayOrder: 5 },
|
||||
{ id: 'all', name: 'Alle zones', description: 'Toon op alle schermen', displayOrder: 0 }
|
||||
];
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT OR IGNORE INTO zones (id, name, description, displayOrder)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
defaultZones.forEach(zone => {
|
||||
stmt.run(zone.id, zone.name, zone.description, zone.displayOrder);
|
||||
});
|
||||
|
||||
stmt.finalize();
|
||||
|
||||
console.log('Database tables created successfully');
|
||||
});
|
||||
}
|
||||
|
||||
// Content methods
|
||||
async addContent(contentData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO content (id, type, title, filename, originalName, mimeType, size, path, url, zone, duration, createdAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
contentData.id,
|
||||
contentData.type,
|
||||
contentData.title,
|
||||
contentData.filename,
|
||||
contentData.originalName,
|
||||
contentData.mimeType,
|
||||
contentData.size,
|
||||
contentData.path,
|
||||
contentData.url,
|
||||
contentData.zone,
|
||||
contentData.duration,
|
||||
contentData.createdAt,
|
||||
function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(contentData);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
stmt.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
async getContent(zone = null, type = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let query = 'SELECT * FROM content WHERE isActive = 1';
|
||||
const params = [];
|
||||
|
||||
if (zone && zone !== 'all') {
|
||||
query += ' AND (zone = ? OR zone = "all")';
|
||||
params.push(zone);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
query += ' AND type = ?';
|
||||
params.push(type);
|
||||
}
|
||||
|
||||
query += ' ORDER BY createdAt DESC';
|
||||
|
||||
this.db.all(query, params, (err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getContentById(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get('SELECT * FROM content WHERE id = ?', [id], (err, row) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async deleteContent(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run('DELETE FROM content WHERE id = ?', [id], function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(this.changes > 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Schedule methods
|
||||
async addSchedule(scheduleData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO schedule (id, contentId, zone, startTime, endTime, priority, createdAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
scheduleData.id,
|
||||
scheduleData.contentId,
|
||||
scheduleData.zone,
|
||||
scheduleData.startTime,
|
||||
scheduleData.endTime,
|
||||
scheduleData.priority,
|
||||
scheduleData.createdAt,
|
||||
function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(scheduleData);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
stmt.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
async getActiveSchedule(zone) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const now = new Date().toISOString();
|
||||
const query = `
|
||||
SELECT s.*, c.* FROM schedule s
|
||||
JOIN content c ON s.contentId = c.id
|
||||
WHERE s.zone = ?
|
||||
AND s.startTime <= ?
|
||||
AND s.endTime >= ?
|
||||
AND s.isActive = 1
|
||||
AND c.isActive = 1
|
||||
ORDER BY s.priority DESC, s.createdAt ASC
|
||||
`;
|
||||
|
||||
this.db.all(query, [zone, now, now], (err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getZones() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all('SELECT * FROM zones WHERE isActive = 1 ORDER BY displayOrder', (err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Logging
|
||||
async addLog(type, message, data = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO logs (id, type, message, data, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const logData = {
|
||||
id: require('uuid').v4(),
|
||||
type,
|
||||
message,
|
||||
data: data ? JSON.stringify(data) : null,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
stmt.run(
|
||||
logData.id,
|
||||
logData.type,
|
||||
logData.message,
|
||||
logData.data,
|
||||
logData.timestamp,
|
||||
function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(logData);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
stmt.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.db) {
|
||||
this.db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing database:', err);
|
||||
} else {
|
||||
console.log('Database connection closed');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DatabaseManager;
|
||||
Reference in New Issue
Block a user