Fix remote training and model validation pipeline
This commit is contained in:
@@ -10,7 +10,7 @@ android {
|
||||
applicationId = "xyz.kusoft.tradebotmonitor"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 18
|
||||
versionName = "0.3.0"
|
||||
versionCode = 20
|
||||
versionName = "0.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,14 @@ class AppPrefs(context: Context) {
|
||||
if (saved.isNullOrBlank() || saved == LEGACY_PI_API_BASE_URL) {
|
||||
prefs.edit().putString("api_base_url", DEFAULT_API_BASE_URL).apply()
|
||||
}
|
||||
if (prefs.getString("training_computer_name", null).isNullOrBlank()) {
|
||||
val trainingComputerName = prefs.getString("training_computer_name", null)?.trim()
|
||||
val trainingComputerPath = prefs.getString("training_computer_path", null)?.trim()
|
||||
if (
|
||||
trainingComputerName.isNullOrBlank() ||
|
||||
trainingComputerName == LEGACY_TRAINING_COMPUTER_NAME ||
|
||||
trainingComputerPath.isNullOrBlank() ||
|
||||
trainingComputerPath == LEGACY_TRAINING_COMPUTER_PATH
|
||||
) {
|
||||
pinDefaultTrainingComputer()
|
||||
}
|
||||
}
|
||||
@@ -134,8 +141,10 @@ class AppPrefs(context: Context) {
|
||||
private companion object {
|
||||
const val DEFAULT_API_BASE_URL = "https://tb.kusoft.xyz"
|
||||
const val LEGACY_PI_API_BASE_URL = "http://192.168.0.185:8787"
|
||||
const val DEFAULT_TRAINING_COMPUTER_NAME = "DESKTOP-TMFDL0H"
|
||||
const val DEFAULT_TRAINING_COMPUTER_PATH = "C:\\Repos\\TradeBot"
|
||||
const val DEFAULT_TRAINING_COMPUTER_NAME = "SEVENHILL"
|
||||
const val DEFAULT_TRAINING_COMPUTER_PATH = "G:\\Repos\\TradeBot"
|
||||
const val LEGACY_TRAINING_COMPUTER_NAME = "DESKTOP-TMFDL0H"
|
||||
const val LEGACY_TRAINING_COMPUTER_PATH = "C:\\Repos\\TradeBot"
|
||||
const val TOKEN_KEY_ALIAS = "tradebot_api_auth_v1"
|
||||
}
|
||||
}
|
||||
|
||||
+62
-49
@@ -489,21 +489,19 @@ class MainActivity : Activity() {
|
||||
liveSignature = liveBlockSignature(snapshot),
|
||||
)
|
||||
|
||||
val (savedLogin, savedPassword) = authParts(prefs.commandToken)
|
||||
val apiInput = input("Адрес API бота", prefs.apiBaseUrl)
|
||||
val loginInput = input("Логин API", savedLogin)
|
||||
val passwordInput = input("Пароль API", savedPassword).apply {
|
||||
val tokenInput = input("API-токен", prefs.commandToken).apply {
|
||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
box.addView(section("Подключение", LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(apiInput)
|
||||
addView(loginInput.top(dp(8)))
|
||||
addView(passwordInput.top(dp(8)))
|
||||
addView(tokenInput.top(dp(8)))
|
||||
addView(text("Токен выдаётся сервером для Android-клиента и хранится в Android Keystore.", 12f, Typeface.NORMAL, palette.muted).top(dp(8)))
|
||||
addView(actionRow(
|
||||
"Сохранить" to {
|
||||
prefs.apiBaseUrl = apiInput.text.toString()
|
||||
prefs.commandToken = authToken(loginInput.text.toString(), passwordInput.text.toString())
|
||||
prefs.commandToken = tokenInput.text.toString()
|
||||
toast("Подключение сохранено")
|
||||
refreshData(silent = false)
|
||||
},
|
||||
@@ -1020,6 +1018,13 @@ class MainActivity : Activity() {
|
||||
LinearLayout(this).apply {
|
||||
val coordination = retrain.optJSONObject("coordination") ?: JSONObject()
|
||||
val activeJob = coordination.optJSONObject("active_job")
|
||||
val worker = coordination.optJSONObject("worker")
|
||||
val trainingComputerName = worker?.optStringClean("name")
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: prefs.trainingComputerName
|
||||
val trainingComputerPath = worker?.optStringClean("path")
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: prefs.trainingComputerPath
|
||||
val agentRecentlySeen = coordination.optBoolean(
|
||||
"agent_recently_seen",
|
||||
coordination.optBoolean("agent_online", false),
|
||||
@@ -1036,8 +1041,8 @@ class MainActivity : Activity() {
|
||||
val connectionColor = if (agentRecentlySeen || agentBusy) palette.green else palette.amber
|
||||
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(text(trainingComputerName, 18f, Typeface.BOLD, palette.green).top(dp(5)))
|
||||
addView(text(trainingComputerPath, 12f, Typeface.NORMAL, palette.muted).top(dp(4)))
|
||||
addView(keyValueLine("Связь агента", connectionText, connectionColor).top(dp(4)))
|
||||
addView(text("Бот ставит задания через tb.kusoft.xyz, а этот Windows-agent сам забирает их через интернет и возвращает результат.", 12f, Typeface.NORMAL, palette.muted).top(dp(8)))
|
||||
}
|
||||
@@ -1052,10 +1057,9 @@ class MainActivity : Activity() {
|
||||
val agentOnline = coordination.optBoolean("agent_online", false)
|
||||
addView(keyValueLine("Состояние", if (agentOnline) "готов к запуску" else "ждет Windows-agent", if (agentOnline) palette.green else palette.amber).top(dp(8)))
|
||||
if (latestJob != null) {
|
||||
val latestStatus = latestJob.optStringClean("status")
|
||||
val latestPhase = latestJob.optStringClean("phase")
|
||||
val latestMessage = latestJob.optStringClean("message")
|
||||
addView(keyValueLine("Последнее обучение", trainingJobLabel(latestStatus, latestPhase), trainingJobColor(latestStatus, latestPhase)).top(dp(8)))
|
||||
addView(keyValueLine("Последнее обучение", trainingJobLabel(latestJob), trainingJobColor(latestJob)).top(dp(8)))
|
||||
addView(trainingModelDecisionLine(latestJob).top(dp(6)))
|
||||
if (latestMessage.isNotBlank()) {
|
||||
addView(text(latestMessage.take(160), 12f, Typeface.NORMAL, palette.muted).top(dp(6)))
|
||||
}
|
||||
@@ -1064,12 +1068,11 @@ class MainActivity : Activity() {
|
||||
}
|
||||
val job = activeJob
|
||||
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(keyValueLine("Состояние", trainingJobLabel(job), trainingJobColor(job)).top(dp(8)))
|
||||
addView(allocationBar(progress / 100.0).top(dp(10)))
|
||||
addView(keyValueLine("Прогресс", "$progress%", trainingJobColor(status, phase)).top(dp(8)))
|
||||
addView(keyValueLine("Прогресс", "$progress%", trainingJobColor(job)).top(dp(8)))
|
||||
if (message.isNotBlank()) {
|
||||
addView(text(message.take(160), 12f, Typeface.NORMAL, palette.text).top(dp(8)))
|
||||
}
|
||||
@@ -1241,22 +1244,19 @@ class MainActivity : Activity() {
|
||||
if (!isAuthError()) {
|
||||
return emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}")
|
||||
}
|
||||
val (savedLogin, savedPassword) = authParts(prefs.commandToken)
|
||||
val apiInput = input("Адрес API бота", prefs.apiBaseUrl)
|
||||
val loginInput = input("Логин API", savedLogin)
|
||||
val passwordInput = input("Пароль API", savedPassword).apply {
|
||||
val tokenInput = input("API-токен", prefs.commandToken).apply {
|
||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
return section("Нужен вход в API", LinearLayout(this).apply {
|
||||
return section("Нужен API-токен", LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(text("Сервер tb.kusoft.xyz отвечает 401, значит он доступен, но требует авторизацию. Введите логин и пароль, приложение само отправит Basic Auth.", 13f, Typeface.NORMAL, palette.muted))
|
||||
addView(text("Сервер доступен, но Android API-токен отсутствует или неверен. Введите отдельный токен клиента; логин и пароль сайта здесь не используются.", 13f, Typeface.NORMAL, palette.muted))
|
||||
addView(apiInput.top(dp(12)))
|
||||
addView(loginInput.top(dp(8)))
|
||||
addView(passwordInput.top(dp(8)))
|
||||
addView(tokenInput.top(dp(8)))
|
||||
addView(actionRow(
|
||||
"Подключиться" to {
|
||||
prefs.apiBaseUrl = apiInput.text.toString()
|
||||
prefs.commandToken = authToken(loginInput.text.toString(), passwordInput.text.toString())
|
||||
prefs.commandToken = tokenInput.text.toString()
|
||||
toast("Доступ сохранен, проверяю API")
|
||||
refreshData(silent = false)
|
||||
},
|
||||
@@ -1642,11 +1642,40 @@ class MainActivity : Activity() {
|
||||
},
|
||||
).joinToString("|")
|
||||
|
||||
private fun trainingJobLabel(status: String, phase: String): String =
|
||||
when (status) {
|
||||
private fun trainingModelDecision(job: JSONObject): String {
|
||||
val explicit = job.optStringClean("model_decision")
|
||||
if (explicit in setOf("accepted", "rejected")) return explicit
|
||||
val summary = job.optJSONObject("summary") ?: return ""
|
||||
return if (summary.has("accepted")) {
|
||||
if (summary.optBoolean("accepted", false)) "accepted" else "rejected"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun trainingModelDecisionLine(job: JSONObject): View {
|
||||
val decision = trainingModelDecision(job)
|
||||
val value = when (decision) {
|
||||
"accepted" -> "кандидат принят"
|
||||
"rejected" -> "кандидат отклонён"
|
||||
else -> "нет решения gate"
|
||||
}
|
||||
val color = when (decision) {
|
||||
"accepted" -> palette.green
|
||||
"rejected" -> palette.amber
|
||||
else -> palette.muted
|
||||
}
|
||||
return keyValueLine("Результат модели", value, color)
|
||||
}
|
||||
|
||||
private fun trainingJobLabel(job: JSONObject): String {
|
||||
val status = job.optStringClean("status")
|
||||
val phase = job.optStringClean("phase")
|
||||
val completedWithDecision = trainingModelDecision(job).isNotBlank()
|
||||
return when (status) {
|
||||
"pending" -> "ждет Windows-agent"
|
||||
"completed" -> "завершено успешно"
|
||||
"failed" -> "ошибка обучения"
|
||||
"failed" -> if (completedWithDecision) "завершено успешно" else "ошибка обучения"
|
||||
"running" -> when (phase) {
|
||||
"claimed" -> "задание получено"
|
||||
"training" -> "идет обучение"
|
||||
@@ -1656,14 +1685,19 @@ class MainActivity : Activity() {
|
||||
}
|
||||
else -> "готов к запуску"
|
||||
}
|
||||
}
|
||||
|
||||
private fun trainingJobColor(status: String, phase: String): Int =
|
||||
when {
|
||||
status == "completed" -> palette.green
|
||||
private fun trainingJobColor(job: JSONObject): Int {
|
||||
val status = job.optStringClean("status")
|
||||
val phase = job.optStringClean("phase")
|
||||
val completedWithDecision = trainingModelDecision(job).isNotBlank()
|
||||
return when {
|
||||
status == "completed" || completedWithDecision -> 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)
|
||||
@@ -1700,27 +1734,6 @@ class MainActivity : Activity() {
|
||||
else -> value.ifBlank { "нет данных" }
|
||||
}
|
||||
|
||||
private fun authParts(value: String): Pair<String, String> {
|
||||
val trimmed = value.trim()
|
||||
if (trimmed.isBlank() || trimmed.startsWith("Bearer ", ignoreCase = true)) {
|
||||
return "" to ""
|
||||
}
|
||||
val basicPrefix = "Basic "
|
||||
val raw = if (trimmed.startsWith(basicPrefix, ignoreCase = true)) "" else trimmed
|
||||
val separator = raw.indexOf(':')
|
||||
return if (separator >= 0) {
|
||||
raw.substring(0, separator) to raw.substring(separator + 1)
|
||||
} else {
|
||||
raw to ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun authToken(login: String, password: String): String {
|
||||
val cleanLogin = login.trim()
|
||||
val cleanPassword = password.trim()
|
||||
return if (cleanLogin.isBlank() && cleanPassword.isBlank()) "" else "$cleanLogin:$cleanPassword"
|
||||
}
|
||||
|
||||
private fun modelLabel(value: String): String =
|
||||
when {
|
||||
value.contains("gru", ignoreCase = true) -> "PyTorch GRU"
|
||||
|
||||
+4
-12
@@ -1,6 +1,5 @@
|
||||
package xyz.kusoft.tradebotmonitor
|
||||
|
||||
import android.util.Base64
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
@@ -98,7 +97,7 @@ class TradeBotApi(
|
||||
connection.disconnect()
|
||||
if (code !in 200..299) {
|
||||
if (code == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
throw IllegalStateException("HTTP 401: сервер требует логин и пароль")
|
||||
throw IllegalStateException("HTTP 401: API-токен отсутствует или неверен")
|
||||
}
|
||||
throw IllegalStateException("HTTP $code: ${text.take(240)}")
|
||||
}
|
||||
@@ -282,15 +281,8 @@ class TradeBotApi(
|
||||
private fun applyAuthHeaders(connection: HttpURLConnection, token: String) {
|
||||
val value = token.trim()
|
||||
if (value.isBlank()) return
|
||||
connection.setRequestProperty("X-TradeBot-Token", value)
|
||||
val authorization = when {
|
||||
value.startsWith("Basic ", ignoreCase = true) -> value
|
||||
value.startsWith("Bearer ", ignoreCase = true) -> value
|
||||
":" in value -> {
|
||||
val encoded = Base64.encodeToString(value.toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP)
|
||||
"Basic $encoded"
|
||||
}
|
||||
else -> "Bearer $value"
|
||||
}
|
||||
val rawToken = value.removePrefix("Bearer ").removePrefix("bearer ").trim()
|
||||
connection.setRequestProperty("X-TradeBot-Token", rawToken)
|
||||
val authorization = "Bearer $rawToken"
|
||||
connection.setRequestProperty("Authorization", authorization)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user