dev #53

Merged
torsten merged 3 commits from dev into main 2026-09-09 13:51:01 +02:00
8 changed files with 72 additions and 7 deletions
Showing only changes of commit e02de6cbb2 - Show all commits

View File

@@ -27,7 +27,10 @@ class PushTokenRepository @Inject constructor(
appVersion = "${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}",
),
)
if (!response.isSuccessful) error("Push-Token konnte nicht registriert werden.")
if (!response.isSuccessful) {
val detail = response.errorBody()?.string().orEmpty().replace(Regex("\\s+"), " ").take(180)
error("Push-Token konnte nicht registriert werden (HTTP ${response.code()})${if (detail.isBlank()) "" else ": $detail"}")
}
}
}.onFailure { error ->
Log.w("PushTokenRepository", "Push-Token Registrierung fehlgeschlagen", error)

View File

@@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import de.harheimertc.notifications.HarheimerNotifications
import de.harheimertc.BuildConfig
import de.harheimertc.repositories.Mannschaft
import de.harheimertc.repositories.NotificationPreferences
import de.harheimertc.ui.components.LoadingState
@@ -103,6 +104,23 @@ fun NotificationSettingsScreen(
}
}
item {
NotificationCard("Geräte-Registrierung") {
Text("Der Push-Token wird dem aktuell angemeldeten Konto auf diesem Server zugeordnet:", color = Accent700)
Text(BuildConfig.API_BASE_URL, color = Accent900, style = MaterialTheme.typography.bodySmall)
Button(
onClick = viewModel::registerCurrentDevice,
enabled = !state.deviceRegistrationInProgress,
modifier = Modifier.fillMaxWidth(),
) {
Text(if (state.deviceRegistrationInProgress) "Gerät wird registriert..." else "Android-Gerät jetzt registrieren")
}
state.deviceRegistrationMessage?.let { message ->
Text(message, color = if (state.deviceRegistrationError) MaterialTheme.colorScheme.error else Color(0xFF166534))
}
}
}
if (state.loading) {
item { LoadingState("Benachrichtigungseinstellungen werden geladen...") }
} else {

View File

@@ -8,6 +8,7 @@ import de.harheimertc.repositories.LoginRepository
import de.harheimertc.repositories.MannschaftenRepository
import de.harheimertc.repositories.NotificationPreferences
import de.harheimertc.repositories.NotificationPreferencesRepository
import de.harheimertc.repositories.PushTokenRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -22,6 +23,9 @@ data class NotificationSettingsUiState(
val seasons: List<String> = emptyList(),
val error: String? = null,
val saveError: String? = null,
val deviceRegistrationMessage: String? = null,
val deviceRegistrationError: Boolean = false,
val deviceRegistrationInProgress: Boolean = false,
)
@HiltViewModel
@@ -29,6 +33,7 @@ class NotificationSettingsViewModel @Inject constructor(
private val preferencesRepository: NotificationPreferencesRepository,
private val mannschaftenRepository: MannschaftenRepository,
private val loginRepository: LoginRepository,
private val pushTokenRepository: PushTokenRepository,
) : ViewModel() {
private val _state = MutableStateFlow(NotificationSettingsUiState())
val state: StateFlow<NotificationSettingsUiState> = _state
@@ -82,6 +87,30 @@ class NotificationSettingsViewModel @Inject constructor(
update(current.copy(selectedTeamSlugs = nextTeams))
}
fun registerCurrentDevice() {
viewModelScope.launch {
_state.value = _state.value.copy(
deviceRegistrationInProgress = true,
deviceRegistrationMessage = null,
deviceRegistrationError = false,
)
pushTokenRepository.registerCurrentDevice()
.onSuccess {
_state.value = _state.value.copy(
deviceRegistrationInProgress = false,
deviceRegistrationMessage = "Dieses Android-Gerät wurde beim Server registriert.",
)
}
.onFailure { error ->
_state.value = _state.value.copy(
deviceRegistrationInProgress = false,
deviceRegistrationError = true,
deviceRegistrationMessage = error.message ?: "Android-Gerät konnte nicht registriert werden.",
)
}
}
}
private suspend fun loadTeams(settings: NotificationPreferences, seasons: List<String>, currentUserName: String, syncRemote: Boolean = false) {
mannschaftenRepository.fetchMannschaften(settings.selectedTeamSeason)
.onSuccess { teams ->

View File

@@ -8,8 +8,8 @@ LOCAL_API_BASE_URL=https://harheimertc.tsschulz.de/
PRODUCTION_API_BASE_URL=https://harheimertc.de/
# Android app versioning for Play Store uploads
ANDROID_VERSION_CODE=34
ANDROID_VERSION_NAME=0.9.29
ANDROID_VERSION_CODE=35
ANDROID_VERSION_NAME=0.9.30
# Temporary hotfix: disable R8 minification for release to avoid Retrofit generic signature stripping.
RELEASE_MINIFY_ENABLED=false

View File

@@ -47,6 +47,6 @@ const formattedLastImport = computed(() => status.value.lastSuccessfulImportAt ?
async function load() { status.value = await $fetch('/api/cms/mytischtennis') }
async function save() { busy.value = true; message.value = ''; error.value = false; try { status.value = await $fetch('/api/cms/mytischtennis', { method: 'PUT', body: form }); form.password = ''; message.value = 'Zugang verschlüsselt gespeichert.' } catch { error.value = true; message.value = 'Zugang konnte nicht gespeichert werden.' } finally { busy.value = false } }
async function runImport() { busy.value = true; message.value = ''; error.value = false; try { const result = await $fetch('/api/cms/mytischtennis/import', { method: 'POST' }); message.value = `${result.rowCount} Werte wurden aktualisiert.`; await load() } catch (caught) { error.value = true; message.value = caught?.data?.message || 'Abruf fehlgeschlagen. Details stehen im Serverlog.'; await load().catch(() => {}) } finally { busy.value = false } }
async function sendTestPush() { busy.value = true; message.value = ''; error.value = false; try { const result = await $fetch('/api/cms/notifications/test', { method: 'POST' }); message.value = result.sent > 0 ? 'Push-Test wurde an deine registrierten Android-Geräte gesendet.' : 'Kein registriertes Android-Gerät gefunden. Bitte App einmal öffnen und Benachrichtigungen erlauben.' } catch (caught) { error.value = true; message.value = caught?.data?.statusMessage || caught?.data?.message || 'Push-Test konnte nicht gesendet werden.' } finally { busy.value = false } }
async function sendTestPush() { busy.value = true; message.value = ''; error.value = false; try { const result = await $fetch('/api/cms/notifications/test', { method: 'POST' }); if (result.registeredTokenCount === 0) { message.value = 'Kein Android-Token für dieses Konto gespeichert.'; error.value = true } else if (result.skipped) { message.value = `${result.registeredTokenCount} Android-Gerät(e) registriert, aber der FCM-Push-Dienst ist auf dem Server nicht konfiguriert.`; error.value = true } else if (result.deliveryError) { message.value = `${result.registeredTokenCount} Android-Gerät(e) registriert, aber: ${result.deliveryError}`; error.value = true } else if (result.sent > 0) { message.value = `Push-Test wurde an ${result.sent} registrierte(s) Android-Gerät(e) gesendet.` } else { message.value = `${result.registeredTokenCount} Android-Gerät(e) registriert, Zustellung fehlgeschlagen. Bitte Serverlog prüfen.`; error.value = true } } catch (caught) { error.value = true; message.value = caught?.data?.statusMessage || caught?.data?.message || 'Push-Test konnte nicht gesendet werden.' } finally { busy.value = false } }
onMounted(load)
</script>

View File

@@ -1,5 +1,5 @@
import { getUserFromToken, hasRole } from '../../../utils/auth.js'
import { sendTestPushToUser } from '../../../utils/push-notifications.js'
import { androidPushTokenCountForUser, sendTestPushToUser } from '../../../utils/push-notifications.js'
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
@@ -7,6 +7,16 @@ export default defineEventHandler(async (event) => {
if (!user) throw createError({ statusCode: 401, statusMessage: 'Nicht authentifiziert' })
if (!hasRole(user, 'admin')) throw createError({ statusCode: 403, statusMessage: 'Nur Administratoren dürfen einen Push-Test auslösen.' })
const result = await sendTestPushToUser(user.id)
return { success: true, ...result }
const registeredTokenCount = await androidPushTokenCountForUser(user.id)
try {
const result = await sendTestPushToUser(user.id)
return { success: true, registeredTokenCount, ...result }
} catch (error) {
console.error('FCM Test-Push konnte nicht vorbereitet werden:', error)
return {
success: false,
registeredTokenCount,
deliveryError: 'Die FCM-Serververbindung konnte nicht aufgebaut werden. Details stehen im Serverlog.'
}
}
})

View File

@@ -102,6 +102,11 @@ export function upsertPushToken(user, { token, platform = 'android', appVersion
return user
}
export async function androidPushTokenCountForUser(userId) {
const user = (await readUsers()).find(entry => entry?.id === userId)
return pushTokensForUser(user).length
}
async function sendFcmMessage({ serviceAccount, accessToken, token, data = {} }) {
const projectId = projectIdFromServiceAccount(serviceAccount)
if (!projectId) throw new Error('FCM project_id fehlt.')