- Item 10: Convert to ES modules with import/export, single module entry point - Item 11: Replace inline styles with CSS classes (background overlay, card animations, highlight effect, config modal form elements) - Item 12: Move ConfigManager modal HTML from JS template literal to <template> element in index.html - Item 13: Replace deprecated url.parse() with new URL() in server.js and update route handlers to use searchParams - Item 14: Replace JSON.parse/stringify deep clone with structuredClone() - Item 15: Remove dead JSON-fixing regex code from departures.js route Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
159 lines
5.0 KiB
JavaScript
159 lines
5.0 KiB
JavaScript
// Load environment variables
|
|
require('dotenv').config();
|
|
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Route handlers
|
|
const departuresRouter = require('./server/routes/departures');
|
|
const sitesRouter = require('./server/routes/sites');
|
|
const configRouter = require('./server/routes/config');
|
|
|
|
const PORT = process.env.PORT || 3002;
|
|
|
|
// Default configuration
|
|
let config = {
|
|
sites: [
|
|
{
|
|
id: '1411',
|
|
name: 'Ambassaderna',
|
|
enabled: true
|
|
}
|
|
]
|
|
};
|
|
|
|
// Function to load configuration from file
|
|
function loadSitesConfig() {
|
|
try {
|
|
const fs = require('fs');
|
|
const configPath = path.join('config', 'sites.json');
|
|
if (fs.existsSync(configPath)) {
|
|
const configData = fs.readFileSync(configPath, 'utf8');
|
|
const loadedConfig = JSON.parse(configData);
|
|
|
|
// Handle old format (array of sites)
|
|
if (Array.isArray(loadedConfig)) {
|
|
config.sites = loadedConfig;
|
|
} else {
|
|
config = loadedConfig;
|
|
}
|
|
|
|
console.log('Loaded configuration:', config);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading configuration:', error);
|
|
}
|
|
}
|
|
|
|
// Load configuration on startup
|
|
loadSitesConfig();
|
|
|
|
// Create HTTP server
|
|
const server = http.createServer(async (req, res) => {
|
|
const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(204);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// Handle API endpoints - use route handlers
|
|
if (parsedUrl.pathname === '/api/departures') {
|
|
await departuresRouter.handleDepartures(req, res, config);
|
|
}
|
|
else if (parsedUrl.pathname === '/api/sites/search') {
|
|
sitesRouter.handleSiteSearch(req, res, parsedUrl);
|
|
}
|
|
else if (parsedUrl.pathname === '/api/sites/nearby') {
|
|
await sitesRouter.handleNearbySites(req, res, parsedUrl);
|
|
}
|
|
else if (parsedUrl.pathname === '/api/config') {
|
|
configRouter.handleGetConfig(req, res, config);
|
|
}
|
|
else if (parsedUrl.pathname === '/api/config/update' && req.method === 'POST') {
|
|
configRouter.handleUpdateConfig(req, res, config);
|
|
}
|
|
else if (parsedUrl.pathname === '/api/config/client') {
|
|
configRouter.handleClientConfig(req, res);
|
|
}
|
|
// Serve static files
|
|
else if (parsedUrl.pathname === '/' || parsedUrl.pathname === '/index.html') {
|
|
fs.readFile('index.html', 'utf8', (err, data) => {
|
|
if (err) {
|
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
res.end('Error loading index.html');
|
|
return;
|
|
}
|
|
// Always inject API key and config into HTML (even if empty, so window vars exist)
|
|
const apiKey = process.env.OPENWEATHERMAP_API_KEY || '';
|
|
if (!data.includes('window.OPENWEATHERMAP_API_KEY')) {
|
|
// Inject before closing </head> tag
|
|
const configScript = `
|
|
<script>
|
|
window.OPENWEATHERMAP_API_KEY = '${apiKey.replace(/'/g, "\\'")}';
|
|
window.DEFAULT_LOCATION = {
|
|
latitude: ${parseFloat(process.env.DEFAULT_LATITUDE) || 59.3293},
|
|
longitude: ${parseFloat(process.env.DEFAULT_LONGITUDE) || 18.0686},
|
|
name: '${(process.env.DEFAULT_LOCATION_NAME || 'Stockholm').replace(/'/g, "\\'")}'
|
|
};
|
|
</script>`;
|
|
data = data.replace('</head>', configScript + '\n</head>');
|
|
}
|
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
res.end(data);
|
|
});
|
|
}
|
|
// Serve static files from public directory or root
|
|
else if (/\.(js|css|png|jpg|jpeg|gif|ico|svg|json)$/.test(parsedUrl.pathname)) {
|
|
let filePath = parsedUrl.pathname.substring(1); // Remove leading /
|
|
|
|
// Try public directory first, then root directory for backward compatibility
|
|
const publicPath = path.join('public', filePath);
|
|
const rootPath = filePath;
|
|
|
|
// Check if file exists in public directory first
|
|
fs.access(publicPath, fs.constants.F_OK, (publicErr) => {
|
|
const targetPath = publicErr ? rootPath : publicPath;
|
|
|
|
fs.readFile(targetPath, (err, data) => {
|
|
if (err) {
|
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
res.end('File not found');
|
|
return;
|
|
}
|
|
|
|
const ext = parsedUrl.pathname.split('.').pop();
|
|
const contentType = {
|
|
'js': 'text/javascript',
|
|
'css': 'text/css',
|
|
'png': 'image/png',
|
|
'jpg': 'image/jpeg',
|
|
'jpeg': 'image/jpeg',
|
|
'gif': 'image/gif',
|
|
'ico': 'image/x-icon',
|
|
'svg': 'image/svg+xml',
|
|
'json': 'application/json'
|
|
}[ext] || 'text/plain';
|
|
|
|
res.writeHead(200, { 'Content-Type': contentType });
|
|
res.end(data);
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
res.end('Not Found');
|
|
}
|
|
});
|
|
|
|
// Start the server
|
|
server.listen(PORT, () => {
|
|
console.log(`Server running at http://localhost:${PORT}/`);
|
|
});
|