Compare commits
21 Commits
e881979da3
...
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 |
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
|
||||||
|
|
||||||
@@ -62,10 +62,22 @@ Caddy und Hördle müssen im gleichen Docker-Netzwerk kommunizieren:
|
|||||||
# Prüfe, ob das Netzwerk bereits existiert
|
# Prüfe, ob das Netzwerk bereits existiert
|
||||||
docker network ls | grep hoerdle
|
docker network ls | grep hoerdle
|
||||||
|
|
||||||
# Falls nicht vorhanden, erstelle es
|
# 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
|
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
|
## Schritt 4: Caddy starten
|
||||||
|
|
||||||
### Option A: Mit docker-compose (Empfohlen)
|
### Option A: Mit docker-compose (Empfohlen)
|
||||||
@@ -198,8 +210,26 @@ openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de < /d
|
|||||||
2. Prüfe, ob hoerdle-Container läuft: `docker ps | grep hoerdle`
|
2. Prüfe, ob hoerdle-Container läuft: `docker ps | grep hoerdle`
|
||||||
3. Teste Verbindung von Caddy zu Hördle:
|
3. Teste Verbindung von Caddy zu Hördle:
|
||||||
```bash
|
```bash
|
||||||
docker exec hoerdle-caddy wget -O- http://hoerdle:3010/api/health
|
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
|
## Deployment-Workflow
|
||||||
|
|
||||||
|
|||||||
95
Caddyfile
95
Caddyfile
@@ -1,15 +1,12 @@
|
|||||||
# Caddy-Konfiguration für Hördle
|
# Caddy-Konfiguration für Hördle
|
||||||
# Wildcard-Zertifikate für hoerdle.de und hördle.de (xn--hrdle-jua.de)
|
# 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
|
||||||
# DNS-01 Challenge für Wildcard-Zertifikate (GoDaddy)
|
# Für Subdomains wären Wildcard-Zertifikate mit DNS-01 Challenge nötig
|
||||||
# Die DNS-TXT-Records müssen manuell in GoDaddy gesetzt werden (siehe CADDY_SETUP.md)
|
|
||||||
|
|
||||||
# Domain 1: hoerdle.de (ASCII)
|
# Domain 1: hoerdle.de (ASCII)
|
||||||
*.hoerdle.de, hoerdle.de {
|
hoerdle.de {
|
||||||
# TLS mit DNS-01 Challenge für Wildcard
|
# TLS mit automatischer HTTP-01 Challenge (funktioniert nur für Root-Domain)
|
||||||
tls {
|
# Caddy verwendet automatisch Let's Encrypt
|
||||||
dns godaddy {env.GODADDY_API_KEY} {env.GODADDY_API_SECRET}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Upload-Limit: 50MB (wie in nginx.conf.example)
|
# Upload-Limit: 50MB (wie in nginx.conf.example)
|
||||||
request_body {
|
request_body {
|
||||||
@@ -17,45 +14,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Reverse Proxy zu hoerdle Container
|
# Reverse Proxy zu hoerdle Container
|
||||||
reverse_proxy hoerdle:3010 {
|
reverse_proxy hoerdle:3000 {
|
||||||
# HTTP/1.1 für WebSocket Support
|
# HTTP/1.1 für WebSocket Support
|
||||||
transport http {
|
transport http {
|
||||||
versions 1.1
|
versions 1.1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Headers für Audio-Streaming (Range Requests)
|
|
||||||
header_up Range {http.request.header.Range}
|
|
||||||
header_up If-Range {http.request.header.If-Range}
|
|
||||||
|
|
||||||
# Kein Caching bei Range Requests
|
|
||||||
header_down Cache-Control "no-cache" {
|
|
||||||
when {http.request.header.Range}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Timeouts für große Dateien (300s wie in nginx)
|
|
||||||
timeout 300s
|
|
||||||
|
|
||||||
# WebSocket Support
|
|
||||||
header_up Connection {>Connection}
|
|
||||||
header_up Upgrade {>Upgrade}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Spezielle Behandlung für /uploads/ Route (Audio-Dateien)
|
|
||||||
handle /uploads/* {
|
|
||||||
reverse_proxy hoerdle:3010 {
|
|
||||||
# Range Requests für Audio-Streaming
|
|
||||||
header_up Range {http.request.header.Range}
|
|
||||||
header_up If-Range {http.request.header.If-Range}
|
|
||||||
|
|
||||||
# Buffering deaktivieren für große Dateien
|
|
||||||
# (Caddy hat kein explizites proxy_buffering, aber Timeouts setzen)
|
|
||||||
timeout 300s
|
|
||||||
|
|
||||||
# Kein Caching bei Range Requests
|
|
||||||
header_down Cache-Control "no-cache" {
|
|
||||||
when {http.request.header.Range}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP zu HTTPS Redirect
|
# HTTP zu HTTPS Redirect
|
||||||
@@ -66,11 +29,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Domain 2: hördle.de (Punycode: xn--hrdle-jua.de)
|
# Domain 2: hördle.de (Punycode: xn--hrdle-jua.de)
|
||||||
*.xn--hrdle-jua.de, xn--hrdle-jua.de {
|
xn--hrdle-jua.de {
|
||||||
# TLS mit DNS-01 Challenge für Wildcard
|
# TLS mit automatischer HTTP-01 Challenge (funktioniert nur für Root-Domain)
|
||||||
tls {
|
# Caddy verwendet automatisch Let's Encrypt
|
||||||
dns godaddy {env.GODADDY_API_KEY} {env.GODADDY_API_SECRET}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Upload-Limit: 50MB
|
# Upload-Limit: 50MB
|
||||||
request_body {
|
request_body {
|
||||||
@@ -78,44 +39,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Reverse Proxy zu hoerdle Container
|
# Reverse Proxy zu hoerdle Container
|
||||||
reverse_proxy hoerdle:3010 {
|
reverse_proxy hoerdle:3000 {
|
||||||
# HTTP/1.1 für WebSocket Support
|
# HTTP/1.1 für WebSocket Support
|
||||||
transport http {
|
transport http {
|
||||||
versions 1.1
|
versions 1.1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Headers für Audio-Streaming (Range Requests)
|
|
||||||
header_up Range {http.request.header.Range}
|
|
||||||
header_up If-Range {http.request.header.If-Range}
|
|
||||||
|
|
||||||
# Kein Caching bei Range Requests
|
|
||||||
header_down Cache-Control "no-cache" {
|
|
||||||
when {http.request.header.Range}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Timeouts für große Dateien (300s)
|
|
||||||
timeout 300s
|
|
||||||
|
|
||||||
# WebSocket Support
|
|
||||||
header_up Connection {>Connection}
|
|
||||||
header_up Upgrade {>Upgrade}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Spezielle Behandlung für /uploads/ Route (Audio-Dateien)
|
|
||||||
handle /uploads/* {
|
|
||||||
reverse_proxy hoerdle:3010 {
|
|
||||||
# Range Requests für Audio-Streaming
|
|
||||||
header_up Range {http.request.header.Range}
|
|
||||||
header_up If-Range {http.request.header.If-Range}
|
|
||||||
|
|
||||||
# Timeouts für große Dateien
|
|
||||||
timeout 300s
|
|
||||||
|
|
||||||
# Kein Caching bei Range Requests
|
|
||||||
header_down Cache-Control "no-cache" {
|
|
||||||
when {http.request.header.Range}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP zu HTTPS Redirect
|
# HTTP zu HTTPS Redirect
|
||||||
@@ -124,4 +52,3 @@
|
|||||||
}
|
}
|
||||||
redir @http https://{host}{uri} permanent
|
redir @http https://{host}{uri} permanent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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)"
|
||||||
|
|
||||||
|
|||||||
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';
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
# Starten: docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
|
# 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
|
# Stoppen: docker compose -f docker-compose.yml -f docker-compose.caddy.yml down
|
||||||
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
caddy:
|
caddy:
|
||||||
# Verwende Custom-Image mit GoDaddy DNS-Plugin
|
# Verwende Custom-Image mit GoDaddy DNS-Plugin
|
||||||
@@ -56,6 +54,6 @@ volumes:
|
|||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
# Netzwerk wird automatisch erstellt wenn beide Compose-Dateien zusammen verwendet werden
|
name: hoerdle_default
|
||||||
# Bei separater Verwendung: docker network create hoerdle_default und external: true setzen
|
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