little app to show songs for an event optimized for a mobile device
  • Svelte 43.2%
  • TypeScript 41.1%
  • CSS 9.6%
  • Shell 3.9%
  • HTML 1%
  • Other 1.2%
Find a file
Thorsten Bus b2f6933fff fix(viewer): keep editor state, disable zoom, guard replaceState
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.
2026-08-31 20:52:26 +02:00
.vscode feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
drizzle feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
src fix(viewer): keep editor state, disable zoom, guard replaceState 2026-08-31 20:52:26 +02:00
static feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
.dockerignore feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
.env.example feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
.gitignore feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
.npmrc feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
AGENTS.md init 2026-08-31 17:56:01 +02:00
docker-compose.yml build(compose): pin container name and caddy alias 2026-08-31 19:15:27 +02:00
Dockerfile feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
drizzle.config.ts feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
migrate.js feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
package-lock.json feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
package.json feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
PLAN.md fix(viewer): keep editor state, disable zoom, guard replaceState 2026-08-31 20:52:26 +02:00
README.md build(compose): pin container name and caddy alias 2026-08-31 19:15:27 +02:00
tsconfig.json feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
update_prod.sh feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00
vite.config.ts feat: add song viewer app with admin and deployment 2026-08-31 19:09:05 +02:00

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.