Compare commits
25 Commits
68c074e9da
...
v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4eae67612 | ||
|
|
891f52b0b8 | ||
|
|
725d3bcff4 | ||
|
|
69f69cf172 | ||
|
|
68c8f9a05a | ||
|
|
2b8733dea0 | ||
|
|
317eed5ea6 | ||
|
|
a503edb220 | ||
|
|
a80c14223b | ||
|
|
8c9c4eb159 | ||
|
|
68dfba38df | ||
|
|
b51ad2ff1a | ||
|
|
5613e5d48e | ||
|
|
09b998ea75 | ||
|
|
74a8a59083 | ||
|
|
f2c64281dd | ||
|
|
ca40b1efb9 | ||
|
|
3c051ec49d | ||
|
|
b268abb7d3 | ||
|
|
c7793dcb9d | ||
|
|
95fd6405be | ||
|
|
e881979da3 | ||
|
|
8ec713297a | ||
|
|
4aef034aa6 | ||
|
|
b120e5df45 |
66
.dockerignore
Normal file
66
.dockerignore
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Next.js build outputs
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
build
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# Git (NICHT ausschließen - wird für Version-Extraktion benötigt!)
|
||||||
|
# .git wird benötigt für: git describe --tags --always
|
||||||
|
# .gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Hördle specific - WICHTIG: Upload-Dateien NICHT ins Image kopieren!
|
||||||
|
# Diese werden als Volume gemountet und sollten nicht im Image sein
|
||||||
|
/public/uploads/*
|
||||||
|
!/public/uploads/.gitkeep
|
||||||
|
|
||||||
|
# Database files - werden als Volume gemountet
|
||||||
|
/data/*
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
|
# Backups
|
||||||
|
/backups
|
||||||
|
|
||||||
|
# Docker files (nicht notwendig im Image)
|
||||||
|
docker-compose*.yml
|
||||||
|
Dockerfile*
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
|
# Scripts die nicht im Container gebraucht werden
|
||||||
|
scripts/fix-*.sh
|
||||||
|
scripts/check-*.sh
|
||||||
|
scripts/debug-*.sh
|
||||||
|
scripts/quick-*.sh
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.log
|
||||||
|
|
||||||
106
.env.example
Normal file
106
.env.example
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# ============================================
|
||||||
|
# Hördle Environment Variables
|
||||||
|
# ============================================
|
||||||
|
# Kopiere diese Datei zu .env und passe die Werte an deine Umgebung an:
|
||||||
|
# cp .env.example .env
|
||||||
|
#
|
||||||
|
# WICHTIG: Die .env-Datei sollte niemals in Git committed werden!
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Build-Time Variables (NEXT_PUBLIC_*)
|
||||||
|
# ============================================
|
||||||
|
# Diese Variablen werden beim Build-Zeitpunkt in die Next.js-App eingebettet.
|
||||||
|
# Nach dem Build können sie nicht mehr geändert werden (ohne Rebuild).
|
||||||
|
|
||||||
|
# App-Name (wird in Browser-Tab, PWA, etc. verwendet)
|
||||||
|
NEXT_PUBLIC_APP_NAME=Hördle
|
||||||
|
|
||||||
|
# App-Beschreibung (für SEO, PWA, etc.)
|
||||||
|
NEXT_PUBLIC_APP_DESCRIPTION=Daily music guessing game - Guess the song from short audio clips
|
||||||
|
|
||||||
|
# Hauptdomain (ohne https://)
|
||||||
|
NEXT_PUBLIC_DOMAIN=hoerdle.de
|
||||||
|
|
||||||
|
# Twitter/X Handle (für Meta-Tags)
|
||||||
|
NEXT_PUBLIC_TWITTER_HANDLE=@hoerdle
|
||||||
|
|
||||||
|
# Plausible Analytics - Domain
|
||||||
|
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=hoerdle.de
|
||||||
|
|
||||||
|
# Plausible Analytics - Script-URL (selbst gehostet oder extern)
|
||||||
|
NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC=https://plausible.example.com/js/script.js
|
||||||
|
|
||||||
|
# Theme-Farbe (für Browser-UI, PWA, etc.)
|
||||||
|
NEXT_PUBLIC_THEME_COLOR=#000000
|
||||||
|
|
||||||
|
# Hintergrundfarbe (für PWA, etc.)
|
||||||
|
NEXT_PUBLIC_BACKGROUND_COLOR=#ffffff
|
||||||
|
|
||||||
|
# Credits im Footer aktivieren (true/false)
|
||||||
|
NEXT_PUBLIC_CREDITS_ENABLED=true
|
||||||
|
|
||||||
|
# Credits-Text (vor dem Link)
|
||||||
|
NEXT_PUBLIC_CREDITS_TEXT=Vibe coded with ☕ and 🍺 by
|
||||||
|
|
||||||
|
# Credits-Link-Text
|
||||||
|
NEXT_PUBLIC_CREDITS_LINK_TEXT=@yourhandle@server.social
|
||||||
|
|
||||||
|
# Credits-Link-URL
|
||||||
|
NEXT_PUBLIC_CREDITS_LINK_URL=https://server.social/@yourhandle
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Runtime Variables
|
||||||
|
# ============================================
|
||||||
|
# Diese Variablen können zur Laufzeit geändert werden (benötigen keinen Rebuild).
|
||||||
|
|
||||||
|
# Datenbank-URL (SQLite für lokale/kleine Deployments)
|
||||||
|
# Format: file:/path/to/database.db
|
||||||
|
DATABASE_URL=file:/app/data/prod.db
|
||||||
|
|
||||||
|
# Admin-Passwort (bcrypt Hash)
|
||||||
|
# Generiere einen Hash mit: node scripts/hash-password.js dein_passwort
|
||||||
|
# In docker-compose.yml müssen $ als $$ escaped werden!
|
||||||
|
ADMIN_PASSWORD=$2b$10$SHOt9G1qUNIvHoWre7499.eEtp5PtOII0daOQGNV.dhDEuPmOUdsq
|
||||||
|
|
||||||
|
# Zeitzone (für tägliche Puzzle-Rotation)
|
||||||
|
TZ=Europe/Berlin
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Optional: Gotify Integration
|
||||||
|
# ============================================
|
||||||
|
# Für Benachrichtigungen (z.B. Fehler-Alerts)
|
||||||
|
|
||||||
|
# Gotify Server URL
|
||||||
|
GOTIFY_URL=https://gotify.example.com
|
||||||
|
|
||||||
|
# Gotify App Token
|
||||||
|
GOTIFY_APP_TOKEN=your_gotify_app_token_here
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Optional: OpenRouter Integration
|
||||||
|
# ============================================
|
||||||
|
# Für AI-Features (falls vorhanden)
|
||||||
|
|
||||||
|
# OpenRouter API Key
|
||||||
|
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Caddy Reverse Proxy (Optional - Production)
|
||||||
|
# ============================================
|
||||||
|
# Nur benötigt, wenn Caddy für SSL/TLS verwendet wird.
|
||||||
|
|
||||||
|
# GoDaddy API Key (für DNS-01 Challenge bei Wildcard-Zertifikaten)
|
||||||
|
# Siehe CADDY_SETUP.md für Anleitung zur Erstellung
|
||||||
|
GODADDY_API_KEY=your_godaddy_api_key_here
|
||||||
|
|
||||||
|
# GoDaddy API Secret
|
||||||
|
GODADDY_API_SECRET=your_godaddy_api_secret_here
|
||||||
|
|
||||||
|
# Email für Let's Encrypt Benachrichtigungen (optional)
|
||||||
|
CADDY_EMAIL=admin@hoerdle.de
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Build-Time Overrides
|
||||||
|
# ============================================
|
||||||
|
# Optional: Spezifische Version beim Build setzen
|
||||||
|
# APP_VERSION=v1.0.0
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,6 +32,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
@@ -50,3 +51,4 @@ next-env.d.ts
|
|||||||
/data
|
/data
|
||||||
.release-years-migrated
|
.release-years-migrated
|
||||||
.covers-migrated
|
.covers-migrated
|
||||||
|
docker-compose.yml
|
||||||
|
|||||||
289
CADDY_SETUP.md
Normal file
289
CADDY_SETUP.md
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
# Caddy-Setup für Hördle
|
||||||
|
|
||||||
|
Diese Anleitung erklärt, wie du Caddy als Reverse-Proxy mit automatischen Let's Encrypt Wildcard-Zertifikaten für die Domains `hoerdle.de` und `hördle.de` (xn--hrdle-jua.de) einrichtest.
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Caddy übernimmt folgende Aufgaben:
|
||||||
|
- Automatische SSL/TLS-Zertifikate via Let's Encrypt
|
||||||
|
- Wildcard-Zertifikate für beide Domains (inkl. Subdomains)
|
||||||
|
- Reverse Proxy zu deinem Hördle-Container
|
||||||
|
- HTTP zu HTTPS Redirect
|
||||||
|
- Optimierte Einstellungen für Audio-Streaming und Uploads
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
1. Docker und Docker Compose installiert
|
||||||
|
2. Zugriff auf deine GoDaddy Domain-Verwaltung
|
||||||
|
3. Ports 80 und 443 müssen frei sein (Caddy übernimmt diese)
|
||||||
|
|
||||||
|
## Schritt 1: GoDaddy DNS-API-Zugangsdaten erstellen
|
||||||
|
|
||||||
|
Für Wildcard-Zertifikate benötigt Caddy DNS-01 Challenge, was API-Zugriff auf dein GoDaddy-Konto erfordert.
|
||||||
|
|
||||||
|
### GoDaddy API-Keys erstellen
|
||||||
|
|
||||||
|
1. Gehe zu [GoDaddy Developer Portal](https://developer.godaddy.com/)
|
||||||
|
2. Melde dich mit deinem GoDaddy-Konto an
|
||||||
|
3. Klicke auf **"Keys"** in der Navigation
|
||||||
|
4. Klicke auf **"Create New API Key"**
|
||||||
|
5. Fülle das Formular aus:
|
||||||
|
- **Key Name**: z.B. "Hördle Caddy DNS"
|
||||||
|
- **Environment**: Production (für echte Domains)
|
||||||
|
6. Klicke auf **"Create"**
|
||||||
|
7. **Wichtig**: Kopiere dir den **API Key** und das **API Secret** - das Secret wird nur einmal angezeigt!
|
||||||
|
|
||||||
|
### Alternative: Manuelle DNS-TXT-Records (ohne API)
|
||||||
|
|
||||||
|
Wenn du keine API-Keys verwenden möchtest, kannst du die DNS-TXT-Records manuell setzen. **Hinweis**: Dies ist nur für die initiale Zertifikatsanfrage möglich, nicht für automatische Erneuerungen.
|
||||||
|
|
||||||
|
Siehe Abschnitt "Manuelle DNS-Konfiguration (ohne API)" weiter unten.
|
||||||
|
|
||||||
|
## Schritt 2: Environment-Variablen konfigurieren
|
||||||
|
|
||||||
|
Erstelle eine `.env`-Datei im Projektverzeichnis (oder erweitere die bestehende):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GoDaddy API-Credentials für DNS-01 Challenge
|
||||||
|
GODADDY_API_KEY=your_api_key_here
|
||||||
|
GODADDY_API_SECRET=your_api_secret_here
|
||||||
|
|
||||||
|
# Optional: Email für Let's Encrypt Benachrichtigungen
|
||||||
|
CADDY_EMAIL=markus@hoerdle.de
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig**: Die `.env`-Datei sollte nicht in Git committed werden (sollte bereits in `.gitignore` sein).
|
||||||
|
|
||||||
|
## Schritt 3: Docker-Netzwerk erstellen
|
||||||
|
|
||||||
|
Caddy und Hördle müssen im gleichen Docker-Netzwerk kommunizieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe, ob das Netzwerk bereits existiert
|
||||||
|
docker network ls | grep hoerdle
|
||||||
|
|
||||||
|
# Falls das Netzwerk bereits existiert, aber falsche Labels hat:
|
||||||
|
# 1. Stoppe alle Container, die das Netzwerk nutzen
|
||||||
|
docker compose -f docker-compose.yml down
|
||||||
|
|
||||||
|
# 2. Lösche das alte Netzwerk (falls keine Container mehr dranhängen)
|
||||||
|
docker network rm hoerdle_default
|
||||||
|
|
||||||
|
# 3. Erstelle das Netzwerk neu
|
||||||
|
docker network create hoerdle_default
|
||||||
|
|
||||||
|
# Falls das Netzwerk nicht existiert, erstelle es:
|
||||||
|
docker network create hoerdle_default
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis**: Die docker-compose.caddy.yml ist so konfiguriert, dass sie das Netzwerk als externes Netzwerk nutzt. Das bedeutet, dass das Netzwerk bereits existieren muss, bevor Caddy gestartet wird.
|
||||||
|
|
||||||
|
## Schritt 4: Caddy starten
|
||||||
|
|
||||||
|
### Option A: Mit docker-compose (Empfohlen)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Starte Hördle + Caddy zusammen
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.caddy.yml --profile production up -d
|
||||||
|
|
||||||
|
# Nur Caddy starten (wenn Hördle bereits läuft)
|
||||||
|
docker compose -f docker-compose.caddy.yml --profile production up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: Nur Caddy starten (Hördle läuft bereits)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.caddy.yml --profile production up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schritt 5: DNS-Konfiguration in GoDaddy
|
||||||
|
|
||||||
|
### Automatisch (mit API-Keys)
|
||||||
|
|
||||||
|
Wenn du API-Keys konfiguriert hast, wird Caddy automatisch die benötigten DNS-TXT-Records erstellen. Keine manuellen DNS-Änderungen nötig!
|
||||||
|
|
||||||
|
### Manuell (ohne API-Keys)
|
||||||
|
|
||||||
|
Wenn du die API-Keys nicht verwenden möchtest, musst du die DNS-TXT-Records manuell setzen:
|
||||||
|
|
||||||
|
#### Für hoerdle.de:
|
||||||
|
|
||||||
|
1. Gehe zu deinem [GoDaddy DNS-Verwaltung](https://dcc.godaddy.com/manage/YOUR_DOMAIN/dns)
|
||||||
|
2. Für jedes Wildcard-Zertifikat benötigst du einen TXT-Record:
|
||||||
|
- **Typ**: TXT
|
||||||
|
- **Name**: `_acme-challenge`
|
||||||
|
- **Wert**: (wird von Let's Encrypt generiert - siehe Caddy-Logs)
|
||||||
|
- **TTL**: 600 (10 Minuten)
|
||||||
|
|
||||||
|
**Wichtig**: Für Wildcard-Zertifikate brauchst du:
|
||||||
|
- Einen TXT-Record für `_acme-challenge.hoerdle.de` (Domain selbst)
|
||||||
|
- Einen TXT-Record für `_acme-challenge.*.hoerdle.de` (Wildcard)
|
||||||
|
|
||||||
|
#### Für hördle.de (xn--hrdle-jua.de):
|
||||||
|
|
||||||
|
Das gleiche Vorgehen für die Punycode-Domain:
|
||||||
|
- `_acme-challenge.xn--hrdle-jua.de`
|
||||||
|
- `_acme-challenge.*.xn--hrdle-jua.de`
|
||||||
|
|
||||||
|
**Hinweis**: Die manuelle Methode funktioniert nur für die initiale Zertifikatsanfrage. Für automatische Erneuerungen benötigst du die API-Keys.
|
||||||
|
|
||||||
|
## Schritt 6: Prüfen, ob alles funktioniert
|
||||||
|
|
||||||
|
### Caddy-Logs ansehen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f hoerdle-caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
Du solltest sehen:
|
||||||
|
- Caddy startet erfolgreich
|
||||||
|
- Let's Encrypt-Zertifikate werden angefordert
|
||||||
|
- Zertifikate sind gültig
|
||||||
|
|
||||||
|
### Zertifikate prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe Zertifikate im Browser
|
||||||
|
# Öffne: https://hoerdle.de
|
||||||
|
# Öffne: https://hördle.de
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder via Command-Line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe Zertifikat für hoerdle.de
|
||||||
|
openssl s_client -connect hoerdle.de:443 -servername hoerdle.de < /dev/null 2>/dev/null | openssl x509 -noout -text | grep "Subject:"
|
||||||
|
|
||||||
|
# Prüfe Zertifikat für hördle.de
|
||||||
|
openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de < /dev/null 2>/dev/null | openssl x509 -noout -text | grep "Subject:"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Caddy startet nicht
|
||||||
|
|
||||||
|
**Problem**: Container stoppt sofort nach Start.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe Caddy-Logs: `docker logs hoerdle-caddy`
|
||||||
|
2. Prüfe Caddyfile-Syntax: `docker run --rm -v $(pwd)/Caddyfile:/etc/caddy/Caddyfile:ro caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile`
|
||||||
|
3. Prüfe, ob Ports 80/443 frei sind: `sudo netstat -tlnp | grep -E ':80|:443'`
|
||||||
|
|
||||||
|
### Zertifikate werden nicht erstellt
|
||||||
|
|
||||||
|
**Problem**: Let's Encrypt-Zertifikate werden nicht angefordert.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe GoDaddy API-Credentials in `.env`
|
||||||
|
2. Prüfe Caddy-Logs für DNS-Challenge-Fehler
|
||||||
|
3. Stelle sicher, dass die Domains korrekt auf deinen Server zeigen (A-Records)
|
||||||
|
4. Bei manueller DNS-Konfiguration: Prüfe, ob TXT-Records korrekt gesetzt sind
|
||||||
|
|
||||||
|
### DNS-Challenge schlägt fehl
|
||||||
|
|
||||||
|
**Problem**: DNS-01 Challenge kann DNS-Records nicht erstellen.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe GoDaddy API-Permissions
|
||||||
|
2. Stelle sicher, dass API-Keys Production-Keys sind (nicht Development)
|
||||||
|
3. Prüfe Domain-Ownership in GoDaddy
|
||||||
|
4. Warte einige Minuten - DNS-Propagierung kann dauern
|
||||||
|
|
||||||
|
### Audio-Dateien funktionieren nicht
|
||||||
|
|
||||||
|
**Problem**: MP3-Dateien werden nicht korrekt gestreamt.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe Caddy-Logs: `docker logs hoerdle-caddy | grep -i range`
|
||||||
|
2. Prüfe, ob Range-Header weitergegeben werden (Browser DevTools → Network)
|
||||||
|
3. Stelle sicher, dass der `/uploads/` Handle korrekt konfiguriert ist
|
||||||
|
|
||||||
|
### Container können nicht kommunizieren
|
||||||
|
|
||||||
|
**Problem**: Caddy kann den hoerdle-Container nicht erreichen.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe, ob beide Container im gleichen Netzwerk sind:
|
||||||
|
```bash
|
||||||
|
docker network inspect hoerdle_default
|
||||||
|
```
|
||||||
|
2. Prüfe, ob hoerdle-Container läuft: `docker ps | grep hoerdle`
|
||||||
|
3. Teste Verbindung von Caddy zu Hördle:
|
||||||
|
```bash
|
||||||
|
docker exec hoerdle-caddy wget -O- http://hoerdle:3000/api/health
|
||||||
|
```
|
||||||
|
**Hinweis**: Der Container-Port ist 3000 (nicht 3010, das ist nur der Host-Port).
|
||||||
|
|
||||||
|
### Netzwerk-Warnung beim Deployment
|
||||||
|
|
||||||
|
**Problem**: Warnung `network hoerdle_default was found but has incorrect label`
|
||||||
|
|
||||||
|
**Erklärung**: Diese Warnung ist **harmlos** und kann ignoriert werden. Docker Compose funktioniert trotzdem einwandfrei. Sie entsteht, wenn das Netzwerk bereits existiert, aber nicht von Docker Compose erstellt wurde.
|
||||||
|
|
||||||
|
**Optional: Warnung beheben** (nur wenn sie stört):
|
||||||
|
```bash
|
||||||
|
# Reparatur-Skript ausführen (stoppt Container kurz)
|
||||||
|
./scripts/fix-network.sh
|
||||||
|
|
||||||
|
# Danach Container neu starten
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis**: Das Reparatur-Skript stoppt alle Container kurz, die das Netzwerk nutzen. In Produktion sollte dies außerhalb der Hauptnutzungszeit erfolgen.
|
||||||
|
|
||||||
|
## Deployment-Workflow
|
||||||
|
|
||||||
|
### Caddy nur in Produktion aktivieren
|
||||||
|
|
||||||
|
Die `docker-compose.caddy.yml` verwendet das `production`-Profile. Um Caddy zu aktivieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mit Production-Profile
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.caddy.yml --profile production up -d
|
||||||
|
|
||||||
|
# Ohne Caddy (nur Hördle)
|
||||||
|
docker compose -f docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caddy aktualisieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull neues Caddy-Image
|
||||||
|
docker compose -f docker-compose.caddy.yml pull
|
||||||
|
|
||||||
|
# Restart Caddy-Container
|
||||||
|
docker compose -f docker-compose.caddy.yml --profile production restart caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caddy-Konfiguration ändern
|
||||||
|
|
||||||
|
Nach Änderungen am Caddyfile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Caddyfile validieren
|
||||||
|
docker run --rm -v $(pwd)/Caddyfile:/etc/caddy/Caddyfile:ro caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
# Caddy neu laden (ohne Downtime)
|
||||||
|
docker compose -f docker-compose.caddy.yml --profile production exec caddy caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sicherheit
|
||||||
|
|
||||||
|
### API-Keys schützen
|
||||||
|
|
||||||
|
- **Niemals** API-Keys in Git committen
|
||||||
|
- Verwende `.env`-Dateien (sollten in `.gitignore` sein)
|
||||||
|
- Setze minimale Berechtigungen für API-Keys in GoDaddy
|
||||||
|
- Rotiere API-Keys regelmäßig
|
||||||
|
|
||||||
|
### Firewall
|
||||||
|
|
||||||
|
Stelle sicher, dass nur Ports 80 und 443 öffentlich erreichbar sind. Port 3010 (Hördle) sollte nicht öffentlich erreichbar sein.
|
||||||
|
|
||||||
|
## Weitere Ressourcen
|
||||||
|
|
||||||
|
- [Caddy Dokumentation](https://caddyserver.com/docs/)
|
||||||
|
- [Caddy DNS-Provider](https://caddyserver.com/docs/modules/tls.dns)
|
||||||
|
- [GoDaddy API Dokumentation](https://developer.godaddy.com/doc/endpoint/domains)
|
||||||
|
- [Let's Encrypt Wildcard-Zertifikate](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge)
|
||||||
|
|
||||||
54
Caddyfile
Normal file
54
Caddyfile
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Caddy-Konfiguration für Hördle
|
||||||
|
# Root-Domains: hoerdle.de und hördle.de (xn--hrdle-jua.de)
|
||||||
|
# Hinweis: Diese Konfiguration funktioniert nur für Root-Domains, nicht für Subdomains
|
||||||
|
# Für Subdomains wären Wildcard-Zertifikate mit DNS-01 Challenge nötig
|
||||||
|
|
||||||
|
# Domain 1: hoerdle.de (ASCII)
|
||||||
|
hoerdle.de {
|
||||||
|
# TLS mit automatischer HTTP-01 Challenge (funktioniert nur für Root-Domain)
|
||||||
|
# Caddy verwendet automatisch Let's Encrypt
|
||||||
|
|
||||||
|
# Upload-Limit: 50MB (wie in nginx.conf.example)
|
||||||
|
request_body {
|
||||||
|
max_size 50MB
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse Proxy zu hoerdle Container
|
||||||
|
reverse_proxy hoerdle:3000 {
|
||||||
|
# HTTP/1.1 für WebSocket Support
|
||||||
|
transport http {
|
||||||
|
versions 1.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP zu HTTPS Redirect
|
||||||
|
@http {
|
||||||
|
protocol http
|
||||||
|
}
|
||||||
|
redir @http https://{host}{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Domain 2: hördle.de (Punycode: xn--hrdle-jua.de)
|
||||||
|
xn--hrdle-jua.de {
|
||||||
|
# TLS mit automatischer HTTP-01 Challenge (funktioniert nur für Root-Domain)
|
||||||
|
# Caddy verwendet automatisch Let's Encrypt
|
||||||
|
|
||||||
|
# Upload-Limit: 50MB
|
||||||
|
request_body {
|
||||||
|
max_size 50MB
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse Proxy zu hoerdle Container
|
||||||
|
reverse_proxy hoerdle:3000 {
|
||||||
|
# HTTP/1.1 für WebSocket Support
|
||||||
|
transport http {
|
||||||
|
versions 1.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP zu HTTPS Redirect
|
||||||
|
@http {
|
||||||
|
protocol http
|
||||||
|
}
|
||||||
|
redir @http https://{host}{uri} permanent
|
||||||
|
}
|
||||||
@@ -82,3 +82,35 @@ docker ps
|
|||||||
```
|
```
|
||||||
|
|
||||||
Look for the "healthy" status in the STATUS column.
|
Look for the "healthy" status in the STATUS column.
|
||||||
|
|
||||||
|
## Caddy Reverse Proxy (Optional - Production)
|
||||||
|
|
||||||
|
For production deployments with automatic SSL/TLS certificates, Caddy can be used as a reverse proxy. Caddy provides:
|
||||||
|
|
||||||
|
- Automatic Let's Encrypt certificates (including wildcard certificates)
|
||||||
|
- HTTP to HTTPS redirect
|
||||||
|
- Optimized settings for audio streaming and file uploads
|
||||||
|
- Support for both `hoerdle.de` and `hördle.de` (Punycode: `xn--hrdle-jua.de`)
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
1. **Follow the setup guide**: See `CADDY_SETUP.md` for detailed instructions
|
||||||
|
2. **Configure environment variables**: Add GoDaddy API credentials to your `.env` file
|
||||||
|
3. **Start with Caddy**:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.caddy.yml --profile production up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Without Caddy
|
||||||
|
|
||||||
|
If you don't want to use Caddy, you can deploy normally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will still be accessible on port 3010, but you'll need to configure SSL/TLS separately (e.g., with nginx).
|
||||||
|
|
||||||
|
### Caddy Troubleshooting
|
||||||
|
|
||||||
|
See `CADDY_SETUP.md` for detailed troubleshooting information.
|
||||||
|
|||||||
106
DOCKER_BUILD_FIX.md
Normal file
106
DOCKER_BUILD_FIX.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Docker Build Fix: Upload-Dateien ausschließen
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Der Docker Build schlug fehl mit:
|
||||||
|
```
|
||||||
|
no space left on device
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ursache**: Die großen MP3-Dateien in `public/uploads/` wurden in den Build-Context kopiert und verbrauchten zu viel Speicherplatz.
|
||||||
|
|
||||||
|
## Lösung
|
||||||
|
|
||||||
|
Eine `.dockerignore` Datei wurde erstellt, die folgende Dateien/Ordner vom Build ausschließt:
|
||||||
|
|
||||||
|
- `public/uploads/*` - Upload-Dateien (werden als Volume gemountet)
|
||||||
|
- `data/*` - Datenbank-Dateien (werden als Volume gemountet)
|
||||||
|
- `node_modules` - werden während des Builds installiert
|
||||||
|
- `.next`, `out`, `build` - Build-Artefakte
|
||||||
|
- Backup-Dateien, Logs, temporäre Dateien
|
||||||
|
|
||||||
|
## Zusätzliche Maßnahmen
|
||||||
|
|
||||||
|
Falls der Build weiterhin Probleme macht:
|
||||||
|
|
||||||
|
### 1. Docker aufräumen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Entferne nicht verwendete Images
|
||||||
|
docker image prune -a
|
||||||
|
|
||||||
|
# Entferne nicht verwendete Container
|
||||||
|
docker container prune
|
||||||
|
|
||||||
|
# Entferne nicht verwendete Volumes (VORSICHT: kann Daten löschen!)
|
||||||
|
docker volume prune
|
||||||
|
|
||||||
|
# Kompletter Cleanup (alles außer laufenden Containern)
|
||||||
|
docker system prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Speicherplatz prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Zeige Speicherplatz
|
||||||
|
df -h
|
||||||
|
|
||||||
|
# Zeige Docker-Speicherverbrauch
|
||||||
|
docker system df
|
||||||
|
|
||||||
|
# Zeige größte Images
|
||||||
|
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | sort -k3 -h
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build-Kontext prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe was in den Build-Context kopiert wird
|
||||||
|
docker build --no-cache --progress=plain -t test-build . 2>&1 | grep "transferring context"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Upload-Dateien manuell ausschließen
|
||||||
|
|
||||||
|
Falls die `.dockerignore` nicht greift, können Upload-Dateien vorübergehend verschoben werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vor dem Build
|
||||||
|
mv public/uploads public/uploads.backup
|
||||||
|
mkdir -p public/uploads
|
||||||
|
touch public/uploads/.gitkeep
|
||||||
|
|
||||||
|
# Build durchführen
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
# Uploads wiederherstellen
|
||||||
|
rm -rf public/uploads
|
||||||
|
mv public/uploads.backup public/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtig
|
||||||
|
|
||||||
|
Die Upload-Dateien werden **nicht** ins Docker-Image kopiert, sondern als Volume gemountet:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./public/uploads:/app/public/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
Das bedeutet:
|
||||||
|
- Upload-Dateien bleiben auf dem Host-System
|
||||||
|
- Sie werden zur Laufzeit gemountet
|
||||||
|
- Sie sollten **nicht** ins Image kopiert werden (spart viel Speicher)
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Nach dem Build sollte das Image deutlich kleiner sein:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Zeige Image-Größe
|
||||||
|
docker images hoerdle-hoerdle
|
||||||
|
|
||||||
|
# Prüfe ob Uploads im Image sind
|
||||||
|
docker run --rm hoerdle-hoerdle ls -lh /app/public/uploads
|
||||||
|
# Sollte nur .gitkeep oder Covers zeigen, keine MP3-Dateien
|
||||||
|
```
|
||||||
|
|
||||||
@@ -23,11 +23,13 @@ RUN apk add --no-cache git
|
|||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Extract version: use build arg if provided, otherwise get from git
|
# Extract version: use build arg if provided, otherwise get from git, fallback to package.json
|
||||||
RUN if [ -n "$APP_VERSION" ]; then \
|
RUN if [ -n "$APP_VERSION" ]; then \
|
||||||
echo "$APP_VERSION" > /tmp/version.txt; \
|
echo "$APP_VERSION" > /tmp/version.txt; \
|
||||||
else \
|
else \
|
||||||
git describe --tags --always 2>/dev/null > /tmp/version.txt || echo "unknown" > /tmp/version.txt; \
|
(git describe --tags --always 2>/dev/null || \
|
||||||
|
(grep -o '"version": "[^"]*"' package.json 2>/dev/null | cut -d'"' -f4 | sed 's/^/v/') || \
|
||||||
|
echo "dev") > /tmp/version.txt; \
|
||||||
fi && \
|
fi && \
|
||||||
echo "Building version: $(cat /tmp/version.txt)"
|
echo "Building version: $(cat /tmp/version.txt)"
|
||||||
|
|
||||||
|
|||||||
10
Dockerfile.caddy
Normal file
10
Dockerfile.caddy
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Dockerfile für Caddy mit GoDaddy DNS-Provider Plugin
|
||||||
|
FROM caddy:2-builder AS builder
|
||||||
|
|
||||||
|
RUN xcaddy build \
|
||||||
|
--with github.com/caddy-dns/godaddy
|
||||||
|
|
||||||
|
FROM caddy:2
|
||||||
|
|
||||||
|
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||||
|
|
||||||
83
FIX_I18N.md
Normal file
83
FIX_I18N.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Fix für i18n-Daten (String → JSON Konvertierung)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
Die Datenbank hat Genre-/Special-/News-Namen als einfache Strings (`"Rock"`) statt JSON (`{"de": "Rock", "en": "Rock"}`) gespeichert, was zu `SyntaxError: "Rock" is not valid JSON` führt.
|
||||||
|
|
||||||
|
## Lösung: Manuell ausführen
|
||||||
|
|
||||||
|
Führe diese Befehle **direkt auf dem Server** aus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/hoerdle
|
||||||
|
|
||||||
|
# 1. Backup erstellen
|
||||||
|
docker cp hoerdle:/app/data/prod.db ./data/prod.db.backup.$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
# 2. Kopiere DB lokal
|
||||||
|
docker cp hoerdle:/app/data/prod.db ./data/prod.db.tmp
|
||||||
|
|
||||||
|
# 3. Setze Berechtigungen
|
||||||
|
sudo chmod 666 ./data/prod.db.tmp
|
||||||
|
sudo chmod 775 ./data
|
||||||
|
|
||||||
|
# 4. Prüfe ob sqlite3 installiert ist
|
||||||
|
which sqlite3 || sudo apt-get install -y sqlite3
|
||||||
|
|
||||||
|
# 5. Fixe die Datenbank (kopiere diesen Block komplett)
|
||||||
|
sqlite3 ./data/prod.db.tmp << 'EOF'
|
||||||
|
UPDATE Genre SET name = json_object('de', name, 'en', name) WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
UPDATE Genre SET subtitle = json_object('de', subtitle, 'en', subtitle) WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
UPDATE Special SET name = json_object('de', name, 'en', name) WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
UPDATE Special SET subtitle = json_object('de', subtitle, 'en', subtitle) WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
UPDATE News SET title = json_object('de', title, 'en', title) WHERE typeof(title) = 'text' AND title NOT LIKE '{%';
|
||||||
|
UPDATE News SET content = json_object('de', content, 'en', content) WHERE typeof(content) = 'text' AND content NOT LIKE '{%';
|
||||||
|
SELECT '✅ Fertig!' as status;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 6. Kopiere zurück
|
||||||
|
docker cp ./data/prod.db.tmp hoerdle:/app/data/prod.db
|
||||||
|
|
||||||
|
# 7. Aufräumen
|
||||||
|
rm ./data/prod.db.tmp
|
||||||
|
|
||||||
|
# 8. Container neu starten
|
||||||
|
docker compose restart hoerdle
|
||||||
|
|
||||||
|
# 9. Logs prüfen
|
||||||
|
docker logs hoerdle --tail=50
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls Schritt 5 mit "permission denied" fehlschlägt, verwende `sudo`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo sqlite3 ./data/prod.db.tmp << 'EOF'
|
||||||
|
[... SQL-Befehle wie oben ...]
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automatisiertes Skript
|
||||||
|
|
||||||
|
Alternativ kannst du das automatische Skript verwenden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/fix-i18n-easy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder das lokale Skript:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/fix-i18n-local.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prüfen ob es funktioniert hat
|
||||||
|
|
||||||
|
Nach dem Neustart sollte die Seite wieder funktionieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe Logs (sollte keine JSON-Fehler mehr zeigen)
|
||||||
|
docker logs hoerdle --tail=100 | grep -i "json\|error" || echo "✅ Keine JSON-Fehler gefunden"
|
||||||
|
|
||||||
|
# Teste die Seite
|
||||||
|
curl -s https://hoerdle.de/de | head -20
|
||||||
|
```
|
||||||
|
|
||||||
206
TROUBLESHOOTING.md
Normal file
206
TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# Troubleshooting Guide
|
||||||
|
|
||||||
|
## Application Error: "a server-side exception has occurred"
|
||||||
|
|
||||||
|
Dieser Fehler tritt auf, wenn die Next.js-Anwendung auf dem Server einen Fehler hat.
|
||||||
|
|
||||||
|
### ⚠️ Datenbank-Berechtigungen (wenn DB von anderem Server kopiert wurde)
|
||||||
|
|
||||||
|
**Symptom**: Application Error nach dem Kopieren einer Datenbank von einem anderen Server
|
||||||
|
|
||||||
|
**Ursache**: SQLite benötigt Schreibrechte auf:
|
||||||
|
- Die Datenbankdatei selbst (`prod.db`)
|
||||||
|
- Das Datenbankverzeichnis (für temporäre Dateien wie `-wal`, `-shm`)
|
||||||
|
|
||||||
|
**Sofort-Lösung (auf dem Server ausführen)**:
|
||||||
|
```bash
|
||||||
|
# 1. Setze Berechtigungen für Datenbankverzeichnis und Datei
|
||||||
|
chmod 775 ./data
|
||||||
|
chmod 664 ./data/prod.db
|
||||||
|
|
||||||
|
# 2. Falls temporäre SQLite-Dateien existieren, auch diese:
|
||||||
|
chmod 664 ./data/*.db-wal 2>/dev/null || true
|
||||||
|
chmod 664 ./data/*.db-shm 2>/dev/null || true
|
||||||
|
|
||||||
|
# 3. Oder verwende das Fix-Skript:
|
||||||
|
./scripts/fix-database-permissions.sh
|
||||||
|
|
||||||
|
# 4. Container neu starten
|
||||||
|
docker compose restart hoerdle
|
||||||
|
|
||||||
|
# 5. Logs prüfen
|
||||||
|
docker logs hoerdle --tail=50
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warum passiert das?**
|
||||||
|
- Wenn du eine Datenbankdatei von einem anderen Server kopierst, behält sie die ursprünglichen Berechtigungen
|
||||||
|
- SQLite muss Schreibrechte haben, um zu funktionieren
|
||||||
|
- Auch das Verzeichnis braucht Schreibrechte (für SQLite-WAL-Modus)
|
||||||
|
|
||||||
|
### Sofort-Diagnose (auf dem Server ausführen)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Container-Logs prüfen (die wichtigste Information!)
|
||||||
|
docker logs hoerdle --tail=100
|
||||||
|
|
||||||
|
# 2. Container-Status prüfen
|
||||||
|
docker ps | grep hoerdle
|
||||||
|
|
||||||
|
# 3. Prüfe ob Datenbank existiert
|
||||||
|
docker exec hoerdle ls -lh /app/data/prod.db
|
||||||
|
|
||||||
|
# 4. Prüfe ob Server auf Port 3000 antwortet (intern)
|
||||||
|
docker exec hoerdle curl -f http://localhost:3000/api/daily
|
||||||
|
|
||||||
|
# 5. Prüfe Health Check
|
||||||
|
docker inspect hoerdle --format='{{json .State.Health}}' | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Häufige Ursachen und Lösungen
|
||||||
|
|
||||||
|
#### 1. Datenbankfehler / Migrationen fehlgeschlagen
|
||||||
|
|
||||||
|
**Symptom**: Logs zeigen Prisma-Fehler oder "database locked"
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Container-Logs prüfen
|
||||||
|
docker logs hoerdle | grep -i "migration\|database\|prisma"
|
||||||
|
|
||||||
|
# Falls Migrationen fehlgeschlagen sind:
|
||||||
|
docker compose restart hoerdle
|
||||||
|
|
||||||
|
# Bei persistierenden Problemen: Datenbank-Backup prüfen
|
||||||
|
ls -lh ./backups/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Container läuft nicht oder ist crashed
|
||||||
|
|
||||||
|
**Symptom**: Container existiert nicht oder Status zeigt "Exited"
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Container-Status prüfen
|
||||||
|
docker ps -a | grep hoerdle
|
||||||
|
|
||||||
|
# Container neu starten
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Falls Container nicht startet, Logs prüfen
|
||||||
|
docker logs hoerdle --tail=200
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Caddy kann Container nicht erreichen
|
||||||
|
|
||||||
|
**Symptom**: 502 Bad Gateway oder "connection refused" in Caddy-Logs
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Prüfe ob hoerdle Container läuft
|
||||||
|
docker ps | grep hoerdle
|
||||||
|
|
||||||
|
# Prüfe Netzwerk
|
||||||
|
docker network inspect hoerdle_default
|
||||||
|
|
||||||
|
# Prüfe Caddy-Logs
|
||||||
|
docker logs hoerdle-caddy --tail=50
|
||||||
|
|
||||||
|
# Stelle sicher, dass Caddyfile Port 3000 verwendet (nicht 3010!)
|
||||||
|
grep "reverse_proxy" Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Fehlende Umgebungsvariablen
|
||||||
|
|
||||||
|
**Symptom**: Logs zeigen undefined variables
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Prüfe wichtige Umgebungsvariablen
|
||||||
|
docker exec hoerdle env | grep -E "DATABASE_URL|NODE_ENV"
|
||||||
|
|
||||||
|
# Prüfe .env Datei (falls vorhanden)
|
||||||
|
cat .env | grep DATABASE_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Build-Fehler oder fehlerhafte Dateien
|
||||||
|
|
||||||
|
**Symptom**: Container startet, aber App crasht sofort
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Container komplett neu bauen
|
||||||
|
docker compose down
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Prüfe Build-Logs
|
||||||
|
docker compose build 2>&1 | tee build.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detaillierte Log-Analyse
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Fehler in Logs finden
|
||||||
|
docker logs hoerdle 2>&1 | grep -i -E "error|exception|fatal|panic" | tail -50
|
||||||
|
|
||||||
|
# Prisma-spezifische Fehler
|
||||||
|
docker logs hoerdle 2>&1 | grep -i prisma | tail -20
|
||||||
|
|
||||||
|
# Next.js-spezifische Fehler
|
||||||
|
docker logs hoerdle 2>&1 | grep -i "next\|react" | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Netzwerk-Debugging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Teste Verbindung von Caddy zu Hördle
|
||||||
|
docker exec hoerdle-caddy wget -O- http://hoerdle:3000/api/daily
|
||||||
|
|
||||||
|
# Prüfe alle Container im Netzwerk
|
||||||
|
docker network inspect hoerdle_default --format='{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenbank-Debugging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe Datenbank-Integrität
|
||||||
|
docker exec hoerdle npx prisma db pull
|
||||||
|
|
||||||
|
# Prüfe Datenbank-Struktur
|
||||||
|
docker exec hoerdle npx prisma studio &
|
||||||
|
# (dann Browser öffnen - erfordert X11 forwarding oder lokalen Zugriff)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick-Fix: Vollständiger Neustart
|
||||||
|
|
||||||
|
Wenn nichts anderes hilft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Backup erstellen
|
||||||
|
cp ./data/prod.db ./backups/prod_$(date +%Y%m%d_%H%M%S).db
|
||||||
|
|
||||||
|
# 2. Container stoppen
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# 3. Container neu starten
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 4. Logs beobachten
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bei weiterem Bedarf
|
||||||
|
|
||||||
|
Sammle folgende Informationen für weitere Hilfe:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "=== Container Status ===" && \
|
||||||
|
docker ps -a | grep hoerdle && \
|
||||||
|
echo -e "\n=== Letzte 50 Log-Zeilen ===" && \
|
||||||
|
docker logs hoerdle --tail=50 && \
|
||||||
|
echo -e "\n=== Fehler in Logs ===" && \
|
||||||
|
docker logs hoerdle 2>&1 | grep -i error | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
Kopiere die vollständige Ausgabe und sende sie weiter.
|
||||||
|
|
||||||
@@ -45,86 +45,86 @@ export default async function Home({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="tour-genres" style={{ textAlign: 'center', padding: '1rem', background: '#f3f4f6' }}>
|
<div id="tour-genres" style={{ textAlign: 'center', padding: '1rem', background: '#f3f4f6', position: 'relative' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem', maxWidth: '1200px', margin: '0 auto', padding: '0 1rem' }}>
|
{/* Language Switcher - rechts oben */}
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ position: 'absolute', top: '1rem', right: '1rem', zIndex: 10 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '1rem', flexWrap: 'wrap', alignItems: 'center', flex: 2 }}>
|
<LanguageSwitcher />
|
||||||
<div className="tooltip">
|
</div>
|
||||||
<Link href="/" style={{ fontWeight: 'bold', textDecoration: 'underline' }}>{tNav('global')}</Link>
|
|
||||||
<span className="tooltip-text">{t('globalTooltip')}</span>
|
{/* Zentrierte Navigation */}
|
||||||
</div>
|
<div style={{ display: 'flex', justifyContent: 'center', gap: '1rem', flexWrap: 'wrap', alignItems: 'center', marginBottom: '0.5rem' }}>
|
||||||
|
<div className="tooltip">
|
||||||
|
<Link href="/" style={{ fontWeight: 'bold', textDecoration: 'underline' }}>{tNav('global')}</Link>
|
||||||
|
<span className="tooltip-text">{t('globalTooltip')}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Genres */}
|
{/* Genres */}
|
||||||
{genres.map(g => {
|
{genres.map(g => {
|
||||||
const name = getLocalizedValue(g.name, locale);
|
const name = getLocalizedValue(g.name, locale);
|
||||||
const subtitle = getLocalizedValue(g.subtitle, locale);
|
const subtitle = getLocalizedValue(g.subtitle, locale);
|
||||||
return (
|
return (
|
||||||
<div key={g.id} className="tooltip">
|
<div key={g.id} className="tooltip">
|
||||||
<Link href={`/${name}`} style={{ color: '#4b5563', textDecoration: 'none' }}>
|
<Link href={`/${name}`} style={{ color: '#4b5563', textDecoration: 'none' }}>
|
||||||
{name}
|
{name}
|
||||||
|
</Link>
|
||||||
|
{subtitle && <span className="tooltip-text">{subtitle}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Separator if both exist */}
|
||||||
|
{genres.length > 0 && activeSpecials.length > 0 && (
|
||||||
|
<span style={{ color: '#d1d5db' }}>|</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Active Specials */}
|
||||||
|
{activeSpecials.map(s => {
|
||||||
|
const name = getLocalizedValue(s.name, locale);
|
||||||
|
const subtitle = getLocalizedValue(s.subtitle, locale);
|
||||||
|
return (
|
||||||
|
<div key={s.id} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||||
|
<div className="tooltip">
|
||||||
|
<Link
|
||||||
|
href={`/special/${name}`}
|
||||||
|
style={{
|
||||||
|
color: '#be185d', // Pink-700
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: '500'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
★ {name}
|
||||||
</Link>
|
</Link>
|
||||||
{subtitle && <span className="tooltip-text">{subtitle}</span>}
|
{subtitle && <span className="tooltip-text">{subtitle}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
{s.curator && (
|
||||||
})}
|
<span style={{ fontSize: '0.75rem', color: '#666' }}>
|
||||||
|
{t('curatedBy')} {s.curator}
|
||||||
{/* Separator if both exist */}
|
</span>
|
||||||
{genres.length > 0 && activeSpecials.length > 0 && (
|
)}
|
||||||
<span style={{ color: '#d1d5db' }}>|</span>
|
</div>
|
||||||
)}
|
);
|
||||||
|
})}
|
||||||
{/* Active Specials */}
|
</div>
|
||||||
{activeSpecials.map(s => {
|
|
||||||
|
{/* Upcoming Specials */}
|
||||||
|
{upcomingSpecials.length > 0 && (
|
||||||
|
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: '#666', textAlign: 'center' }}>
|
||||||
|
{t('comingSoon')}: {upcomingSpecials.map(s => {
|
||||||
const name = getLocalizedValue(s.name, locale);
|
const name = getLocalizedValue(s.name, locale);
|
||||||
const subtitle = getLocalizedValue(s.subtitle, locale);
|
|
||||||
return (
|
return (
|
||||||
<div key={s.id} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
<span key={s.id} style={{ marginLeft: '0.5rem' }}>
|
||||||
<div className="tooltip">
|
★ {name} ({s.launchDate ? new Date(s.launchDate).toLocaleDateString(locale, {
|
||||||
<Link
|
day: '2-digit',
|
||||||
href={`/special/${name}`}
|
month: '2-digit',
|
||||||
style={{
|
year: 'numeric',
|
||||||
color: '#be185d', // Pink-700
|
timeZone: process.env.TZ
|
||||||
textDecoration: 'none',
|
}) : ''})
|
||||||
fontWeight: '500'
|
{s.curator && <span style={{ fontStyle: 'italic', marginLeft: '0.25rem' }}>{t('curatedBy')} {s.curator}</span>}
|
||||||
}}
|
</span>
|
||||||
>
|
|
||||||
★ {name}
|
|
||||||
</Link>
|
|
||||||
{subtitle && <span className="tooltip-text">{subtitle}</span>}
|
|
||||||
</div>
|
|
||||||
{s.curator && (
|
|
||||||
<span style={{ fontSize: '0.75rem', color: '#666' }}>
|
|
||||||
{t('curatedBy')} {s.curator}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{/* Upcoming Specials */}
|
|
||||||
{upcomingSpecials.length > 0 && (
|
|
||||||
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: '#666' }}>
|
|
||||||
{t('comingSoon')}: {upcomingSpecials.map(s => {
|
|
||||||
const name = getLocalizedValue(s.name, locale);
|
|
||||||
return (
|
|
||||||
<span key={s.id} style={{ marginLeft: '0.5rem' }}>
|
|
||||||
★ {name} ({s.launchDate ? new Date(s.launchDate).toLocaleDateString(locale, {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
timeZone: process.env.TZ
|
|
||||||
}) : ''})
|
|
||||||
{s.curator && <span style={{ fontStyle: 'italic', marginLeft: '0.25rem' }}>{t('curatedBy')} {s.curator}</span>}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-end' }}>
|
|
||||||
<LanguageSwitcher />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tour-news">
|
<div id="tour-news">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export async function GET() {
|
|||||||
for (const versionFilePath of versionPaths) {
|
for (const versionFilePath of versionPaths) {
|
||||||
if (existsSync(versionFilePath)) {
|
if (existsSync(versionFilePath)) {
|
||||||
const version = readFileSync(versionFilePath, 'utf-8').trim();
|
const version = readFileSync(versionFilePath, 'utf-8').trim();
|
||||||
if (version && version !== 'unknown') {
|
if (version && version !== 'unknown' && version !== '') {
|
||||||
return NextResponse.json({ version });
|
return NextResponse.json({ version });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,19 @@ export async function GET() {
|
|||||||
return NextResponse.json({ version: process.env.APP_VERSION });
|
return NextResponse.json({ version: process.env.APP_VERSION });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: check package.json
|
||||||
|
try {
|
||||||
|
const packageJsonPath = join(process.cwd(), 'package.json');
|
||||||
|
if (existsSync(packageJsonPath)) {
|
||||||
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||||
|
if (packageJson.version) {
|
||||||
|
return NextResponse.json({ version: `v${packageJson.version}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore package.json read errors
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback: try to get from git (local development)
|
// Fallback: try to get from git (local development)
|
||||||
let version = 'dev';
|
let version = 'dev';
|
||||||
|
|
||||||
|
|||||||
59
docker-compose.caddy.yml
Normal file
59
docker-compose.caddy.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Docker Compose Konfiguration für Caddy Reverse Proxy
|
||||||
|
# Optional: Nur in Produktionsumgebung verwenden
|
||||||
|
#
|
||||||
|
# Starten: docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
|
||||||
|
# Stoppen: docker compose -f docker-compose.yml -f docker-compose.caddy.yml down
|
||||||
|
|
||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
# Verwende Custom-Image mit GoDaddy DNS-Plugin
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.caddy
|
||||||
|
# Alternativ: Verwende Standard-Caddy und manuelle DNS-Konfiguration
|
||||||
|
# image: caddy:2-alpine
|
||||||
|
container_name: hoerdle-caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# Standard HTTP/HTTPS Ports
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "443:443/udp" # Für HTTP/3 (QUIC)
|
||||||
|
environment:
|
||||||
|
# GoDaddy API-Credentials für DNS-01 Challenge
|
||||||
|
# Diese müssen in einer .env-Datei gesetzt werden:
|
||||||
|
# GODADDY_API_KEY=your_api_key
|
||||||
|
# GODADDY_API_SECRET=your_api_secret
|
||||||
|
- GODADDY_API_KEY=${GODADDY_API_KEY:-}
|
||||||
|
- GODADDY_API_SECRET=${GODADDY_API_SECRET:-}
|
||||||
|
# Optional: Email für Let's Encrypt Benachrichtigungen
|
||||||
|
- CADDY_EMAIL=${CADDY_EMAIL:-}
|
||||||
|
volumes:
|
||||||
|
# Caddyfile-Konfiguration
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
# Persistente Zertifikat-Speicherung
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
# Health Check
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "caddy", "version"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
# Nur starten, wenn ENABLE_CADDY=true gesetzt ist
|
||||||
|
profiles:
|
||||||
|
- production
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
|
driver: local
|
||||||
|
caddy_config:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: hoerdle_default
|
||||||
|
external: true
|
||||||
|
|
||||||
@@ -37,4 +37,11 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
# docker-entrypoint.sh handles migrations and server startup (with baseline fallback)
|
# docker-entrypoint.sh handles migrations and server startup (with baseline fallback)
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: hoerdle_default
|
||||||
|
external: true
|
||||||
|
|||||||
304
messages/de.json
304
messages/de.json
@@ -1,156 +1,156 @@
|
|||||||
{
|
{
|
||||||
"Common": {
|
"Common": {
|
||||||
"loading": "Laden...",
|
"loading": "Laden...",
|
||||||
"error": "Ein Fehler ist aufgetreten",
|
"error": "Ein Fehler ist aufgetreten",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"back": "Zurück"
|
"back": "Zurück"
|
||||||
},
|
},
|
||||||
"Navigation": {
|
"Navigation": {
|
||||||
"home": "Startseite",
|
"home": "Startseite",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"global": "Global",
|
"global": "Global",
|
||||||
"news": "Neuigkeiten"
|
"news": "Neuigkeiten"
|
||||||
},
|
},
|
||||||
"Game": {
|
"Game": {
|
||||||
"play": "Abspielen",
|
"play": "Abspielen",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
"skip": "Überspringen",
|
"skip": "Überspringen",
|
||||||
"submit": "Raten",
|
"submit": "Raten",
|
||||||
"next": "Nächstes",
|
"next": "Nächstes",
|
||||||
"won": "Gewonnen!",
|
"won": "Gewonnen!",
|
||||||
"lost": "Verloren",
|
"lost": "Verloren",
|
||||||
"correct": "Richtig!",
|
"correct": "Richtig!",
|
||||||
"wrong": "Falsch",
|
"wrong": "Falsch",
|
||||||
"guessPlaceholder": "Lied oder Interpret eingeben...",
|
"guessPlaceholder": "Lied oder Interpret eingeben...",
|
||||||
"attempts": "Versuche",
|
"attempts": "Versuche",
|
||||||
"share": "Teilen",
|
"share": "Teilen",
|
||||||
"nextPuzzle": "Nächstes Rätsel in",
|
"nextPuzzle": "Nächstes Rätsel in",
|
||||||
"noPuzzleAvailable": "Kein Rätsel verfügbar",
|
"noPuzzleAvailable": "Kein Rätsel verfügbar",
|
||||||
"noPuzzleDescription": "Tägliches Rätsel konnte nicht generiert werden.",
|
"noPuzzleDescription": "Tägliches Rätsel konnte nicht generiert werden.",
|
||||||
"noPuzzleGenre": "Bitte stelle sicher, dass Songs in der Datenbank vorhanden sind",
|
"noPuzzleGenre": "Bitte stelle sicher, dass Songs in der Datenbank vorhanden sind",
|
||||||
"goToAdmin": "Zum Admin-Dashboard gehen",
|
"goToAdmin": "Zum Admin-Dashboard gehen",
|
||||||
"loadingState": "Lade Status...",
|
"loadingState": "Lade Status...",
|
||||||
"attempt": "Versuch",
|
"attempt": "Versuch",
|
||||||
"unlocked": "freigeschaltet",
|
"unlocked": "freigeschaltet",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"skipWithBonus": "Überspringen (+{seconds}s)",
|
"skipWithBonus": "Überspringen (+{seconds}s)",
|
||||||
"solveGiveUp": "Lösen (Aufgeben)",
|
"solveGiveUp": "Lösen (Aufgeben)",
|
||||||
"comeBackTomorrow": "Komm morgen zurück für ein neues Lied.",
|
"comeBackTomorrow": "Komm morgen zurück für ein neues Lied.",
|
||||||
"theSongWas": "Das Lied war:",
|
"theSongWas": "Das Lied war:",
|
||||||
"score": "Punkte",
|
"score": "Punkte",
|
||||||
"scoreBreakdown": "Punkteaufschlüsselung",
|
"scoreBreakdown": "Punkteaufschlüsselung",
|
||||||
"albumCover": "Album-Cover",
|
"albumCover": "Album-Cover",
|
||||||
"released": "Veröffentlicht",
|
"released": "Veröffentlicht",
|
||||||
"yourBrowserDoesNotSupport": "Ihr Browser unterstützt das Audio-Element nicht.",
|
"yourBrowserDoesNotSupport": "Ihr Browser unterstützt das Audio-Element nicht.",
|
||||||
"thanksForRating": "Danke für die Bewertung!",
|
"thanksForRating": "Danke für die Bewertung!",
|
||||||
"rateThisPuzzle": "Bewerte dieses Rätsel:",
|
"rateThisPuzzle": "Bewerte dieses Rätsel:",
|
||||||
"shared": "✓ Geteilt!",
|
"shared": "✓ Geteilt!",
|
||||||
"copied": "✓ Kopiert!",
|
"copied": "✓ Kopiert!",
|
||||||
"shareFailed": "✗ Fehlgeschlagen",
|
"shareFailed": "✗ Fehlgeschlagen",
|
||||||
"bonusRound": "Bonus-Runde!",
|
"bonusRound": "Bonus-Runde!",
|
||||||
"guessReleaseYear": "Errate das Veröffentlichungsjahr für",
|
"guessReleaseYear": "Errate das Veröffentlichungsjahr für",
|
||||||
"points": "Punkte",
|
"points": "Punkte",
|
||||||
"skipBonus": "Bonus überspringen",
|
"skipBonus": "Bonus überspringen",
|
||||||
"notQuite": "Nicht ganz!",
|
"notQuite": "Nicht ganz!",
|
||||||
"youGuessed": "Du hast geraten",
|
"youGuessed": "Du hast geraten",
|
||||||
"actuallyReleasedIn": "Tatsächlich veröffentlicht in",
|
"actuallyReleasedIn": "Tatsächlich veröffentlicht in",
|
||||||
"skipped": "Übersprungen",
|
"skipped": "Übersprungen",
|
||||||
"gameOverPlaceholder": "Spiel beendet",
|
"gameOverPlaceholder": "Spiel beendet",
|
||||||
"knowItSearch": "Weißt du es? Suche nach Interpret / Titel",
|
"knowItSearch": "Weißt du es? Suche nach Interpret / Titel",
|
||||||
"special": "Special",
|
"special": "Special",
|
||||||
"genre": "Genre"
|
"genre": "Genre"
|
||||||
},
|
},
|
||||||
"Statistics": {
|
"Statistics": {
|
||||||
"yourStatistics": "Deine Statistiken",
|
"yourStatistics": "Deine Statistiken",
|
||||||
"totalPuzzles": "Gesamte Rätsel",
|
"totalPuzzles": "Gesamte Rätsel",
|
||||||
"try": "Versuch",
|
"try": "Versuch",
|
||||||
"failed": "Verloren"
|
"failed": "Verloren"
|
||||||
},
|
},
|
||||||
"OnboardingTour": {
|
"OnboardingTour": {
|
||||||
"done": "Fertig",
|
"done": "Fertig",
|
||||||
"next": "Weiter",
|
"next": "Weiter",
|
||||||
"previous": "Zurück",
|
"previous": "Zurück",
|
||||||
"genresSpecials": "Genres & Specials",
|
"genresSpecials": "Genres & Specials",
|
||||||
"genresSpecialsDescription": "Wähle hier ein bestimmtes Genre oder ein kuratiertes Special-Event.",
|
"genresSpecialsDescription": "Wähle hier ein bestimmtes Genre oder ein kuratiertes Special-Event.",
|
||||||
"news": "Neuigkeiten",
|
"news": "Neuigkeiten",
|
||||||
"newsDescription": "Bleibe auf dem Laufenden mit den neuesten Nachrichten und Ankündigungen.",
|
"newsDescription": "Bleibe auf dem Laufenden mit den neuesten Nachrichten und Ankündigungen.",
|
||||||
"hoerdle": "Hördle",
|
"hoerdle": "Hördle",
|
||||||
"hoerdleDescription": "Das ist das tägliche Rätsel. Ein neues Lied jeden Tag pro Genre.",
|
"hoerdleDescription": "Das ist das tägliche Rätsel. Ein neues Lied jeden Tag pro Genre.",
|
||||||
"attempts": "Versuche",
|
"attempts": "Versuche",
|
||||||
"attemptsDescription": "Du hast eine begrenzte Anzahl von Versuchen, um das Lied zu erraten.",
|
"attemptsDescription": "Du hast eine begrenzte Anzahl von Versuchen, um das Lied zu erraten.",
|
||||||
"score": "Punkte",
|
"score": "Punkte",
|
||||||
"scoreDescription": "Deine aktuelle Punktzahl. Versuche sie hoch zu halten!",
|
"scoreDescription": "Deine aktuelle Punktzahl. Versuche sie hoch zu halten!",
|
||||||
"player": "Player",
|
"player": "Player",
|
||||||
"playerDescription": "Höre dir den Ausschnitt an. Jedes zusätzliche Abspielen reduziert deine mögliche Punktzahl.",
|
"playerDescription": "Höre dir den Ausschnitt an. Jedes zusätzliche Abspielen reduziert deine mögliche Punktzahl.",
|
||||||
"input": "Eingabe",
|
"input": "Eingabe",
|
||||||
"inputDescription": "Gib hier deine Vermutung ein. Suche nach Interpret oder Titel.",
|
"inputDescription": "Gib hier deine Vermutung ein. Suche nach Interpret oder Titel.",
|
||||||
"controls": "Steuerung",
|
"controls": "Steuerung",
|
||||||
"controlsDescription": "Starte die Musik oder überspringe zum nächsten Ausschnitt, wenn du feststeckst."
|
"controlsDescription": "Starte die Musik oder überspringe zum nächsten Ausschnitt, wenn du feststeckst."
|
||||||
},
|
},
|
||||||
"InstallPrompt": {
|
"InstallPrompt": {
|
||||||
"installApp": "Hördle App installieren",
|
"installApp": "Hördle App installieren",
|
||||||
"installDescription": "Installiere die App für eine bessere Erfahrung und schnellen Zugriff!",
|
"installDescription": "Installiere die App für eine bessere Erfahrung und schnellen Zugriff!",
|
||||||
"iosInstructions": "Tippe auf",
|
"iosInstructions": "Tippe auf",
|
||||||
"iosShare": "Teilen",
|
"iosShare": "Teilen",
|
||||||
"iosThen": "dann \"Zum Home-Bildschirm hinzufügen\"",
|
"iosThen": "dann \"Zum Home-Bildschirm hinzufügen\"",
|
||||||
"installButton": "App installieren"
|
"installButton": "App installieren"
|
||||||
},
|
},
|
||||||
"Home": {
|
"Home": {
|
||||||
"welcome": "Willkommen bei Hördle",
|
"welcome": "Willkommen bei Hördle",
|
||||||
"subtitle": "Errate den Song anhand kurzer Ausschnitte",
|
"subtitle": "Errate den Song anhand kurzer Ausschnitte",
|
||||||
"globalTooltip": "Ein zufälliger Song aus der gesamten Sammlung",
|
"globalTooltip": "Ein zufälliger Song aus der gesamten Sammlung",
|
||||||
"comingSoon": "Demnächst",
|
"comingSoon": "Demnächst",
|
||||||
"curatedBy": "Kuratiert von"
|
"curatedBy": "Kuratiert von"
|
||||||
},
|
},
|
||||||
"Admin": {
|
"Admin": {
|
||||||
"title": "Hördle Admin Dashboard",
|
"title": "Hördle Admin Dashboard",
|
||||||
"login": "Admin Login",
|
"login": "Admin Login",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"loginButton": "Login",
|
"loginButton": "Login",
|
||||||
"logout": "Abmelden",
|
"logout": "Abmelden",
|
||||||
"manageSpecials": "Specials verwalten",
|
"manageSpecials": "Specials verwalten",
|
||||||
"manageGenres": "Genres verwalten",
|
"manageGenres": "Genres verwalten",
|
||||||
"manageNews": "News & Ankündigungen verwalten",
|
"manageNews": "News & Ankündigungen verwalten",
|
||||||
"uploadSongs": "Songs hochladen",
|
"uploadSongs": "Songs hochladen",
|
||||||
"todaysPuzzles": "Heutige tägliche Rätsel",
|
"todaysPuzzles": "Heutige tägliche Rätsel",
|
||||||
"show": "▶ Anzeigen",
|
"show": "▶ Anzeigen",
|
||||||
"hide": "▼ Ausblenden",
|
"hide": "▼ Ausblenden",
|
||||||
"addSpecial": "Special hinzufügen",
|
"addSpecial": "Special hinzufügen",
|
||||||
"addGenre": "Genre hinzufügen",
|
"addGenre": "Genre hinzufügen",
|
||||||
"addNews": "News hinzufügen",
|
"addNews": "News hinzufügen",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"curate": "Kurieren",
|
"curate": "Kurieren",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"subtitle": "Untertitel",
|
"subtitle": "Untertitel",
|
||||||
"maxAttempts": "Max. Versuche",
|
"maxAttempts": "Max. Versuche",
|
||||||
"unlockSteps": "Freischalt-Schritte",
|
"unlockSteps": "Freischalt-Schritte",
|
||||||
"launchDate": "Startdatum",
|
"launchDate": "Startdatum",
|
||||||
"endDate": "Enddatum",
|
"endDate": "Enddatum",
|
||||||
"curator": "Kurator",
|
"curator": "Kurator",
|
||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"newGenreName": "Neuer Genre-Name",
|
"newGenreName": "Neuer Genre-Name",
|
||||||
"editSpecial": "Special bearbeiten",
|
"editSpecial": "Special bearbeiten",
|
||||||
"editGenre": "Genre bearbeiten",
|
"editGenre": "Genre bearbeiten",
|
||||||
"editNews": "News bearbeiten",
|
"editNews": "News bearbeiten",
|
||||||
"newsTitle": "News-Titel",
|
"newsTitle": "News-Titel",
|
||||||
"content": "Inhalt (Markdown unterstützt)",
|
"content": "Inhalt (Markdown unterstützt)",
|
||||||
"author": "Autor (optional)",
|
"author": "Autor (optional)",
|
||||||
"featured": "Hervorgehoben",
|
"featured": "Hervorgehoben",
|
||||||
"noSpecialLink": "Kein Special-Link",
|
"noSpecialLink": "Kein Special-Link",
|
||||||
"noNewsItems": "Noch keine News-Einträge. Erstelle einen oben!",
|
"noNewsItems": "Noch keine News-Einträge. Erstelle einen oben!",
|
||||||
"noPuzzlesToday": "Keine täglichen Rätsel für heute gefunden.",
|
"noPuzzlesToday": "Keine täglichen Rätsel für heute gefunden.",
|
||||||
"category": "Kategorie",
|
"category": "Kategorie",
|
||||||
"song": "Song",
|
"song": "Song",
|
||||||
"artist": "Interpret",
|
"artist": "Interpret",
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"deletePuzzle": "Löschen",
|
"deletePuzzle": "Löschen",
|
||||||
"wrongPassword": "Falsches Passwort"
|
"wrongPassword": "Falsches Passwort"
|
||||||
},
|
},
|
||||||
"About": {
|
"About": {
|
||||||
"title": "Über Hördle & Impressum",
|
"title": "Über Hördle & Impressum",
|
||||||
|
|||||||
304
messages/en.json
304
messages/en.json
@@ -1,156 +1,156 @@
|
|||||||
{
|
{
|
||||||
"Common": {
|
"Common": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"error": "An error occurred",
|
"error": "An error occurred",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"back": "Back"
|
"back": "Back"
|
||||||
},
|
},
|
||||||
"Navigation": {
|
"Navigation": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"global": "Global",
|
"global": "Global",
|
||||||
"news": "News"
|
"news": "News"
|
||||||
},
|
},
|
||||||
"Game": {
|
"Game": {
|
||||||
"play": "Play",
|
"play": "Play",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
"skip": "Skip",
|
"skip": "Skip",
|
||||||
"submit": "Guess",
|
"submit": "Guess",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"won": "You won!",
|
"won": "You won!",
|
||||||
"lost": "Game Over",
|
"lost": "Game Over",
|
||||||
"correct": "Correct!",
|
"correct": "Correct!",
|
||||||
"wrong": "Wrong",
|
"wrong": "Wrong",
|
||||||
"guessPlaceholder": "Type song or artist...",
|
"guessPlaceholder": "Type song or artist...",
|
||||||
"attempts": "Attempts",
|
"attempts": "Attempts",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"nextPuzzle": "Next puzzle in",
|
"nextPuzzle": "Next puzzle in",
|
||||||
"noPuzzleAvailable": "No Puzzle Available",
|
"noPuzzleAvailable": "No Puzzle Available",
|
||||||
"noPuzzleDescription": "Could not generate a daily puzzle.",
|
"noPuzzleDescription": "Could not generate a daily puzzle.",
|
||||||
"noPuzzleGenre": "Please ensure there are songs in the database",
|
"noPuzzleGenre": "Please ensure there are songs in the database",
|
||||||
"goToAdmin": "Go to Admin Dashboard",
|
"goToAdmin": "Go to Admin Dashboard",
|
||||||
"loadingState": "Loading state...",
|
"loadingState": "Loading state...",
|
||||||
"attempt": "Attempt",
|
"attempt": "Attempt",
|
||||||
"unlocked": "unlocked",
|
"unlocked": "unlocked",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"skipWithBonus": "Skip (+{seconds}s)",
|
"skipWithBonus": "Skip (+{seconds}s)",
|
||||||
"solveGiveUp": "Solve (Give Up)",
|
"solveGiveUp": "Solve (Give Up)",
|
||||||
"comeBackTomorrow": "Come back tomorrow for a new song.",
|
"comeBackTomorrow": "Come back tomorrow for a new song.",
|
||||||
"theSongWas": "The song was:",
|
"theSongWas": "The song was:",
|
||||||
"score": "Score",
|
"score": "Score",
|
||||||
"scoreBreakdown": "Score Breakdown",
|
"scoreBreakdown": "Score Breakdown",
|
||||||
"albumCover": "Album Cover",
|
"albumCover": "Album Cover",
|
||||||
"released": "Released",
|
"released": "Released",
|
||||||
"yourBrowserDoesNotSupport": "Your browser does not support the audio element.",
|
"yourBrowserDoesNotSupport": "Your browser does not support the audio element.",
|
||||||
"thanksForRating": "Thanks for rating!",
|
"thanksForRating": "Thanks for rating!",
|
||||||
"rateThisPuzzle": "Rate this puzzle:",
|
"rateThisPuzzle": "Rate this puzzle:",
|
||||||
"shared": "✓ Shared!",
|
"shared": "✓ Shared!",
|
||||||
"copied": "✓ Copied!",
|
"copied": "✓ Copied!",
|
||||||
"shareFailed": "✗ Failed",
|
"shareFailed": "✗ Failed",
|
||||||
"bonusRound": "Bonus Round!",
|
"bonusRound": "Bonus Round!",
|
||||||
"guessReleaseYear": "Guess the release year for",
|
"guessReleaseYear": "Guess the release year for",
|
||||||
"points": "points",
|
"points": "points",
|
||||||
"skipBonus": "Skip Bonus",
|
"skipBonus": "Skip Bonus",
|
||||||
"notQuite": "Not quite!",
|
"notQuite": "Not quite!",
|
||||||
"youGuessed": "You guessed",
|
"youGuessed": "You guessed",
|
||||||
"actuallyReleasedIn": "Actually released in",
|
"actuallyReleasedIn": "Actually released in",
|
||||||
"skipped": "Skipped",
|
"skipped": "Skipped",
|
||||||
"gameOverPlaceholder": "Game Over",
|
"gameOverPlaceholder": "Game Over",
|
||||||
"knowItSearch": "Know it? Search for the artist / title",
|
"knowItSearch": "Know it? Search for the artist / title",
|
||||||
"special": "Special",
|
"special": "Special",
|
||||||
"genre": "Genre"
|
"genre": "Genre"
|
||||||
},
|
},
|
||||||
"Statistics": {
|
"Statistics": {
|
||||||
"yourStatistics": "Your Statistics",
|
"yourStatistics": "Your Statistics",
|
||||||
"totalPuzzles": "Total puzzles",
|
"totalPuzzles": "Total puzzles",
|
||||||
"try": "try",
|
"try": "try",
|
||||||
"failed": "Failed"
|
"failed": "Failed"
|
||||||
},
|
},
|
||||||
"OnboardingTour": {
|
"OnboardingTour": {
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"genresSpecials": "Genres & Specials",
|
"genresSpecials": "Genres & Specials",
|
||||||
"genresSpecialsDescription": "Choose a specific genre or a curated special event here.",
|
"genresSpecialsDescription": "Choose a specific genre or a curated special event here.",
|
||||||
"news": "News",
|
"news": "News",
|
||||||
"newsDescription": "Stay updated with the latest news and announcements.",
|
"newsDescription": "Stay updated with the latest news and announcements.",
|
||||||
"hoerdle": "Hördle",
|
"hoerdle": "Hördle",
|
||||||
"hoerdleDescription": "This is the daily puzzle. One new song every day per genre.",
|
"hoerdleDescription": "This is the daily puzzle. One new song every day per genre.",
|
||||||
"attempts": "Attempts",
|
"attempts": "Attempts",
|
||||||
"attemptsDescription": "You have a limited number of attempts to guess the song.",
|
"attemptsDescription": "You have a limited number of attempts to guess the song.",
|
||||||
"score": "Score",
|
"score": "Score",
|
||||||
"scoreDescription": "Your current score. Try to keep it high!",
|
"scoreDescription": "Your current score. Try to keep it high!",
|
||||||
"player": "Player",
|
"player": "Player",
|
||||||
"playerDescription": "Listen to the snippet. Each additional play reduces your potential score.",
|
"playerDescription": "Listen to the snippet. Each additional play reduces your potential score.",
|
||||||
"input": "Input",
|
"input": "Input",
|
||||||
"inputDescription": "Type your guess here. Search for artist or title.",
|
"inputDescription": "Type your guess here. Search for artist or title.",
|
||||||
"controls": "Controls",
|
"controls": "Controls",
|
||||||
"controlsDescription": "Start the music or skip to the next snippet if you're stuck."
|
"controlsDescription": "Start the music or skip to the next snippet if you're stuck."
|
||||||
},
|
},
|
||||||
"InstallPrompt": {
|
"InstallPrompt": {
|
||||||
"installApp": "Install Hördle App",
|
"installApp": "Install Hördle App",
|
||||||
"installDescription": "Install the app for a better experience and quick access!",
|
"installDescription": "Install the app for a better experience and quick access!",
|
||||||
"iosInstructions": "Tap",
|
"iosInstructions": "Tap",
|
||||||
"iosShare": "share",
|
"iosShare": "share",
|
||||||
"iosThen": "then \"Add to Home Screen\"",
|
"iosThen": "then \"Add to Home Screen\"",
|
||||||
"installButton": "Install App"
|
"installButton": "Install App"
|
||||||
},
|
},
|
||||||
"Home": {
|
"Home": {
|
||||||
"welcome": "Welcome to Hördle",
|
"welcome": "Welcome to Hördle",
|
||||||
"subtitle": "Guess the song from short snippets",
|
"subtitle": "Guess the song from short snippets",
|
||||||
"globalTooltip": "A random song from the entire collection",
|
"globalTooltip": "A random song from the entire collection",
|
||||||
"comingSoon": "Coming soon",
|
"comingSoon": "Coming soon",
|
||||||
"curatedBy": "Curated by"
|
"curatedBy": "Curated by"
|
||||||
},
|
},
|
||||||
"Admin": {
|
"Admin": {
|
||||||
"title": "Hördle Admin Dashboard",
|
"title": "Hördle Admin Dashboard",
|
||||||
"login": "Admin Login",
|
"login": "Admin Login",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"loginButton": "Login",
|
"loginButton": "Login",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"manageSpecials": "Manage Specials",
|
"manageSpecials": "Manage Specials",
|
||||||
"manageGenres": "Manage Genres",
|
"manageGenres": "Manage Genres",
|
||||||
"manageNews": "Manage News & Announcements",
|
"manageNews": "Manage News & Announcements",
|
||||||
"uploadSongs": "Upload Songs",
|
"uploadSongs": "Upload Songs",
|
||||||
"todaysPuzzles": "Today's Daily Puzzles",
|
"todaysPuzzles": "Today's Daily Puzzles",
|
||||||
"show": "▶ Show",
|
"show": "▶ Show",
|
||||||
"hide": "▼ Hide",
|
"hide": "▼ Hide",
|
||||||
"addSpecial": "Add Special",
|
"addSpecial": "Add Special",
|
||||||
"addGenre": "Add Genre",
|
"addGenre": "Add Genre",
|
||||||
"addNews": "Add News",
|
"addNews": "Add News",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"curate": "Curate",
|
"curate": "Curate",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"subtitle": "Subtitle",
|
"subtitle": "Subtitle",
|
||||||
"maxAttempts": "Max Attempts",
|
"maxAttempts": "Max Attempts",
|
||||||
"unlockSteps": "Unlock Steps",
|
"unlockSteps": "Unlock Steps",
|
||||||
"launchDate": "Launch Date",
|
"launchDate": "Launch Date",
|
||||||
"endDate": "End Date",
|
"endDate": "End Date",
|
||||||
"curator": "Curator",
|
"curator": "Curator",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
"newGenreName": "New Genre Name",
|
"newGenreName": "New Genre Name",
|
||||||
"editSpecial": "Edit Special",
|
"editSpecial": "Edit Special",
|
||||||
"editGenre": "Edit Genre",
|
"editGenre": "Edit Genre",
|
||||||
"editNews": "Edit News",
|
"editNews": "Edit News",
|
||||||
"newsTitle": "News Title",
|
"newsTitle": "News Title",
|
||||||
"content": "Content (Markdown supported)",
|
"content": "Content (Markdown supported)",
|
||||||
"author": "Author (optional)",
|
"author": "Author (optional)",
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"noSpecialLink": "No Special Link",
|
"noSpecialLink": "No Special Link",
|
||||||
"noNewsItems": "No news items yet. Create one above!",
|
"noNewsItems": "No news items yet. Create one above!",
|
||||||
"noPuzzlesToday": "No daily puzzles found for today.",
|
"noPuzzlesToday": "No daily puzzles found for today.",
|
||||||
"category": "Category",
|
"category": "Category",
|
||||||
"song": "Song",
|
"song": "Song",
|
||||||
"artist": "Artist",
|
"artist": "Artist",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"deletePuzzle": "Delete",
|
"deletePuzzle": "Delete",
|
||||||
"wrongPassword": "Wrong password"
|
"wrongPassword": "Wrong password"
|
||||||
},
|
},
|
||||||
"About": {
|
"About": {
|
||||||
"title": "About Hördle & Imprint",
|
"title": "About Hördle & Imprint",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoerdle",
|
"name": "hoerdle",
|
||||||
"version": "0.1.0.15",
|
"version": "0.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
65
scripts/check-db-permissions.sh
Executable file
65
scripts/check-db-permissions.sh
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script zum Prüfen der Datenbank-Berechtigungen und User-Konfiguration
|
||||||
|
|
||||||
|
echo "🔍 Datenbank-Berechtigungen und User-Check"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe User im Container
|
||||||
|
echo "👤 User im Container:"
|
||||||
|
docker exec hoerdle whoami
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe UID/GID
|
||||||
|
echo "🆔 UID/GID des laufenden Prozesses:"
|
||||||
|
docker exec hoerdle id
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Datenbankdatei
|
||||||
|
echo "💾 Datenbank-Datei-Informationen (im Container):"
|
||||||
|
docker exec hoerdle ls -lh /app/data/prod.db
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Datenbankverzeichnis
|
||||||
|
echo "📁 Datenbankverzeichnis-Berechtigungen (im Container):"
|
||||||
|
docker exec hoerdle ls -ld /app/data
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Datei schreibbar ist
|
||||||
|
echo "✍️ Schreibbarkeitstest:"
|
||||||
|
if docker exec hoerdle sh -c "test -w /app/data/prod.db && echo '✅ Datei ist schreibbar' || echo '❌ Datei ist NICHT schreibbar'"; then
|
||||||
|
echo " Datei ist schreibbar"
|
||||||
|
else
|
||||||
|
echo " ❌ Datei ist NICHT schreibbar!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe ob Verzeichnis schreibbar ist
|
||||||
|
if docker exec hoerdle sh -c "test -w /app/data && echo '✅ Verzeichnis ist schreibbar' || echo '❌ Verzeichnis ist NICHT schreibbar'"; then
|
||||||
|
echo " Verzeichnis ist schreibbar"
|
||||||
|
else
|
||||||
|
echo " ❌ Verzeichnis ist NICHT schreibbar!"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Host-Seite
|
||||||
|
echo "🖥️ Host-Seite Berechtigungen:"
|
||||||
|
ls -ld ./data
|
||||||
|
ls -lh ./data/prod.db
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Container-User aus docker-compose
|
||||||
|
echo "🐳 Docker Compose Konfiguration:"
|
||||||
|
if [ -f "docker-compose.yml" ]; then
|
||||||
|
grep -E "^[[:space:]]*user:" docker-compose.yml || echo " Keine 'user:' Direktive gefunden"
|
||||||
|
else
|
||||||
|
echo " ⚠️ docker-compose.yml nicht gefunden (verwende example?)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Empfehlung
|
||||||
|
echo "💡 Empfehlung:"
|
||||||
|
echo " Wenn die Datei 'node:node' gehört und Container als 'root' läuft,"
|
||||||
|
echo " sollte es funktionieren. Falls nicht, setze Besitzer auf root:"
|
||||||
|
echo " sudo chown root:root ./data/prod.db"
|
||||||
|
echo " Oder entferne 'user: root' aus docker-compose.yml"
|
||||||
|
|
||||||
99
scripts/debug-server-error.sh
Executable file
99
scripts/debug-server-error.sh
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script zum Debuggen von Server-Errors in Hördle
|
||||||
|
# Zeigt relevante Logs und Status-Informationen
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔍 Hördle Server Error Debugging"
|
||||||
|
echo "=================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Container-Status prüfen
|
||||||
|
echo "📦 Container-Status:"
|
||||||
|
docker ps --filter "name=hoerdle" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Container läuft
|
||||||
|
if ! docker ps | grep -q "hoerdle"; then
|
||||||
|
echo "❌ FEHLER: hoerdle Container läuft nicht!"
|
||||||
|
echo ""
|
||||||
|
echo "Versuche Container zu starten..."
|
||||||
|
docker compose up -d
|
||||||
|
sleep 5
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Letzte Logs anzeigen
|
||||||
|
echo "📋 Letzte 50 Zeilen der Container-Logs:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker logs --tail=50 hoerdle 2>&1 | tail -50
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Suche nach Fehlern in den Logs
|
||||||
|
echo "🚨 Fehler in den Logs (letzte 100 Zeilen):"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker logs --tail=100 hoerdle 2>&1 | grep -i -E "error|exception|failed|fatal|panic" || echo "Keine offensichtlichen Fehler gefunden"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Container Health Status
|
||||||
|
echo "💚 Health Check Status:"
|
||||||
|
docker inspect hoerdle --format='{{json .State.Health}}' | python3 -m json.tool 2>/dev/null || docker inspect hoerdle --format='{{.State.Status}}'
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob der Server auf Port 3000 antwortet (intern)
|
||||||
|
echo "🔌 Port-Verbindungstest (intern, Port 3000):"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker exec hoerdle curl -f -s -o /dev/null -w "HTTP Status: %{http_code}\n" http://localhost:3000/api/daily 2>&1 || echo "❌ Verbindung fehlgeschlagen"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Datenbank
|
||||||
|
echo "💾 Datenbank-Status:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
if docker exec hoerdle test -f /app/data/prod.db; then
|
||||||
|
echo "✅ Datenbankdatei existiert"
|
||||||
|
DB_SIZE=$(docker exec hoerdle stat -c%s /app/data/prod.db 2>/dev/null || echo "unbekannt")
|
||||||
|
echo " Größe: $DB_SIZE Bytes"
|
||||||
|
else
|
||||||
|
echo "❌ Datenbankdatei nicht gefunden!"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Umgebungsvariablen (wichtige)
|
||||||
|
echo "🔐 Wichtige Umgebungsvariablen:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker exec hoerdle env | grep -E "DATABASE_URL|NODE_ENV|PORT|HOSTNAME" || echo "Keine gefunden"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Next.js Server läuft
|
||||||
|
echo "🌐 Next.js Prozess-Status:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker exec hoerdle ps aux | grep -E "node|next" | grep -v grep || echo "Keine Next.js Prozesse gefunden"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Netzwerk-Verbindung prüfen
|
||||||
|
echo "🌐 Netzwerk-Verbindungen:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker network inspect hoerdle_default --format='{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}' 2>/dev/null || echo "Netzwerk nicht gefunden"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Caddy Status (falls vorhanden)
|
||||||
|
if docker ps | grep -q "hoerdle-caddy"; then
|
||||||
|
echo "🚪 Caddy-Container Status:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker logs --tail=20 hoerdle-caddy 2>&1 | tail -20
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=================================="
|
||||||
|
echo "✅ Debug-Informationen gesammelt"
|
||||||
|
echo ""
|
||||||
|
echo "💡 Nächste Schritte:"
|
||||||
|
echo "1. Prüfe die Fehler-Logs oben"
|
||||||
|
echo "2. Prüfe ob die Datenbank erreichbar ist"
|
||||||
|
echo "3. Prüfe ob alle Umgebungsvariablen gesetzt sind"
|
||||||
|
echo "4. Bei weiteren Problemen: docker logs hoerdle --tail=200"
|
||||||
|
|
||||||
@@ -54,6 +54,19 @@ git pull
|
|||||||
echo "🏷️ Fetching git tags..."
|
echo "🏷️ Fetching git tags..."
|
||||||
git fetch --tags
|
git fetch --tags
|
||||||
|
|
||||||
|
# Prüfe und erstelle/repariere Netzwerk falls nötig
|
||||||
|
echo "🌐 Prüfe Docker-Netzwerk..."
|
||||||
|
if ! docker network ls | grep -q "hoerdle_default"; then
|
||||||
|
echo " Netzwerk existiert nicht, erstelle es..."
|
||||||
|
docker network create hoerdle_default
|
||||||
|
echo "✅ Netzwerk erstellt"
|
||||||
|
else
|
||||||
|
# Prüfe ob Netzwerk falsche Labels hat (wird durch external: true umgangen)
|
||||||
|
echo "✅ Netzwerk existiert bereits"
|
||||||
|
echo " (Hinweis: Falls Warnungen über falsche Labels erscheinen, verwende: ./scripts/fix-network.sh)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Build new image in background (doesn't stop running container)
|
# Build new image in background (doesn't stop running container)
|
||||||
echo "🔨 Building new Docker image (this runs while app is still online)..."
|
echo "🔨 Building new Docker image (this runs while app is still online)..."
|
||||||
docker compose build
|
docker compose build
|
||||||
|
|||||||
71
scripts/docker-cleanup.sh
Executable file
71
scripts/docker-cleanup.sh
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Docker Cleanup-Skript
|
||||||
|
# Räumt nicht verwendete Docker-Images, Container, Volumes und Build-Cache auf
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🧹 Docker Cleanup..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Zeige aktuellen Speicherverbrauch
|
||||||
|
echo "📊 Aktueller Docker-Speicherverbrauch:"
|
||||||
|
docker system df
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Frage nach Bestätigung (falls interaktiv)
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
echo "⚠️ Dies wird folgende Ressourcen entfernen:"
|
||||||
|
echo " - Alle nicht verwendeten Images"
|
||||||
|
echo " - Alle gestoppten Container"
|
||||||
|
echo " - Alle nicht verwendeten Netzwerke"
|
||||||
|
echo " - Build-Cache"
|
||||||
|
echo ""
|
||||||
|
echo "Möchtest du fortfahren? (j/n)"
|
||||||
|
read -r response
|
||||||
|
if [ "$response" != "j" ] && [ "$response" != "J" ]; then
|
||||||
|
echo "❌ Abgebrochen."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Entferne gestoppte Container
|
||||||
|
echo "🗑️ Entferne gestoppte Container..."
|
||||||
|
STOPPED_CONTAINERS=$(docker ps -a -q -f status=exited | wc -l)
|
||||||
|
if [ "$STOPPED_CONTAINERS" -gt 0 ]; then
|
||||||
|
docker container prune -f
|
||||||
|
echo "✅ Gestoppte Container entfernt"
|
||||||
|
else
|
||||||
|
echo " Keine gestoppten Container gefunden"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. Entferne nicht verwendete Images
|
||||||
|
echo "🗑️ Entferne nicht verwendete Images..."
|
||||||
|
docker image prune -a -f
|
||||||
|
echo "✅ Nicht verwendete Images entfernt"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Entferne nicht verwendete Netzwerke
|
||||||
|
echo "🗑️ Entferne nicht verwendete Netzwerke..."
|
||||||
|
docker network prune -f
|
||||||
|
echo "✅ Nicht verwendete Netzwerke entfernt"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. Entferne Build-Cache (optional, kann lange dauern)
|
||||||
|
echo "🗑️ Entferne Build-Cache..."
|
||||||
|
docker builder prune -f
|
||||||
|
echo "✅ Build-Cache entfernt"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Zeige neuen Speicherverbrauch
|
||||||
|
echo "📊 Neuer Docker-Speicherverbrauch:"
|
||||||
|
docker system df
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Zeige verfügbaren Speicherplatz
|
||||||
|
echo "💾 Verfügbarer Speicherplatz:"
|
||||||
|
df -h / | tail -1 | awk '{print " Gesamt: " $2 ", Verfügbar: " $4 ", Belegt: " $5}'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "✅ Cleanup abgeschlossen!"
|
||||||
|
|
||||||
150
scripts/fix-i18n-local.sh
Executable file
150
scripts/fix-i18n-local.sh
Executable file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Fix für i18n-Daten: Kopiert DB lokal, fixt sie, kopiert zurück
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Fixe i18n-Daten (lokal kopieren, fixen, zurück kopieren)..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Container läuft
|
||||||
|
if ! docker ps | grep -q hoerdle; then
|
||||||
|
echo "❌ Container 'hoerdle' läuft nicht!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup erstellen
|
||||||
|
BACKUP_FILE="./data/prod.db.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
echo "💾 Erstelle Backup..."
|
||||||
|
docker cp hoerdle:/app/data/prod.db "$BACKUP_FILE"
|
||||||
|
# Setze Berechtigungen (kann root gehören)
|
||||||
|
sudo chmod 666 "$BACKUP_FILE" 2>/dev/null || chmod 666 "$BACKUP_FILE" 2>/dev/null || true
|
||||||
|
echo "✅ Backup erstellt: $BACKUP_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Kopiere DB lokal
|
||||||
|
echo "📥 Kopiere Datenbank lokal..."
|
||||||
|
docker cp hoerdle:/app/data/prod.db ./data/prod.db.tmp
|
||||||
|
# Setze Berechtigungen (Datei kann root gehören)
|
||||||
|
sudo chmod 666 ./data/prod.db.tmp 2>/dev/null || chmod 666 ./data/prod.db.tmp 2>/dev/null || {
|
||||||
|
echo "⚠️ Konnte Berechtigungen nicht setzen. Versuche mit sudo..."
|
||||||
|
sudo chmod 666 ./data/prod.db.tmp
|
||||||
|
}
|
||||||
|
chmod 775 ./data 2>/dev/null || sudo chmod 775 ./data 2>/dev/null || true
|
||||||
|
echo "✅ Datenbank kopiert"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob sqlite3 verfügbar ist
|
||||||
|
if ! command -v sqlite3 &> /dev/null; then
|
||||||
|
echo "❌ sqlite3 ist nicht installiert!"
|
||||||
|
echo " Installiere es mit: sudo apt-get install sqlite3"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fixe die Datenbank
|
||||||
|
echo "🔧 Fixe i18n-Daten..."
|
||||||
|
# Stelle sicher, dass wir Schreibrechte haben (auch für WAL-Dateien)
|
||||||
|
chmod 666 ./data/prod.db.tmp 2>/dev/null || sudo chmod 666 ./data/prod.db.tmp
|
||||||
|
chmod 775 ./data 2>/dev/null || sudo chmod 775 ./data
|
||||||
|
# Prüfe ob wir die Datei lesen können
|
||||||
|
if [ ! -r "./data/prod.db.tmp" ]; then
|
||||||
|
echo "❌ Kann Datenbankdatei nicht lesen. Setze Besitzer..."
|
||||||
|
sudo chown $(whoami):$(whoami) ./data/prod.db.tmp || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Führe SQL-Befehle aus (mit sudo falls nötig)
|
||||||
|
if [ -r "./data/prod.db.tmp" ] && [ -w "./data" ]; then
|
||||||
|
sqlite3 ./data/prod.db.tmp << 'SQL'
|
||||||
|
-- Fix Genre.name
|
||||||
|
UPDATE Genre
|
||||||
|
SET name = json_object('de', name, 'en', name)
|
||||||
|
WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Genre.subtitle
|
||||||
|
UPDATE Genre
|
||||||
|
SET subtitle = json_object('de', subtitle, 'en', subtitle)
|
||||||
|
WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Special.name
|
||||||
|
UPDATE Special
|
||||||
|
SET name = json_object('de', name, 'en', name)
|
||||||
|
WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Special.subtitle
|
||||||
|
UPDATE Special
|
||||||
|
SET subtitle = json_object('de', subtitle, 'en', subtitle)
|
||||||
|
WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix News.title
|
||||||
|
UPDATE News
|
||||||
|
SET title = json_object('de', title, 'en', title)
|
||||||
|
WHERE typeof(title) = 'text' AND title NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix News.content
|
||||||
|
UPDATE News
|
||||||
|
SET content = json_object('de', content, 'en', content)
|
||||||
|
WHERE typeof(content) = 'text' AND content NOT LIKE '{%';
|
||||||
|
|
||||||
|
SELECT '✅ Alle i18n-Daten wurden gefixt!' as status;
|
||||||
|
SQL
|
||||||
|
else
|
||||||
|
echo "❌ Kann Datenbankdatei nicht lesen oder schreiben!"
|
||||||
|
echo " Versuche mit sudo..."
|
||||||
|
sudo sqlite3 ./data/prod.db.tmp << 'SQL'
|
||||||
|
-- Fix Genre.name
|
||||||
|
UPDATE Genre
|
||||||
|
SET name = json_object('de', name, 'en', name)
|
||||||
|
WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Genre.subtitle
|
||||||
|
UPDATE Genre
|
||||||
|
SET subtitle = json_object('de', subtitle, 'en', subtitle)
|
||||||
|
WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Special.name
|
||||||
|
UPDATE Special
|
||||||
|
SET name = json_object('de', name, 'en', name)
|
||||||
|
WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Special.subtitle
|
||||||
|
UPDATE Special
|
||||||
|
SET subtitle = json_object('de', subtitle, 'en', subtitle)
|
||||||
|
WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix News.title
|
||||||
|
UPDATE News
|
||||||
|
SET title = json_object('de', title, 'en', title)
|
||||||
|
WHERE typeof(title) = 'text' AND title NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix News.content
|
||||||
|
UPDATE News
|
||||||
|
SET content = json_object('de', content, 'en', content)
|
||||||
|
WHERE typeof(content) = 'text' AND content NOT LIKE '{%';
|
||||||
|
|
||||||
|
SELECT '✅ Alle i18n-Daten wurden gefixt!' as status;
|
||||||
|
SQL
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "❌ Fehler beim Fixen der Datenbank!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Datenbank gefixt"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Kopiere zurück
|
||||||
|
echo "📤 Kopiere gefixte Datenbank zurück..."
|
||||||
|
docker cp ./data/prod.db.tmp hoerdle:/app/data/prod.db
|
||||||
|
echo "✅ Datenbank zurück kopiert"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Aufräumen
|
||||||
|
rm -f ./data/prod.db.tmp ./data/prod.db.tmp.backup
|
||||||
|
|
||||||
|
echo "🔄 Starte Container neu..."
|
||||||
|
docker compose restart hoerdle
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "✅ Fertig! Prüfe die Logs:"
|
||||||
|
echo " docker logs hoerdle --tail=50"
|
||||||
|
|
||||||
51
scripts/fix-network.sh
Executable file
51
scripts/fix-network.sh
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script zum Reparieren des Docker-Netzwerks hoerdle_default
|
||||||
|
# Dieses Script behebt die Warnung über falsche Netzwerk-Labels
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Repariere Docker-Netzwerk hoerdle_default..."
|
||||||
|
|
||||||
|
# Prüfe, ob Container laufen
|
||||||
|
RUNNING_CONTAINERS=$(docker ps --filter "network=hoerdle_default" --format "{{.Names}}" | wc -l)
|
||||||
|
|
||||||
|
if [ "$RUNNING_CONTAINERS" -gt 0 ]; then
|
||||||
|
echo "⚠️ Warnung: Es laufen noch Container, die das Netzwerk nutzen."
|
||||||
|
echo "📋 Container, die betroffen sind:"
|
||||||
|
docker ps --filter "network=hoerdle_default" --format " - {{.Names}}"
|
||||||
|
echo ""
|
||||||
|
echo "Möchtest du fortfahren? Die Container müssen neu gestartet werden. (j/n)"
|
||||||
|
read -r response
|
||||||
|
if [ "$response" != "j" ] && [ "$response" != "J" ]; then
|
||||||
|
echo "❌ Abgebrochen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🛑 Stoppe Container..."
|
||||||
|
docker compose down || true
|
||||||
|
if [ -f "docker-compose.caddy.yml" ]; then
|
||||||
|
docker compose -f docker-compose.caddy.yml down || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe, ob Netzwerk existiert
|
||||||
|
if docker network ls | grep -q "hoerdle_default"; then
|
||||||
|
echo "🗑️ Lösche altes Netzwerk..."
|
||||||
|
docker network rm hoerdle_default || {
|
||||||
|
echo "❌ Netzwerk konnte nicht gelöscht werden. Möglicherweise sind noch Container verbunden."
|
||||||
|
echo " Versuche, alle Container zu trennen..."
|
||||||
|
docker network disconnect hoerdle_default $(docker ps -q --filter "network=hoerdle_default") 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
docker network rm hoerdle_default || {
|
||||||
|
echo "❌ Netzwerk konnte immer noch nicht gelöscht werden."
|
||||||
|
echo " Bitte manuell prüfen: docker network inspect hoerdle_default"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✨ Netzwerk erfolgreich gelöscht."
|
||||||
|
echo "📝 Das Netzwerk wird beim nächsten 'docker compose up' automatisch neu erstellt."
|
||||||
|
echo ""
|
||||||
|
echo "✅ Fertig! Du kannst jetzt 'docker compose up -d' ausführen."
|
||||||
|
|
||||||
47
scripts/quick-fix-db.sh
Executable file
47
scripts/quick-fix-db.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick-Fix für Datenbank-Berechtigungen
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Quick-Fix für Datenbank-Berechtigungen..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
|
||||||
|
# Setze Berechtigungen
|
||||||
|
echo "1️⃣ Setze Berechtigungen..."
|
||||||
|
chmod 775 ./data
|
||||||
|
chmod 664 ./data/prod.db 2>/dev/null || echo " ⚠️ Datenbankdatei nicht gefunden"
|
||||||
|
|
||||||
|
# Setze Besitzer auf root (Container läuft als root)
|
||||||
|
echo "2️⃣ Setze Besitzer auf root..."
|
||||||
|
sudo chown -R root:root ./data
|
||||||
|
|
||||||
|
# Zeige aktuelle Berechtigungen
|
||||||
|
echo ""
|
||||||
|
echo "✅ Berechtigungen gesetzt!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Aktuelle Berechtigungen:"
|
||||||
|
ls -ld ./data
|
||||||
|
ls -lh ./data/prod.db 2>/dev/null || echo " (Datei nicht gefunden)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Teste im Container
|
||||||
|
echo "3️⃣ Teste Zugriff im Container..."
|
||||||
|
if docker ps | grep -q hoerdle; then
|
||||||
|
echo " Container läuft, teste Zugriff..."
|
||||||
|
docker exec hoerdle sh -c "test -r /app/data/prod.db && echo '✅ Lesbar' || echo '❌ Nicht lesbar'"
|
||||||
|
docker exec hoerdle sh -c "test -w /app/data/prod.db && echo '✅ Schreibbar' || echo '❌ Nicht schreibbar'"
|
||||||
|
docker exec hoerdle sh -c "test -w /app/data && echo '✅ Verzeichnis schreibbar' || echo '❌ Verzeichnis nicht schreibbar'"
|
||||||
|
else
|
||||||
|
echo " ⚠️ Container läuft nicht"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Starte Container neu..."
|
||||||
|
docker compose restart hoerdle 2>/dev/null || echo " ⚠️ Konnte Container nicht neustarten (vielleicht läuft docker compose nicht?)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Fertig! Prüfe jetzt die Logs:"
|
||||||
|
echo " docker logs hoerdle --tail=50"
|
||||||
|
|
||||||
Reference in New Issue
Block a user