Compare commits

...

32 Commits

Author SHA1 Message Date
Hördle Bot
ac0bb02ba0 Bump version to 0.1.3 2025-12-01 18:18:17 +01:00
Hördle Bot
63269c2600 Change: Standard-Sprache von Deutsch auf Englisch geändert
- defaultLocale in proxy.ts auf 'en' geändert
- Fallback in i18n/request.ts auf 'en' geändert
- Fallback-Reihenfolge in lib/i18n.ts angepasst (en vor de)
- Share-URL-Logik in Game.tsx angepasst
- Dokumentation aktualisiert
2025-12-01 18:18:11 +01:00
Hördle Bot
17a39d677d Update: package-lock.json aktualisiert für baseline-browser-mapping 2025-12-01 18:15:22 +01:00
Hördle Bot
1ff0787e4e Bump version to 0.1.2 2025-12-01 18:11:39 +01:00
Hördle Bot
ed5f02bdec Fix: 'Kurieren' zu 'Kuratieren' korrigiert im Admin-Dashboard 2025-12-01 18:11:24 +01:00
Hördle Bot
e3a09864a6 Refactor: Dokumentation nach docs/ verschoben
- Alle Markdown-Dateien (außer README.md) nach docs/ verschoben
- Referenzen in README.md aktualisiert
- /docs zu .dockerignore hinzugefügt
2025-12-01 17:58:32 +01:00
Hördle Bot
107739ade9 Fix: Update Dockerfile to optimize build process and reduce image size
- Refactored the Dockerfile to streamline the build process.
- Removed unnecessary layers and combined commands for efficiency.
- Improved caching strategy to enhance build performance.
2025-12-01 17:54:12 +01:00
Hördle Bot
e4eae67612 Bump version to 0.1.1 2025-12-01 17:43:40 +01:00
Hördle Bot
891f52b0b8 Fix: Versionsanzeige im Footer - Git-Tags während Docker-Build verfügbar machen und Fallbacks hinzufügen 2025-12-01 17:43:24 +01:00
Hördle Bot
725d3bcff4 Fix: Docker-Netzwerk als external markieren um Warnung zu beheben
- Setze external: true in docker-compose.example.yml für hoerdle_default Netzwerk
- Erweitere deploy.sh um Netzwerk-Prüfung
- Behebt Warnung über falsche Netzwerk-Labels
2025-12-01 17:34:26 +01:00
Hördle Bot
69f69cf172 Fix: Zentriere Genre-Navigation in der oberen Navigation
- Vereinfache Flexbox-Struktur für bessere Zentrierung
- LanguageSwitcher bleibt absolut positioniert rechts oben
- Entferne verschachtelte Container die Zentrierung störten
2025-12-01 17:31:55 +01:00
Hördle Bot
68c8f9a05a Add .dockerignore and Docker cleanup script to fix build space issues
- Add .dockerignore to exclude large upload files from Docker builds
- Add docker-cleanup.sh script to free up Docker disk space
- Add DOCKER_BUILD_FIX.md documentation for troubleshooting build issues

This prevents large MP3 files from being copied into the Docker image,
saving significant disk space during builds.
2025-12-01 17:24:14 +01:00
Hördle Bot
2b8733dea0 refactor: update layout and styling for upcoming specials and language switcher
- Adjusted the layout of the upcoming specials section for improved readability and alignment.
- Enhanced the styling of the language switcher for better visibility and accessibility.
- Simplified the structure of the tour genres section to improve overall user experience.
2025-12-01 17:15:03 +01:00
Hördle Bot
317eed5ea6 refactor: remove obsolete i18n fix scripts
- Deleted multiple outdated scripts for fixing internationalization data in the database, including various approaches (bash and Node.js).
- Consolidated functionality into more efficient and modern solutions, improving maintainability and reducing redundancy.
- Ensured that the remaining scripts are up-to-date with current database handling practices.
2025-12-01 17:11:03 +01:00
Hördle Bot
a503edb220 fix: enhance database script with improved permission handling and error checks
- Added permission setting for backup and temporary database files to ensure proper access.
- Implemented checks for file readability and ownership adjustments to enhance robustness.
- Included detailed SQL commands to fix internationalization data in the database, improving data integrity.
2025-12-01 17:06:07 +01:00
Hördle Bot
a80c14223b fix: improve database permissions script with enhanced logging and user feedback
- Added detailed logging to track changes and errors in the database permissions script.
- Implemented user feedback prompts to enhance usability and inform users of script progress.
- Strengthened error handling for ownership changes to ensure robustness.
2025-12-01 17:01:27 +01:00
Hördle Bot
8c9c4eb159 fix: enhance database permissions script with logging and user feedback
- Added logging functionality to the database permissions script to track changes and errors.
- Implemented user feedback prompts to inform users of the script's progress and outcomes, improving usability.
- Ensured the script maintains robust error handling for ownership changes.
2025-12-01 16:58:47 +01:00
Hördle Bot
68dfba38df fix: update database permissions script to set ownership to root
- Modify the script to explicitly set the owner of the database directory to root, ensuring proper access for SQLite.
- Include error handling to attempt ownership change with sudo if the initial command fails, improving robustness of the script.
2025-12-01 16:53:48 +01:00
Hördle Bot
b51ad2ff1a docs: add troubleshooting guide and fix permissions script for database issues
- Introduced a comprehensive troubleshooting guide for common application errors, particularly focusing on database permission issues after migrating databases.
- Added a script to automate the fixing of database permissions, ensuring SQLite can write to the necessary files and directories.
- Included detailed steps for diagnosing and resolving various container and database-related problems to enhance user support.
2025-12-01 16:50:24 +01:00
Hördle Bot
5613e5d48e docs: update Caddy setup documentation and clarify network warnings
- Corrected the container port for health check from 3010 to 3000 in Caddy setup instructions.
- Added a section addressing a harmless network warning during deployment, including an optional fix script for user convenience.
- Enhanced clarity and usability of the documentation for better user experience.
2025-12-01 16:48:04 +01:00
Hördle Bot
09b998ea75 fix: update reverse proxy port for hoerdle service in Caddyfile
- Change reverse proxy port from 3010 to 3000 for both hoerdle.de and xn--hrdle-jua.de
- Ensure consistency in service configuration across domains
2025-12-01 16:04:39 +01:00
Hördle Bot
74a8a59083 refactor: remove timeout settings for large files in Caddyfile
- Eliminate timeout configurations for large files in hoerdle.de and xn--hrdle-jua.de sections
- Streamline the Caddyfile for improved clarity and maintainability
2025-12-01 16:02:25 +01:00
Hördle Bot
f2c64281dd refactor: clean up Caddyfile by removing audio streaming headers and related configurations
- Eliminate unnecessary headers and caching settings for audio streaming
- Streamline the configuration for better readability and maintainability
- Maintain timeout settings for large files
2025-12-01 16:01:20 +01:00
Hördle Bot
ca40b1efb9 refactor: update Caddyfile for root-domain TLS configuration
- Simplify TLS setup for hoerdle.de and hördle.de by using automatic HTTP-01 challenge
- Remove wildcard certificate configuration and clarify usage for root-domains only
- Enhance comments for better understanding of the configuration
2025-12-01 15:59:58 +01:00
Hördle Bot
3c051ec49d refactor: simplify GoDaddy DNS configuration in Caddyfile
- Consolidate API key and secret syntax for GoDaddy DNS settings
- Improve readability by removing unnecessary nested structure
2025-12-01 15:58:00 +01:00
Hördle Bot
b268abb7d3 fix: correct environment variable syntax in Caddyfile and clean up Docker Compose configuration
- Update Caddyfile to use correct syntax for environment variables in GoDaddy DNS settings
- Remove unnecessary version declaration from docker-compose.caddy.yml for clarity
2025-12-01 15:50:54 +01:00
Hördle Bot
c7793dcb9d fix: update Caddyfile DNS configuration for GoDaddy API
- Refactor DNS settings to use named parameters for API key and secret
- Enhance readability and maintainability of the Caddyfile
2025-12-01 15:45:47 +01:00
Hördle Bot
95fd6405be docs: enhance Caddy setup documentation and update Docker Compose configurations
- Add instructions for handling existing Docker networks in Caddy setup
- Update docker-compose.caddy.yml to specify external network name
- Modify docker-compose.example.yml to include network configuration for the default network
2025-12-01 15:39:54 +01:00
Hördle Bot
e881979da3 chore: update .gitignore to include docker-compose.yml 2025-12-01 15:37:28 +01:00
Hördle Bot
8ec713297a chore: remove Docker Compose configuration for hoerdle service 2025-12-01 15:37:23 +01:00
Hördle Bot
4aef034aa6 feat: add Docker Compose configuration for hoerdle service
- Introduce docker-compose.yml to define the hoerdle service
- Configure build arguments, environment variables, and volume mappings
- Set up health checks and restart policy for the service
2025-12-01 15:34:08 +01:00
Hördle Bot
b120e5df45 chore: update .gitignore to include .env.example and enhance deployment documentation with Caddy reverse proxy setup 2025-12-01 15:19:19 +01:00
39 changed files with 2407 additions and 402 deletions

67
.dockerignore Normal file
View File

@@ -0,0 +1,67 @@
# 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
/docs
*.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
View 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
View File

@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel
@@ -50,3 +51,4 @@ next-env.d.ts
/data
.release-years-migrated
.covers-migrated
docker-compose.yml

54
Caddyfile Normal file
View 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
}

View File

@@ -23,11 +23,13 @@ RUN apk add --no-cache git
COPY --from=deps /app/node_modules ./node_modules
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 \
echo "$APP_VERSION" > /tmp/version.txt; \
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 && \
echo "Building version: $(cat /tmp/version.txt)"
@@ -36,6 +38,9 @@ RUN if [ -n "$APP_VERSION" ]; then \
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
# Suppress baseline-browser-mapping warning about old data (informational only)
ENV NODE_ENV=production
# Generate Prisma Client
ENV DATABASE_URL="file:./dev.db"
RUN node_modules/.bin/prisma generate

10
Dockerfile.caddy Normal file
View 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

View File

@@ -56,18 +56,18 @@ Eine Web-App inspiriert von Heardle, bei der Nutzer täglich einen Song anhand k
Hördle unterstützt vollständige Mehrsprachigkeit für Deutsch und Englisch.
👉 **[Vollständige i18n-Dokumentation](I18N.md)**
👉 **[Vollständige i18n-Dokumentation](docs/I18N.md)**
**Schnellstart:**
- Deutsche Version: `http://localhost:3000/de`
- Englische Version: `http://localhost:3000/en`
- Root (`/`) leitet automatisch zur Standardsprache (Deutsch) um
- Root (`/`) leitet automatisch zur Standardsprache (Englisch) um
## White Labeling
Hördle ist "White Label Ready". Das bedeutet, du kannst das Branding (Name, Farben, Logos) komplett anpassen, ohne den Code zu ändern.
👉 **[Anleitung zur Anpassung (White Label Guide)](WHITE_LABEL.md)**
👉 **[Anleitung zur Anpassung (White Label Guide)](docs/WHITE_LABEL.md)**
Die Konfiguration erfolgt einfach über Umgebungsvariablen und CSS-Variablen.
@@ -115,13 +115,13 @@ Das Ziel ist es, den Song mit so wenigen Hinweisen wie möglich zu erraten und d
```bash
npm run dev
```
Die App läuft unter `http://localhost:3000` (leitet automatisch zu `/de` um).
Die App läuft unter `http://localhost:3000` (leitet automatisch zu `/en` um).
## Deployment mit Docker
Das Projekt ist für den Betrieb mit Docker optimiert.
👉 **[White Labeling mit Docker? Hier klicken!](WHITE_LABEL.md#docker-deployment)**
👉 **[White Labeling mit Docker? Hier klicken!](docs/WHITE_LABEL.md#docker-deployment)**
1. **Vorbereitung:**
Kopiere die Beispiel-Konfiguration:

View File

@@ -45,86 +45,86 @@ export default async function Home({
return (
<>
<div id="tour-genres" style={{ textAlign: 'center', padding: '1rem', background: '#f3f4f6' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem', maxWidth: '1200px', margin: '0 auto', padding: '0 1rem' }}>
<div style={{ flex: 1 }} />
<div style={{ display: 'flex', justifyContent: 'center', gap: '1rem', flexWrap: 'wrap', alignItems: 'center', flex: 2 }}>
<div className="tooltip">
<Link href="/" style={{ fontWeight: 'bold', textDecoration: 'underline' }}>{tNav('global')}</Link>
<span className="tooltip-text">{t('globalTooltip')}</span>
</div>
<div id="tour-genres" style={{ textAlign: 'center', padding: '1rem', background: '#f3f4f6', position: 'relative' }}>
{/* Language Switcher - rechts oben */}
<div style={{ position: 'absolute', top: '1rem', right: '1rem', zIndex: 10 }}>
<LanguageSwitcher />
</div>
{/* Zentrierte Navigation */}
<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.map(g => {
const name = getLocalizedValue(g.name, locale);
const subtitle = getLocalizedValue(g.subtitle, locale);
return (
<div key={g.id} className="tooltip">
<Link href={`/${name}`} style={{ color: '#4b5563', textDecoration: 'none' }}>
{name}
{/* Genres */}
{genres.map(g => {
const name = getLocalizedValue(g.name, locale);
const subtitle = getLocalizedValue(g.subtitle, locale);
return (
<div key={g.id} className="tooltip">
<Link href={`/${name}`} style={{ color: '#4b5563', textDecoration: 'none' }}>
{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>
{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 => {
{s.curator && (
<span style={{ fontSize: '0.75rem', color: '#666' }}>
{t('curatedBy')} {s.curator}
</span>
)}
</div>
);
})}
</div>
{/* 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 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>
{subtitle && <span className="tooltip-text">{subtitle}</span>}
</div>
{s.curator && (
<span style={{ fontSize: '0.75rem', color: '#666' }}>
{t('curatedBy')} {s.curator}
</span>
)}
</div>
<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>
{/* 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 id="tour-news">

View File

@@ -15,7 +15,7 @@ export async function GET() {
for (const versionFilePath of versionPaths) {
if (existsSync(versionFilePath)) {
const version = readFileSync(versionFilePath, 'utf-8').trim();
if (version && version !== 'unknown') {
if (version && version !== 'unknown' && version !== '') {
return NextResponse.json({ version });
}
}
@@ -26,6 +26,19 @@ export async function GET() {
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)
let version = 'dev';

View File

@@ -275,8 +275,8 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
const genreText = genre ? `${isSpecial ? t('special') : t('genre')}: ${genre}\n` : '';
let shareUrl = `https://${config.domain}`;
// Add locale prefix if not default (de)
if (locale !== 'de') {
// Add locale prefix if not default (en)
if (locale !== 'en') {
shareUrl += `/${locale}`;
}
if (genre) {

59
docker-compose.caddy.yml Normal file
View 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

View File

@@ -37,4 +37,11 @@ services:
timeout: 10s
retries: 3
start_period: 40s
networks:
- default
# docker-entrypoint.sh handles migrations and server startup (with baseline fallback)
networks:
default:
name: hoerdle_default
external: true

289
docs/CADDY_SETUP.md Normal file
View 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)

View File

@@ -0,0 +1,183 @@
# Caddy Zertifikat-Troubleshooting
## Problem: Zertifikat für Punycode-Domain (hördle.de / xn--hrdle-jua.de) fehlt
Wenn die Domain `hördle.de` (xn--hrdle-jua.de) einen `ERR_SSL_PROTOCOL_ERROR` zeigt, bedeutet das, dass kein gültiges SSL-Zertifikat vorhanden ist.
### Schritt 1: Zertifikat-Status prüfen
Führe das Check-Script aus:
```bash
./scripts/check-caddy-certificates.sh
```
Dieses Script prüft:
- Ob Caddy läuft
- Welche Zertifikate vorhanden sind
- Ob die DNS-Einträge korrekt sind
- Ob die HTTPS-Verbindungen funktionieren
### Schritt 2: DNS-Einträge prüfen
**Wichtig**: Beide Domains müssen auf die gleiche Server-IP zeigen!
#### In GoDaddy prüfen:
1. Gehe zu [GoDaddy DNS-Verwaltung](https://dcc.godaddy.com/manage/hoerdle.de/dns)
2. Prüfe die A-Records:
**Für hoerdle.de:**
- Name: `@` oder `hoerdle.de`
- Typ: `A`
- Wert: `DEINE_SERVER_IP`
**Für hördle.de (Punycode):**
- Name: `@` oder `xn--hrdle-jua.de` (oder der Unicode-Name, falls unterstützt)
- Typ: `A`
- Wert: **GLEICHE_SERVER_IP wie hoerdle.de**
#### DNS manuell testen:
```bash
# Prüfe hoerdle.de
dig +short hoerdle.de @8.8.8.8
# Prüfe xn--hrdle-jua.de (Punycode)
dig +short xn--hrdle-jua.de @8.8.8.8
# Beide sollten die gleiche IP zurückgeben!
```
### Schritt 3: Zertifikat neu erstellen
Wenn die DNS-Einträge korrekt sind, lösche das alte (fehlgeschlagene) Zertifikat und lass Caddy es neu erstellen:
```bash
./scripts/renew-caddy-certificates.sh
```
Wähle Option 2: "Nur Zertifikat für xn--hrdle-jua.de löschen"
### Schritt 4: Caddy-Logs überwachen
Während Caddy das Zertifikat erstellt, überwache die Logs:
```bash
docker logs hoerdle-caddy -f
```
Du solltest sehen:
- `[INFO] attempting ACME challenge` - Caddy versucht die Challenge
- `[INFO] successfully completed ACME challenge` - Challenge erfolgreich
- `[INFO] certificate obtained successfully` - Zertifikat erstellt
Bei Fehlern siehst du:
- `[ERROR] acme: error` - Challenge fehlgeschlagen
- `[ERROR] unable to validate` - Validierung fehlgeschlagen
### Schritt 5: Häufige Probleme und Lösungen
#### Problem 1: DNS zeigt auf falsche IP
**Symptom**: `dig` zeigt eine andere IP als erwartet
**Lösung**:
1. Prüfe DNS-Einträge in GoDaddy
2. Warte auf DNS-Propagierung (kann 5-60 Minuten dauern)
3. Verwende einen DNS-Checker: https://www.whatsmydns.net/
#### Problem 2: Port 80 nicht erreichbar
**Symptom**: Caddy-Logs zeigen "connection refused" oder Timeout
**Lösung**:
1. Prüfe Firewall: `sudo ufw status`
2. Prüfe ob Port 80 offen ist: `sudo netstat -tulpn | grep :80`
3. Prüfe ob Caddy auf Port 80 lauscht: `docker exec hoerdle-caddy netstat -tulpn | grep :80`
#### Problem 3: Let's Encrypt Rate Limit
**Symptom**: Logs zeigen "too many certificates already issued"
**Lösung**:
- Warte 1 Woche (Rate Limit von Let's Encrypt)
- Oder verwende Staging-Environment zum Testen:
```caddyfile
tls {
staging
}
```
#### Problem 4: Punycode-Domain wird nicht erkannt
**Symptom**: Caddy erstellt Zertifikat nur für hoerdle.de, nicht für xn--hrdle-jua.de
**Lösung**:
1. Prüfe ob beide Domains in der Caddyfile stehen
2. Prüfe DNS-Einträge (siehe Schritt 2)
3. Erzwinge Zertifikat-Erstellung (siehe Schritt 3)
### Manuelle Zertifikat-Löschung
Falls das Script nicht funktioniert, kannst du Zertifikate manuell löschen:
```bash
# Alle Zertifikate löschen
docker exec hoerdle-caddy rm -rf /data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/*
# Nur Punycode-Zertifikat löschen (manuell)
docker exec hoerdle-caddy find /data/caddy/certificates -name "*xn--*" -delete
# Container neu starten
docker compose -f docker-compose.caddy.yml --profile production restart caddy
```
### DNS-Propagierung prüfen
Nach DNS-Änderungen kann es bis zu 60 Minuten dauern, bis alle DNS-Server aktualisiert sind:
```bash
# Prüfe DNS-Propagierung weltweit
curl "https://dnschecker.org/#A/hoerdle.de"
curl "https://dnschecker.org/#A/xn--hrdle-jua.de"
```
### Test-Zertifikat erstellen (Staging)
Zum Testen ohne Rate-Limits kannst du ein Staging-Zertifikat erstellen:
1. Temporär Caddyfile ändern (in beiden Domain-Blocks):
```caddyfile
tls {
staging
}
```
2. Container neu starten
3. Zertifikat erstellen lassen
4. Zurück zu Produktion ändern (Staging-Block entfernen)
5. Erneut Container neu starten
### Verifizieren, dass es funktioniert
Nach erfolgreicher Zertifikats-Erstellung:
```bash
# Teste HTTPS-Verbindung
curl -I https://hoerdle.de
curl -I https://xn--hrdle-jua.de
# Prüfe Zertifikat-Details
echo | openssl s_client -connect hoerdle.de:443 -servername hoerdle.de 2>/dev/null | openssl x509 -noout -subject -dates
echo | openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de 2>/dev/null | openssl x509 -noout -subject -dates
```
### Support
Falls das Problem weiterhin besteht:
1. Prüfe Caddy-Logs: `docker logs hoerdle-caddy`
2. Prüfe DNS: `dig +short xn--hrdle-jua.de @8.8.8.8`
3. Prüfe Firewall: `sudo ufw status`
4. Prüfe Port-Zugriff: `curl -I http://hoerdle.de`

View File

@@ -82,3 +82,35 @@ docker ps
```
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
docs/DOCKER_BUILD_FIX.md Normal file
View 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
```

83
docs/FIX_I18N.md Normal file
View 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
```

View File

@@ -8,14 +8,14 @@ Die i18n-Implementierung basiert auf [next-intl](https://next-intl-docs.vercel.a
## Unterstützte Sprachen
- **Deutsch (de)** - Standardsprache
- **Englisch (en)**
- **Englisch (en)** - Standardsprache
- **Deutsch (de)**
## URL-Struktur
Alle Routen sind lokalisiert:
- `http://localhost:3000/` → Redirect zu `/de` (Standard)
- `http://localhost:3000/` → Redirect zu `/en` (Standard)
- `http://localhost:3000/de` → Deutsche Version
- `http://localhost:3000/en` → Englische Version
- `http://localhost:3000/de/admin` → Admin-Dashboard (Deutsch)
@@ -103,8 +103,8 @@ const genreNameEn = getLocalizedValue(genre.name, 'en'); // "Rock"
**Fallback-Verhalten:**
1. Versucht die angeforderte Locale (`de` oder `en`)
2. Fallback zu `de` falls nicht vorhanden
3. Fallback zu `en` falls nicht vorhanden
2. Fallback zu `en` falls nicht vorhanden
3. Fallback zu `de` falls nicht vorhanden
4. Fallback zum ersten verfügbaren Schlüssel
5. Fallback zum übergebenen `fallback`-Parameter
@@ -195,7 +195,7 @@ Bestehende Daten werden automatisch migriert:
Der Proxy (`proxy.ts`) leitet Anfragen automatisch um:
- `/``/de` (Standard)
- `/``/en` (Standard)
- Ungültige Locales → 404
- Validiert Locale-Parameter
@@ -223,7 +223,7 @@ GET /api/specials?locale=en
GET /api/news?locale=de
```
Falls kein `locale` angegeben wird, wird `de` als Standard verwendet.
Falls kein `locale` angegeben wird, wird `en` als Standard verwendet.
## Best Practices

206
docs/TROUBLESHOOTING.md Normal file
View 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.

View File

@@ -9,7 +9,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
console.log('[i18n/request] incoming requestLocale:', locale);
if (!locale || !locales.includes(locale as (typeof locales)[number])) {
locale = 'de';
locale = 'en';
console.log('[i18n/request] falling back to default locale:', locale);
}

View File

@@ -16,12 +16,12 @@ export function getLocalizedValue(
if (typeof value === 'object') {
if (value[locale]) return value[locale];
// Fallback to 'de'
if (value['de']) return value['de'];
// Fallback to 'en'
if (value['en']) return value['en'];
// Fallback to 'de'
if (value['de']) return value['de'];
// Fallback to first key
const keys = Object.keys(value);
if (keys.length > 0) return value[keys[0]];

View File

@@ -1,156 +1,156 @@
{
"Common": {
"loading": "Laden...",
"error": "Ein Fehler ist aufgetreten",
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"back": "Zurück"
},
"Navigation": {
"home": "Startseite",
"admin": "Admin",
"global": "Global",
"news": "Neuigkeiten"
},
"Game": {
"play": "Abspielen",
"pause": "Pause",
"skip": "Überspringen",
"submit": "Raten",
"next": "Nächstes",
"won": "Gewonnen!",
"lost": "Verloren",
"correct": "Richtig!",
"wrong": "Falsch",
"guessPlaceholder": "Lied oder Interpret eingeben...",
"attempts": "Versuche",
"share": "Teilen",
"nextPuzzle": "Nächstes Rätsel in",
"noPuzzleAvailable": "Kein Rätsel verfügbar",
"noPuzzleDescription": "Tägliches Rätsel konnte nicht generiert werden.",
"noPuzzleGenre": "Bitte stelle sicher, dass Songs in der Datenbank vorhanden sind",
"goToAdmin": "Zum Admin-Dashboard gehen",
"loadingState": "Lade Status...",
"attempt": "Versuch",
"unlocked": "freigeschaltet",
"start": "Start",
"skipWithBonus": "Überspringen (+{seconds}s)",
"solveGiveUp": "Lösen (Aufgeben)",
"comeBackTomorrow": "Komm morgen zurück für ein neues Lied.",
"theSongWas": "Das Lied war:",
"score": "Punkte",
"scoreBreakdown": "Punkteaufschlüsselung",
"albumCover": "Album-Cover",
"released": "Veröffentlicht",
"yourBrowserDoesNotSupport": "Ihr Browser unterstützt das Audio-Element nicht.",
"thanksForRating": "Danke für die Bewertung!",
"rateThisPuzzle": "Bewerte dieses Rätsel:",
"shared": "✓ Geteilt!",
"copied": "✓ Kopiert!",
"shareFailed": "✗ Fehlgeschlagen",
"bonusRound": "Bonus-Runde!",
"guessReleaseYear": "Errate das Veröffentlichungsjahr für",
"points": "Punkte",
"skipBonus": "Bonus überspringen",
"notQuite": "Nicht ganz!",
"youGuessed": "Du hast geraten",
"actuallyReleasedIn": "Tatsächlich veröffentlicht in",
"skipped": "Übersprungen",
"gameOverPlaceholder": "Spiel beendet",
"knowItSearch": "Weißt du es? Suche nach Interpret / Titel",
"special": "Special",
"genre": "Genre"
},
"Statistics": {
"yourStatistics": "Deine Statistiken",
"totalPuzzles": "Gesamte Rätsel",
"try": "Versuch",
"failed": "Verloren"
},
"OnboardingTour": {
"done": "Fertig",
"next": "Weiter",
"previous": "Zurück",
"genresSpecials": "Genres & Specials",
"genresSpecialsDescription": "Wähle hier ein bestimmtes Genre oder ein kuratiertes Special-Event.",
"news": "Neuigkeiten",
"newsDescription": "Bleibe auf dem Laufenden mit den neuesten Nachrichten und Ankündigungen.",
"hoerdle": "Hördle",
"hoerdleDescription": "Das ist das tägliche Rätsel. Ein neues Lied jeden Tag pro Genre.",
"attempts": "Versuche",
"attemptsDescription": "Du hast eine begrenzte Anzahl von Versuchen, um das Lied zu erraten.",
"score": "Punkte",
"scoreDescription": "Deine aktuelle Punktzahl. Versuche sie hoch zu halten!",
"player": "Player",
"playerDescription": "Höre dir den Ausschnitt an. Jedes zusätzliche Abspielen reduziert deine mögliche Punktzahl.",
"input": "Eingabe",
"inputDescription": "Gib hier deine Vermutung ein. Suche nach Interpret oder Titel.",
"controls": "Steuerung",
"controlsDescription": "Starte die Musik oder überspringe zum nächsten Ausschnitt, wenn du feststeckst."
},
"InstallPrompt": {
"installApp": "Hördle App installieren",
"installDescription": "Installiere die App für eine bessere Erfahrung und schnellen Zugriff!",
"iosInstructions": "Tippe auf",
"iosShare": "Teilen",
"iosThen": "dann \"Zum Home-Bildschirm hinzufügen\"",
"installButton": "App installieren"
},
"Home": {
"welcome": "Willkommen bei Hördle",
"subtitle": "Errate den Song anhand kurzer Ausschnitte",
"globalTooltip": "Ein zufälliger Song aus der gesamten Sammlung",
"comingSoon": "Demnächst",
"curatedBy": "Kuratiert von"
},
"Admin": {
"title": "Hördle Admin Dashboard",
"login": "Admin Login",
"password": "Passwort",
"loginButton": "Login",
"logout": "Abmelden",
"manageSpecials": "Specials verwalten",
"manageGenres": "Genres verwalten",
"manageNews": "News & Ankündigungen verwalten",
"uploadSongs": "Songs hochladen",
"todaysPuzzles": "Heutige tägliche Rätsel",
"show": "▶ Anzeigen",
"hide": "▼ Ausblenden",
"addSpecial": "Special hinzufügen",
"addGenre": "Genre hinzufügen",
"addNews": "News hinzufügen",
"edit": "Bearbeiten",
"delete": "Löschen",
"save": "Speichern",
"cancel": "Abbrechen",
"curate": "Kurieren",
"name": "Name",
"subtitle": "Untertitel",
"maxAttempts": "Max. Versuche",
"unlockSteps": "Freischalt-Schritte",
"launchDate": "Startdatum",
"endDate": "Enddatum",
"curator": "Kurator",
"active": "Aktiv",
"newGenreName": "Neuer Genre-Name",
"editSpecial": "Special bearbeiten",
"editGenre": "Genre bearbeiten",
"editNews": "News bearbeiten",
"newsTitle": "News-Titel",
"content": "Inhalt (Markdown unterstützt)",
"author": "Autor (optional)",
"featured": "Hervorgehoben",
"noSpecialLink": "Kein Special-Link",
"noNewsItems": "Noch keine News-Einträge. Erstelle einen oben!",
"noPuzzlesToday": "Keine täglichen Rätsel für heute gefunden.",
"category": "Kategorie",
"song": "Song",
"artist": "Interpret",
"actions": "Aktionen",
"deletePuzzle": "Löschen",
"wrongPassword": "Falsches Passwort"
"Common": {
"loading": "Laden...",
"error": "Ein Fehler ist aufgetreten",
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"back": "Zurück"
},
"Navigation": {
"home": "Startseite",
"admin": "Admin",
"global": "Global",
"news": "Neuigkeiten"
},
"Game": {
"play": "Abspielen",
"pause": "Pause",
"skip": "Überspringen",
"submit": "Raten",
"next": "Nächstes",
"won": "Gewonnen!",
"lost": "Verloren",
"correct": "Richtig!",
"wrong": "Falsch",
"guessPlaceholder": "Lied oder Interpret eingeben...",
"attempts": "Versuche",
"share": "Teilen",
"nextPuzzle": "Nächstes Rätsel in",
"noPuzzleAvailable": "Kein Rätsel verfügbar",
"noPuzzleDescription": "Tägliches Rätsel konnte nicht generiert werden.",
"noPuzzleGenre": "Bitte stelle sicher, dass Songs in der Datenbank vorhanden sind",
"goToAdmin": "Zum Admin-Dashboard gehen",
"loadingState": "Lade Status...",
"attempt": "Versuch",
"unlocked": "freigeschaltet",
"start": "Start",
"skipWithBonus": "Überspringen (+{seconds}s)",
"solveGiveUp": "Lösen (Aufgeben)",
"comeBackTomorrow": "Komm morgen zurück für ein neues Lied.",
"theSongWas": "Das Lied war:",
"score": "Punkte",
"scoreBreakdown": "Punkteaufschlüsselung",
"albumCover": "Album-Cover",
"released": "Veröffentlicht",
"yourBrowserDoesNotSupport": "Ihr Browser unterstützt das Audio-Element nicht.",
"thanksForRating": "Danke für die Bewertung!",
"rateThisPuzzle": "Bewerte dieses Rätsel:",
"shared": "✓ Geteilt!",
"copied": "✓ Kopiert!",
"shareFailed": "✗ Fehlgeschlagen",
"bonusRound": "Bonus-Runde!",
"guessReleaseYear": "Errate das Veröffentlichungsjahr für",
"points": "Punkte",
"skipBonus": "Bonus überspringen",
"notQuite": "Nicht ganz!",
"youGuessed": "Du hast geraten",
"actuallyReleasedIn": "Tatsächlich veröffentlicht in",
"skipped": "Übersprungen",
"gameOverPlaceholder": "Spiel beendet",
"knowItSearch": "Weißt du es? Suche nach Interpret / Titel",
"special": "Special",
"genre": "Genre"
},
"Statistics": {
"yourStatistics": "Deine Statistiken",
"totalPuzzles": "Gesamte Rätsel",
"try": "Versuch",
"failed": "Verloren"
},
"OnboardingTour": {
"done": "Fertig",
"next": "Weiter",
"previous": "Zurück",
"genresSpecials": "Genres & Specials",
"genresSpecialsDescription": "Wähle hier ein bestimmtes Genre oder ein kuratiertes Special-Event.",
"news": "Neuigkeiten",
"newsDescription": "Bleibe auf dem Laufenden mit den neuesten Nachrichten und Ankündigungen.",
"hoerdle": "Hördle",
"hoerdleDescription": "Das ist das tägliche Rätsel. Ein neues Lied jeden Tag pro Genre.",
"attempts": "Versuche",
"attemptsDescription": "Du hast eine begrenzte Anzahl von Versuchen, um das Lied zu erraten.",
"score": "Punkte",
"scoreDescription": "Deine aktuelle Punktzahl. Versuche sie hoch zu halten!",
"player": "Player",
"playerDescription": "Höre dir den Ausschnitt an. Jedes zusätzliche Abspielen reduziert deine mögliche Punktzahl.",
"input": "Eingabe",
"inputDescription": "Gib hier deine Vermutung ein. Suche nach Interpret oder Titel.",
"controls": "Steuerung",
"controlsDescription": "Starte die Musik oder überspringe zum nächsten Ausschnitt, wenn du feststeckst."
},
"InstallPrompt": {
"installApp": "Hördle App installieren",
"installDescription": "Installiere die App für eine bessere Erfahrung und schnellen Zugriff!",
"iosInstructions": "Tippe auf",
"iosShare": "Teilen",
"iosThen": "dann \"Zum Home-Bildschirm hinzufügen\"",
"installButton": "App installieren"
},
"Home": {
"welcome": "Willkommen bei Hördle",
"subtitle": "Errate den Song anhand kurzer Ausschnitte",
"globalTooltip": "Ein zufälliger Song aus der gesamten Sammlung",
"comingSoon": "Demnächst",
"curatedBy": "Kuratiert von"
},
"Admin": {
"title": "Hördle Admin Dashboard",
"login": "Admin Login",
"password": "Passwort",
"loginButton": "Login",
"logout": "Abmelden",
"manageSpecials": "Specials verwalten",
"manageGenres": "Genres verwalten",
"manageNews": "News & Ankündigungen verwalten",
"uploadSongs": "Songs hochladen",
"todaysPuzzles": "Heutige tägliche Rätsel",
"show": "▶ Anzeigen",
"hide": "▼ Ausblenden",
"addSpecial": "Special hinzufügen",
"addGenre": "Genre hinzufügen",
"addNews": "News hinzufügen",
"edit": "Bearbeiten",
"delete": "Löschen",
"save": "Speichern",
"cancel": "Abbrechen",
"curate": "Kuratieren",
"name": "Name",
"subtitle": "Untertitel",
"maxAttempts": "Max. Versuche",
"unlockSteps": "Freischalt-Schritte",
"launchDate": "Startdatum",
"endDate": "Enddatum",
"curator": "Kurator",
"active": "Aktiv",
"newGenreName": "Neuer Genre-Name",
"editSpecial": "Special bearbeiten",
"editGenre": "Genre bearbeiten",
"editNews": "News bearbeiten",
"newsTitle": "News-Titel",
"content": "Inhalt (Markdown unterstützt)",
"author": "Autor (optional)",
"featured": "Hervorgehoben",
"noSpecialLink": "Kein Special-Link",
"noNewsItems": "Noch keine News-Einträge. Erstelle einen oben!",
"noPuzzlesToday": "Keine täglichen Rätsel für heute gefunden.",
"category": "Kategorie",
"song": "Song",
"artist": "Interpret",
"actions": "Aktionen",
"deletePuzzle": "Löschen",
"wrongPassword": "Falsches Passwort"
},
"About": {
"title": "Über Hördle & Impressum",

View File

@@ -1,156 +1,156 @@
{
"Common": {
"loading": "Loading...",
"error": "An error occurred",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"back": "Back"
},
"Navigation": {
"home": "Home",
"admin": "Admin",
"global": "Global",
"news": "News"
},
"Game": {
"play": "Play",
"pause": "Pause",
"skip": "Skip",
"submit": "Guess",
"next": "Next",
"won": "You won!",
"lost": "Game Over",
"correct": "Correct!",
"wrong": "Wrong",
"guessPlaceholder": "Type song or artist...",
"attempts": "Attempts",
"share": "Share",
"nextPuzzle": "Next puzzle in",
"noPuzzleAvailable": "No Puzzle Available",
"noPuzzleDescription": "Could not generate a daily puzzle.",
"noPuzzleGenre": "Please ensure there are songs in the database",
"goToAdmin": "Go to Admin Dashboard",
"loadingState": "Loading state...",
"attempt": "Attempt",
"unlocked": "unlocked",
"start": "Start",
"skipWithBonus": "Skip (+{seconds}s)",
"solveGiveUp": "Solve (Give Up)",
"comeBackTomorrow": "Come back tomorrow for a new song.",
"theSongWas": "The song was:",
"score": "Score",
"scoreBreakdown": "Score Breakdown",
"albumCover": "Album Cover",
"released": "Released",
"yourBrowserDoesNotSupport": "Your browser does not support the audio element.",
"thanksForRating": "Thanks for rating!",
"rateThisPuzzle": "Rate this puzzle:",
"shared": "✓ Shared!",
"copied": "✓ Copied!",
"shareFailed": "✗ Failed",
"bonusRound": "Bonus Round!",
"guessReleaseYear": "Guess the release year for",
"points": "points",
"skipBonus": "Skip Bonus",
"notQuite": "Not quite!",
"youGuessed": "You guessed",
"actuallyReleasedIn": "Actually released in",
"skipped": "Skipped",
"gameOverPlaceholder": "Game Over",
"knowItSearch": "Know it? Search for the artist / title",
"special": "Special",
"genre": "Genre"
},
"Statistics": {
"yourStatistics": "Your Statistics",
"totalPuzzles": "Total puzzles",
"try": "try",
"failed": "Failed"
},
"OnboardingTour": {
"done": "Done",
"next": "Next",
"previous": "Previous",
"genresSpecials": "Genres & Specials",
"genresSpecialsDescription": "Choose a specific genre or a curated special event here.",
"news": "News",
"newsDescription": "Stay updated with the latest news and announcements.",
"hoerdle": "Hördle",
"hoerdleDescription": "This is the daily puzzle. One new song every day per genre.",
"attempts": "Attempts",
"attemptsDescription": "You have a limited number of attempts to guess the song.",
"score": "Score",
"scoreDescription": "Your current score. Try to keep it high!",
"player": "Player",
"playerDescription": "Listen to the snippet. Each additional play reduces your potential score.",
"input": "Input",
"inputDescription": "Type your guess here. Search for artist or title.",
"controls": "Controls",
"controlsDescription": "Start the music or skip to the next snippet if you're stuck."
},
"InstallPrompt": {
"installApp": "Install Hördle App",
"installDescription": "Install the app for a better experience and quick access!",
"iosInstructions": "Tap",
"iosShare": "share",
"iosThen": "then \"Add to Home Screen\"",
"installButton": "Install App"
},
"Home": {
"welcome": "Welcome to Hördle",
"subtitle": "Guess the song from short snippets",
"globalTooltip": "A random song from the entire collection",
"comingSoon": "Coming soon",
"curatedBy": "Curated by"
},
"Admin": {
"title": "Hördle Admin Dashboard",
"login": "Admin Login",
"password": "Password",
"loginButton": "Login",
"logout": "Logout",
"manageSpecials": "Manage Specials",
"manageGenres": "Manage Genres",
"manageNews": "Manage News & Announcements",
"uploadSongs": "Upload Songs",
"todaysPuzzles": "Today's Daily Puzzles",
"show": "▶ Show",
"hide": "▼ Hide",
"addSpecial": "Add Special",
"addGenre": "Add Genre",
"addNews": "Add News",
"edit": "Edit",
"delete": "Delete",
"save": "Save",
"cancel": "Cancel",
"curate": "Curate",
"name": "Name",
"subtitle": "Subtitle",
"maxAttempts": "Max Attempts",
"unlockSteps": "Unlock Steps",
"launchDate": "Launch Date",
"endDate": "End Date",
"curator": "Curator",
"active": "Active",
"newGenreName": "New Genre Name",
"editSpecial": "Edit Special",
"editGenre": "Edit Genre",
"editNews": "Edit News",
"newsTitle": "News Title",
"content": "Content (Markdown supported)",
"author": "Author (optional)",
"featured": "Featured",
"noSpecialLink": "No Special Link",
"noNewsItems": "No news items yet. Create one above!",
"noPuzzlesToday": "No daily puzzles found for today.",
"category": "Category",
"song": "Song",
"artist": "Artist",
"actions": "Actions",
"deletePuzzle": "Delete",
"wrongPassword": "Wrong password"
"Common": {
"loading": "Loading...",
"error": "An error occurred",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"back": "Back"
},
"Navigation": {
"home": "Home",
"admin": "Admin",
"global": "Global",
"news": "News"
},
"Game": {
"play": "Play",
"pause": "Pause",
"skip": "Skip",
"submit": "Guess",
"next": "Next",
"won": "You won!",
"lost": "Game Over",
"correct": "Correct!",
"wrong": "Wrong",
"guessPlaceholder": "Type song or artist...",
"attempts": "Attempts",
"share": "Share",
"nextPuzzle": "Next puzzle in",
"noPuzzleAvailable": "No Puzzle Available",
"noPuzzleDescription": "Could not generate a daily puzzle.",
"noPuzzleGenre": "Please ensure there are songs in the database",
"goToAdmin": "Go to Admin Dashboard",
"loadingState": "Loading state...",
"attempt": "Attempt",
"unlocked": "unlocked",
"start": "Start",
"skipWithBonus": "Skip (+{seconds}s)",
"solveGiveUp": "Solve (Give Up)",
"comeBackTomorrow": "Come back tomorrow for a new song.",
"theSongWas": "The song was:",
"score": "Score",
"scoreBreakdown": "Score Breakdown",
"albumCover": "Album Cover",
"released": "Released",
"yourBrowserDoesNotSupport": "Your browser does not support the audio element.",
"thanksForRating": "Thanks for rating!",
"rateThisPuzzle": "Rate this puzzle:",
"shared": "✓ Shared!",
"copied": "✓ Copied!",
"shareFailed": "✗ Failed",
"bonusRound": "Bonus Round!",
"guessReleaseYear": "Guess the release year for",
"points": "points",
"skipBonus": "Skip Bonus",
"notQuite": "Not quite!",
"youGuessed": "You guessed",
"actuallyReleasedIn": "Actually released in",
"skipped": "Skipped",
"gameOverPlaceholder": "Game Over",
"knowItSearch": "Know it? Search for the artist / title",
"special": "Special",
"genre": "Genre"
},
"Statistics": {
"yourStatistics": "Your Statistics",
"totalPuzzles": "Total puzzles",
"try": "try",
"failed": "Failed"
},
"OnboardingTour": {
"done": "Done",
"next": "Next",
"previous": "Previous",
"genresSpecials": "Genres & Specials",
"genresSpecialsDescription": "Choose a specific genre or a curated special event here.",
"news": "News",
"newsDescription": "Stay updated with the latest news and announcements.",
"hoerdle": "Hördle",
"hoerdleDescription": "This is the daily puzzle. One new song every day per genre.",
"attempts": "Attempts",
"attemptsDescription": "You have a limited number of attempts to guess the song.",
"score": "Score",
"scoreDescription": "Your current score. Try to keep it high!",
"player": "Player",
"playerDescription": "Listen to the snippet. Each additional play reduces your potential score.",
"input": "Input",
"inputDescription": "Type your guess here. Search for artist or title.",
"controls": "Controls",
"controlsDescription": "Start the music or skip to the next snippet if you're stuck."
},
"InstallPrompt": {
"installApp": "Install Hördle App",
"installDescription": "Install the app for a better experience and quick access!",
"iosInstructions": "Tap",
"iosShare": "share",
"iosThen": "then \"Add to Home Screen\"",
"installButton": "Install App"
},
"Home": {
"welcome": "Welcome to Hördle",
"subtitle": "Guess the song from short snippets",
"globalTooltip": "A random song from the entire collection",
"comingSoon": "Coming soon",
"curatedBy": "Curated by"
},
"Admin": {
"title": "Hördle Admin Dashboard",
"login": "Admin Login",
"password": "Password",
"loginButton": "Login",
"logout": "Logout",
"manageSpecials": "Manage Specials",
"manageGenres": "Manage Genres",
"manageNews": "Manage News & Announcements",
"uploadSongs": "Upload Songs",
"todaysPuzzles": "Today's Daily Puzzles",
"show": "▶ Show",
"hide": "▼ Hide",
"addSpecial": "Add Special",
"addGenre": "Add Genre",
"addNews": "Add News",
"edit": "Edit",
"delete": "Delete",
"save": "Save",
"cancel": "Cancel",
"curate": "Curate",
"name": "Name",
"subtitle": "Subtitle",
"maxAttempts": "Max Attempts",
"unlockSteps": "Unlock Steps",
"launchDate": "Launch Date",
"endDate": "End Date",
"curator": "Curator",
"active": "Active",
"newGenreName": "New Genre Name",
"editSpecial": "Edit Special",
"editGenre": "Edit Genre",
"editNews": "Edit News",
"newsTitle": "News Title",
"content": "Content (Markdown supported)",
"author": "Author (optional)",
"featured": "Featured",
"noSpecialLink": "No Special Link",
"noNewsItems": "No news items yet. Create one above!",
"noPuzzlesToday": "No daily puzzles found for today.",
"category": "Category",
"song": "Song",
"artist": "Artist",
"actions": "Actions",
"deletePuzzle": "Delete",
"wrongPassword": "Wrong password"
},
"About": {
"title": "About Hördle & Imprint",

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "hoerdle",
"version": "0.1.0.15",
"version": "0.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hoerdle",
"version": "0.1.0.15",
"version": "0.1.2",
"dependencies": {
"@prisma/client": "^6.19.0",
"bcryptjs": "^3.0.3",

View File

@@ -1,6 +1,6 @@
{
"name": "hoerdle",
"version": "0.1.0.15",
"version": "0.1.3",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -2,9 +2,9 @@ import createMiddleware from 'next-intl/middleware';
import type { NextRequest } from 'next/server';
const i18nMiddleware = createMiddleware({
locales: ['de', 'en'],
defaultLocale: 'de',
// Wir nutzen überall Locale-Präfixe (`/de`, `/en`)
locales: ['en', 'de'],
defaultLocale: 'en',
// Wir nutzen überall Locale-Präfixe (`/en`, `/de`)
localePrefix: 'always'
});

View File

@@ -0,0 +1,94 @@
#!/bin/bash
# Script zum Prüfen der Caddy-Zertifikate
set -e
echo "🔍 Prüfe Caddy-Zertifikat-Status..."
echo ""
# Prüfe ob Caddy-Container läuft
if ! docker ps | grep -q hoerdle-caddy; then
echo "❌ Caddy-Container läuft nicht!"
echo " Starte ihn mit: docker compose -f docker-compose.caddy.yml --profile production up -d"
exit 1
fi
echo "✅ Caddy-Container läuft"
echo ""
# Prüfe Zertifikate im Volume
echo "📜 Gespeicherte Zertifikate im Volume:"
docker exec hoerdle-caddy find /data/caddy/certificates -name "*.crt" -o -name "*.key" 2>/dev/null | head -20 || echo " (Keine Zertifikate gefunden oder Fehler beim Zugriff)"
echo ""
# Prüfe Caddy-Logs für Zertifikats-Fehler
echo "📋 Letzte Caddy-Logs (Zertifikat-bezogen):"
docker logs hoerdle-caddy 2>&1 | grep -i -E "(certificate|tls|acme|challenge|error)" | tail -20 || echo " (Keine relevanten Log-Einträge gefunden)"
echo ""
# Prüfe DNS-Einträge
echo "🌐 DNS-Einträge prüfen:"
echo ""
# Prüfe hoerdle.de
echo "1⃣ hoerdle.de:"
HOERDLE_IP=$(dig +short hoerdle.de @8.8.8.8 | head -1 || echo "DNS-Fehler")
if [ -n "$HOERDLE_IP" ] && [ "$HOERDLE_IP" != "DNS-Fehler" ]; then
echo " ✅ DNS-A-Record: $HOERDLE_IP"
else
echo " ❌ DNS-A-Record: nicht aufgelöst"
fi
# Prüfe xn--hrdle-jua.de (Punycode)
echo "2⃣ xn--hrdle-jua.de (hördle.de):"
PUNYCODE_IP=$(dig +short xn--hrdle-jua.de @8.8.8.8 | head -1 || echo "DNS-Fehler")
if [ -n "$PUNYCODE_IP" ] && [ "$PUNYCODE_IP" != "DNS-Fehler" ]; then
echo " ✅ DNS-A-Record: $PUNYCODE_IP"
else
echo " ❌ DNS-A-Record: nicht aufgelöst"
fi
# Prüfe auch die Unicode-Domain direkt (falls unterstützt)
echo "3⃣ hördle.de (Unicode):"
UNICODE_IP=$(dig +short hördle.de @8.8.8.8 2>/dev/null | head -1 || echo "Nicht unterstützt")
if [ -n "$UNICODE_IP" ] && [ "$UNICODE_IP" != "Nicht unterstützt" ]; then
echo " ✅ DNS-A-Record: $UNICODE_IP"
else
echo " ⚠️ Unicode-Domain-Abfrage nicht unterstützt (normal)"
fi
echo ""
echo "🔒 Zertifikat-Test:"
echo ""
# Teste HTTPS-Verbindung zu hoerdle.de
echo "1⃣ hoerdle.de HTTPS:"
if echo | timeout 5 openssl s_client -connect hoerdle.de:443 -servername hoerdle.de 2>/dev/null | grep -q "Verify return code: 0"; then
echo " ✅ Zertifikat gültig"
CERT_INFO=$(echo | timeout 5 openssl s_client -connect hoerdle.de:443 -servername hoerdle.de 2>/dev/null | openssl x509 -noout -subject -dates 2>/dev/null || echo "")
echo " $CERT_INFO" | sed 's/^/ /'
else
echo " ❌ Zertifikat fehlt oder ungültig"
fi
echo ""
echo "2⃣ hördle.de (xn--hrdle-jua.de) HTTPS:"
if echo | timeout 5 openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de 2>/dev/null | grep -q "Verify return code: 0"; then
echo " ✅ Zertifikat gültig"
CERT_INFO=$(echo | timeout 5 openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de 2>/dev/null | openssl x509 -noout -subject -dates 2>/dev/null || echo "")
echo " $CERT_INFO" | sed 's/^/ /'
else
echo " ❌ Zertifikat fehlt oder ungültig"
fi
echo ""
echo "📝 Zertifikat-Details aus Caddy Volume:"
echo ""
docker exec hoerdle-caddy ls -la /data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/ 2>/dev/null | head -10 || echo " (Verzeichnis nicht gefunden oder leer)"
echo ""
echo "💡 Tipps:"
echo " - Wenn kein Zertifikat vorhanden ist, führe aus: ./scripts/renew-caddy-certificates.sh"
echo " - Prüfe die Caddy-Logs: docker logs hoerdle-caddy"
echo " - Prüfe DNS-Einträge in GoDaddy: https://dcc.godaddy.com/manage/hoerdle.de/dns"

65
scripts/check-db-permissions.sh Executable file
View 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
View 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"

View File

@@ -54,6 +54,19 @@ git pull
echo "🏷️ Fetching git 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)
echo "🔨 Building new Docker image (this runs while app is still online)..."
docker compose build

71
scripts/docker-cleanup.sh Executable file
View 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
View 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
View 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."

View File

@@ -0,0 +1,110 @@
#!/bin/bash
# Quick-Check für Punycode-Domain DNS und Zertifikat
set -e
echo "🔍 Quick-Check für hördle.de (xn--hrdle-jua.de)"
echo ""
# Prüfe DNS
echo "1⃣ DNS-Auflösung:"
echo ""
echo " hoerdle.de:"
HOERDLE_IP=$(dig +short hoerdle.de @8.8.8.8 | head -1 || echo "")
if [ -z "$HOERDLE_IP" ]; then
echo " ❌ Konnte nicht aufgelöst werden"
else
echo " ✅ IP: $HOERDLE_IP"
fi
echo ""
echo " xn--hrdle-jua.de:"
PUNYCODE_IP=$(dig +short xn--hrdle-jua.de @8.8.8.8 | head -1 || echo "")
if [ -z "$PUNYCODE_IP" ]; then
echo " ❌ Konnte nicht aufgelöst werden"
else
echo " ✅ IP: $PUNYCODE_IP"
fi
if [ -n "$HOERDLE_IP" ] && [ -n "$PUNYCODE_IP" ]; then
if [ "$HOERDLE_IP" = "$PUNYCODE_IP" ]; then
echo ""
echo " ✅ Beide Domains zeigen auf die gleiche IP ($HOERDLE_IP)"
else
echo ""
echo " ⚠️ WARNUNG: Domains zeigen auf unterschiedliche IPs!"
echo " hoerdle.de: $HOERDLE_IP"
echo " xn--hrdle-jua.de: $PUNYCODE_IP"
echo " → Beide sollten auf die gleiche IP zeigen!"
fi
fi
echo ""
echo "2⃣ HTTPS-Verbindungstest:"
echo ""
# Test hoerdle.de
echo " hoerdle.de:"
if timeout 5 bash -c "echo > /dev/tcp/hoerdle.de/443" 2>/dev/null; then
echo " ✅ Port 443 ist erreichbar"
if echo | timeout 5 openssl s_client -connect hoerdle.de:443 -servername hoerdle.de 2>/dev/null | grep -q "Verify return code: 0"; then
CERT_VALID_UNTIL=$(echo | timeout 5 openssl s_client -connect hoerdle.de:443 -servername hoerdle.de 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || echo "")
echo " ✅ Zertifikat gültig bis: $CERT_VALID_UNTIL"
else
echo " ❌ Zertifikat ungültig oder fehlt"
fi
else
echo " ❌ Port 443 nicht erreichbar"
fi
echo ""
echo " xn--hrdle-jua.de:"
if timeout 5 bash -c "echo > /dev/tcp/xn--hrdle-jua.de/443" 2>/dev/null; then
echo " ✅ Port 443 ist erreichbar"
if echo | timeout 5 openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de 2>/dev/null | grep -q "Verify return code: 0"; then
CERT_VALID_UNTIL=$(echo | timeout 5 openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || echo "")
echo " ✅ Zertifikat gültig bis: $CERT_VALID_UNTIL"
else
echo " ❌ Zertifikat ungültig oder fehlt (ERR_SSL_PROTOCOL_ERROR)"
echo ""
echo " 💡 Lösung: Führe aus: ./scripts/renew-caddy-certificates.sh"
fi
else
echo " ❌ Port 443 nicht erreichbar"
fi
echo ""
echo "3⃣ Caddy-Container Status:"
echo ""
if docker ps | grep -q hoerdle-caddy; then
echo " ✅ Caddy-Container läuft"
echo ""
echo " 📋 Letzte Caddy-Logs (Zertifikat-bezogen):"
docker logs hoerdle-caddy 2>&1 | grep -i -E "(xn--hrdle|punycode|certificate|tls|acme|challenge)" | tail -5 || echo " (Keine relevanten Einträge gefunden)"
else
echo " ❌ Caddy-Container läuft nicht"
echo " Starte mit: docker compose -f docker-compose.caddy.yml --profile production up -d"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "💡 Nächste Schritte:"
echo ""
if [ -n "$HOERDLE_IP" ] && [ -n "$PUNYCODE_IP" ] && [ "$HOERDLE_IP" != "$PUNYCODE_IP" ]; then
echo " 1. ❗ DNS-Konfiguration prüfen!"
echo " Beide Domains müssen auf die gleiche IP zeigen."
echo " Prüfe in GoDaddy: https://dcc.godaddy.com/manage/hoerdle.de/dns"
echo ""
fi
echo " 2. Wenn DNS korrekt ist, Zertifikat neu erstellen:"
echo " ./scripts/renew-caddy-certificates.sh"
echo ""
echo " 3. Caddy-Logs überwachen:"
echo " docker logs hoerdle-caddy -f"
echo ""
echo " 4. Detaillierte Diagnose:"
echo " ./scripts/check-caddy-certificates.sh"
echo ""

47
scripts/quick-fix-db.sh Executable file
View 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"

View File

@@ -0,0 +1,83 @@
#!/bin/bash
# Script zum Erneuern/Löschen und Neu-Erstellen von Caddy-Zertifikaten
set -e
echo "🔄 Caddy-Zertifikat-Erneuerung"
echo ""
echo "⚠️ WICHTIG: Dieses Script wird die Zertifikate löschen und Caddy zwingen, sie neu zu erstellen."
echo ""
# Prüfe ob Caddy-Container läuft
if ! docker ps | grep -q hoerdle-caddy; then
echo "❌ Caddy-Container läuft nicht!"
echo " Starte ihn mit: docker compose -f docker-compose.caddy.yml --profile production up -d"
exit 1
fi
echo "📋 Verfügbare Optionen:"
echo " 1) Alle Zertifikate löschen und neu erstellen (empfohlen bei Problemen)"
echo " 2) Nur Zertifikat für xn--hrdle-jua.de löschen (Punycode-Domain)"
echo " 3) Nur Zertifikat für hoerdle.de löschen"
echo " 4) Abbrechen"
echo ""
read -p "Wähle eine Option (1-4): " choice
case $choice in
1)
echo ""
echo "🗑️ Lösche ALLE Zertifikate..."
docker exec hoerdle-caddy rm -rf /data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/* 2>/dev/null || true
echo "✅ Alle Zertifikate gelöscht"
;;
2)
echo ""
echo "🗑️ Lösche Zertifikat für xn--hrdle-jua.de..."
docker exec hoerdle-caddy find /data/caddy/certificates -name "*xn--hrdle-jua.de*" -delete 2>/dev/null || true
docker exec hoerdle-caddy find /data/caddy/certificates -name "*xn--*" -delete 2>/dev/null || true
echo "✅ Zertifikat für Punycode-Domain gelöscht"
;;
3)
echo ""
echo "🗑️ Lösche Zertifikat für hoerdle.de..."
docker exec hoerdle-caddy find /data/caddy/certificates -name "*hoerdle.de*" ! -name "*xn--*" -delete 2>/dev/null || true
echo "✅ Zertifikat für hoerdle.de gelöscht"
;;
4)
echo "❌ Abgebrochen."
exit 0
;;
*)
echo "❌ Ungültige Option. Abgebrochen."
exit 1
;;
esac
echo ""
echo "🔄 Starte Caddy-Container neu, um Zertifikate neu zu erstellen..."
docker compose -f docker-compose.caddy.yml --profile production restart caddy
echo ""
echo "⏳ Warte 5 Sekunden, damit Caddy startet..."
sleep 5
echo ""
echo "📋 Prüfe Caddy-Logs auf Zertifikats-Erstellung:"
echo ""
docker logs hoerdle-caddy --tail=30 2>&1 | grep -i -E "(certificate|tls|acme|challenge|error|success)" || echo " (Keine relevanten Log-Einträge in den letzten 30 Zeilen)"
echo ""
echo "✅ Container wurde neu gestartet."
echo ""
echo "💡 Nächste Schritte:"
echo " 1. Warte 1-2 Minuten, damit Caddy die Zertifikate erstellt"
echo " 2. Prüfe die Logs: docker logs hoerdle-caddy -f"
echo " 3. Prüfe den Status: ./scripts/check-caddy-certificates.sh"
echo " 4. Teste die Domain: curl -I https://xn--hrdle-jua.de"
echo ""
echo "⚠️ Falls das Zertifikat nicht erstellt wird:"
echo " - Prüfe DNS-Einträge: Beide Domains müssen auf die Server-IP zeigen"
echo " - Prüfe Port 80: Muss von außen erreichbar sein (für HTTP-01 Challenge)"
echo " - Prüfe Firewall: Ports 80 und 443 müssen offen sein"
echo " - Prüfe Caddy-Logs: docker logs hoerdle-caddy"