- Svelte 43.2%
- TypeScript 41.1%
- CSS 9.6%
- Shell 3.9%
- HTML 1%
- Other 1.2%
Admin-Editor: Der Formularzustand lebt jetzt im Client (SongForm mit lokalem $state-Entwurf und stabilen Block-uids). Vorher haben Re-Render und Action-Ergebnisse getippten Text verworfen. Die no-JS-Pfade (add/remove/moveUp/moveDown) bleiben serverseitig erhalten. Zoom: Pinch- und Doppeltipp-Zoom stoerten das Swipe-Handling des Karussells. Viewport-Meta plus gesture*-Handler fangen das ab, die In-App-Schriftgroesse bleibt als Ersatz bestehen. replaceState: Der ?s=-Sync lief im $effect schon vor der Router-Initialisierung und warf "Cannot call replaceState(...) before router is initialized". Der Fehler brach den kompletten Effect-Flush ab und riss fremde Effects mit. Ein routerReady-Flag aus afterNavigate gated den Aufruf jetzt. |
||
|---|---|---|
| .vscode | ||
| drizzle | ||
| src | ||
| static | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .npmrc | ||
| AGENTS.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| drizzle.config.ts | ||
| migrate.js | ||
| package-lock.json | ||
| package.json | ||
| PLAN.md | ||
| README.md | ||
| tsconfig.json | ||
| update_prod.sh | ||
| vite.config.ts | ||
Mobile Song Viewer
Web-App für Mobiltelefone: Die Anbetungslieder einer Veranstaltung zum Mitsingen anzeigen. Besucher öffnen einen Veranstaltungs-Link und blättern per Wischen durch die Lieder — mit einstellbarer Schriftgröße und Hell-/Dunkelmodus.
Stack: SvelteKit 2 + Svelte 5 (Runes) · TypeScript · SQLite (Drizzle, better-sqlite3) · adapter-node · Docker Compose hinter einem externen Caddy.
Die vollständige Spezifikation steht in PLAN.md.
Setup (lokal)
Du brauchst Node.js ≥ 22.
# 1. Abhängigkeiten installieren
npm install
# 2. Konfiguration anlegen und anpassen
cp .env.example .env
# 3. Migrationen aus dem Drizzle-Schema erzeugen
npm run db:generate
# 4. Migrationen auf die lokale Datenbank anwenden
npm run db:migrate
# 5. Entwicklungsserver starten
npm run dev
Die App läuft dann auf http://localhost:5173.
Den Admin-Bereich erreichst Du unter /admin/login mit dem Passwort aus ADMIN_PASSWORD.
Weitere Skripte:
| Befehl | Zweck |
|---|---|
npm run build |
Produktions-Build nach build/ |
npm run preview |
Produktions-Build lokal ansehen |
npm run check |
TypeScript- und Svelte-Prüfung |
npm run db:generate |
SQL-Migrationen nach ./drizzle erzeugen (einchecken!) |
npm run db:push |
Schema direkt in die DB schreiben — nur lokal |
npm run db:migrate |
migrate.js ausführen |
Deployment
Die App läuft als Container song_viewer hinter einem bereits vorhandenen Caddy im Docker-Netz
caddy. Im Netz caddy ist der Container unter dem Alias song_viewer erreichbar.
# 1. Externes Netz anlegen (einmalig, falls noch nicht vorhanden)
docker network create caddy
# 2. .env auf dem Server anlegen (ORIGIN, ADMIN_PASSWORD, SESSION_SECRET)
cp .env.example .env
# 3. Bauen und starten
docker compose up -d --build
ORIGIN, ADMIN_PASSWORD und SESSION_SECRET zieht Compose aus der .env im selben Verzeichnis —
trage dort Deine echte Domain ein. DB_PATH setzt die docker-compose.yml direkt.
Ein Secret erzeugst Du z.B. so:
openssl rand -hex 32
Updates einspielen
Auf dem Server erledigt update_prod.sh den kompletten Ablauf: neuen Stand holen, Image bauen,
Container neu starten, auf den Healthcheck warten und alte Images wegräumen.
cd /opt/mobile-song-viewer
./update_prod.sh
Liegt Dein Deployment woanders, gib das Verzeichnis mit:
./update_prod.sh /srv/lieder
# oder
APP_DIR=/srv/lieder ./update_prod.sh
Das Skript setzt Deinen Arbeitsstand per git reset --hard origin/<branch> zurück — lokale
Änderungen im Deployment-Verzeichnis gehen dabei verloren. Fehlt die .env, bricht es ab, bevor
etwas passiert. Mit HEALTH_TIMEOUT (Default 120) stellst Du ein, wie lange auf einen gesunden
Container gewartet wird.
Hinweis: ARM-Mac → x86-Server
better-sqlite3 enthält ein natives Binary. Wenn Du auf einem Apple-Silicon-Mac baust und das
Image auf einem x86-Server läuft, musst Du die Zielplattform explizit angeben:
docker build --platform linux/amd64 -t mobile-song-viewer .
# oder mit Compose:
docker compose build --build-arg BUILDPLATFORM=linux/amd64
Am einfachsten ist es, direkt auf dem Server zu bauen (docker compose up -d --build).
Kopiere node_modules niemals über Architektur- oder libc-Grenzen hinweg.
Caddy
Caddy setzt X-Forwarded-Proto, -Host und -For automatisch, Du brauchst nichts zusätzlich zu
konfigurieren:
lieder.example.com {
encode zstd gzip
reverse_proxy song_viewer:3000
}
Caddy spricht den Container über den Namen bzw. Netz-Alias song_viewer an.
Setze in der .env ORIGIN=https://lieder.example.com. Das verhindert den CSRF-Fehler bei den
Form-Actions. Nutze entweder ORIGIN oder PROTOCOL_HEADER + HOST_HEADER — nicht beides.
Umgebungsvariablen
| Variable | Zweck |
|---|---|
ADMIN_PASSWORD |
Passwort für den Admin-Login |
SESSION_SECRET |
HMAC-Schlüssel für das Session-Cookie |
DB_PATH |
Pfad zur SQLite-Datei, im Container /data/app.db |
ORIGIN |
Öffentliche URL, für die CSRF-Prüfung |
PORT |
Port des Node-Servers, Default 3000 |
Alle Variablen stehen mit Kommentaren in .env.example. Die .env selbst ist in .gitignore und
gehört nicht ins Repository.
Backup
Die Datenbank liegt im Named Volume songdb unter /data/app.db. Sie läuft im WAL-Modus — ein
einfaches cp der laufenden Datei ist deshalb inkonsistent, weil die letzten Schreibvorgänge
noch im .db-wal stehen.
Nutze stattdessen VACUUM INTO. Das schreibt eine konsistente Kopie in einem Rutsch:
docker compose exec app node -e "
const Database = require('better-sqlite3');
const db = new Database(process.env.DB_PATH, { readonly: true });
db.exec(\"VACUUM INTO '/data/backup.db'\");
db.close();
"
# Kopie herausholen
docker compose cp app:/data/backup.db ./backup-$(date +%F).db
Sichere die entstandene Datei anschließend an einen Ort außerhalb des Servers.
Lege das Volume nicht auf NFS ab — WAL braucht echte Datei-Locks.