import express from "express"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { CONFIG, DISCORD_WEBHOOK_URL, PUBLIC_BASE_URL, PORT } from "./config.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); // Trust proxy - CRITICAL for reverse proxy setup app.set('trust proxy', true); app.use(express.json()); // Logging middleware app.use((req, res, next) => { console.log(`${req.method} ${req.url} - ${req.headers['x-forwarded-proto'] || 'http'}`); next(); }); // CRITICAL: Serve static files with proper headers app.use(express.static(path.join(__dirname, "public"), { setHeaders: (res, filepath) => { if (filepath.endsWith('.js')) { res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); } if (filepath.endsWith('.json')) { res.setHeader('Content-Type', 'application/json; charset=utf-8'); } if (filepath.endsWith('service-worker.js')) { res.setHeader('Service-Worker-Allowed', '/'); res.setHeader('Cache-Control', 'no-cache'); } } })); const stateFile = path.join(__dirname, "state.json"); // -------------------- // State Handling - MIT PERSISTENT lastReason // -------------------- function getState() { if (!fs.existsSync(stateFile)) { const initialState = { current: "A", lastReason: null }; fs.writeFileSync( stateFile, JSON.stringify(initialState, null, 2) ); console.log("βœ… Neue state.json erstellt"); return initialState; } try { const state = JSON.parse(fs.readFileSync(stateFile, "utf-8")); // Ensure lastReason exists in state if (!state.hasOwnProperty('lastReason')) { state.lastReason = null; } console.log(`πŸ“– State geladen: ${state.current}, Letzter Grund: ${state.lastReason || 'keiner'}`); return state; } catch (err) { console.error("❌ Fehler beim Lesen von state.json:", err); return { current: "A", lastReason: null }; } } function setState(newState) { try { fs.writeFileSync(stateFile, JSON.stringify(newState, null, 2)); console.log(`πŸ’Ύ State gespeichert: ${newState.current}, Grund: ${newState.lastReason || 'keiner'}`); } catch (err) { console.error("❌ Fehler beim Speichern von state.json:", err); } } // -------------------- // Discord Webhook // -------------------- async function sendDiscordEmbed(previousKey, currentKey, reason) { const previous = CONFIG[previousKey]; const current = CONFIG[currentKey]; if (!DISCORD_WEBHOOK_URL) { console.log("⚠️ DISCORD_WEBHOOK_URL nicht gesetzt, keine Benachrichtigung gesendet"); return; } const fields = [ { name: "Paid", value: previous.name, inline: true }, { name: "Now's turn", value: current.name, inline: true } ]; // Grund hinzufΓΌgen falls vorhanden if (reason) { fields.push({ name: "πŸ“ Grund", value: reason, inline: false }); } const embed = { title: "πŸ’Έ Pay-Switch", description: `**${current.name}**'s turn`, color: current.colorDiscord, thumbnail: { url: `${PUBLIC_BASE_URL}/img/${current.icon}` }, fields: fields, footer: { text: new Date().toLocaleString("de-DE") } }; // Debug-Ausgabe console.log(`\nπŸ“€ Sende Discord Webhook:`); console.log(` Von: ${previous.name} β†’ Zu: ${current.name}`); console.log(` Grund: ${reason || '(keiner)'}`); console.log(` Farbe: 0x${current.colorDiscord.toString(16)}`); console.log(` Webhook URL: ${DISCORD_WEBHOOK_URL.substring(0, 50)}...`); try { const response = await fetch(DISCORD_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ embeds: [embed] }) }); if (response.ok) { console.log(`βœ… Discord Benachrichtigung gesendet (HTTP ${response.status})`); } else { const errorText = await response.text(); console.error(`❌ Discord Webhook Fehler (HTTP ${response.status}):`, errorText); } } catch (err) { console.error("❌ Discord Webhook Fehler:", err.message); } console.log(""); // Leerzeile fΓΌr bessere Lesbarkeit } // -------------------- // API Routes // -------------------- // Config API endpoint - sendet Config an Frontend app.get("/config", (req, res) => { // Nur relevante Daten fΓΌrs Frontend senden const frontendConfig = { A: { name: CONFIG.A.name, colorHex: CONFIG.A.colorHex, icon: CONFIG.A.icon, background: CONFIG.A.background }, B: { name: CONFIG.B.name, colorHex: CONFIG.B.colorHex, icon: CONFIG.B.icon, background: CONFIG.B.background } }; res.json(frontendConfig); }); app.get("/status", (req, res) => { const state = getState(); res.json(state); }); app.post("/toggle", async (req, res) => { const state = getState(); const previousKey = state.current; const reason = req.body?.reason; // Wechseln state.current = state.current === "A" ? "B" : "A"; // Grund speichern (oder null wenn keiner angegeben) state.lastReason = reason || null; // State persistent speichern setState(state); const logMessage = reason ? `Pay-Switch: ${CONFIG[previousKey].name} β†’ ${CONFIG[state.current].name} (Grund: ${reason})` : `Pay-Switch: ${CONFIG[previousKey].name} β†’ ${CONFIG[state.current].name} (kein Grund)`; console.log(logMessage); // Discord Benachrichtigung - IMMER senden, auch ohne Grund! await sendDiscordEmbed(previousKey, state.current, reason); res.json({ current: state.current, name: CONFIG[state.current].name, lastReason: state.lastReason }); }); app.get("/health", (req, res) => { res.json({ status: "ok" }); }); // -------------------- // SPA Fallback - NO CRASH // -------------------- app.use((req, res, next) => { if (res.headersSent) return; if (req.url.startsWith('/api/')) { return res.status(404).json({ error: 'Not found' }); } const acceptsHtml = req.accepts('html'); if (acceptsHtml) { return res.sendFile(path.join(__dirname, "public/index.html")); } res.status(404).send('Not found'); }); // Error handler app.use((err, req, res, next) => { console.error('Server error:', err); res.status(500).json({ error: 'Internal server error' }); }); // -------------------- // Start // -------------------- app.listen(PORT, "0.0.0.0", () => { console.log(`\nβœ… WhoPays lΓ€uft auf http://0.0.0.0:${PORT}\n`); console.log(`πŸ“ Static files directory: ${path.join(__dirname, "public")}`); console.log(`πŸ”’ Trust proxy: enabled (for reverse proxy support)`); // Initial state laden und anzeigen const initialState = getState(); console.log(`\n🎲 Aktueller Status:`); console.log(` πŸ‘€ Wer zahlt: ${CONFIG[initialState.current].name}`); console.log(` πŸ“ Letzter Grund: ${initialState.lastReason || 'Keiner gespeichert'}`); console.log(`\nπŸ‘₯ Konfigurierte Personen:`); console.log(` A: ${CONFIG.A.name} (${CONFIG.A.colorHex})`); console.log(` B: ${CONFIG.B.name} (${CONFIG.B.colorHex})`); // Discord Webhook Test console.log(`\nπŸ”” Discord Webhook:`); if (DISCORD_WEBHOOK_URL) { console.log(` βœ… Konfiguriert: ${DISCORD_WEBHOOK_URL.substring(0, 50)}...`); } else { console.log(` ❌ NICHT konfiguriert!`); } // Verify critical files exist const criticalFiles = [ 'public/index.html', 'public/service-worker.js', 'public/manifest.json' ]; console.log('\nπŸ” Checking critical files:'); criticalFiles.forEach(file => { const fullPath = path.join(__dirname, file); const exists = fs.existsSync(fullPath); console.log(` ${exists ? 'βœ…' : '❌'} ${file}`); }); console.log('\nπŸ“ Test URLs:'); console.log(` http://localhost:${PORT}/`); console.log(` http://localhost:${PORT}/service-worker.js`); console.log(` http://localhost:${PORT}/manifest.json\n`); });