diff --git a/Dockerfile b/Dockerfile index 1b3d8a6..62050c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,22 @@ FROM node:20-alpine + WORKDIR /app + +# Copy package files COPY package*.json ./ -RUN npm install + +# Install dependencies +RUN npm install --production + +# Copy all application files COPY . . + +# Expose port EXPOSE 3000 -CMD ["npm", "start"] \ No newline at end of file + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3000 + +# Start the server +CMD ["node", "server.js"] \ No newline at end of file diff --git a/package.json b/package.json index 9fb853a..657e0b1 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,20 @@ { "name": "whopays", "version": "1.0.0", + "description": "Who pays app with PWA support", + "main": "server.js", "type": "module", "scripts": { - "start": "node server.js" + "start": "node server.js", + "dev": "node server.js" }, + "keywords": [ + "pwa", + "whopays" + ], + "author": "", + "license": "ISC", "dependencies": { - "express": "^4.19.2" + "express": "^4.18.2" } } \ No newline at end of file diff --git a/public/index.html b/public/index.html index c5af9ed..49b6280 100644 --- a/public/index.html +++ b/public/index.html @@ -2,12 +2,22 @@ - - + + + + + + + - + + + + + + Wer zahlt? @@ -15,7 +25,20 @@ -
+ + + +

Who pays?

@@ -65,11 +88,45 @@ diff --git a/public/manifest.json b/public/manifest.json index 36706a5..f812ea0 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,23 +1,40 @@ { - "name": "Who´s gonna pay this time?", + "name": "Wer zahlt? - Who Pays", "short_name": "WhoPays", + "description": "Entscheide spielerisch, wer zahlt", "start_url": "/", + "scope": "/", "display": "standalone", "background_color": "#fef3c7", "theme_color": "#ec4899", - "orientation": "portrait", + "orientation": "portrait-primary", + "categories": ["utilities", "lifestyle"], "icons": [ { "src": "/img/icon-192.png", "sizes": "192x192", "type": "image/png", - "purpose": "any maskable" + "purpose": "any" + }, + { + "src": "/img/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" }, { "src": "/img/icon-512.png", "sizes": "512x512", "type": "image/png", - "purpose": "any maskable" + "purpose": "any" + }, + { + "src": "/img/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } - ] + ], + "screenshots": [], + "prefer_related_applications": false } \ No newline at end of file diff --git a/public/serviceworker.js b/public/serviceworker.js index 310d5f8..94d1459 100644 --- a/public/serviceworker.js +++ b/public/serviceworker.js @@ -1,76 +1,114 @@ -const CACHE_NAME = "whopays-v1"; +const CACHE_NAME = "whopays-v2"; const urlsToCache = [ "/", "/index.html", "/style.css", "/script.js", - "/img/a.png", - "/img/b.png", - "/img/bgA.jpg", - "/img/bgB.jpg", "/manifest.json" ]; -// Install event - cache assets +console.log("🔧 Service Worker: Loading..."); + +// Install event self.addEventListener("install", (event) => { + console.log("⚙️ Service Worker: Installing..."); event.waitUntil( - caches.open(CACHE_NAME).then((cache) => { - console.log("Cache geöffnet"); - return cache.addAll(urlsToCache).catch(err => { - console.error("Cache fehler:", err); - }); - }) + caches.open(CACHE_NAME) + .then((cache) => { + console.log("✅ Service Worker: Cache opened"); + // Try to cache files, but don't fail if some are missing + return Promise.allSettled( + urlsToCache.map(url => + cache.add(url).catch(err => { + console.warn(`⚠️ Failed to cache: ${url}`, err); + return null; + }) + ) + ); + }) + .then(() => { + console.log("✅ Service Worker: Installed"); + }) + .catch(err => { + console.error("❌ Service Worker: Install failed", err); + }) ); self.skipWaiting(); }); -// Activate event - clean old caches +// Activate event self.addEventListener("activate", (event) => { + console.log("🔄 Service Worker: Activating..."); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { - console.log("Alte Cache löschen:", cacheName); + console.log(`🗑️ Service Worker: Deleting old cache: ${cacheName}`); return caches.delete(cacheName); } }) ); + }).then(() => { + console.log("✅ Service Worker: Activated"); }) ); self.clients.claim(); }); -// Fetch event - serve from cache, fallback to network +// Fetch event self.addEventListener("fetch", (event) => { + const url = new URL(event.request.url); + // Network first for API calls - if (event.request.url.includes("/status") || - event.request.url.includes("/toggle")) { + if (url.pathname.startsWith("/status") || url.pathname.startsWith("/toggle")) { event.respondWith( - fetch(event.request).catch(() => { - return new Response( - JSON.stringify({ error: "Offline" }), - { headers: { "Content-Type": "application/json" } } - ); - }) + fetch(event.request) + .catch(() => { + console.warn("⚠️ API offline, returning error"); + return new Response( + JSON.stringify({ error: "Offline" }), + { + headers: { "Content-Type": "application/json" }, + status: 503 + } + ); + }) ); return; } // Cache first for static assets event.respondWith( - caches.match(event.request).then((response) => { - return response || fetch(event.request).then((fetchResponse) => { - return caches.open(CACHE_NAME).then((cache) => { - cache.put(event.request, fetchResponse.clone()); + caches.match(event.request) + .then((response) => { + if (response) { + return response; + } + + return fetch(event.request).then((fetchResponse) => { + // Only cache successful responses + if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') { + return fetchResponse; + } + + const responseToCache = fetchResponse.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseToCache); + }); + return fetchResponse; }); - }); - }).catch(() => { - // Fallback for navigation requests - if (event.request.mode === 'navigate') { - return caches.match('/index.html'); - } - }) + }) + .catch((err) => { + console.warn("⚠️ Fetch failed:", err); + // Fallback for navigation requests + if (event.request.mode === 'navigate') { + return caches.match('/index.html'); + } + return new Response('Offline', { status: 503 }); + }) ); -}); \ No newline at end of file +}); + +console.log("✅ Service Worker: Script loaded"); \ No newline at end of file diff --git a/server.js b/server.js index c2e3836..37f0292 100644 --- a/server.js +++ b/server.js @@ -7,8 +7,33 @@ 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()); -app.use(express.static(path.join(__dirname, "public"))); + +// 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"); @@ -18,26 +43,19 @@ const stateFile = path.join(__dirname, "state.json"); const CONFIG = { A: { name: "Marc", - color: 0x2ecc71, // grün + color: 0x2ecc71, icon: "a.png" }, B: { name: "Laila", - color: 0xe74c3c, // rot + color: 0xe74c3c, icon: "b.png" } }; -// Öffentliche URL zu deinem Server (WICHTIG für Discord Icons!) -const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL - || "http://localhost:3000"; +const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL || "http://localhost:3000"; +const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL || "https://discord.com/api/webhooks/1450872860607381515/zRbiXOykKPUfLJ_H87wD2OxQJPLdro6yQzU2T48Pg4fxSNaXEqBFgxdHGTiIDmmKGHIF"; - -// 🔔 Discord Webhook URL -const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL - || "https://discord.com/api/webhooks/1450872860607381515/zRbiXOykKPUfLJ_H87wD2OxQJPLdro6yQzU2T48Pg4fxSNaXEqBFgxdHGTiIDmmKGHIF"; - - // -------------------- // State Handling // -------------------- @@ -77,7 +95,6 @@ async function sendDiscordEmbed(previousKey, currentKey, reason) { } ]; - // Add reason field if provided if (reason) { fields.push({ name: "📝 Grund", @@ -103,9 +120,7 @@ async function sendDiscordEmbed(previousKey, currentKey, reason) { await fetch(DISCORD_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - embeds: [embed] - }) + body: JSON.stringify({ embeds: [embed] }) }); console.log("✅ Discord Benachrichtigung gesendet"); } catch (err) { @@ -113,17 +128,14 @@ async function sendDiscordEmbed(previousKey, currentKey, reason) { } } - // -------------------- -// Routes +// API Routes // -------------------- -// GET: aktueller Zustand app.get("/status", (req, res) => { res.json(getState()); }); -// POST: wechseln app.post("/toggle", async (req, res) => { const state = getState(); const previousKey = state.current; @@ -148,15 +160,64 @@ app.post("/toggle", async (req, res) => { }); }); -// Fallback -app.get(/.*/, (req, res) => { - res.sendFile(path.join(__dirname, "public/index.html")); +app.get("/health", (req, res) => { + res.json({ status: "ok" }); +}); + +// -------------------- +// SPA Fallback - NO CRASH +// -------------------- +// Only serve index.html for navigation requests, not for missing static files +app.use((req, res, next) => { + // If request already handled by static middleware, skip + if (res.headersSent) return; + + // If it's an API route that wasn't matched, 404 + if (req.url.startsWith('/api/')) { + return res.status(404).json({ error: 'Not found' }); + } + + // If Accept header indicates browser navigation (HTML), serve index.html + const acceptsHtml = req.accepts('html'); + if (acceptsHtml) { + return res.sendFile(path.join(__dirname, "public/index.html")); + } + + // Otherwise 404 + res.status(404).send('Not found'); +}); + +// Error handler to prevent crashes +app.use((err, req, res, next) => { + console.error('Server error:', err); + res.status(500).json({ error: 'Internal server error' }); }); // -------------------- // Start // -------------------- const PORT = process.env.PORT || 3000; -app.listen(PORT, () => - console.log(`WhoPays läuft auf http://localhost:${PORT}`) -); \ No newline at end of file +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)`); + + // 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 (adjust domain for your setup):'); + console.log(` http://localhost:${PORT}/`); + console.log(` http://localhost:${PORT}/service-worker.js`); + console.log(` http://localhost:${PORT}/manifest.json\n`); +}); \ No newline at end of file