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 @@
+
+
+
+
📱 App installieren für bessere Erfahrung!
+
+
+
+
+
+
+
+
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