diff --git a/.dockerignore b/.dockerignore index ea330a6..96f204d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,7 @@ node_modules .env .env.local data +backups docs/screenshots *.log test-results diff --git a/.env.example b/.env.example index cae9b4a..ec90fa9 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,9 @@ AUTH_SECRET=bitte-durch-zufallswert-ersetzen # Ablage für Bild-Cache und sonstige Laufzeitdaten. DATA_DIR=/app/data +# Port, auf dem Flimmer auf dem Host lauscht. +APP_PORT=3000 + # Zeitzone für Kalender, Statistiken und geplante Läufe TZ=Europe/Berlin diff --git a/.gitignore b/.gitignore index 4db15c5..3593591 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ next-env.d.ts # Produktion / Laufzeit data/ +backups/ *.tsbuildinfo # Umgebung — niemals committen! diff --git a/CHANGELOG.md b/CHANGELOG.md index d85e7f4..396ba0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/). ## [Unveröffentlicht] +## [0.7.1] — 2026-09-01 + +### Hinzugefügt + +- **`update.sh`** für den Betrieb unter Linux — ausführbar im Repository + hinterlegt, also direkt nach dem Klonen startklar. Der Ablauf: Datenbank + sichern, Änderungen holen, Image bauen, neu starten, warten bis die Instanz + wieder antwortet. + + Grundhaltung: erst sichern, dann anfassen. Schlägt die Sicherung fehl, bricht + das Skript ab, statt weiterzumachen. Geht danach etwas schief, nennt es den + **konkreten** Weg zurück — mit dem Commit, zu dem man zurückspringt, und der + Datei, die man einspielt, nicht bloß dem Rat, ein Backup zu haben. + + Weitere Eigenheiten: + - Lokale Änderungen im Arbeitsverzeichnis werden erkannt; `git pull` + unterbleibt dann, statt sie zu überfahren. + - Erreichbarkeit wird über den veröffentlichten Port geprüft und, falls der + nicht nach außen geht (Reverse Proxy im selben Docker-Netz), über den + Healthcheck des Containers. + - Alte Sicherungen werden aufgeräumt, die letzten zehn bleiben (`--behalte N`). + - Kommt mit `docker compose` und dem alten `docker-compose` zurecht. + +- `APP_PORT` in der `.env.example` — der Port stand bisher nur in der + Compose-Datei als Vorgabewert. + ## [0.7.0] — 2026-09-01 „Hab ich" — Titel, die in der eigenen Sammlung liegen. Dazu die Nacharbeiten @@ -321,7 +347,8 @@ Erste lauffähige Etappe: Gerüst, Marke, Datenmodell und Anmeldung. dass weitere Sprachen als zusätzliche Datei dazukommen können. - **Seed-Skript** mit Demo-Konten sowie README und Design-Dokumentation. -[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/flimmer/compare/v0.7.0...HEAD +[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/flimmer/compare/v0.7.1...HEAD +[0.7.1]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.7.1 [0.7.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.7.0 [0.6.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.6.0 [0.5.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.5.0 diff --git a/README.md b/README.md index cadc874..473d1d0 100644 --- a/README.md +++ b/README.md @@ -114,14 +114,31 @@ flimmer.example.com { ```bash cd flimmer -git pull -docker compose build -docker compose up -d +./update.sh ``` -Datenbankmigrationen laufen automatisch beim Start des Containers — ein eigener -Handgriff ist nicht nötig. **Vor** einem Update ein Backup zu ziehen ist -trotzdem eine gute Angewohnheit. +Das Skript sichert zuerst die Datenbank, holt die Änderungen, baut das Image neu +und wartet, bis die Instanz wieder antwortet. Schlägt etwas fehl, nennt es den +konkreten Weg zurück — mit dem Befehl und der Sicherungsdatei, nicht bloß dem +Rat, ein Backup zu haben. + +``` +./update.sh --ohne-backup ohne Datenbanksicherung +./update.sh --ohne-pull nur neu bauen und starten +./update.sh --behalte 20 wie viele Sicherungen aufgehoben werden (Standard: 10) +./update.sh --hilfe +``` + +Die Sicherungen landen unter `backups/` und sind gzip-komprimierte `pg_dump`s. + +Von Hand geht es genauso: + +```bash +git pull && docker compose build && docker compose up -d +``` + +Datenbankmigrationen laufen in beiden Fällen automatisch beim Start des +Containers — ein eigener Handgriff ist nicht nötig. ## Backup diff --git a/package.json b/package.json index e5c4a50..1838ddc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flimmer", - "version": "0.7.0", + "version": "0.7.1", "private": true, "description": "Flimmer — selbst hostbarer Serien- und Film-Tracker", "license": "MIT", diff --git a/releases/v0.7.1.md b/releases/v0.7.1.md new file mode 100644 index 0000000..64dc770 --- /dev/null +++ b/releases/v0.7.1.md @@ -0,0 +1,25 @@ +# Flimmer 0.7.1 + +### Hinzugefügt + +- **`update.sh`** für den Betrieb unter Linux — ausführbar im Repository + hinterlegt, also direkt nach dem Klonen startklar. Der Ablauf: Datenbank + sichern, Änderungen holen, Image bauen, neu starten, warten bis die Instanz + wieder antwortet. + + Grundhaltung: erst sichern, dann anfassen. Schlägt die Sicherung fehl, bricht + das Skript ab, statt weiterzumachen. Geht danach etwas schief, nennt es den + **konkreten** Weg zurück — mit dem Commit, zu dem man zurückspringt, und der + Datei, die man einspielt, nicht bloß dem Rat, ein Backup zu haben. + + Weitere Eigenheiten: + - Lokale Änderungen im Arbeitsverzeichnis werden erkannt; `git pull` + unterbleibt dann, statt sie zu überfahren. + - Erreichbarkeit wird über den veröffentlichten Port geprüft und, falls der + nicht nach außen geht (Reverse Proxy im selben Docker-Netz), über den + Healthcheck des Containers. + - Alte Sicherungen werden aufgeräumt, die letzten zehn bleiben (`--behalte N`). + - Kommt mit `docker compose` und dem alten `docker-compose` zurecht. + +- `APP_PORT` in der `.env.example` — der Port stand bisher nur in der + Compose-Datei als Vorgabewert. diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..5bc2eb4 --- /dev/null +++ b/update.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash +# +# Flimmer aktualisieren. +# +# ./update.sh sichern, holen, bauen, neu starten +# ./update.sh --ohne-backup ohne Datenbanksicherung +# ./update.sh --ohne-pull nur neu bauen und starten (kein git pull) +# ./update.sh --hilfe +# +# Grundhaltung: Erst sichern, dann anfassen. Bricht etwas ab, steht am Ende, +# wie der Weg zurück aussieht — mit dem konkreten Befehl und der konkreten +# Sicherungsdatei, nicht bloß dem Rat, ein Backup zu haben. + +set -euo pipefail + +# --- Ausgabe ----------------------------------------------------------------- + +if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then + AKZENT=$'\033[38;5;214m'; GUT=$'\033[32m'; WARN=$'\033[33m' + FEHLER=$'\033[31m'; LEISE=$'\033[2m'; AUS=$'\033[0m' +else + AKZENT=""; GUT=""; WARN=""; FEHLER=""; LEISE=""; AUS="" +fi + +schritt() { printf '\n%s▸ %s%s\n' "$AKZENT" "$1" "$AUS"; } +info() { printf ' %s\n' "$1"; } +leise() { printf ' %s%s%s\n' "$LEISE" "$1" "$AUS"; } +gut() { printf ' %s✓ %s%s\n' "$GUT" "$1" "$AUS"; } +warnung() { printf ' %s! %s%s\n' "$WARN" "$1" "$AUS"; } +abbruch() { printf '\n%s✗ %s%s\n' "$FEHLER" "$1" "$AUS" >&2; exit 1; } + +hilfe() { + cat <<'ENDE' +Flimmer aktualisieren + + ./update.sh [OPTIONEN] + +Ablauf: Datenbank sichern → Änderungen holen → Image bauen → neu starten → +warten, bis die Instanz wieder antwortet. Die Datenbankmigrationen spielt der +Container beim Start selbst ein. + +Optionen: + --ohne-backup Keine Sicherung anlegen (nicht empfohlen) + --ohne-pull Nichts aus dem Repository holen, nur neu bauen und starten + --behalte N Wie viele Sicherungen aufgehoben werden (Standard: 10) + --hilfe, -h Diese Übersicht + +Die Sicherungen liegen unter backups/ und sind gzip-komprimierte pg_dumps. +ENDE +} + +# --- Optionen ---------------------------------------------------------------- + +MIT_BACKUP=1 +MIT_PULL=1 +BEHALTE=10 + +while [ $# -gt 0 ]; do + case "$1" in + --ohne-backup) MIT_BACKUP=0 ;; + --ohne-pull) MIT_PULL=0 ;; + --behalte) shift; BEHALTE="${1:-10}" ;; + --hilfe|-h) hilfe; exit 0 ;; + *) abbruch "Unbekannte Option: $1 (./update.sh --hilfe)" ;; + esac + shift +done + +# --- Voraussetzungen --------------------------------------------------------- + +cd "$(dirname "$0")" + +[ -f docker-compose.yml ] || abbruch "Hier liegt keine docker-compose.yml. Das Skript gehört ins Flimmer-Verzeichnis." +[ -f .env ] || abbruch "Es fehlt die .env. Lege sie an: cp .env.example .env" + +command -v docker >/dev/null 2>&1 || abbruch "docker ist nicht installiert oder nicht im Pfad." + +if docker compose version >/dev/null 2>&1; then + COMPOSE="docker compose" +elif command -v docker-compose >/dev/null 2>&1; then + COMPOSE="docker-compose" +else + abbruch "Weder 'docker compose' noch 'docker-compose' gefunden." +fi + +docker info >/dev/null 2>&1 || abbruch "Der Docker-Dienst antwortet nicht. Läuft er?" + +# Werte aus der .env lesen, ohne sie auszuführen. +env_wert() { + local schluessel="$1" standard="${2:-}" + local zeile + zeile=$(grep -E "^[[:space:]]*${schluessel}=" .env 2>/dev/null | tail -1 || true) + if [ -z "$zeile" ]; then printf '%s' "$standard"; return; fi + zeile="${zeile#*=}" + zeile="${zeile%\"}"; zeile="${zeile#\"}" + zeile="${zeile%\'}"; zeile="${zeile#\'}" + printf '%s' "$zeile" +} + +DB_USER=$(env_wert POSTGRES_USER flimmer) +DB_NAME=$(env_wert POSTGRES_DB flimmer) +PORT=$(env_wert APP_PORT 3000) + +printf '%s\n' "Flimmer aktualisieren" +leise "Verzeichnis: $(pwd)" +leise "Compose: $COMPOSE" + +# --- Stand festhalten -------------------------------------------------------- + +IST_GIT=0 +VORHER_REF="" +if [ -d .git ] && command -v git >/dev/null 2>&1; then + IST_GIT=1 + VORHER_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unbekannt") +fi + +VORHER_VERSION=$(grep -m1 '"version"' package.json 2>/dev/null | sed 's/.*"version"[^"]*"\([^"]*\)".*/\1/' || echo "?") +leise "Aktuell: Version $VORHER_VERSION${VORHER_REF:+ (${VORHER_REF})}" + +# --- Sicherung --------------------------------------------------------------- + +SICHERUNG="" +if [ "$MIT_BACKUP" -eq 1 ]; then + schritt "Datenbank sichern" + + # -q gibt es in beiden Compose-Fassungen; --status erst ab v2. + if [ -z "$($COMPOSE ps -q db 2>/dev/null)" ]; then + warnung "Die Datenbank läuft nicht — es gibt also auch nichts zu sichern." + else + mkdir -p backups + SICHERUNG="backups/flimmer-$(date +%Y-%m-%d-%H%M%S).sql.gz" + + if $COMPOSE exec -T db pg_dump -U "$DB_USER" -d "$DB_NAME" --clean --if-exists \ + | gzip > "$SICHERUNG" 2>/dev/null && [ -s "$SICHERUNG" ]; then + gut "$SICHERUNG ($(du -h "$SICHERUNG" | cut -f1))" + else + rm -f "$SICHERUNG" + SICHERUNG="" + abbruch "Die Sicherung ist fehlgeschlagen. Abbruch — ohne Sicherung wird hier nichts aktualisiert. + Wenn du es trotzdem willst: ./update.sh --ohne-backup" + fi + + # Ältere Sicherungen aufräumen, die jüngsten bleiben. + if [ "$BEHALTE" -gt 0 ]; then + ANZAHL=$(ls -1 backups/flimmer-*.sql.gz 2>/dev/null | wc -l) + if [ "$ANZAHL" -gt "$BEHALTE" ]; then + ls -1t backups/flimmer-*.sql.gz | tail -n +$((BEHALTE + 1)) | xargs -r rm -- + leise "Ältere Sicherungen entfernt, die letzten $BEHALTE bleiben." + fi + fi + fi +else + warnung "Ohne Sicherung — auf eigenes Risiko." +fi + +# --- Änderungen holen -------------------------------------------------------- + +if [ "$MIT_PULL" -eq 1 ]; then + schritt "Änderungen holen" + + if [ "$IST_GIT" -eq 0 ]; then + warnung "Kein Git-Repository — übersprungen." + elif [ -n "$(git status --porcelain 2>/dev/null)" ]; then + warnung "Es gibt lokale Änderungen; git pull wird übersprungen." + leise "Ansehen mit: git status" + else + ZWEIG=$(git rev-parse --abbrev-ref HEAD) + if git pull --ff-only 2>&1 | sed 's/^/ /'; then + NACHHER_REF=$(git rev-parse --short HEAD) + if [ "$NACHHER_REF" = "$VORHER_REF" ]; then + gut "Schon auf dem neuesten Stand ($ZWEIG)." + else + gut "$VORHER_REF → $NACHHER_REF" + fi + else + abbruch "git pull ist fehlgeschlagen. Vermutlich sind Zweig und Gegenstelle auseinandergelaufen." + fi + fi +fi + +NEUE_VERSION=$(grep -m1 '"version"' package.json 2>/dev/null | sed 's/.*"version"[^"]*"\([^"]*\)".*/\1/' || echo "?") + +# --- Bauen und starten ------------------------------------------------------- + +schritt "Image bauen" +info "Das dauert beim ersten Mal einige Minuten." +$COMPOSE build 2>&1 | sed 's/^/ /' || abbruch "Der Build ist fehlgeschlagen. Die laufende Instanz ist unangetastet." +gut "Gebaut." + +schritt "Neu starten" +$COMPOSE up -d 2>&1 | sed 's/^/ /' || abbruch "Der Start ist fehlgeschlagen. Log ansehen: $COMPOSE logs -f app" +gut "Container laufen." + +# --- Warten, bis die Instanz antwortet --------------------------------------- + +schritt "Auf die Instanz warten" +info "Die Datenbankmigrationen spielt der Container beim Start selbst ein." + +GESUND=0 +for _ in $(seq 1 60); do + if curl -fsS "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then + GESUND=1 + break + fi + sleep 2 +done + +if [ "$GESUND" -eq 1 ]; then + ANTWORT=$(curl -fsS "http://127.0.0.1:${PORT}/api/health" 2>/dev/null || true) + gut "Flimmer antwortet auf Port ${PORT}." + leise "$ANTWORT" + + printf '\n%s✓ Fertig — Version %s%s\n' "$GUT" "$NEUE_VERSION" "$AUS" + if [ "$VORHER_VERSION" != "$NEUE_VERSION" ]; then + info "Was sich geändert hat, steht in CHANGELOG.md." + fi + [ -n "$SICHERUNG" ] && leise "Sicherung: $SICHERUNG" + exit 0 +fi + +# --- Fehlschlag: konkreter Weg zurück ---------------------------------------- + +printf '\n%s✗ Die Instanz antwortet nach zwei Minuten nicht.%s\n\n' "$FEHLER" "$AUS" >&2 +info "Log ansehen:" +printf ' %s logs --tail 50 app\n\n' "$COMPOSE" + +if [ -n "$SICHERUNG" ] || [ -n "$VORHER_REF" ]; then + info "Weg zurück:" + if [ "$IST_GIT" -eq 1 ] && [ -n "$VORHER_REF" ]; then + printf ' git checkout %s && %s up -d --build\n' "$VORHER_REF" "$COMPOSE" + fi + if [ -n "$SICHERUNG" ]; then + printf ' gunzip -c %s | %s exec -T db psql -U %s -d %s\n' \ + "$SICHERUNG" "$COMPOSE" "$DB_USER" "$DB_NAME" + fi + printf '\n' +fi + +exit 1