Compare commits

..

14 Commits

Author SHA1 Message Date
Andrej Spielmann 11d9267b2f feat(docker): add PostgreSQL database container 2026-03-26 16:13:16 +01:00
Andrej Spielmann 6d0312ffd7 fix: use HTTPS API URL with Nginx proxy 2026-03-26 15:59:25 +01:00
Andrej Spielmann bdcec35383 fix: use internal IP for backend API 2026-03-26 15:56:29 +01:00
Andrej Spielmann 153b83c417 docs: add Nginx Proxy Manager configuration guide
- Document reverse proxy setup for API forwarding
- Frontend public via HTTPS
- Backend internal only via Nginx proxy
2026-03-26 15:55:54 +01:00
Andrej Spielmann ee96df11bf fix: update CORS and API URL for production deployment
- Change API URL from local IP to https://rce.playman.top
- Add Unraid IP to ALLOWED_HOSTS and CORS_ALLOWED_ORIGINS
- Fix CORS policy for direct IP access
2026-03-26 15:53:25 +01:00
Andrej Spielmann fb43a8ee9c fix: add standalone output config for Docker build 2026-03-26 15:48:18 +01:00
Andrej Spielmann 0721cbf879 feat(docker): update ports to 10001 (frontend) and 10002 (backend) 2026-03-26 15:31:58 +01:00
Andrej Spielmann 7210d09821 feat(pwa): generate PNG icons 2026-03-26 15:19:42 +01:00
Andrej Spielmann d6c95a232b chore: add .env.local with network API URL 2026-03-26 15:12:49 +01:00
Andrej Spielmann 8a6db1527b chore: add dev:host script for network testing 2026-03-26 15:10:02 +01:00
Andrej Spielmann ee07a817b0 feat(pwa): integrate InstallPrompt in dashboard layout 2026-03-26 15:08:31 +01:00
Andrej Spielmann 1314982590 feat(pwa): add InstallPrompt component for Add to Home Screen 2026-03-26 15:01:26 +01:00
Andrej Spielmann 6edff1613c feat(pwa): add mobile optimizations (safe areas, touch targets, standalone mode) 2026-03-26 14:57:49 +01:00
Andrej Spielmann 80897a7a6e feat(pwa): add viewport and PWA meta tags 2026-03-26 14:54:09 +01:00
18 changed files with 585 additions and 3 deletions
+6
View File
@@ -0,0 +1,6 @@
# Backend Environment
SECRET_KEY=change-me-to-a-long-random-string-in-production
DB_PASSWORD=change-me-to-a-secure-password
# Optional: PostgreSQL (empfohlen für production)
# DATABASE_URL wird automatisch gesetzt: postgresql://wrestledesk:DB_PASSWORD@db:5432/wrestledesk
+202
View File
@@ -0,0 +1,202 @@
# Docker Deployment Guide für Unraid
Diese Anleitung beschreibt die Deployment von WrestleDesk auf einem Unraid-Server mit Docker Compose. **Nginx Proxy Manager läuft bereits zentral.**
## Voraussetzungen
- Unraid Server mit Docker Compose Plugin
- Domain (z.B. rce.playman.top) mit DNS A-Record auf Unraid-IP
- Nginx Proxy Manager läuft bereits (Port 81 für Admin-UI)
## Schnellstart
### 1. Repository klonen
```bash
cd /mnt/user/appdata/
mkdir wrestledesk && cd wrestledesk
git clone http://192.168.101.42:3023/PlayMan/WrestleDesk.git .
git checkout feature/pwa # Wichtig: PWA Branch verwenden
```
### 2. Umgebungsvariablen konfigurieren
```bash
cp .env.example .env
nano .env # Oder dein bevorzugter Editor
```
**Wichtige Werte in .env ändern:**
```env
# Backend
SECRET_KEY=dein-sehr-langer-zufälliger-schlüssel-mindestens-50-zeichen
DATABASE_URL=sqlite:///db.sqlite3
```
**Tipp für SECRET_KEY:**
```bash
openssl rand -base64 50
```
### 3. Docker Container starten
```bash
docker-compose up -d --build
```
Das erste Bauen kann 5-10 Minuten dauern.
### 4. Im Nginx Proxy Manager konfigurieren
1. Öffne deinen Nginx Proxy Manager (normalerweise `http://deine-unraid-ip:81`)
2. **Proxy Hosts****Add Proxy Host**
**Einstellungen für Frontend:**
- **Domain Names:** `rce.playman.top`
- **Scheme:** `http`
- **Forward Hostname / IP:** `deine-unraid-ip`
- **Forward Port:** `3000`
- **Block Common Exploits:** ✅ Aktivieren
**Einstellungen für Backend (API):**
- **Domain Names:** `rce.playman.top`
- **Scheme:** `http`
- **Forward Hostname / IP:** `deine-unraid-ip`
- **Forward Port:** `8000`
- **Advanced Tab** → **Custom Locations**:
- Location: `/api/v1`
- Forward Hostname: `deine-unraid-ip`
- Forward Port: `8000`
**SSL Tab:**
- **SSL Certificate:** `Request a new SSL Certificate`
- **Force SSL:** ✅ Aktivieren
- **HTTP/2 Support:** ✅ Aktivieren
- **Save**
### 5. DNS einrichten
In deinem Domain-Provider:
- **Type:** A
- **Name:** rce (oder @ für Root)
- **Value:** Deine Unraid Server IP
- **TTL:** 300
### 6. Fertig!
Nach wenigen Minuten sollte WrestleDesk erreichbar sein unter:
- **https://rce.playman.top**
## Wartung
### Updates durchführen
```bash
cd /mnt/user/appdata/wrestledesk
git pull origin feature/pwa # Oder main
docker-compose down
docker-compose up -d --build
```
### Logs ansehen
```bash
# Alle Services
docker-compose logs -f
# Nur Frontend
docker-compose logs -f frontend
# Nur Backend
docker-compose logs -f backend
```
### Backup
```bash
# Datenbank + Media Backup erstellen
cd /mnt/user/appdata/wrestledesk
tar czf backup-$(date +%Y%m%d).tar.gz backend/db.sqlite3 backend/media/
# Backup zu Unraid Array verschieben
mv backup-*.tar.gz /mnt/user/backups/wrestledesk/
```
### Container stoppen
```bash
docker-compose down
```
## Port-Belegung
| Service | Port | Beschreibung |
|---------|------|--------------|
| Frontend | 3000 | Next.js App |
| Backend | 8000 | Django API |
**Wichtig:** Diese Ports müssen von außen NICHT erreichbar sein - nur über Nginx Proxy Manager!
## Fehlerbehebung
### Frontend zeigt "Backend not reachable"
Prüfe ob die API-URL korrekt ist:
```bash
docker-compose exec frontend env | grep API_URL
# Sollte zeigen: NEXT_PUBLIC_API_URL=https://rce.playman.top/api/v1
```
### CORS Fehler im Browser
Backend .env prüfen:
```bash
docker-compose exec backend env | grep CORS
# Sollte zeigen: CORS_ALLOWED_ORIGINS=https://rce.playman.top
```
### Container starten nicht
```bash
# Prüfen was den Port blockiert
netstat -tlnp | grep -E '(:3000|:8000)'
# Oder: Container logs prüfen
docker-compose logs --tail 50
```
### SSL Zertifikat wird nicht erstellt
- Prüfe ob Port 80 und 443 von außen erreichbar sind
- Prüfe DNS A-Record (sollte auf Unraid-IP zeigen)
- Warte 24h nach DNS-Änderungen
- Versuche "Renew" im Nginx Proxy Manager
## Sicherheitshinweise
1. **Ändere das Nginx Proxy Manager Passwort** nach dem ersten Login
2. **Verwende ein starkes SECRET_KEY** in .env (min. 50 Zeichen)
3. **Aktiviere "Block Common Exploits"** im Nginx Proxy Manager
4. **Halte Docker Images aktuell:** `docker-compose pull && docker-compose up -d`
5. **Backup regelmäßig durchführen**
## Architektur
```
Internet
Nginx Proxy Manager (auf Unraid, zentral)
↓ (Reverse Proxy)
┌──────────────┐ ┌──────────────┐
│ Frontend │←──→│ Backend │
│ Port 3000 │ │ Port 8000 │
└──────────────┘ └──────────────┘
```
## Support
Bei Problemen:
1. Logs prüfen: `docker-compose logs`
2. Container Status: `docker-compose ps`
3. Netzwerk prüfen: `docker network ls`
+121
View File
@@ -0,0 +1,121 @@
# Nginx Proxy Manager Konfiguration für WrestleDesk
Diese Anleitung beschreibt die Einrichtung von Nginx Proxy Manager, damit das Frontend öffentlich erreichbar ist, das Backend aber intern bleibt.
## Architektur
```
Internet
↓ (HTTPS)
rce.playman.top (Nginx Proxy Manager)
├── / → 192.168.101.42:10001 (Frontend)
└── /api/v1 → 192.168.101.42:10002 (Backend intern)
```
## Schritt 1: Proxy Host für Frontend erstellen
1. Nginx Proxy Manager öffnen (http://deine-unraid-ip:81)
2. **Proxy Hosts****Add Proxy Host**
**Details Tab:**
- **Domain Names:** `rce.playman.top`
- **Scheme:** `http`
- **Forward Hostname / IP:** `192.168.101.42`
- **Forward Port:** `10001`
- **Cache Assets:** ❌ (optional)
- **Block Common Exploits:** ✅ (empfohlen)
**SSL Tab:**
- **SSL Certificate:** `Request a new SSL Certificate`
- **Force SSL:** ✅
- **HTTP/2 Support:** ✅
- **Accept Terms:** ✅
- **Save**
## Schritt 2: API Weiterleitung (Location) hinzufügen
1. Auf den gerade erstellten Host klicken (**Edit**)
2. **Advanced** Tab öffnen
**Custom Locations einfügen:**
```nginx
location /api/v1 {
proxy_pass http://192.168.101.42:10002/api/v1;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CORS Headers für API
add_header 'Access-Control-Allow-Origin' 'https://rce.playman.top' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
# Preflight Requests
if ($request_method = 'OPTIONS') {
return 204;
}
}
```
3. **Save**
## Schritt 3: DNS einrichten
In deinem Domain-Provider:
- **Type:** A
- **Name:** `rce` (oder @ für Root)
- **Value:** `192.168.101.42` (deine Unraid IP)
- **TTL:** 300
## Schritt 4: Testen
Warte 1-2 Minuten für DNS, dann teste:
```bash
# Frontend erreichbar?
curl https://rce.playman.top
# API erreichbar?
curl https://rce.playman.top/api/v1/
# Sollte zurückgeben: {"detail":"Authentication credentials were not provided."}
```
## Sicherheitshinweise
1. **Backend Port 10002** ist nur intern erreichbar (Unraid Firewall)
2. **Niemals** Port 10002 im Router öffnen!
3. Nur Port 80 und 443 (für Nginx Proxy Manager) sollten vom Internet erreichbar sein
## Fehlerbehebung
### "Mixed Content" Fehler im Browser
- Prüfe ob SSL aktiv ist (https://)
- Frontend .env.local muss `https://` enthalten
### CORS Fehler
- Custom Locations müssen korrekt sein
- Backend CORS_ALLOWED_ORIGINS muss `https://rce.playman.top` enthalten
### API nicht erreichbar
```bash
# Teste direkten Backend-Zugriff (nur intern)
curl http://192.168.101.42:10002/api/v1/
# Teste über Proxy
curl https://rce.playman.top/api/v1/
```
## Wichtige Ports
| Port | Service | Öffentlich | Intern |
|------|---------|------------|--------|
| 80 | Nginx HTTP | ✅ | ✅ |
| 443 | Nginx HTTPS | ✅ | ✅ |
| 10001 | Frontend | ❌ | ✅ |
| 10002 | Backend | ❌ | ✅ |
**Backend ist NUR über Nginx Proxy erreichbar, niemals direkt!**
+41
View File
@@ -0,0 +1,41 @@
# Nginx Proxy Manager Konfiguration (Einfach)
## Schritt 1: Proxy Host erstellen
1. Nginx Proxy Manager öffnen (http://deine-unraid-ip:81)
2. **Proxy Hosts****Add Proxy Host**
**Details:**
- Domain Names: `rce.playman.top`
- Scheme: `http`
- Forward Hostname: `192.168.101.42`
- Forward Port: `10001`
- Block Common Exploits: ✅
**SSL:**
- SSL Certificate: Request a new SSL Certificate
- Force SSL: ✅
- HTTP/2 Support: ✅
- Save
## Schritt 2: API Weiterleitung
Auf den Host klicken (Edit) → **Advanced** Tab:
**Custom Locations:**
```nginx
location /api {
proxy_pass http://192.168.101.42:10002;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
```
**Save**
## Fertig!
- Frontend: https://rce.playman.top
- Backend intern: https://rce.playman.top/api/v1
+23
View File
@@ -0,0 +1,23 @@
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /app/media /app/staticfiles
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--threads", "2", "--timeout", "60", "wrestleDesk.wsgi:application"]
+51
View File
@@ -0,0 +1,51 @@
version: '3.8'
services:
db:
image: postgres:15-alpine
container_name: wrestledesk-db
restart: unless-stopped
volumes:
- ./postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=wrestledesk
- POSTGRES_USER=wrestledesk
- POSTGRES_PASSWORD=${DB_PASSWORD}
networks:
- wrestledesk-network
backend:
build: ./backend
container_name: wrestledesk-backend
restart: unless-stopped
depends_on:
- db
ports:
- '10002:8000'
volumes:
- ./backend/media:/app/media
- ./backend/staticfiles:/app/staticfiles
environment:
- SECRET_KEY=${SECRET_KEY}
- DEBUG=False
- ALLOWED_HOSTS=localhost,127.0.0.1,rce.playman.top,192.168.101.42,nginx-proxy-manager
- CORS_ALLOWED_ORIGINS=https://rce.playman.top,http://192.168.101.42:10001,http://192.168.101.42:10002
- DATABASE_URL=postgresql://wrestledesk:${DB_PASSWORD}@db:5432/wrestledesk
networks:
- wrestledesk-network
frontend:
build: ./frontend
container_name: wrestledesk-frontend
restart: unless-stopped
ports:
- '10001:3000'
environment:
- NEXT_PUBLIC_API_URL=https://rce.playman.top/api/v1
- NODE_ENV=production
networks:
- wrestledesk-network
networks:
wrestledesk-network:
driver: bridge
+1
View File
@@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=https://rce.playman.top/api/v1
+26
View File
@@ -0,0 +1,26 @@
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
+4 -1
View File
@@ -1,7 +1,10 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ output: 'standalone',
images: {
unoptimized: true,
},
}; };
export default nextConfig; export default nextConfig;
+1
View File
@@ -4,6 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"dev:host": "next dev --hostname 192.168.101.111",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint"
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+2
View File
@@ -5,6 +5,7 @@ import { useRouter, usePathname } from "next/navigation"
import { useAuth } from "@/lib/auth" import { useAuth } from "@/lib/auth"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { Sidebar } from "@/components/layout/Sidebar" import { Sidebar } from "@/components/layout/Sidebar"
import { InstallPrompt } from "@/components/ui/install-prompt"
export default function DashboardLayout({ export default function DashboardLayout({
children, children,
@@ -46,6 +47,7 @@ export default function DashboardLayout({
{children} {children}
</div> </div>
</main> </main>
<InstallPrompt />
</div> </div>
) )
} }
+26
View File
@@ -146,3 +146,29 @@
background: oklch(0.5 0 0 / 35%); background: oklch(0.5 0 0 / 35%);
} }
} }
/* Mobile/PWA Optimizations */
html { -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
/* iOS Safe Areas */
body { padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }
/* Prevent zoom on input focus */
input, select, textarea { font-size: 16px; }
/* Hide scrollbar on mobile */
@media (max-width: 768px) {
::-webkit-scrollbar { display: none; }
body { scrollbar-width: none; }
}
/* Minimum touch target 44x44px */
button, a, input, select, textarea, [role="button"] { min-height: 44px; min-width: 44px; }
/* Disable hover effects on touch devices */
@media (hover: none) { *:hover { transform: none !important; } }
/* PWA standalone mode styles */
@media (display-mode: standalone) {
html { height: 100vh; height: 100dvh; }
body { overflow: hidden; position: fixed; width: 100%; height: 100%; }
}
+25 -1
View File
@@ -1,4 +1,4 @@
import type { Metadata } from "next" import type { Metadata, Viewport } from "next"
import { Syne, DM_Sans } from "next/font/google" import { Syne, DM_Sans } from "next/font/google"
import "./globals.css" import "./globals.css"
import { Providers } from "./providers" import { Providers } from "./providers"
@@ -15,9 +15,33 @@ const dmSans = DM_Sans({
weight: ["400", "500", "600", "700"], weight: ["400", "500", "600", "700"],
}) })
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
themeColor: "#1B1A55",
viewportFit: "cover",
}
export const metadata: Metadata = { export const metadata: Metadata = {
title: "WrestleDesk", title: "WrestleDesk",
description: "Wrestling Club Management System", description: "Wrestling Club Management System",
manifest: "/manifest.json",
appleWebApp: {
capable: true,
statusBarStyle: "black-translucent",
title: "WrestleDesk",
},
icons: {
icon: [
{ url: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ url: "/icon-512.png", sizes: "512x512", type: "image/png" },
],
apple: [
{ url: "/icon-192.png", sizes: "192x192", type: "image/png" },
],
},
} }
export default function RootLayout({ export default function RootLayout({
@@ -0,0 +1,55 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { X } from "lucide-react"
export function InstallPrompt() {
const [show, setShow] = useState(false)
const [isIOS, setIsIOS] = useState(false)
const [isStandalone, setIsStandalone] = useState(false)
useEffect(() => {
const standalone = window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone ||
document.referrer.includes('android-app://')
setIsStandalone(standalone)
const isIOSDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
setIsIOS(isIOSDevice)
const dismissed = localStorage.getItem('install-prompt-dismissed')
if (!standalone && !dismissed) {
setTimeout(() => setShow(true), 3000)
}
}, [])
const handleDismiss = () => {
setShow(false)
localStorage.setItem('install-prompt-dismissed', 'true')
}
if (!show || isStandalone) return null
return (
<div className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-card border-t shadow-lg">
<div className="flex items-center justify-between max-w-lg mx-auto">
<div className="flex-1">
<p className="text-sm font-medium">WrestleDesk als App installieren</p>
{isIOS ? (
<p className="text-xs text-muted-foreground">
Tippe auf Teilen "Zum Home Screen hinzufügen"
</p>
) : (
<p className="text-xs text-muted-foreground">
Installieren für schnellen Zugriff
</p>
)}
</div>
<Button size="sm" variant="ghost" onClick={handleDismiss}>
<X className="w-4 h-4" />
</Button>
</div>
</div>
)
}