feat(admin): toggle "mode debug" — masque le bouton logs/métriques côté patient
Nouveau flag ConfigStore.debugEnabled (défaut false), propagé via le ContentProvider (/config colonne debug_enabled) et piloté par un Switch dans l'écran Système de l'app admin. - ConfigStore : champ debugEnabled + (de)sérialisation JSON - KazeiaTelemetryProvider : colonne debug_enabled (query + update) - KazeiaService : applique debugEnabled → _debugMode au boot et au reload config - ChatActivity : _debugMode gate désormais la VISIBILITÉ du bouton debug (applyDebugGate). Off → bouton masqué + panneau forcé fermé (écran patient épuré). On → bouton visible, ouvre logs + métriques CPU/GPU/NPU/RAM. - activity_chat.xml : btnDebugToggle visibility=gone par défaut (pas de flash) - app-admin : KazeiaConfigClient/ConfigRepository + DebugCard dans SystemScreen Vérifié device : round-trip provider debug_enabled 0→1→0 OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d1baacd096
commit
93a3953614
|
|
@ -22,6 +22,9 @@ class ConfigRepository(context: Context) {
|
||||||
fun updateTts(enabled: Boolean): Boolean =
|
fun updateTts(enabled: Boolean): Boolean =
|
||||||
client.updateConfig(ttsEnabled = enabled)
|
client.updateConfig(ttsEnabled = enabled)
|
||||||
|
|
||||||
|
fun updateDebug(enabled: Boolean): Boolean =
|
||||||
|
client.updateConfig(debugEnabled = enabled)
|
||||||
|
|
||||||
fun updateSpeakerModel(modelId: String): Boolean =
|
fun updateSpeakerModel(modelId: String): Boolean =
|
||||||
client.updateConfig(speakerModelId = modelId)
|
client.updateConfig(speakerModelId = modelId)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ class KazeiaConfigClient(private val resolver: ContentResolver) {
|
||||||
val thinkerTemperature: Float,
|
val thinkerTemperature: Float,
|
||||||
val ragEnabled: Boolean = false,
|
val ragEnabled: Boolean = false,
|
||||||
val ragThreshold: Float = 0.82f,
|
val ragThreshold: Float = 0.82f,
|
||||||
val ragTopK: Int = 3
|
val ragTopK: Int = 3,
|
||||||
|
val debugEnabled: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ModelInfo(
|
data class ModelInfo(
|
||||||
|
|
@ -57,7 +58,8 @@ class KazeiaConfigClient(private val resolver: ContentResolver) {
|
||||||
thinkerTemperature = c.getFloat(c.getColumnIndexOrThrow("thinker_temperature")),
|
thinkerTemperature = c.getFloat(c.getColumnIndexOrThrow("thinker_temperature")),
|
||||||
ragEnabled = c.getColumnIndex("rag_enabled").let { if (it >= 0) c.getInt(it) == 1 else false },
|
ragEnabled = c.getColumnIndex("rag_enabled").let { if (it >= 0) c.getInt(it) == 1 else false },
|
||||||
ragThreshold = c.getColumnIndex("rag_threshold").let { if (it >= 0) c.getFloat(it) else 0.82f },
|
ragThreshold = c.getColumnIndex("rag_threshold").let { if (it >= 0) c.getFloat(it) else 0.82f },
|
||||||
ragTopK = c.getColumnIndex("rag_top_k").let { if (it >= 0) c.getInt(it) else 3 }
|
ragTopK = c.getColumnIndex("rag_top_k").let { if (it >= 0) c.getInt(it) else 3 },
|
||||||
|
debugEnabled = c.getColumnIndex("debug_enabled").let { if (it >= 0) c.getInt(it) == 1 else false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
@ -95,7 +97,8 @@ class KazeiaConfigClient(private val resolver: ContentResolver) {
|
||||||
thinkerTemperature: Float? = null,
|
thinkerTemperature: Float? = null,
|
||||||
ragEnabled: Boolean? = null,
|
ragEnabled: Boolean? = null,
|
||||||
ragThreshold: Float? = null,
|
ragThreshold: Float? = null,
|
||||||
ragTopK: Int? = null
|
ragTopK: Int? = null,
|
||||||
|
debugEnabled: Boolean? = null
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val v = ContentValues()
|
val v = ContentValues()
|
||||||
cascadeEnabled?.let { v.put("cascade_enabled", it) }
|
cascadeEnabled?.let { v.put("cascade_enabled", it) }
|
||||||
|
|
@ -109,6 +112,7 @@ class KazeiaConfigClient(private val resolver: ContentResolver) {
|
||||||
ragEnabled?.let { v.put("rag_enabled", it) }
|
ragEnabled?.let { v.put("rag_enabled", it) }
|
||||||
ragThreshold?.let { v.put("rag_threshold", it) }
|
ragThreshold?.let { v.put("rag_threshold", it) }
|
||||||
ragTopK?.let { v.put("rag_top_k", it) }
|
ragTopK?.let { v.put("rag_top_k", it) }
|
||||||
|
debugEnabled?.let { v.put("debug_enabled", it) }
|
||||||
if (v.size() == 0) return true
|
if (v.size() == 0) return true
|
||||||
return runCatching {
|
return runCatching {
|
||||||
resolver.update(CONFIG_URI, v, null, null) > 0
|
resolver.update(CONFIG_URI, v, null, null) > 0
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,21 @@ fun SystemScreen() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DebugCard(
|
||||||
|
enabled = cfg.debugEnabled,
|
||||||
|
onToggle = { newValue ->
|
||||||
|
val ok = repo.updateDebug(newValue)
|
||||||
|
if (ok) config = repo.config()
|
||||||
|
scope.launch {
|
||||||
|
snackbar.showSnackbar(
|
||||||
|
if (ok) "Mode debug ${if (newValue) "activé" else "désactivé"} — appliqué immédiatement côté patient."
|
||||||
|
else "Échec : Kazeia patient injoignable.",
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
ModelSection(
|
ModelSection(
|
||||||
title = "Speaker",
|
title = "Speaker",
|
||||||
subtitle = "LLM qui répond au patient",
|
subtitle = "LLM qui répond au patient",
|
||||||
|
|
@ -373,6 +388,45 @@ private fun TtsCard(enabled: Boolean, onToggle: (Boolean) -> Unit) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DebugCard(enabled: Boolean, onToggle: (Boolean) -> Unit) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
"Mode debug",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
if (enabled)
|
||||||
|
"ACTIVÉ — le bouton debug (logs + métriques CPU/GPU/NPU/RAM) est visible dans l'app patient."
|
||||||
|
else
|
||||||
|
"DÉSACTIVÉ — écran patient épuré, bouton debug masqué.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = enabled,
|
||||||
|
onCheckedChange = onToggle,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = StateReady,
|
||||||
|
checkedTrackColor = StateReady.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ModelSection(
|
private fun ModelSection(
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,12 @@ class ConfigStore private constructor(private val file: File) {
|
||||||
* pertinent ~0.88). Plus haut = plus strict. Tunable depuis l'admin. */
|
* pertinent ~0.88). Plus haut = plus strict. Tunable depuis l'admin. */
|
||||||
val ragThreshold: Float = 0.82f,
|
val ragThreshold: Float = 0.82f,
|
||||||
/** Nombre max de chunks récupérés/injectés par tour. */
|
/** Nombre max de chunks récupérés/injectés par tour. */
|
||||||
val ragTopK: Int = 3
|
val ragTopK: Int = 3,
|
||||||
|
/** Mode debug : quand true, l'app patient affiche le bouton qui ouvre le
|
||||||
|
* panneau de logs + métriques système (CPU/GPU/NPU/RAM). Quand false
|
||||||
|
* (défaut prod), le bouton est masqué — l'écran reste épuré pour le
|
||||||
|
* patient. Pilotable depuis l'app admin. */
|
||||||
|
val debugEnabled: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -172,6 +177,7 @@ class ConfigStore private constructor(private val file: File) {
|
||||||
root.put("rag_enabled", cfg.ragEnabled)
|
root.put("rag_enabled", cfg.ragEnabled)
|
||||||
root.put("rag_threshold", cfg.ragThreshold.toDouble())
|
root.put("rag_threshold", cfg.ragThreshold.toDouble())
|
||||||
root.put("rag_top_k", cfg.ragTopK)
|
root.put("rag_top_k", cfg.ragTopK)
|
||||||
|
root.put("debug_enabled", cfg.debugEnabled)
|
||||||
root.put("speaker", JSONObject().apply {
|
root.put("speaker", JSONObject().apply {
|
||||||
put("model_id", cfg.speaker.modelId)
|
put("model_id", cfg.speaker.modelId)
|
||||||
put("system_prompt", cfg.speaker.systemPrompt)
|
put("system_prompt", cfg.speaker.systemPrompt)
|
||||||
|
|
@ -197,6 +203,7 @@ class ConfigStore private constructor(private val file: File) {
|
||||||
ragEnabled = root.optBoolean("rag_enabled", defaults.ragEnabled),
|
ragEnabled = root.optBoolean("rag_enabled", defaults.ragEnabled),
|
||||||
ragThreshold = root.optDouble("rag_threshold", defaults.ragThreshold.toDouble()).toFloat(),
|
ragThreshold = root.optDouble("rag_threshold", defaults.ragThreshold.toDouble()).toFloat(),
|
||||||
ragTopK = root.optInt("rag_top_k", defaults.ragTopK),
|
ragTopK = root.optInt("rag_top_k", defaults.ragTopK),
|
||||||
|
debugEnabled = root.optBoolean("debug_enabled", defaults.debugEnabled),
|
||||||
speaker = root.optJSONObject("speaker")?.let { js ->
|
speaker = root.optJSONObject("speaker")?.let { js ->
|
||||||
ModelConfig(
|
ModelConfig(
|
||||||
modelId = js.optString("model_id", defaults.speaker.modelId),
|
modelId = js.optString("model_id", defaults.speaker.modelId),
|
||||||
|
|
|
||||||
|
|
@ -703,6 +703,7 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
|
||||||
// (admin app → ContentProvider.update → ConfigStore.save → listener)
|
// (admin app → ContentProvider.update → ConfigStore.save → listener)
|
||||||
val store = com.kazeia.config.ConfigStore.get(this)
|
val store = com.kazeia.config.ConfigStore.get(this)
|
||||||
runtimeConfig = store.current()
|
runtimeConfig = store.current()
|
||||||
|
_debugMode.value = runtimeConfig.debugEnabled // gate UI debug (bouton logs/métriques)
|
||||||
store.addListener { new -> handleConfigChange(new) }
|
store.addListener { new -> handleConfigChange(new) }
|
||||||
registerConfigReloadReceiver()
|
registerConfigReloadReceiver()
|
||||||
registerRagReloadReceiver()
|
registerRagReloadReceiver()
|
||||||
|
|
@ -2173,6 +2174,10 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
|
||||||
private fun handleConfigChange(new: com.kazeia.config.ConfigStore.RuntimeConfig) {
|
private fun handleConfigChange(new: com.kazeia.config.ConfigStore.RuntimeConfig) {
|
||||||
val old = runtimeConfig
|
val old = runtimeConfig
|
||||||
runtimeConfig = new
|
runtimeConfig = new
|
||||||
|
if (new.debugEnabled != old.debugEnabled) {
|
||||||
|
_debugMode.value = new.debugEnabled // affiche/masque le bouton debug côté patient
|
||||||
|
log("[CONFIG] debug ${old.debugEnabled}→${new.debugEnabled}")
|
||||||
|
}
|
||||||
log("[CONFIG] applied : cascade ${old.cascadeEnabled}→${new.cascadeEnabled}, " +
|
log("[CONFIG] applied : cascade ${old.cascadeEnabled}→${new.cascadeEnabled}, " +
|
||||||
"speaker ${old.speaker.modelId}→${new.speaker.modelId}, " +
|
"speaker ${old.speaker.modelId}→${new.speaker.modelId}, " +
|
||||||
"thinker ${old.thinker.modelId}→${new.thinker.modelId}")
|
"thinker ${old.thinker.modelId}→${new.thinker.modelId}")
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@ class KazeiaTelemetryProvider : ContentProvider() {
|
||||||
val cols = arrayOf(
|
val cols = arrayOf(
|
||||||
"cascade_enabled", "tts_enabled",
|
"cascade_enabled", "tts_enabled",
|
||||||
"stt_engine", "llm_engine", "tts_engine", "rag_enabled",
|
"stt_engine", "llm_engine", "tts_engine", "rag_enabled",
|
||||||
"rag_threshold", "rag_top_k",
|
"rag_threshold", "rag_top_k", "debug_enabled",
|
||||||
"speaker_model_id", "speaker_system_prompt", "speaker_temperature",
|
"speaker_model_id", "speaker_system_prompt", "speaker_temperature",
|
||||||
"thinker_model_id", "thinker_system_prompt", "thinker_temperature"
|
"thinker_model_id", "thinker_system_prompt", "thinker_temperature"
|
||||||
)
|
)
|
||||||
|
|
@ -267,6 +267,7 @@ class KazeiaTelemetryProvider : ContentProvider() {
|
||||||
cfg.sttEngine, cfg.llmEngine, cfg.ttsEngine,
|
cfg.sttEngine, cfg.llmEngine, cfg.ttsEngine,
|
||||||
if (cfg.ragEnabled) 1 else 0,
|
if (cfg.ragEnabled) 1 else 0,
|
||||||
cfg.ragThreshold, cfg.ragTopK,
|
cfg.ragThreshold, cfg.ragTopK,
|
||||||
|
if (cfg.debugEnabled) 1 else 0,
|
||||||
cfg.speaker.modelId, cfg.speaker.systemPrompt, cfg.speaker.temperature,
|
cfg.speaker.modelId, cfg.speaker.systemPrompt, cfg.speaker.temperature,
|
||||||
cfg.thinker.modelId, cfg.thinker.systemPrompt, cfg.thinker.temperature
|
cfg.thinker.modelId, cfg.thinker.systemPrompt, cfg.thinker.temperature
|
||||||
))
|
))
|
||||||
|
|
@ -361,6 +362,8 @@ class KazeiaTelemetryProvider : ContentProvider() {
|
||||||
values.getAsBoolean("rag_enabled") else cur.ragEnabled,
|
values.getAsBoolean("rag_enabled") else cur.ragEnabled,
|
||||||
ragThreshold = values.getAsFloat("rag_threshold") ?: cur.ragThreshold,
|
ragThreshold = values.getAsFloat("rag_threshold") ?: cur.ragThreshold,
|
||||||
ragTopK = values.getAsInteger("rag_top_k") ?: cur.ragTopK,
|
ragTopK = values.getAsInteger("rag_top_k") ?: cur.ragTopK,
|
||||||
|
debugEnabled = if (values.containsKey("debug_enabled"))
|
||||||
|
values.getAsBoolean("debug_enabled") else cur.debugEnabled,
|
||||||
speaker = com.kazeia.config.ConfigStore.ModelConfig(
|
speaker = com.kazeia.config.ConfigStore.ModelConfig(
|
||||||
modelId = values.getAsString("speaker_model_id") ?: cur.speaker.modelId,
|
modelId = values.getAsString("speaker_model_id") ?: cur.speaker.modelId,
|
||||||
systemPrompt = values.getAsString("speaker_system_prompt")
|
systemPrompt = values.getAsString("speaker_system_prompt")
|
||||||
|
|
|
||||||
|
|
@ -319,8 +319,12 @@ class ChatActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
|
// debugMode (piloté par l'admin via ConfigStore.debugEnabled) gate le
|
||||||
|
// BOUTON debug. Si off : bouton masqué + panneau forcé fermé (écran
|
||||||
|
// patient épuré). Si on : bouton visible, le patient/soignant ouvre le
|
||||||
|
// panneau logs+métriques via le bouton.
|
||||||
service.debugMode.collect { debug ->
|
service.debugMode.collect { debug ->
|
||||||
setDebugPanelVisible(debug)
|
applyDebugGate(debug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
|
|
@ -404,6 +408,19 @@ class ChatActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mode debug (ConfigStore.debugEnabled, piloté admin) : affiche/masque le bouton
|
||||||
|
* qui ouvre le panneau logs+métriques. Off → bouton caché + panneau forcé fermé. */
|
||||||
|
private fun applyDebugGate(enabled: Boolean) {
|
||||||
|
runOnUiThread {
|
||||||
|
findViewById<View>(R.id.btnDebugToggle)?.visibility =
|
||||||
|
if (enabled) View.VISIBLE else View.GONE
|
||||||
|
if (!enabled) {
|
||||||
|
debugPanelVisible = false
|
||||||
|
setDebugPanelVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var logFile: java.io.FileWriter? = null
|
private var logFile: java.io.FileWriter? = null
|
||||||
|
|
||||||
private fun appendLog(msg: String) {
|
private fun appendLog(msg: String) {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
android:src="@android:drawable/ic_menu_manage"
|
android:src="@android:drawable/ic_menu_manage"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
android:contentDescription="Debug"
|
android:contentDescription="Debug"
|
||||||
|
android:visibility="gone"
|
||||||
android:tint="@android:color/white" />
|
android:tint="@android:color/white" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue