diff --git a/CLAUDE.md b/CLAUDE.md
index 963bad8..b0ea386 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@ Dashboard für Vibe-Coding-Projekte. Next.js 15 (App Router), TypeScript,
Tailwind 4, Prisma, PostgreSQL. Oberfläche und Texte sind **deutsch**.
Funktionsumfang: [FEATURES.md](FEATURES.md) · Geplantes: [ROADMAP.md](ROADMAP.md) ·
-Betrieb: [README.md](README.md)
+Betrieb: [README.md](README.md) · Server: [DEPLOYMENT.md](DEPLOYMENT.md)
## Womit man arbeitet
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
new file mode 100644
index 0000000..e3b01b8
--- /dev/null
+++ b/DEPLOYMENT.md
@@ -0,0 +1,342 @@
+# Nebula auf einem Server betreiben
+
+Schritt für Schritt: von der leeren Maschine bis zur laufenden Instanz hinter
+TLS — und wie Updates danach ablaufen.
+
+Für Installation zum Ausprobieren und die Entwicklungsvarianten siehe
+[README.md](README.md). Hier geht es um den Dauerbetrieb.
+
+---
+
+## Was du brauchst
+
+| | |
+| --- | --- |
+| Server | Linux mit Docker Engine ≥ 24 und Compose v2 |
+| Arbeitsspeicher | 1 GB reichen; beim Bauen kurzzeitig ~2 GB |
+| Plattenplatz | ~2 GB für Abbilder, dazu die Datenbank |
+| Domain | Eine (Unter-)Domain, die auf den Server zeigt |
+| TLS | Pflicht, wenn du Passkeys willst — siehe Schritt 6 |
+
+> **Warum TLS Pflicht ist:** Passkeys verlangen einen sicheren Kontext.
+> Über `http://` an einer LAN-IP funktionieren sie nicht. Passwort und
+> Zwei-Faktor gehen auch ohne, aber dann läuft dein Passwort im Klartext
+> übers Netz — betreibe Nebula nicht ohne TLS.
+
+---
+
+## 1 · Docker installieren
+
+```bash
+curl -fsSL https://get.docker.com | sh
+sudo usermod -aG docker "$USER"
+newgrp docker # oder ab- und wieder anmelden
+
+docker --version && docker compose version
+```
+
+## 2 · Nebula holen
+
+```bash
+sudo mkdir -p /opt/nebula && sudo chown "$USER" /opt/nebula
+git clone https://git.example.com/dein-name/nebula.git /opt/nebula
+cd /opt/nebula
+```
+
+## 3 · Konfigurieren
+
+```bash
+node scripts/setup.mjs # legt .env an und würfelt ein APP_SECRET
+```
+
+Ohne Node auf dem Server: `cp .env.example .env` und das Geheimnis von Hand
+setzen —
+
+```bash
+openssl rand -base64 48
+```
+
+Dann die `.env` öffnen und mindestens diese vier Werte anpassen:
+
+```env
+# Die Adresse, unter der du Nebula im Browser aufrufst.
+# Bestimmt die WebAuthn-rpID — passt sie nicht, scheitern Passkeys.
+APP_URL="https://nebula.deine-domain.de"
+
+# Verschlüsselt die Git-Tokens. Lang, zufällig, niemals teilen.
+APP_SECRET="…dein-langer-zufallswert…"
+
+# Das Datenbankpasswort. Die Voreinstellung „nebula" ist für Entwicklung.
+POSTGRES_PASSWORD="…noch-ein-langer-zufallswert…"
+
+# Nur lokal lauschen — nach außen geht es über den Reverse Proxy (Schritt 6).
+APP_PORT=127.0.0.1:3000
+```
+
+> **`APP_SECRET` später zu ändern** macht gespeicherte Git-Tokens unlesbar.
+> Sie müssen dann neu hinterlegt werden. Konten, Passwörter und Passkeys
+> bleiben unberührt.
+>
+> **`APP_URL` später zu ändern** macht bestehende **Passkeys ungültig**, weil
+> sie an die rpID gebunden sind. Passwörter funktionieren weiter.
+
+### Datenbankport schließen
+
+`docker-compose.yml` bildet Postgres standardmäßig auf `5432` ab, damit man
+in der Entwicklung von außen drankommt. Auf einem Server hat die Datenbank
+nichts im Internet zu suchen:
+
+```env
+POSTGRES_PORT=127.0.0.1:5432
+```
+
+Noch besser: die `ports:`-Zeile des `db`-Dienstes ganz entfernen. Die
+Anwendung erreicht die Datenbank über das interne Docker-Netz, nicht über
+den Host.
+
+## 4 · Starten
+
+```bash
+docker compose --profile full up -d --build
+```
+
+Der erste Bau dauert ein paar Minuten. Der Container spielt beim Start
+selbst die Migrationen ein und wartet dabei auf die Datenbank.
+
+```bash
+docker compose --profile full logs -f app # mitlesen, Strg+C beendet nur das Lesen
+curl -s localhost:3000/api/health # {"status":"ok","database":"up",…}
+```
+
+## 5 · Ersten Zugang anlegen
+
+Ruf `https://nebula.deine-domain.de/setup` auf (nach Schritt 6) oder
+vorübergehend über einen SSH-Tunnel:
+
+```bash
+ssh -L 3000:127.0.0.1:3000 benutzer@server
+# dann lokal http://localhost:3000/setup öffnen
+```
+
+Dort legst du das Administratorkonto an und wählst die Betriebsart.
+**Danach ist `/setup` dauerhaft gesperrt.**
+
+Anschließend gleich unter *Mein Konto*:
+
+1. **Passkey hinzufügen** — geht erst, wenn TLS steht
+2. **Zwei-Faktor einrichten** und die Wiederherstellungscodes wegschreiben
+
+## 6 · Reverse Proxy mit TLS
+
+Nebula bringt keinen eigenen TLS-Abschluss mit. Beispiel mit **Caddy**, das
+Zertifikate von selbst besorgt und erneuert:
+
+```caddyfile
+# /etc/caddy/Caddyfile
+nebula.deine-domain.de {
+ reverse_proxy 127.0.0.1:3000
+}
+```
+
+```bash
+sudo systemctl reload caddy
+```
+
+Das genügt. Caddy setzt `X-Forwarded-*` von selbst, holt das Zertifikat bei
+Let's Encrypt und leitet `http` auf `https` um.
+
+
+Dasselbe mit nginx
+
+```nginx
+server {
+ listen 443 ssl http2;
+ server_name nebula.deine-domain.de;
+
+ ssl_certificate /etc/letsencrypt/live/nebula.deine-domain.de/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/nebula.deine-domain.de/privkey.pem;
+
+ location / {
+ proxy_pass http://127.0.0.1:3000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
+```
+
+Zertifikat mit `certbot --nginx -d nebula.deine-domain.de`.
+
+
+> Steht `APP_URL` auf `https://…`, setzt Nebula das `Secure`-Flag auf die
+> Sitzungscookies. Läuft der Proxy auf `https`, die `APP_URL` aber noch auf
+> `http`, funktioniert die Anmeldung — nur ohne dieses Flag. Also beides
+> gleichziehen.
+
+## 7 · Firewall
+
+```bash
+sudo ufw allow 22/tcp
+sudo ufw allow 80,443/tcp
+sudo ufw enable
+```
+
+Port 3000 und 5432 bleiben **zu**. Beide sind nach Schritt 3 ohnehin nur an
+`127.0.0.1` gebunden; die Firewall ist der zweite Riegel.
+
+## 8 · Sicherungen automatisieren
+
+`scripts/backup.sh` legt einen `pg_dump` unter `backups/` ab. Täglich per
+Cron:
+
+```bash
+crontab -e
+```
+
+```cron
+# Jede Nacht um 3:15 sichern, 30 Tage aufheben
+15 3 * * * cd /opt/nebula && ./scripts/backup.sh >> /var/log/nebula-backup.log 2>&1
+30 3 * * * find /opt/nebula/backups -name '*.sql.gz' -mtime +30 -delete
+```
+
+Zurückspielen:
+
+```bash
+./scripts/restore.sh backups/nebula-2026-07-27-03-15-00.sql.gz
+```
+
+> Die Sicherungen liegen auf **demselben Server**. Das hilft gegen einen
+> verpatzten Import, nicht gegen einen Plattenausfall. Zieh sie regelmäßig
+> woandershin — `rsync`, `restic`, was du ohnehin nutzt.
+
+---
+
+## Updates
+
+Dafür gibt es `scripts/update.sh`. Es macht alles in der richtigen
+Reihenfolge und bricht ab, sobald ein Schritt scheitert:
+
+```bash
+cd /opt/nebula
+./scripts/update.sh
+```
+
+Was es tut:
+
+| Schritt | |
+| --- | --- |
+| **1 Prüfen** | docker, compose, `.env` da? Ist `APP_SECRET` gesetzt und nicht mehr der Entwicklungswert? |
+| **2 Holen** | `git pull --ff-only`; zeigt die neuen Commits und fragt nach |
+| **3 Sichern** | `pg_dump` **vor** jeder Schemaänderung |
+| **4 Bauen** | Erst bauen, dann umschalten — ein gescheiterter Bau lässt die alte Fassung laufen |
+| **5 Starten** | Container neu; die Migrationen laufen im Entrypoint |
+| **6 Prüfen** | Bis zu 60 Sekunden auf `/api/health` warten |
+
+Scheitert die Gesundheitsprüfung, zeigt das Skript die letzten Logzeilen und
+nennt den Weg zurück.
+
+```bash
+./scripts/update.sh --yes # ohne Rückfrage, für Cron
+./scripts/update.sh --ref v1.4.0 # auf eine bestimmte Fassung
+./scripts/update.sh --no-pull # nur neu bauen, ohne git pull
+```
+
+### Wenn doch etwas schiefgeht
+
+```bash
+# Zurück auf den vorigen Stand (die Kennung nennt das Skript beim Abbruch)
+git checkout
+./scripts/update.sh --no-pull --yes
+
+# Nur wenn eine Migration die Daten beschädigt hat:
+./scripts/restore.sh backups/.sql.gz
+```
+
+> **Migrationen werden nicht zurückgerollt.** Eine Migration, die eine Spalte
+> entfernt, ist nicht umkehrbar — deshalb sichert das Skript vorher. Steht
+> ein Update mit Datenmigration an, lies vorher den Änderungsverlauf
+> (Versionsnummer in der Fußzeile).
+
+---
+
+## Betrieb
+
+### Zustand ansehen
+
+```bash
+docker compose --profile full ps
+docker compose --profile full logs -f app
+curl -s localhost:3000/api/health
+```
+
+### Neu starten
+
+```bash
+docker compose --profile full restart app
+```
+
+### Speicher- und Plattenverbrauch
+
+```bash
+docker stats --no-stream nebula-app nebula-db
+docker system df
+docker image prune -f # alte Abbilder nach mehreren Updates
+```
+
+### Automatischer Abgleich
+
+Standardmäßig **aus**. Einzuschalten unter *Admin-Einstellungen →
+Automatischer Abgleich*: Abstand, Repositories je Durchlauf, ob fertige
+Projekte ausgelassen werden. Der Zeitgeber läuft im Serverprozess selbst —
+kein zusätzlicher Container, kein Cron.
+
+Wer lieber von außen taktet, lässt ihn aus und ruft
+`POST /api/admin/sync/run` auf.
+
+---
+
+## Härtung, die schon eingebaut ist
+
+Damit du weißt, worum du dich **nicht** kümmern musst:
+
+- Der Container läuft als **unprivilegierter Benutzer** (`nextjs`, UID 1001),
+ nicht als root.
+- **Content-Security-Policy** mit Nonce, `strict-dynamic` und ohne
+ `unsafe-inline` für Skripte.
+- **CSRF-Prüfung** über `Sec-Fetch-Site`/`Origin` bei jedem schreibenden
+ Zugriff.
+- **SSRF-Schutz** bei allen ausgehenden Aufrufen: jede Adresse wird
+ aufgelöst und gegen Loopback, Link-Local und Metadaten-Endpunkte geprüft,
+ Weiterleitungen eingeschlossen.
+- **Ratenbegrenzung** auf Anmeldung, ausgehende Abrufe und schreibende
+ Zugriffe; Kontosperre nach acht Fehlversuchen.
+- **Git-Tokens** liegen AES-256-GCM-verschlüsselt in der Datenbank und
+ verlassen den Server nie — auch nicht im Export.
+- Der **Bildoptimierer** von Next ist abgeschaltet: Nebula benutzt ihn nicht,
+ und `/_next/image` wäre sonst eine Angriffsfläche ohne Gegenwert.
+
+### Was du beisteuern musst
+
+- **TLS** über den Reverse Proxy
+- **`APP_SECRET`** lang und zufällig
+- **Datenbankport** nicht nach außen öffnen
+- **Sicherungen** wegschaffen
+- **Systemupdates**: `unattended-upgrades` für das Betriebssystem,
+ `./scripts/update.sh` für Nebula
+
+---
+
+## Mehrbenutzerbetrieb
+
+Nebula kann beides. Im Einzelbetrieb gibt es genau ein Konto und keine
+Registrierung. Umschalten unter *Admin-Einstellungen*.
+
+Wichtig zu wissen: **Projekte sind zwischen Konten hart getrennt.** Auch ein
+Administrator sieht fremde Projekte nicht — er verwaltet Konten, keine
+Inhalte. Wer gemeinsam an einem Projekt arbeiten will, ist mit Nebula falsch
+beraten; das ist [bewusst nicht](ROADMAP.md) vorgesehen.
+
+Die Selbstregistrierung ist standardmäßig aus. Lass sie aus, wenn die
+Instanz aus dem Internet erreichbar ist und du nicht willst, dass sich
+Fremde Konten anlegen.
diff --git a/README.md b/README.md
index 33a6737..6090ef7 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,8 @@ Alle Daten liegen in **deiner lokalen PostgreSQL**. Es gibt keinen Cloud-Dienst
Access Tokens werden verschlüsselt abgelegt.
Eine vollständige Auflistung aller Funktionen steht in **[FEATURES.md](FEATURES.md)**,
-geplante Erweiterungen in **[ROADMAP.md](ROADMAP.md)**.
+geplante Erweiterungen in **[ROADMAP.md](ROADMAP.md)**. Für den Dauerbetrieb auf einem
+Server — TLS, Firewall, Sicherungen und Updates — gibt es **[DEPLOYMENT.md](DEPLOYMENT.md)**.
---
diff --git a/ROADMAP.md b/ROADMAP.md
index b0e6592..1bdac23 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -110,6 +110,7 @@ Damit klar ist, was **nicht** kommt und warum:
| **v1.2.0** Commit-Heatmap über ein Jahr, Projektvorlagen | ✓ |
| **v1.3.0** Wiederkehrende Aufgaben, Barrierefreiheit nach WCAG 2.1 AA | ✓ |
| **v1.4.0** README-Vorschau auf der Projektseite | ✓ |
+| **v1.5.0** Sicherheitsdurchsicht, Server-Anleitung und Update-Skript | ✓ |
Zur Entscheidung beim Export: Git-Tokens gehen **nicht** mit. Die Verbindung wird nur
beschrieben, das Token ist nach dem Import neu zu hinterlegen. Snapshots bleiben
diff --git a/package.json b/package.json
index b46b8e5..6190e68 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nebula",
- "version": "1.4.0",
+ "version": "1.5.0",
"private": true,
"description": "Nebula – Dashboard fuer Vibe-Coding Projekte mit Git-Anbindung (GitHub, GitLab, Gitea)",
"scripts": {
diff --git a/scripts/update.sh b/scripts/update.sh
new file mode 100644
index 0000000..9599f9d
--- /dev/null
+++ b/scripts/update.sh
@@ -0,0 +1,163 @@
+#!/usr/bin/env bash
+# ─────────────────────────────────────────────────────────────
+# Nebula auf einem Server aktualisieren.
+#
+# Holt den neuen Stand, sichert die Datenbank, baut das Abbild, spielt die
+# Migrationen ein und startet neu. Bricht ab, sobald ein Schritt scheitert –
+# und sagt dann, wie zurueckzukommen ist.
+#
+# Aufruf: ./scripts/update.sh
+# ./scripts/update.sh --no-pull (nur neu bauen und starten)
+# ./scripts/update.sh --ref v1.4.0 (auf einen Stand festlegen)
+# ./scripts/update.sh --yes (ohne Rueckfrage)
+#
+# Rueckgabe 0 heisst: die neue Fassung laeuft und antwortet gesund.
+# ─────────────────────────────────────────────────────────────
+set -euo pipefail
+
+root="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$root"
+
+pull=1
+ref=""
+zusage=0
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --no-pull) pull=0 ;;
+ --ref) ref="${2:-}"; shift ;;
+ --yes|-y) zusage=1 ;;
+ -h|--help) sed -n '3,15p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
+ *) echo "Unbekannte Option: $1" >&2; exit 2 ;;
+ esac
+ shift
+done
+
+# ── Farben, aber nur wenn ein Terminal zusieht ───────────────
+if [ -t 1 ]; then
+ rot=$'\033[31m'; gruen=$'\033[32m'; gelb=$'\033[33m'; fett=$'\033[1m'; aus=$'\033[0m'
+else
+ rot=""; gruen=""; gelb=""; fett=""; aus=""
+fi
+
+schritt() { printf '\n%s▸ %s%s\n' "$fett" "$1" "$aus"; }
+ok() { printf ' %s✓%s %s\n' "$gruen" "$aus" "$1"; }
+warnung() { printf ' %s!%s %s\n' "$gelb" "$aus" "$1"; }
+abbruch() { printf '\n%s✗ %s%s\n' "$rot" "$1" "$aus" >&2; exit 1; }
+
+# ── Voraussetzungen ──────────────────────────────────────────
+schritt "Voraussetzungen"
+
+command -v docker >/dev/null || abbruch "docker nicht gefunden."
+docker compose version >/dev/null 2>&1 || abbruch "docker compose (v2) nicht gefunden."
+[ -f "$root/.env" ] || abbruch "Keine .env in $root. Zuerst 'node scripts/setup.mjs' ausfuehren."
+[ -f "$root/docker-compose.yml" ] || abbruch "Kein docker-compose.yml in $root."
+ok "docker, compose und .env vorhanden"
+
+# APP_SECRET muss gesetzt sein – sonst liegen die Git-Tokens faktisch offen.
+if grep -qE '^\s*APP_SECRET\s*=\s*("?)(nebula-dev-secret[^"]*|)\1\s*$' "$root/.env"; then
+ abbruch "APP_SECRET ist leer oder noch der Entwicklungswert. Bitte in der .env setzen."
+fi
+ok "APP_SECRET gesetzt"
+
+vorher="$(git rev-parse --short HEAD 2>/dev/null || echo "unbekannt")"
+version_vorher="$(node -p "require('./package.json').version" 2>/dev/null || echo "?")"
+printf ' Aktuell: v%s (%s)\n' "$version_vorher" "$vorher"
+
+# ── Neuen Stand holen ────────────────────────────────────────
+if [ "$pull" -eq 1 ]; then
+ schritt "Neuen Stand holen"
+
+ if [ -n "$(git status --porcelain)" ]; then
+ abbruch "Das Arbeitsverzeichnis ist nicht sauber. Bitte erst committen oder verwerfen."
+ fi
+
+ git fetch --all --tags --quiet
+ if [ -n "$ref" ]; then
+ git checkout --quiet "$ref"
+ ok "Auf $ref gewechselt"
+ else
+ zweig="$(git rev-parse --abbrev-ref HEAD)"
+ git pull --ff-only --quiet origin "$zweig"
+ ok "$zweig aktualisiert"
+ fi
+
+ nachher="$(git rev-parse --short HEAD)"
+ version_nachher="$(node -p "require('./package.json').version" 2>/dev/null || echo "?")"
+
+ if [ "$vorher" = "$nachher" ]; then
+ warnung "Kein neuer Stand – es wird trotzdem neu gebaut."
+ else
+ printf ' Neu: v%s (%s)\n' "$version_nachher" "$nachher"
+ printf '\n Was dazukam:\n'
+ git --no-pager log --oneline --no-decorate "$vorher..$nachher" 2>/dev/null | sed 's/^/ /' || true
+ fi
+else
+ warnung "Ohne --pull: der vorhandene Stand wird neu gebaut."
+fi
+
+# ── Rueckfrage ───────────────────────────────────────────────
+if [ "$zusage" -eq 0 ] && [ -t 0 ]; then
+ printf '\n%sWeiter? Die Datenbank wird vorher gesichert. [j/N] %s' "$fett" "$aus"
+ read -r antwort
+ case "$antwort" in [jJyY]*) ;; *) abbruch "Abgebrochen." ;; esac
+fi
+
+# ── Datenbank sichern ────────────────────────────────────────
+schritt "Datenbank sichern"
+
+# Die Datenbank muss laufen, sonst gibt es nichts zu sichern.
+docker compose up -d db >/dev/null
+sicherung=""
+if [ -x "$root/scripts/backup.sh" ]; then
+ ausgabe="$("$root/scripts/backup.sh" 2>&1)" || abbruch "Sicherung fehlgeschlagen:\n$ausgabe"
+ sicherung="$(printf '%s' "$ausgabe" | grep -oE '[^ ]*backups/[^ ]*\.sql[^ ]*' | tail -n 1 || true)"
+ ok "Gesichert${sicherung:+: $sicherung}"
+else
+ warnung "scripts/backup.sh fehlt – es wird ohne Sicherung fortgefahren."
+fi
+
+# ── Bauen ────────────────────────────────────────────────────
+schritt "Abbild bauen"
+# Erst bauen, dann umschalten: ein Fehlschlag laesst die alte Fassung laufen.
+docker compose --profile full build app || abbruch "Der Bau ist gescheitert. Die laufende Fassung ist unveraendert."
+ok "Abbild gebaut"
+
+# ── Starten ──────────────────────────────────────────────────
+schritt "Starten"
+# Die Migrationen laufen im entrypoint.sh, bevor der Server hochkommt.
+docker compose --profile full up -d || abbruch "Der Start ist gescheitert."
+ok "Container gestartet"
+
+# ── Gesundheitspruefung ──────────────────────────────────────
+schritt "Gesundheitspruefung"
+
+port="$(sed -n 's/^[[:space:]]*APP_PORT[[:space:]]*=[[:space:]]*"\{0,1\}\([^"#]*\)"\{0,1\}[[:space:]]*$/\1/p' "$root/.env" | tail -n 1)"
+port="${port:-3000}"
+
+gesund=0
+for versuch in $(seq 1 30); 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" -ne 1 ]; then
+ printf '\n%sLetzte Zeilen aus dem Log:%s\n' "$fett" "$aus"
+ docker compose --profile full logs --tail 40 app || true
+ printf '\n%sZurueck zur vorigen Fassung:%s\n' "$fett" "$aus"
+ printf ' git checkout %s && ./scripts/update.sh --no-pull --yes\n' "$vorher"
+ [ -n "$sicherung" ] && printf ' Datenbank notfalls: ./scripts/restore.sh %s\n' "$sicherung"
+ abbruch "Die Anwendung antwortet nach 60 Sekunden nicht gesund."
+fi
+
+antwort_json="$(curl -fsS "http://127.0.0.1:${port}/api/health")"
+ok "Gesund: $antwort_json"
+
+# ── Fertig ───────────────────────────────────────────────────
+version_jetzt="$(node -p "require('./package.json').version" 2>/dev/null || echo "?")"
+printf '\n%s%s✓ Nebula v%s laeuft auf Port %s%s\n' "$gruen" "$fett" "$version_jetzt" "$port" "$aus"
+[ -n "$sicherung" ] && printf ' Sicherung vor dem Update: %s\n' "$sicherung"
+printf ' Log mitlesen: docker compose --profile full logs -f app\n\n'
diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts
index 783efbb..9b6525f 100644
--- a/src/lib/changelog.ts
+++ b/src/lib/changelog.ts
@@ -41,6 +41,51 @@ export const KIND_META: Record
};
export const CHANGELOG: Release[] = [
+ {
+ version: "1.5.0",
+ date: "2026-07-27",
+ headline: "Sicherheitsdurchsicht und eine Anleitung für den Server.",
+ changes: [
+ {
+ kind: "behoben",
+ text:
+ "Ein Einmalkennwort ließ sich innerhalb seiner Gültigkeit mehrfach verwenden. " +
+ "Das eingelöste Zeitfenster wird jetzt gemerkt — RFC 6238 verlangt das " +
+ "ausdrücklich.",
+ },
+ {
+ kind: "behoben",
+ text:
+ "Der Bildoptimierer von Next war ohne Anmeldung erreichbar und reichte Bilder " +
+ "durch libvips, für das es offene Schwachstellen gibt. Nebula benutzt ihn " +
+ "nirgends — jetzt ist er abgeschaltet.",
+ },
+ {
+ kind: "neu",
+ text:
+ "DEPLOYMENT.md: Schritt für Schritt vom leeren Server bis zur Instanz hinter " +
+ "TLS, mit Firewall, Sicherungen und Betrieb.",
+ },
+ {
+ kind: "neu",
+ text:
+ "scripts/update.sh rollt ein Update aus: prüfen, holen, sichern, bauen, " +
+ "starten, Gesundheit prüfen — und nennt bei einem Fehlschlag den Weg zurück.",
+ },
+ {
+ kind: "besser",
+ text:
+ "Umsortieren im Brett und das Anlegen von Vorlagen sind jetzt ebenfalls " +
+ "ratenbegrenzt.",
+ },
+ {
+ kind: "besser",
+ text:
+ "Die Bereinigung des Markdowns ist mit elf Tests festgehalten: Skripte, " +
+ "Ereignis-Attribute, javascript: und data: kommen nicht durch.",
+ },
+ ],
+ },
{
version: "1.4.0",
date: "2026-07-26",