# ATM10 Stats – Design-Spezifikation Stand: 2026-10-13 · Status: zur Freigabe ## 1. Ziel Ein öffentliches, modernes Statistikportal für den bei Mine-Hoster gehosteten All-The-Mods-10-Server (ATM10). Vorbild für Inhalt und Anmutung ist craftattack.one/stats (Kennzahlen, „Gerade online“, Top-10-Listen); das Portal geht darüber hinaus mit Spielerprofilen, historischen Diagrammen und ATM10-spezifischen Auswertungen (Mods, FTB Quests, Servergesundheit). Erfolgskriterien: - Spieler sehen ihre echten Werte innerhalb weniger Sekunden (Ereignisse) bzw. ≤ 5 Minuten (Zähler) nach dem Geschehen. - Keine erfundenen Daten: Jede angezeigte Kennzahl hat eine dokumentierte, nachprüfbare Quelle. - Die Mod verursacht keine messbare Tick-Last; kein Datenverlust bei Neustarts von Gameserver oder Backend. - Betrieb per Docker Compose hinter dem vorhandenen Traefik in der DMZ. ## 2. Verifizierte Ausgangslage Quelle: `server-snapshot/` (vom Betreiber per SFTP bereitgestellt). | Fakt | Beleg | |---|---| | Minecraft 1.21.1 | `DataVersion: 3955` in `world/stats/*.json` | | NeoForge 21.1.x, Java 21 | Modliste (`*-neoforge-1.21.1-*`), MC 1.21.1 setzt Java 21 voraus. Exakte NeoForge-/Java-Version: fehlt im Log-Ausschnitt → wird von der Mod beim Start ermittelt und gemeldet | | 464 Mod-JARs | `mod-list.txt` | | FTB Quests 2101.1.36, FTB Teams 2101.1.11, FTB Chunks, FTB Ranks, FTB Essentials | `mod-list.txt` | | Quest-Fortschritt pro **Team** als SNBT (`task_progress`, `completed`, `started`, `claimed_rewards`, `completion_count`) | `world-ftbquests/*.snbt` | | Spark 1.10.124 installiert | `mod-list.txt` (wird **nicht** als API genutzt) | | Log enthält Login (UUID + IP), Join/Leave, Advancements/Goals/Challenges im Klartext, „Can't keep up“ | `latest.log` | | Server läuft mit ~2,2 GB Heap und hat Lags bis 10 s | `latest.log` (AllTheLeaks, Can't keep up) | | Mine-Hoster: SFTP-Vollzugriff, eigene Mods in `mods/` möglich | Mine-Hoster-Support (SFTP-, Mods-, Modpack-Artikel) | | Ausgehendes HTTPS vom Gameserver | **unbekannt** → Hybrid-Design (Push + SFTP-Pull) | Mod-spezifische Zähler, die bereits in Vanilla-Stats auftauchen: `apotheosis:world_tiers_activated`, `lootr:looted_stat`, `waystones:waystone_activated`. Item-/Block-Statistiken enthalten Einträge aus u. a. mekanism, powah, mysticalagriculture, ars_nouveau, irons_spellbooks, modern_industrialization, oritech, create, refinedstorage, draconicevolution, silentgear, occultism, theurgy, xycraft_world, minecolonies. Nicht installiert (daher keine Integration): Blood Magic, Botania. Datenschutz: `latest.log` enthält Spieler-IP-Adressen. `server-snapshot/` wird nicht versioniert; Testfixtures aus dem Log werden anonymisiert (IP entfernt). ## 3. Architektur ``` Mine-Hoster (ATM10, NeoForge 1.21.1) Eigener Server in der DMZ ┌──────────────────────────────┐ HTTPS POST OPNsense → Traefik → Docker Compose │ atmstats (serverseitige Mod) │ ──────────────▶ ┌────────────────────────────────┐ │ Event-Hooks, Outbox-Journal │ Bearer + HMAC │ api Fastify/TS: ingest, │ │ async Sender mit Backoff │ │ REST, SSE, admin │ └──────────────────────────────┘ │ worker SFTP-Pull, Snapshots, │ ▲ world/stats, world/ftbquests, │ Retention │ │ config/ftbquests, logs │ web Next.js (App Router) │ └───────────── SFTP-Pull ◀────────────────│ db PostgreSQL 16 │ │ backup pg_dump täglich │ └────────────────────────────────┘ ``` Monorepo: ``` mod/ NeoForge-Mod (Gradle, Java 21, ModDevGradle) backend/ api + worker (TypeScript, Fastify, gemeinsamer Code, zwei Entrypoints) web/ Next.js-Frontend (TypeScript) shared/ gemeinsame TS-Typen + Zod-Schemas (Ingest-Protokoll, API-DTOs) deploy/ docker-compose.yml, .env.example, Backup-Skript tools/ Simulator (sendet realistische Mod-Batches), Fixture-Anonymisierer docs/ README-Ergänzungen, ARCHITECTURE, DATA-SOURCES, OPERATIONS ``` ### 3.1 Datenquellen und Zuständigkeit | Daten | Primärquelle | Fallback | |---|---|---| | Online-Status, Sessions | Mod: Login/Logout-Events | SFTP: Log-Parser (`joined/left the game`, `UUID of player`) | | Gesamtzähler (Stats) | SFTP: `world/stats/.json` (maßgeblich, absolut) | — | | Zähler zwischen Speicherungen | Mod: 5-min-Snapshot der Live-`ServerStatsCounter` online Spieler (absolute Werte) | — | | Tode (Ursache, Killer, Dimension, Position grob) | Mod: `LivingDeathEvent` | Log: nicht verlässlich (Death-Messages lokalisiert/mod-abhängig) → nur Zähler aus Stats | | Spieler-/Boss-Kills mit Waffe/Dimension | Mod: `LivingDeathEvent` mit Spieler als Quelle | Stats `minecraft:killed` (nur Zähler) | | Advancements | Mod: `AdvancementEvent.AdvancementEarnEvent` | Log-Parser (`has made the advancement/reached the goal/completed the challenge`) | | Dimensionswechsel | Mod: `PlayerChangedDimensionEvent` | — | | FTB-Quest-Fortschritt | Mod liest `world/ftbquests/*.snbt` (eigener Thread, nur bei Änderung) | SFTP: dieselben Dateien | | Quest-Definitionen (Kapitel, Titel) | Mod liest `config/ftbquests/quests/**` beim Start | SFTP | | Teams | SNBT-Dateien von FTB Teams/Quests (`world/ftbteams/` wird beim Bau verifiziert) | SFTP | | Versionen, Modliste | Mod: `ModList`, `SharedConstants`, `System.getProperty("java.version")` beim Start | Log-Header (falls vorhanden) | | Servergesundheit (MSPT/TPS, Spieler, geladene Chunks, Heap) | Mod: eigene Messung über `ServerTickEvent.Pre/Post`, 60-s-Aggregat | Log: „Can't keep up“-Zählung | Konfliktregel für Zähler: Werte sind **absolut** (nicht Deltas). `stat_current` übernimmt pro Schlüssel den Wert mit dem neuesten Beobachtungszeitpunkt; ein kleinerer Wert ersetzt einen größeren nur, wenn er aus einer neueren Stats-Datei stammt (z. B. nach Admin-Reset). Damit sind wiederholte und verspätete Lieferungen unschädlich. Quest-Texte: Titel können als Übersetzungsschlüssel vorliegen (`ftbquestslangsplitter` ist installiert). Die Mod liest zusätzlich `config/ftbquests/quests/lang/en_us.snbt` bzw. die Lang-Dateien, falls vorhanden; sonst wird die Quest-ID angezeigt. Exakte Pfade werden in der Umsetzung anhand einer Kopie von `config/ftbquests/` verifiziert, bevor der Parser fest verdrahtet wird. ### 3.2 Bewusst nicht unterstützt Ohne verlässliche, dokumentierte serverseitige Quelle oder nur mit ungeprüften Mod-Interna erreichbar; wird im Portal unter „Datenquellen“ transparent ausgewiesen: - Energieproduktion/-verbrauch von Maschinen (Mekanism, Powah …) - Inhalte von AE2-/Refined-Storage-Netzwerken - Mana, Zauber, Source (Ars Nouveau, Iron's Spellbooks), Occultism-Rituale - MineColonies-Kolonien - exakte Spielerposition/Heatmaps (Datenschutz + Last) Spätere Erweiterung ist über das Ereignisprotokoll möglich (neue Event-Typen), jeweils erst nach Prüfung der jeweiligen Mod-API gegen die installierte Version. ## 4. Mod `atmstats` - NeoForge 21.1.x (Build gegen die zur ATM10-Version passende Version), Java 21, ModDevGradle. Mod-ID `atmstats`. - Rein serverseitig: `displayTest = "IGNORE_ALL_VERSION"` in `neoforge.mods.toml`; Clients brauchen die Mod nicht. Keine Registrierungen (Blöcke, Items, Netzwerkpakete). - Keine Compile-Abhängigkeit auf andere Mods. ### 4.1 Threading und Last - Event-Handler auf dem Server-Thread erzeugen nur kleine, unveränderliche Event-Objekte und legen sie in eine `ConcurrentLinkedQueue` (O(1), keine I/O, keine Serialisierung). - Stats-Snapshot alle 5 min (konfigurierbar): auf dem Server-Thread werden je online Spieler die Zählerwerte in ein `Object2IntMap` kopiert; Serialisierung und Versand erfolgen im Mod-eigenen Daemon-Thread. Der Snapshot wird über mehrere Ticks verteilt, falls viele Spieler online sind (max. 2 Spieler/Tick). - Ein einzelner Daemon-Worker-Thread: Queue → Journal → Sender. SNBT-Dateilesen ebenfalls dort (nur bei geänderter mtime, alle 2 min). - Tick-Messung: `System.nanoTime()` in Pre/Post, Aggregat (avg/p95/max MSPT) im Ringpuffer, Versand einmal pro Minute. ### 4.2 Zuverlässigkeit - Event-ID: UUIDv7 (zeitlich sortierbar), erzeugt beim Entstehen des Events. - Outbox-Journal: `/atmstats/outbox/` als Segmentdateien (JSON Lines, fsync beim Rotieren, max. 1 MB/Segment). Ein Segment wird erst gelöscht, wenn das Backend alle IDs bestätigt hat. - Begrenzung: max. 50 MB bzw. 7 Tage; bei Überschreitung werden die ältesten **Stats-Snapshots** zuerst verworfen (sind durch spätere ersetzbar), Ereignisse zuletzt; Verwerfen wird geloggt. - Beim Serverstopp (`ServerStoppingEvent`): Queue synchron ins Journal flushen (Zeitlimit 2 s), Session-Ende für alle online Spieler erzeugen. - Beim Start: Journal-Segmente werden erneut gesendet; ein `server_start`-Event ermöglicht dem Backend, offene Sessions eines Absturzes zu schließen. - Versand: `java.net.http.HttpClient` (HTTP/2, TLS-Verifikation an), Batches bis 500 Events/256 KB, gzip, Timeout 10 s. Backoff exponentiell 1 s → 5 min mit Jitter. Bei 4xx (außer 408/429) wird der Batch nicht endlos wiederholt, sondern in `outbox/rejected/` abgelegt und geloggt. - Startup-Selbsttest: `GET /api/ingest/ping` → Ergebnis ins Log („atmstats: backend reachable / NOT reachable: “). Damit wird die offene Frage „ausgehendes HTTPS erlaubt?“ beim ersten Start beantwortet. ### 4.3 Konfiguration (`config/atmstats-server.toml`, NeoForge ModConfigSpec) `enabled`, `endpoint`, `serverId`, `token`, `hmacSecret`, `statsSnapshotMinutes` (5), `questScanMinutes` (2), `healthIntervalSeconds` (60), `maxOutboxMb` (50). Ohne Token bleibt die Mod passiv (nur Log-Hinweis). ### 4.4 Protokoll (Version 1) ``` POST /api/ingest Authorization: Bearer X-ATM-Server: X-ATM-Timestamp: X-ATM-Signature: hex(HMAC-SHA256(secret, timestamp + "." + sha256(body))) Content-Encoding: gzip { "protocol": 1, "events": [ { "id": "", "type": "...", "ts": , ...payload } ] } → 200 { "accepted": ["", ...], "duplicate": ["", ...], "rejected": [{ "id", "reason" }] } ``` Event-Typen: `server_start` (Versionen, Modliste), `server_stop`, `player_join`, `player_leave`, `player_death`, `entity_kill`, `advancement`, `dimension_change`, `stats_snapshot` (absolute Werte je Kategorie), `quest_progress` (Team-ID, abgeschlossene Quest-IDs mit Zeitstempel, Task-Fortschritt-Zähler), `quest_defs` (Kapitel/Quests/Titel, nur bei Änderung, mit Hash), `team_info`, `health`. Schemas als Zod in `shared/` und als JSON-Schema-Testfixtures, gegen die auch die Java-Serialisierung getestet wird. ## 5. Backend TypeScript, Node 22 LTS, Fastify, Zod, `pg` mit SQL (kein ORM), Migrationen mit `node-pg-migrate`. Zwei Prozesse aus einer Codebasis: **api** - `GET /api/ingest/ping` (authentifiziert), `POST /api/ingest`. Prüft Token (konstante Zeit), Zeitfenster ±5 min, HMAC; Rate-Limit; Body-Limit 2 MB entpackt. Verarbeitung pro Batch in einer Transaktion: `INSERT INTO events … ON CONFLICT (event_id) DO NOTHING`, nur neu eingefügte Events werden projiziert (sessions, deaths, stat_current …). Danach Benachrichtigung via `pg_notify('atm_live', …)`. - Öffentlich: `GET /api/overview`, `/api/online`, `/api/leaderboards` (Liste der Boards), `/api/leaderboards/:board?period=all|30d|7d`, `/api/players?search=`, `/api/players/:uuid`, `/api/players/:uuid/history?metric=&range=`, `/api/players/:uuid/items?mod=`, `/api/mods` (Modliste + Aktivität je Mod), `/api/quests` (Kapitelfortschritt), `/api/server` (Versionen, Health-Verlauf), `/api/sources` (Datenquellen-Status: letzter Push, letzter SFTP-Sync), `/api/avatar/:uuid` (Proxy-Cache für Spielerköpfe, 24 h; keine Besucher-IPs an Dritte). - `GET /api/live` (SSE): abonniert `LISTEN atm_live`; Events `online`, `death`, `advancement`, `quest`, `counters`; Heartbeat 25 s, `Last-Event-ID`-Resume aus einem In-Memory-Ringpuffer (500 Einträge). - Admin (`/api/admin/*`): Login mit Benutzer/Passwort (argon2id), Session-Cookie (httpOnly, Secure, SameSite=Strict), CSRF-Token im Header, Login-Rate-Limit. Funktionen: Status Mod/SFTP, letzte Fehler, manueller Sync, Spieler ausblenden/einblenden, Ingest-Token rotieren (Anzeige einmalig). Erster Admin wird per CLI (`npm run admin:create`) angelegt. - Ausgeblendete Spieler erscheinen in keiner öffentlichen Antwort. - Sicherheitsheader (über Fastify-Helmet), CORS nur eigene Domain. **worker** - SFTP-Pull (ssh2-sftp-client) alle 15 min (konfigurierbar): Verzeichnislisten von `world/stats`, `world/advancements`, `world/ftbquests`, `config/ftbquests/quests`, `logs/latest.log`. Herunterladen nur bei geänderter Größe/mtime (Zustand in `sync_state`); Log inkrementell ab Byte-Offset, Rotation erkannt über kleineren Größenwert. Ergebnisse durchlaufen dieselben Projektionen wie Ingest (synthetische, deterministische Event-IDs aus Datei+Inhalt-Hash → idempotent). - Advancements-Dateien (`world/advancements/.json`) liefern die vollständige Liste erreichter Advancements inkl. Zeitpunkten. - Snapshot-Job: stündlich `stat_snapshots` (Auswahl relevanter Metriken je Spieler), täglich um 00:05 Tageswerte; Retention: stündlich 30 Tage, täglich unbegrenzt; `server_health` Minutenwerte 14 Tage, danach Stundenaggregate. - Sitzungsbereinigung: offene Sessions ohne Lebenszeichen > 10 min nach `server_start` werden mit letztem bekannten Zeitpunkt geschlossen. - Job-Locking über `pg_try_advisory_lock`, damit Jobs nie doppelt laufen. Der Worker ist optional: ohne SFTP-Zugangsdaten läuft das System nur mit Push. Ohne erreichbaren Push läuft es nur mit Pull (dann Online-Status und Tode mit bis zu 15 min Verzögerung bzw. nur als Zähler). ## 6. Datenbank Kern-Tabellen (Details in Migrationen): - `players(uuid PK, name, first_seen, last_seen, hidden bool)` - `name_history(uuid, name, seen_at)` - `teams(team_id PK, name, type)`, `team_members(team_id, uuid)` - `events(event_id PK, server_id, type, ts, received_at, source 'push'|'sftp', payload jsonb)` - `sessions(id, uuid, started_at, ended_at, end_reason)` - `stat_current(uuid, category, key, value bigint, observed_at, source, PK(uuid, category, key))` - `stat_snapshots(uuid, metric, bucket timestamptz, granularity 'hour'|'day', value bigint)` - `deaths(event_id, uuid, ts, dimension, cause, killer_type, killer_name)` - `kills(event_id, uuid, ts, dimension, victim_type, is_boss, weapon)` - `advancements(uuid, advancement_id, achieved_at, title, PK(uuid, advancement_id))` - `quest_defs(quest_id PK, chapter_id, chapter_title, title, def_hash)` - `quest_progress(team_id, quest_id, completed_at, PK(team_id, quest_id))` - `server_info(id, started_at, mc_version, neoforge_version, java_version, mods jsonb)` - `server_health(bucket, mspt_avg, mspt_p95, mspt_max, tps, players, chunks, heap_used_mb, heap_max_mb)` - `sync_state(path PK, size, mtime, offset, hash, synced_at)` - `admin_users`, `admin_sessions`, `ingest_tokens(id, hash, created_at, revoked_at)` Ranglisten werden aus `stat_current` mit Indizes auf `(category, key, value DESC)` berechnet. Abgeleitete Metriken (z. B. Gesamtstrecke = Summe aller `*_one_cm`, Items je Mod = Summe nach Namespace) liegen als SQL-Views vor. Eine Materialized View `leaderboard_cache` wird höchstens alle 60 s `REFRESH … CONCURRENTLY` aktualisiert. ## 7. Ranglisten (Start-Set, alle aus belegten Quellen) | Board | Metrik | |---|---| | Dauerzocker | `minecraft:custom/minecraft:play_time` (Ticks → h) | | Friedhof | `minecraft:custom/minecraft:deaths` | | Monsterjäger | `minecraft:custom/minecraft:mob_kills` | | Abrissbirne | Summe `minecraft:mined/*` | | Weltenbummler | Summe `minecraft:custom/*_one_cm` (→ km) | | Flieger | `minecraft:custom/minecraft:fly_one_cm` | | Diamantenbank | `minecraft:mined/minecraft:diamond_ore` + `deepslate_diamond_ore` (Abbau, nicht Besitz – so ausgewiesen) | | Techniker | Summe `crafted`+`used` in Tech-Namespaces (mekanism, mekanismgenerators, powah, modern_industrialization, oritech, create, immersiveengineering, appliedenergistics2, refinedstorage, draconicevolution, integrateddynamics, pneumaticcraft) | | Magier | dito Magie-Namespaces (ars_nouveau, ars_elemental, ars_additions, irons_spellbooks, occultism, theurgy, rootsclassic, forbidden_arcanus, evilcraft, reliquary) | | Farmer | dito (mysticalagriculture, mysticalagradditions, productivebees, botanypots, pamhc2*, farmersdelight, herbsandharvest) | | Questmeister | Anzahl abgeschlossener Quests des Teams (Teamleaderboard + Spieler über Mitgliedschaft) | | Abenteurer | Anzahl Advancements | | Schatzjäger | `lootr:looted_stat` | | Weltenstufe | `apotheosis:world_tiers_activated` | Die Namespace-Kategorien liegen in einer editierbaren Konfigurationsdatei (`backend/config/mod-categories.json`) und werden mit der echten Modliste abgeglichen; Namespaces ohne installierte Mod werden ignoriert und geloggt. ## 8. Frontend Next.js (App Router), TypeScript, Tailwind CSS, Recharts. Dunkles, eigenständiges Gaming-Design (kein Nachbau von craftattack-Assets), responsiv, zugänglich (Kontrast, Tastatur, `prefers-reduced-motion`). Server-Rendering mit kurzer Revalidierung; Live-Teile clientseitig per SSE. Seiten: - `/` Übersicht: Kennzahlen (Online, Spielstunden gesamt, Tode, Quests, Advancements), „Gerade online“, Live-Ticker, Top-10-Karten. - `/leaderboards` und `/leaderboards/[board]` (Zeitraum all/30d/7d aus Snapshots). - `/players` (Suche) und `/players/[name]`: Kopf, Spielzeit, Sessions-Heatmap (Wochentag × Stunde), Verlaufsdiagramme, Bewegung nach Art, Kampf (Kills nach Mob, Todesursachen), Items nach Mod (Tabs mined/crafted/used/picked_up), Advancements, Team-Quests, Rangpositionen. - `/atm10`: Aktivität je Mod/Kategorie, Quest-Kapitelfortschritt, meistgenutzte Mods. - `/server`: Versionen (MC, NeoForge, Java, ATM10-Modliste mit Suche), TPS/MSPT-, Spieler- und Heap-Verlauf. - `/sources`: Datenquellen, Aktualität, Einschränkungen (aus `DATA-SOURCES`). - `/admin`: Login, Status, Sync, Spieler ausblenden, Token-Rotation. ## 9. Deployment und Betrieb - `deploy/docker-compose.yml`: `db` (postgres:16-alpine, Volume), `api`, `worker`, `web`, `backup`. Nur `web` und `api` hängen am externen Traefik-Netz (`TRAEFIK_NETWORK`); `db` nur im internen Netz. - Traefik-Labels: Router `Host(${DOMAIN}) && PathPrefix(/api)` → api, `Host(${DOMAIN})` → web; Entrypoint/Cert-Resolver per `.env`. Optionale Middleware `ipAllowList` für `/api/ingest` (`INGEST_ALLOWED_IPS`). - OPNsense: eingehend 443 → Traefik (bereits vorhanden); für den Pull-Fallback ausgehend DMZ-Host → Mine-Hoster-SFTP-Host/-Port. - Healthchecks für alle Container; `api` und `worker` warten auf DB und führen Migrationen beim Start aus (Advisory-Lock). - Backup: täglicher `pg_dump -Fc` mit 14-Tage-Rotation in ein Volume; Restore-Anleitung in `OPERATIONS.md`. - Logs strukturiert (pino, JSON), keine Tokens/IPs im Log. ## 10. Tests - Mod (JUnit 5, ohne Minecraft-Laufzeit): Journal (Schreiben, Rotation, Recovery nach abgeschnittener letzter Zeile), Backoff, Batch-Bildung, HMAC, JSON gegen Schema-Fixtures, SNBT-Parser an den echten Quest-Dateien. Minecraft-abhängiger Code ist dünn und von der Logik getrennt. Zusätzlich manueller Testlauf auf einem lokalen NeoForge-Server (`runServer`). - Backend (Vitest + Postgres im Compose-Testprofil): Ingest-Auth (falsches Token, abgelaufener Zeitstempel, falsche Signatur), Idempotenz (gleicher Batch doppelt), Projektionen, Stats-Parser an echten `world/stats`-Dateien, Log-Parser an anonymisiertem Log, Ranglisten-SQL, SSE. - Frontend: Komponententests (Vitest + Testing Library) und Playwright-Smoke-Test gegen den Compose-Stack mit Simulator-Daten. - Simulator (`tools/simulator`): erzeugt signierte Batches im echten Protokoll. ## 11. Dokumentation `README.md` (Überblick, Schnellstart), `docs/ARCHITECTURE.md`, `docs/DATA-SOURCES.md` (jede Kennzahl → Quelle, Aktualität, Einschränkungen), `docs/OPERATIONS.md` (Deployment Traefik/OPNsense, Mod-Installation per Mine-Hoster-SFTP, Konfiguration, Token-Rotation, Backup/Restore, Neustartverhalten, Fehlerbilder), `mod/README.md`. ## 12. Offene Punkte, die in der Umsetzung geklärt werden 1. Exakte NeoForge-Version des Servers → Mod-Build gegen 21.1.x; Kompatibilität innerhalb 21.1 ist gegeben, finale Version per Startup-Log bestätigen. 2. Ausgehendes HTTPS bei Mine-Hoster → Selbsttest beim ersten Start. 3. Struktur von `config/ftbquests/quests/` und `world/ftbteams/` → Betreiber stellt Kopie bereit, bevor Quest-Titel/Team-Namen-Parser finalisiert werden; bis dahin werden Quest-IDs angezeigt. 4. SFTP-Host/Port/Benutzer → `.env` des Betreibers (nicht im Repo).