Show retrain progress in Android

This commit is contained in:
Codex
2026-06-27 09:18:18 +03:00
parent f8611a1c3f
commit b6d616ec2a
6 changed files with 192 additions and 29 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ android {
applicationId = "xyz.kusoft.tradebotmonitor"
minSdk = 26
targetSdk = 36
versionCode = 6
versionName = "0.2.3"
versionCode = 7
versionName = "0.2.4"
}
}
@@ -38,6 +38,7 @@ class MainActivity : Activity() {
private var lastError: String = ""
private var activeTab: String = "ai"
private var palette: AppPalette = AppPalette.dark()
private var trainingStatusSignature: String = ""
private val refreshRunnable = object : Runnable {
override fun run() {
@@ -103,10 +104,13 @@ class MainActivity : Activity() {
mainHandler.post {
val hadSnapshot = snapshot != null
val hadError = lastError.isNotBlank()
val newTrainingStatusSignature = trainingStatusSignature(data.retrain)
val trainingChanged = newTrainingStatusSignature != trainingStatusSignature
snapshot = data
trainingStatusSignature = newTrainingStatusSignature
lastError = ""
ensureSelectedSymbol()
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError)) {
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged)) {
render()
}
}
@@ -116,7 +120,7 @@ class MainActivity : Activity() {
val hadError = lastError.isNotBlank()
lastError = error.message ?: "ошибка подключения"
if (!silent) toast(lastError)
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError)) {
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged = false)) {
render()
}
}
@@ -124,7 +128,7 @@ class MainActivity : Activity() {
}
}
private fun shouldRenderAfterRefresh(silent: Boolean, hadSnapshot: Boolean, hadError: Boolean): Boolean {
private fun shouldRenderAfterRefresh(silent: Boolean, hadSnapshot: Boolean, hadError: Boolean, trainingChanged: Boolean): Boolean {
val focused = contentHost.findFocus()
if (activeTab == "settings" && focused is EditText) {
return false
@@ -132,6 +136,9 @@ class MainActivity : Activity() {
if (!silent) {
return true
}
if (activeTab == "settings" && trainingChanged) {
return true
}
return !hadSnapshot || hadError
}
@@ -568,42 +575,71 @@ class MainActivity : Activity() {
private fun trainingComputerPanel(retrain: JSONObject): View =
LinearLayout(this).apply {
val coordination = retrain.optJSONObject("coordination") ?: JSONObject()
val activeJob = coordination.optJSONObject("active_job")
val agentOnline = coordination.optBoolean("agent_online", false)
orientation = LinearLayout.VERTICAL
addView(text("Компьютер обучения", 12f, Typeface.NORMAL, palette.muted))
addView(text(prefs.trainingComputerName, 18f, Typeface.BOLD, palette.green).top(dp(5)))
addView(text(prefs.trainingComputerPath, 12f, Typeface.NORMAL, palette.muted).top(dp(4)))
addView(keyValueLine("Статус", if (prefs.trainingComputerPinned) "закреплен" else "не закреплен", if (prefs.trainingComputerPinned) palette.green else palette.amber).top(dp(8)))
addView(keyValueLine("Связь агента", if (agentOnline) "онлайн" else "нет связи", if (agentOnline) palette.green else palette.amber).top(dp(4)))
if (activeJob != null) {
addView(keyValueLine("Задание", activeJob.optStringClean("status").ifBlank { "активно" }).top(dp(4)))
addView(text("Бот ставит задания через tb.kusoft.xyz, а этот Windows-agent сам забирает их через интернет и возвращает результат.", 12f, Typeface.NORMAL, palette.muted).top(dp(8)))
}
private fun trainingProcessPanel(coordination: JSONObject): View =
LinearLayout(this).apply {
val activeJob = coordination.optJSONObject("active_job")
val latestJob = coordination.optJSONObject("latest_job")
val job = activeJob ?: latestJob
orientation = LinearLayout.VERTICAL
addView(text("Процесс переобучения", 12f, Typeface.NORMAL, palette.muted))
if (job == null) {
val agentOnline = coordination.optBoolean("agent_online", false)
addView(keyValueLine("Состояние", if (agentOnline) "готов к запуску" else "ждет Windows-agent", if (agentOnline) palette.green else palette.amber).top(dp(8)))
return@apply
}
val status = job.optStringClean("status")
val phase = job.optStringClean("phase")
val progress = job.optInt("progress_percent", if (status == "completed") 100 else 0).coerceIn(0, 100)
val message = job.optStringClean("message")
addView(keyValueLine("Состояние", trainingJobLabel(status, phase), trainingJobColor(status, phase)).top(dp(8)))
addView(allocationBar(progress / 100.0).top(dp(10)))
addView(keyValueLine("Прогресс", "$progress%", trainingJobColor(status, phase)).top(dp(8)))
if (message.isNotBlank()) {
addView(text(message.take(160), 12f, Typeface.NORMAL, palette.text).top(dp(8)))
}
val completedAt = job.optStringClean("completed_at")
if (completedAt.isNotBlank()) {
addView(keyValueLine("Завершено", completedAt.take(19).replace("T", " ")).top(dp(8)))
}
val artifacts = job.optJSONArray("artifacts")
if (artifacts != null && artifacts.length() > 0) {
addView(keyValueLine("Артефакты", artifacts.length().toString(), palette.green).top(dp(4)))
}
addView(text("Переобучение Torch закреплено за этой Windows-машиной. Телефон только управляет запуском и расписанием, Pi только исполняет бота.", 12f, Typeface.NORMAL, palette.muted).top(dp(8)))
addView(actionRow(
"Закрепить эту машину" to {
prefs.pinDefaultTrainingComputer()
toast("Компьютер обучения закреплен")
render()
},
).top(dp(10)))
}
private fun retrainSettingsBlock(retrain: JSONObject, backtest: JSONObject): View {
val coordination = retrain.optJSONObject("coordination") ?: JSONObject()
val activeJob = coordination.optJSONObject("active_job")
val isTrainingActive = activeJob?.optStringClean("status") in setOf("pending", "running")
return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
addView(keyValueLine("Доступно", yesNo(retrain.optBoolean("available", false))))
addView(keyValueLine("Принята новая модель", yesNo(retrain.optBoolean("accepted", false))).top(dp(4)))
addView(trainingComputerPanel(retrain).top(dp(12)))
addView(thinDivider().top(dp(12)))
addView(trainingProcessPanel(coordination))
if (retrain.optBoolean("available", false)) {
addView(keyValueLine("Последний guard", if (retrain.optBoolean("accepted", false)) "модель принята" else "модель отклонена", if (retrain.optBoolean("accepted", false)) palette.green else palette.amber).top(dp(10)))
}
val replay = backtest.optJSONObject("full_replay") ?: JSONObject()
addView(keyValueLine("Replay сделок", replay.optInt("trades", 0).toString()).top(dp(4)))
addView(keyValueLine("Replay PnL", signedMoney(replay.optDouble("net_pnl", 0.0)), colorForSigned(replay.optDouble("net_pnl", 0.0))).top(dp(4)))
addView(actionRow(
"Запустить сейчас" to {
triggerRetrainNow()
},
).top(dp(10)))
if (isTrainingActive) {
addView(disabledActionButton("Обучение уже идет").top(dp(10)))
} else {
addView(actionRow(
"Запустить сейчас" to {
triggerRetrainNow()
},
).top(dp(10)))
}
addView(actionRow(
"" to { setRetrainInterval(1) },
"" to { setRetrainInterval(3) },
@@ -842,6 +878,21 @@ class MainActivity : Activity() {
isFocusableInTouchMode = false
}
private fun disabledActionButton(title: String): TextView =
TextView(this).apply {
text = title
textSize = 13f
typeface = Typeface.DEFAULT_BOLD
gravity = Gravity.CENTER
setTextColor(palette.muted)
background = boxDrawable(withAlpha(palette.muted, if (palette.isDark) 14 else 8), palette.line, 7)
isEnabled = false
isClickable = false
isFocusable = false
isFocusableInTouchMode = false
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(48))
}
private fun navItem(title: String, selected: Boolean, action: () -> Unit): TextView =
TextView(this).apply {
text = title
@@ -957,9 +1008,15 @@ class MainActivity : Activity() {
executor.execute {
try {
val message = TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).requestRetrain()
mainHandler.post { toast(message.take(180)) }
mainHandler.post {
toast(message.take(180))
refreshData(silent = false)
}
} catch (error: Exception) {
mainHandler.post { toast(error.message ?: "retrain не запущен") }
mainHandler.post {
toast(error.message ?: "retrain не запущен")
refreshData(silent = false)
}
}
}
}
@@ -984,6 +1041,30 @@ class MainActivity : Activity() {
return if (index >= 0) index else FIXED_SYMBOLS.size + 1
}
private fun trainingStatusSignature(retrain: JSONObject): String =
(retrain.optJSONObject("coordination") ?: JSONObject()).toString()
private fun trainingJobLabel(status: String, phase: String): String =
when {
status == "pending" -> "ждет Windows-agent"
phase == "claimed" -> "задание получено"
phase == "training" -> "идет обучение"
phase == "guard" -> "проверка качества"
phase == "uploading" -> "загрузка результата"
status == "completed" -> "завершено успешно"
status == "failed" -> "ошибка обучения"
status == "running" -> "идет работа"
else -> "готов к запуску"
}
private fun trainingJobColor(status: String, phase: String): Int =
when {
status == "completed" -> palette.green
status == "failed" -> palette.red
status == "pending" || status == "running" || phase.isNotBlank() -> palette.amber
else -> palette.text
}
private fun normalizedAction(raw: String?): String {
val value = raw.orEmpty().uppercase(Locale.US)
return when {