diff --git a/ANDROID_KOTLIN_PLAN.md b/ANDROID_KOTLIN_PLAN.md
index da1755d..5999d73 100644
--- a/ANDROID_KOTLIN_PLAN.md
+++ b/ANDROID_KOTLIN_PLAN.md
@@ -280,6 +280,7 @@ Detaillierte Aufgaben (priorisiert):
- B5: Config / Seiten (Inhalte)
- B5.1: Sichern/Zurücksetzen von Seiteninhalten mit Undo-Hinweis
- B5.2: Satzung: PDF-Upload-Feld und native PDF-Viewer-Integration (falls serverseitig gespeichert)
+ - B5.6: Android-Startseite weiter ausbauen: Nutzer sollen Elemente und Reihenfolge der Startseite selbst zusammenstellen koennen; Detailkonzept und Feinschliff folgen spaeter
- B6: Diagnostics / Passwort-Reset-Diagnose
- B6.1: Detail-View mit exportierbaren Logs (bei Bedarf)
- C1: Offline-/Caching-Strategie
@@ -328,33 +329,36 @@ Wenn du willst, trage ich die einzelnen Subtickets in unserem lokalen Issue-Trac
- [x] B4.1: Entwurf → Senden Flow mit Preview
- [x] B4.2: Gruppenverwaltung (CRUD)
- - [ ] B5: Config / Seiten
+ - [x] B5: Config / Seiten
- Web‑Status: Die Web‑UI bietet bereits umfassende CMS‑UIs für `cms/startseite`, `cms/vereinsmeisterschaften`, `cms/sportbetrieb` und `cms/einstellungen` (Drag&Drop, CSV‑Import/Export, Tabbed‑UIs, ImageUpload, native‑like Modals). `cms/startseite` speichert `homepage.sections` via `PUT /api/config`, `vereinsmeisterschaften` arbeitet mit CSV‑Export/Import, `sportbetrieb` kapselt Termine/Mannschaften/Spielpläne in Tabs, `einstellungen` ist ein umfangreicher Config‑Editor.
- - Android‑Status: In der Android‑App sind diese Bereiche derzeit nur rudimentär bzw. als Platzhalter umgesetzt (Startseite, Vereinsmeisterschaften, Sportbetrieb, Einstellungen, Passwort‑Reset‑Diagnose fehlen noch als vollwertige Admin‑Tools).
- - Konkrete Android‑ToDos (B5.x):
- - B5.1: `cms/startseite` (Startseiten‑Layout)
- - Implementieren: Reorderable list + Visibility Toggle, Save → `PUT /api/config` (`homepage.sections`), Lade/Save‑Snackbar, Undo/Historie.
- - B5.2: `cms/vereinsmeisterschaften`
- - Implementieren: CSV‑Load/Parser, UI zur Anzeige gruppiert nach Jahr/Kategorie, Modal für Ergebnis‑CRUD, CSV Export via `/api/cms/save-csv`.
- - B5.3: `cms/sportbetrieb`
- - Implementieren: Tabbed UI (Termine / Mannschaften / Spielpläne), Wiederverwendung von bestehenden native Komponenten (`TermineScreen`, `MannschaftenScreen`, `SpielplanScreen`) und Admin‑Modi (Add/Edit/Delete).
- - B5.4: `cms/einstellungen`
- - Implementieren: Tabbed Config Editor (Vereinsdaten, Training, Trainer, Mitgliedschaft), ImageUpload, PDF‑Feld für Satzung, Validierung + Save/Preview.
- - B5.5: Roundtrip & Tests
- - Roundtrip‑Tests: RichText ↔ Web (Quill/HTML), CSV parser/tests für Vereinsmeisterschaften, ViewModel‑Unit‑Tests und Compose‑UI‑smoke tests für Save/Load flows.
+ - Android‑Status: Implementiert — die Android‑App enthält native CMS‑Screens (`CmsStartseiteScreen`, `CmsVereinsmeisterschaftenScreen`, `CmsSportbetriebScreen`, `CmsEinstellungenScreen`) mit Save/Load‑Flows via `CmsViewModel`.
+ - Umsetzung (B5.x):
+ - [x] B5.1: `cms/startseite` (Startseiten‑Layout) — Reorderable/Visibility + Save → `PUT /api/config` (via `CmsViewModel`).
+ - [x] B5.2: `cms/vereinsmeisterschaften` — CSV‑Parser/CSV‑Save integration and modal CRUD (native UI present).
+ - [x] B5.3: `cms/sportbetrieb` — Tabbed UI reusing `Termine`, `Mannschaften`, `Spielplan` components.
+ - [x] B5.4: `cms/einstellungen` — Tabbed config editor with Vereinsdaten/Training/Trainer/Mitgliedschaft and save.
+ - [x] B5.5: Roundtrip & Tests — basic ViewModel unit tests and roundtrip checks exist; Compose UI smoke tests remain for hardening.
+ - [x] B5.6: Startseite weiter ausgebaut — zusaetzliche Elemente (`training`, `links`, `vereinsmeisterschaften`) sind konfigurierbar; Android kann Reihenfolge/Sichtbarkeit lokal speichern und Web nutzt Marker (`cookie`, `eingeloggt`) mit marker-spezifischer Persistenz: `eingeloggt` wird als individuelles User-Setting serverseitig gespeichert, `cookie` wird ausschliesslich im Browser-Cookie gehalten. Neu umgesetzt: konfigurierbare Startseiten-Widgets vom Typ `spielplan_team` (Saison + Mannschaft beim Hinzufuegen waehlbar, spaeter jederzeit aenderbar, mehrfach pro Startseite moeglich, persistiert ueber `key` + `config`).
- - [ ] B6: Diagnostics / Passwort-Reset-Diagnose (Export/Detail)
+ - [x] B6: Diagnostics / Passwort-Reset-Diagnose (Export/Detail)
- Web‑Status: `cms/passwort-reset-diagnose` zeigt vollständige Diagnose‑UI mit Suche, Maskierung, Filter (nur Auffälligkeiten) und listbaren Reset‑Versuchen; Backend: `/api/cms/password-reset-diagnostics` liefert `matchingUsers`, `attempts`, `retentionHours`.
- - Android‑Status: rudimentär/fehlend — Admin‑Diagnose ist nicht vollständig portiert.
+ - Android‑Status: umgesetzt — native Diagnose‑UI mit Suche (`email`/Name), Filter `Nur Auffälligkeiten`, `matchingUsers`‑Liste mit Schnellfilter, detaillierter Schrittansicht je Versuch (Zeit/Schritt/Status/Grund), Refresh und Share‑Export der maskierten Logs.
- Konkrete Android‑ToDos (B6.x):
- - B6.1: Implementieren Suche + Filter UI, Rendering der `attempts` mit Zeitstempeln, Status‑Badges und Details.
- - B6.2: Logs exportieren / share (falls API Export unterstützt) und Datenschutz: E‑Mail Maskierung beibehalten.
+ - [x] B6.1: Implementieren Suche + Filter UI, Rendering der `attempts` mit Zeitstempeln, Status‑Badges und Details.
+ - [x] B6.2: Logs exportieren / share (falls API Export unterstützt) und Datenschutz: E‑Mail Maskierung beibehalten.
-- [ ] C1: Offline-/Caching-Strategie (verschlüsselt für geschützte CMS-Daten)
-- [ ] C2: Tests & CI
- - [ ] C2.1: ViewModel-Unit-Tests für CMS-Flows (`CmsViewModel.load()` / `saveConfig()`)
- - [ ] C2.2: Compose-UI-Tests für kritische Flows
- - [ ] C2.3: androidTest Hilt-Stubs erweitern (falls nötig)
+- [x] C1: Offline-/Caching-Strategie (verschlüsselt für geschützte CMS-Daten)
+ - Umgesetzt: EncryptedSharedPreferences-basierter Offline-Cache mit Zeitstempel/TTL pro Cache-Key (CMS standard 24h, Reset-Diagnose 6h).
+ - Umgesetzt: Fallback auf verschlüsselte Cache-Daten bei Ladefehlern nur innerhalb der TTL, um veraltete geschützte CMS-Daten zu begrenzen.
+ - Umgesetzt: Gezielte Cache-Invalidierung bei schreibenden CMS-Operationen (Konfiguration, Benutzerverwaltung, Kontaktanfragen, Newsletter, interne News), damit Offline-Daten nach Änderungen konsistent bleiben.
+ - Umgesetzt: Passwort-Reset-Diagnose-Cache wird nur für den Standardfilter (ohne Suchbegriff) verwendet, um falsche Treffer bei gefilterten Diagnosen zu vermeiden.
+- [x] C2: Tests & CI
+ - [x] C2.1: ViewModel-Unit-Tests für CMS-Flows (`CmsViewModel.load()` / `saveConfig()`)
+ - Status: `:app:testLocalDebugUnitTest` läuft grün; `CmsViewModelTest` wurde auf aktuelle Repository-Signaturen und vollständige `load()`-Abhängigkeiten (inkl. `vereinsmeisterschaften`) aktualisiert.
+ - [x] C2.2: Compose-UI-Tests für kritische Flows
+ - Status: neuer Instrumentation-Test für `CmsPasswordResetDiagnosticsScreen` ergänzt (`diagnosticsScreen_showsFilterAndAttemptDetails`) und gezielt per `connectedLocalDebugAndroidTest` erfolgreich ausgeführt.
+ - [x] C2.3: androidTest Hilt-Stubs erweitern (falls nötig)
+ - Status: androidTest-ApiService-Stubs und Hilt-Testmodul auf neue `passwordResetDiagnostics(email, failedOnly)`-Signatur erweitert; `:app:assembleLocalDebugAndroidTest` läuft grün.
Markiere die Items, wenn erledigt — ich kann die einzelnen Punkte jetzt in Branches/PRs umsetzen.
diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts
index 702f3e4..90cdb9b 100644
--- a/android-app/app/build.gradle.kts
+++ b/android-app/app/build.gradle.kts
@@ -135,6 +135,7 @@ dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.5.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
// Hilt testing
androidTestImplementation("com.google.dagger:hilt-android-testing:2.59.2")
// Ensure Hilt runtime is available in the test APK so HiltTestApplication can be instantiated
diff --git a/android-app/app/src/androidTest/AndroidManifest.xml b/android-app/app/src/androidTest/AndroidManifest.xml
index 1e8e6d5..2dd3e86 100644
--- a/android-app/app/src/androidTest/AndroidManifest.xml
+++ b/android-app/app/src/androidTest/AndroidManifest.xml
@@ -1,9 +1,7 @@
-
+
diff --git a/android-app/app/src/androidTest/java/de/harheimertc/test/TestHiltModules.kt b/android-app/app/src/androidTest/java/de/harheimertc/test/TestHiltModules.kt
index d8d9cc0..f07a56c 100644
--- a/android-app/app/src/androidTest/java/de/harheimertc/test/TestHiltModules.kt
+++ b/android-app/app/src/androidTest/java/de/harheimertc/test/TestHiltModules.kt
@@ -41,6 +41,7 @@ object TestHiltModules {
"authStatus" -> Response.success(AuthStatusResponse(isLoggedIn = false))
"publicNews" -> Response.success(de.harheimertc.data.NewsPublicResponse(news = listOf()))
"memberNews" -> Response.success(de.harheimertc.data.NewsResponse(success = true, news = listOf()))
+ "passwordResetDiagnostics" -> Response.success(de.harheimertc.data.PasswordResetDiagnosticsResponse())
else -> throw UnsupportedOperationException("ApiService method not implemented in test double: ${method.name}")
}
}
diff --git a/android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsExistingScreensSmokeTest.kt b/android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsExistingScreensSmokeTest.kt
new file mode 100644
index 0000000..8c674f2
--- /dev/null
+++ b/android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsExistingScreensSmokeTest.kt
@@ -0,0 +1,390 @@
+package de.harheimertc.ui.screens.cms
+
+import android.content.Context
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.*
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.navigation.compose.rememberNavController
+import com.squareup.moshi.Moshi
+import de.harheimertc.data.ApiService
+import de.harheimertc.data.AuthMessageResponse
+import de.harheimertc.data.AuthStatusResponse
+import de.harheimertc.data.BirthdaysResponse
+import de.harheimertc.data.CmsUserDto
+import de.harheimertc.data.CmsUsersResponse
+import de.harheimertc.data.ConfigResponse
+import de.harheimertc.data.ContactRequest
+import de.harheimertc.data.ContactRequestDto
+import de.harheimertc.data.ContactResponse
+import de.harheimertc.data.LoginRequest
+import de.harheimertc.data.LoginResponse
+import de.harheimertc.data.LogoutRequest
+import de.harheimertc.data.MembersResponse
+import de.harheimertc.data.MembershipRequest
+import de.harheimertc.data.MembershipResponse
+import de.harheimertc.data.NewsletterCreateRequest
+import de.harheimertc.data.NewsletterCreateResponse
+import de.harheimertc.data.NewsletterDto
+import de.harheimertc.data.NewsletterGroupDto
+import de.harheimertc.data.NewsletterGroupsResponse
+import de.harheimertc.data.NewsletterListResponse
+import de.harheimertc.data.NewsletterSendResponse
+import de.harheimertc.data.NewsletterSubscriptionRequest
+import de.harheimertc.data.NewsPublicResponse
+import de.harheimertc.data.NewsResponse
+import de.harheimertc.data.NewsSaveRequest
+import de.harheimertc.data.PasskeyAuthenticationOptionsRequest
+import de.harheimertc.data.PasskeyRegistrationOptionsRequest
+import de.harheimertc.data.PasskeysResponse
+import de.harheimertc.data.PasswordResetAttemptDto
+import de.harheimertc.data.PasswordResetDiagnosticsResponse
+import de.harheimertc.data.ProfileResponse
+import de.harheimertc.data.ProfileUpdateRequest
+import de.harheimertc.data.PublicGalleryImageDto
+import de.harheimertc.data.GalleryListResponse
+import de.harheimertc.data.GalleryUploadResponse
+import de.harheimertc.data.RefreshRequest
+import de.harheimertc.data.RegistrationRequest
+import de.harheimertc.data.RemovePasskeyRequest
+import de.harheimertc.data.ResetPasswordRequest
+import de.harheimertc.data.SaveCsvRequest
+import de.harheimertc.data.SaveCsvResponse
+import de.harheimertc.data.SecureOfflineCache
+import de.harheimertc.data.SpielplanResponse
+import de.harheimertc.data.TeamTableResponse
+import de.harheimertc.data.TermineResponse
+import de.harheimertc.repositories.CmsRepository
+import de.harheimertc.repositories.MeisterschaftResult
+import kotlinx.coroutines.flow.MutableStateFlow
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import okhttp3.ResponseBody
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import retrofit2.Response
+
+@RunWith(AndroidJUnit4::class)
+class CmsExistingScreensSmokeTest {
+ @get:Rule
+ val composeTestRule = createAndroidComposeRule()
+
+ @Test
+ fun cmsDashboard_renders() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsDashboardScreen(nav, showBackNavigation = false, viewModel = viewModel)
+ }
+ composeTestRule.onNodeWithText("Vereinsmeisterschaften", substring = true).assertExists()
+ }
+
+ @Test
+ fun cmsStartseite_saveWorks() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsStartseiteScreen(nav, showBackNavigation = false, viewModel = viewModel)
+ }
+ composeTestRule.onNodeWithText("Speichern").performClick()
+ composeTestRule.waitForIdle()
+ assertTrue(api.updateConfigCalls >= 1)
+ }
+
+ @Test
+ fun cmsInhalte_saveWorks() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsInhalteScreen(nav, showBackNavigation = false, viewModel = viewModel)
+ }
+ composeTestRule.onNodeWithText("Inhalte speichern").performClick()
+ composeTestRule.waitForIdle()
+ assertTrue(api.updateConfigCalls >= 1)
+ }
+
+ @Test
+ fun cmsVereinsmeisterschaften_saveWorks() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsVereinsmeisterschaftenScreen(nav, showBackNavigation = false, viewModel = viewModel)
+ }
+ composeTestRule.onNodeWithText("Speichern").performClick()
+ composeTestRule.waitForIdle()
+ assertEquals(1, api.saveCsvCalls)
+ }
+
+ @Test
+ fun cmsSportbetrieb_saveWorks() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsSportbetriebScreen(nav, showBackNavigation = false, viewModel = viewModel)
+ }
+ composeTestRule.onNodeWithText("Speichern").performClick()
+ composeTestRule.waitForIdle()
+ assertTrue(api.updateConfigCalls >= 1)
+ }
+
+ @Test
+ fun cmsEinstellungen_saveWorks() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsEinstellungenScreen(nav, showBackNavigation = false, viewModel = viewModel)
+ }
+ composeTestRule.onNodeWithText("Speichern").performClick()
+ composeTestRule.waitForIdle()
+ assertTrue(api.updateConfigCalls >= 1)
+ }
+
+ @Test
+ fun cmsMitgliederverwaltung_clickFreischalten() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsMitgliederverwaltungScreen(nav, showBackNavigation = false, viewModel = viewModel)
+ }
+ composeTestRule.onNodeWithText("Freischalten").performClick()
+ composeTestRule.waitForIdle()
+ assertEquals(1, api.updateUserActiveCalls)
+ }
+
+ @Test
+ fun cmsBenutzer_clickRollenSpeichern() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsBenutzerScreen(nav, showBackNavigation = false, viewModel = viewModel)
+ }
+ composeTestRule.onNodeWithText("Rollen").performClick()
+ composeTestRule.onNodeWithText("Speichern").performClick()
+ composeTestRule.waitForIdle()
+ assertEquals(1, api.updateUserRolesCalls)
+ }
+
+ @Test
+ fun cmsContactRequests_replySenden() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsContactRequestsScreen(nav, showBackNavigation = false, viewModel = viewModel)
+ }
+ composeTestRule.onNodeWithText("Antworten").performClick()
+ composeTestRule.onNode(hasSetTextAction()).performTextInput("Kurze Testantwort")
+ composeTestRule.onNodeWithText("Senden").performClick()
+ composeTestRule.waitForIdle()
+ assertEquals(1, api.replyContactCalls)
+ }
+
+ @Test
+ fun cmsNewsletter_createAndSave() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsNewsletterScreen(nav, showBackNavigation = false, viewModel = viewModel, canWriteOverride = true)
+ }
+ composeTestRule.onNodeWithText("Newsletter erstellen").performClick()
+ composeTestRule.onNode(hasSetTextAction()).performTextInput("Testnewsletter")
+ composeTestRule.onAllNodes(hasText("Speichern")).onFirst().performClick()
+ composeTestRule.waitForIdle()
+ assertEquals(1, api.createNewsletterCalls)
+ }
+
+ @Test
+ fun cmsPasswordResetDiagnostics_renders() {
+ val api = RecordingApiService()
+ val viewModel = createViewModel(api)
+ renderWithState(viewModel)
+
+ composeTestRule.setContent {
+ val nav = rememberNavController()
+ CmsPasswordResetDiagnosticsScreen(nav, showBackNavigation = false, viewModel = viewModel)
+ }
+ composeTestRule.onNodeWithText("Passwort-Reset-Diagnose", substring = true).assertExists()
+ }
+
+ private fun createViewModel(api: RecordingApiService): CmsViewModel {
+ val context = ApplicationProvider.getApplicationContext()
+ val cache = SecureOfflineCache(context, Moshi.Builder().build())
+ val repository = CmsRepository(api, cache)
+ return CmsViewModel(repository)
+ }
+
+ private fun renderWithState(viewModel: CmsViewModel) {
+ val readyState = CmsUiState(
+ loading = false,
+ saving = false,
+ error = null,
+ message = null,
+ config = ConfigResponse(),
+ users = listOf(
+ CmsUserDto(id = "pending-1", name = "Pending User", email = "pending@example.com", active = false, roles = emptyList()),
+ CmsUserDto(id = "active-1", name = "Active User", email = "active@example.com", active = true, roles = listOf("vorstand")),
+ ),
+ contactRequests = listOf(
+ ContactRequestDto(id = "request-1", name = "Kontakt Test", email = "kontakt@example.com", message = "Bitte melden", status = "offen"),
+ ),
+ newsletters = listOf(
+ NewsletterDto(id = "newsletter-1", title = "Sommerinfo", subject = "Sommerinfo", status = "draft"),
+ ),
+ newsletterGroups = listOf(
+ NewsletterGroupDto(id = "group-1", name = "Mitglieder", description = "Alle Mitglieder"),
+ ),
+ passwordResetAttempts = listOf(
+ PasswordResetAttemptDto(requestId = "diag-1", emailMasked = "m***@example.com", failed = true),
+ ),
+ news = emptyList(),
+ meisterschaften = listOf(
+ MeisterschaftResult(year = "2025", category = "Herren", rank = "1", playerOne = "Erika Muster", playerTwo = "", note = "Titel verteidigt", imageOne = "", imageTwo = ""),
+ ),
+ )
+ val field = CmsViewModel::class.java.getDeclaredField("_state")
+ field.isAccessible = true
+ @Suppress("UNCHECKED_CAST")
+ (field.get(viewModel) as MutableStateFlow).value = readyState
+ }
+}
+
+private class RecordingApiService : ApiService {
+ var updateConfigCalls = 0
+ var saveCsvCalls = 0
+ var updateUserActiveCalls = 0
+ var updateUserRolesCalls = 0
+ var replyContactCalls = 0
+ var createNewsletterCalls = 0
+
+ private val config = ConfigResponse()
+ private val users = listOf(
+ CmsUserDto(id = "pending-1", name = "Pending User", email = "pending@example.com", active = false, roles = emptyList()),
+ CmsUserDto(id = "active-1", name = "Active User", email = "active@example.com", active = true, roles = listOf("vorstand")),
+ )
+ private val contactRequests = listOf(
+ ContactRequestDto(id = "request-1", name = "Kontakt Test", email = "kontakt@example.com", message = "Bitte melden", status = "offen"),
+ )
+ private val newsletters = listOf(
+ NewsletterDto(id = "newsletter-1", title = "Sommerinfo", subject = "Sommerinfo", status = "draft"),
+ )
+ private val groups = listOf(
+ NewsletterGroupDto(id = "group-1", name = "Mitglieder", description = "Alle Mitglieder"),
+ )
+ private val diagnostics = listOf(
+ PasswordResetAttemptDto(requestId = "diag-1", emailMasked = "m***@example.com", failed = true),
+ )
+
+ override suspend fun publicGalleryImages(): Response> = Response.success(emptyList())
+ override suspend fun postContact(req: ContactRequest): Response = Response.success(ContactResponse(ok = true))
+ override suspend fun galerieList(page: Int, perPage: Int): Response = Response.success(GalleryListResponse())
+ override suspend fun uploadGalleryImage(image: MultipartBody.Part, title: RequestBody, description: RequestBody, isPublic: RequestBody): Response = Response.success(GalleryUploadResponse())
+ override suspend fun termine(): Response = Response.success(TermineResponse())
+ override suspend fun spielplan(season: String?): Response = Response.success(SpielplanResponse())
+ override suspend fun spielplanTable(team: String, season: String?): Response = Response.success(TeamTableResponse())
+ override suspend fun publicNews(): Response = Response.success(NewsPublicResponse())
+ override suspend fun memberNews(): Response = Response.success(NewsResponse(success = true, news = emptyList()))
+ override suspend fun saveNews(request: NewsSaveRequest): Response = Response.success(AuthMessageResponse(success = true, message = "ok"))
+ override suspend fun deleteNews(id: Int): Response = Response.success(AuthMessageResponse(success = true))
+ override suspend fun mannschaften(season: String?): Response = Response.success(null)
+ override suspend fun config(): Response = Response.success(config)
+ override suspend fun updateConfig(request: ConfigResponse): Response {
+ updateConfigCalls++
+ return Response.success(request)
+ }
+ override suspend fun spielsysteme(): Response = Response.success(null)
+ override suspend fun vereinsmeisterschaften(): Response = Response.success(ResponseBody.create(null, "Jahr,Kategorie,Platz,Spieler1,Spieler2,Bemerkung,imageFilename1,imageFilename2\n\"2025\",\"Herren\",\"1\",\"Erika Muster\",\"\",\"Titel verteidigt\",\"\",\"\""))
+ override suspend fun saveCsv(request: SaveCsvRequest): Response {
+ saveCsvCalls++
+ return Response.success(SaveCsvResponse(success = true, message = "CSV gespeichert"))
+ }
+ override suspend fun generateMembershipPdf(request: MembershipRequest): Response = Response.success(MembershipResponse())
+ override suspend fun downloadMembershipPdf(downloadUrl: String): Response = Response.success(null)
+ override suspend fun login(request: LoginRequest): Response = Response.success(LoginResponse())
+ override suspend fun logout(request: LogoutRequest): Response = Response.success(Unit)
+ override suspend fun refresh(request: RefreshRequest): Response = Response.success(LoginResponse())
+ override suspend fun authStatus(): Response = Response.success(AuthStatusResponse())
+ override suspend fun resetPassword(request: ResetPasswordRequest): Response = Response.success(AuthMessageResponse())
+ override suspend fun register(request: RegistrationRequest): Response = Response.success(AuthMessageResponse())
+ override suspend fun passkeyAuthenticationOptions(request: PasskeyAuthenticationOptionsRequest): Response = Response.success(null)
+ override suspend fun passkeyLogin(request: RequestBody): Response = Response.success(LoginResponse())
+ override suspend fun passkeys(): Response = Response.success(PasskeysResponse())
+ override suspend fun passkeyRegistrationOptions(request: PasskeyRegistrationOptionsRequest): Response = Response.success(null)
+ override suspend fun registerPasskey(request: RequestBody): Response = Response.success(AuthMessageResponse())
+ override suspend fun removePasskey(request: RemovePasskeyRequest): Response = Response.success(AuthMessageResponse())
+ override suspend fun profile(): Response = Response.success(ProfileResponse())
+ override suspend fun updateProfile(request: ProfileUpdateRequest): Response = Response.success(ProfileResponse())
+ override suspend fun birthdays(): Response = Response.success(BirthdaysResponse())
+ override suspend fun members(): Response = Response.success(MembersResponse())
+ override suspend fun saveMember(request: ApiService.MemberSaveRequest): Response = Response.success(AuthMessageResponse())
+ override suspend fun deleteMember(body: Map): Response = Response.success(AuthMessageResponse())
+ override suspend fun bulkImportMembers(request: ApiService.BulkImportRequest): Response = Response.success(ApiService.BulkImportResponse())
+ override suspend fun toggleMannschaftsspieler(body: Map): Response