Provider-Wechsel nur für Admin (403 für Nutzer); App blendet Provider-Auswahl für Nicht-Admins aus; v0.3.1

This commit is contained in:
OpenClaw Dev 2026-11-01 04:17:53 +01:00
parent 161bc02bb9
commit 70d316851a
12 changed files with 102 additions and 17 deletions

View file

@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View file

@ -31,8 +31,8 @@ android {
applicationId = "dev.byteclaw.client"
minSdk = 26
targetSdk = 35
versionCode = 3
versionName = "0.3.0"
versionCode = 4
versionName = "0.3.1"
}
signingConfigs {

View file

@ -44,6 +44,8 @@ data class UiState(
/** Login screen state. */
val authBusy: Boolean = false,
val authError: String? = null,
/** From /api/auth/me: only the admin may change providers. */
val isAdmin: Boolean = false,
) {
/** What the current chat effectively uses: session override, else the global active provider. */
val effectiveProvider: ActiveProvider? get() = sessionProvider ?: provider.active
@ -266,6 +268,7 @@ class ChatViewModel(app: Application) : AndroidViewModel(app) {
}
fun loadProviders() = viewModelScope.launch {
runCatching { api().me() }.onSuccess { u -> _state.update { it.copy(isAdmin = u.admin) } }
runCatching { api().providers() }
.onSuccess { r -> _state.update { it.copy(provider = it.provider.copy(providers = r.providers, active = r.active, status = null)) } }
.onFailure { e -> if (e is UnauthorizedException) fail(e) else _state.update { it.copy(provider = it.provider.copy(status = "Provider laden fehlgeschlagen: ${e.message}")) } }

View file

@ -296,7 +296,14 @@ fun SettingsScreen(
}
status?.let { item { Text(it) } }
if (state.settings.isConfigured) {
item { ProviderSection(state.provider, onLoadProviders, onLoadModels, onSelectProvider) }
if (state.isAdmin) {
item { ProviderSection(state.provider, onLoadProviders, onLoadModels, onSelectProvider) }
} else {
item {
Text("LLM-Provider: " + (state.provider.active?.let { ProviderPresets.label(it.provider) + " · " + it.model } ?: "–") +
"\nNur der Server-Admin kann den Provider ändern.", style = MaterialTheme.typography.bodySmall)
}
}
}
}
}
@ -370,7 +377,7 @@ private fun SessionProviderChip(state: UiState, onPick: (String) -> Unit, initia
Box {
AssistChip(
onClick = { open = true }, label = { Text(label, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelSmall) },
enabled = state.settings.isConfigured && !state.busy,
enabled = state.settings.isConfigured && !state.busy && state.isAdmin,
modifier = Modifier.height(28.dp).widthIn(max = 230.dp),
)
DropdownMenu(expanded = open, onDismissRequest = { open = false }) {

View file

@ -43,7 +43,7 @@ class PreviewScreenshots(private val dark: Boolean) {
@Test fun chat() = snap("chat") {
ChatScreen(
state = UiState(
state = UiState(isAdmin = true,
settings = settings, sessionId = "demo", busy = true,
lines = listOf(
ChatLine("user", "[DEMO] Wie spät ist es in Berlin, und was steht auf golang.org?"),
@ -68,7 +68,7 @@ class PreviewScreenshots(private val dark: Boolean) {
@Test fun chatProvider() = snap("chat_provider") {
ChatScreen(
state = UiState(
state = UiState(isAdmin = true,
settings = settings, sessionId = "demo", provider = demoProviders,
sessionProvider = ActiveProvider("gonkarouter", "MiniMaxAI/MiniMax-M2.7"),
lines = listOf(
@ -83,7 +83,7 @@ class PreviewScreenshots(private val dark: Boolean) {
@Test fun chatMarkdown() = snap("chat_markdown") {
ChatScreen(
state = UiState(
state = UiState(isAdmin = true,
settings = settings, sessionId = "demo", online = true,
lines = listOf(
ChatLine("user", "[DEMO] Wie lese ich in Go eine Datei zeilenweise?"),
@ -96,7 +96,7 @@ class PreviewScreenshots(private val dark: Boolean) {
@Test fun sessionsSearch() = snap("sessions_search") {
SessionsScreen(
state = UiState(
state = UiState(isAdmin = true,
settings = settings,
sessions = listOf(
Session("1", "[DEMO] Go-Datei zeilenweise lesen", "2026-10-25T03:32:00Z"),
@ -110,7 +110,7 @@ class PreviewScreenshots(private val dark: Boolean) {
@Test fun sessions() = snap("sessions") {
SessionsScreen(
state = UiState(
state = UiState(isAdmin = true,
settings = settings,
sessions = listOf(
Session("1", "[DEMO] Wie spät ist es in Berlin?", "2026-10-25T03:32:00Z"),
@ -126,14 +126,14 @@ class PreviewScreenshots(private val dark: Boolean) {
@Test fun settings() = snap("settings") {
SettingsScreen(
state = UiState(settings = settings), onSave = {}, onTest = { _, _ -> }, onBack = {},
state = UiState(isAdmin = true, settings = settings), onSave = {}, onTest = { _, _ -> }, onBack = {},
initialStatus = "Verbindung OK · Provider: Dahl (dahl.global) · Modell: MiniMaxAI/MiniMax-M2.7 [DEMO]",
)
}
@Test fun providers() = snap("providers") {
SettingsScreen(
state = UiState(
state = UiState(isAdmin = true,
settings = settings,
provider = ProviderUi(
active = ActiveProvider("dahl", "MiniMaxAI/MiniMax-M2.7"),
@ -152,12 +152,12 @@ class PreviewScreenshots(private val dark: Boolean) {
}
@Test fun login() = snap("login") {
LoginScreen(state = UiState(settings = ServerSettings("https://demo.byteclaw.example", "", "crypto")), onAuthenticate = { _, _, _, _ -> })
LoginScreen(state = UiState(isAdmin = true, settings = ServerSettings("https://demo.byteclaw.example", "", "crypto")), onAuthenticate = { _, _, _, _ -> })
}
@Test fun loginRegister() = snap("login_register") {
LoginScreen(
state = UiState(settings = ServerSettings("https://demo.byteclaw.example"), authError = "Benutzername ist bereits vergeben"),
state = UiState(isAdmin = true, settings = ServerSettings("https://demo.byteclaw.example"), authError = "Benutzername ist bereits vergeben"),
onAuthenticate = { _, _, _, _ -> }, initialRegister = true, initialUser = "crypto",
)
}