From c0842eb84bb86db911272e2c51b01b0c9f2324b7 Mon Sep 17 00:00:00 2001 From: PTah Date: Tue, 23 Jun 2026 19:08:41 +1000 Subject: [PATCH] feat: RDG session break aligned with SAC rdg_qwinsta_enabled (0.5.14) Co-authored-by: Cursor --- app/build.gradle.kts | 4 +- .../ru/kalinamall/seaca/data/SacModels.kt | 4 ++ .../seaca/ui/components/RdgAccessBadge.kt | 39 +++++++++++++++++++ .../seaca/ui/components/RdgQwinstaDialog.kt | 10 ++--- .../seaca/ui/screens/DetailScreens.kt | 19 +++++---- .../seaca/ui/screens/ListScreens.kt | 19 +++++++-- .../ru/kalinamall/seaca/ui/util/RdgQwinsta.kt | 27 ++++++++++--- 7 files changed, 98 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/ru/kalinamall/seaca/ui/components/RdgAccessBadge.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 560012c..d8e468a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "ru.kalinamall.seaca" minSdk = 26 targetSdk = 35 - versionCode = 27 - versionName = "0.5.13" + versionCode = 28 + versionName = "0.5.14" } buildTypes { diff --git a/app/src/main/java/ru/kalinamall/seaca/data/SacModels.kt b/app/src/main/java/ru/kalinamall/seaca/data/SacModels.kt index ddc6f82..8df1968 100644 --- a/app/src/main/java/ru/kalinamall/seaca/data/SacModels.kt +++ b/app/src/main/java/ru/kalinamall/seaca/data/SacModels.kt @@ -61,6 +61,8 @@ data class EventSummary( @SerialName("rdg_flap") val rdgFlap: Boolean = false, @SerialName("rdg_flap_pair_event_id") val rdgFlapPairEventId: Long? = null, @SerialName("rdg_flap_qwinsta_event_id") val rdgFlapQwinstaEventId: Long? = null, + @SerialName("rdg_access_path") val rdgAccessPath: String? = null, + @SerialName("rdg_qwinsta_enabled") val rdgQwinstaEnabled: Boolean = false, ) @Serializable @@ -87,6 +89,8 @@ data class EventDetail( @SerialName("rdg_flap") val rdgFlap: Boolean = false, @SerialName("rdg_flap_pair_event_id") val rdgFlapPairEventId: Long? = null, @SerialName("rdg_flap_qwinsta_event_id") val rdgFlapQwinstaEventId: Long? = null, + @SerialName("rdg_access_path") val rdgAccessPath: String? = null, + @SerialName("rdg_qwinsta_enabled") val rdgQwinstaEnabled: Boolean = false, val details: JsonElement? = null, val raw: JsonElement? = null, val payload: JsonElement? = null, diff --git a/app/src/main/java/ru/kalinamall/seaca/ui/components/RdgAccessBadge.kt b/app/src/main/java/ru/kalinamall/seaca/ui/components/RdgAccessBadge.kt new file mode 100644 index 0000000..b7cc1de --- /dev/null +++ b/app/src/main/java/ru/kalinamall/seaca/ui/components/RdgAccessBadge.kt @@ -0,0 +1,39 @@ +package ru.kalinamall.seaca.ui.components + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +private val RdgAccessText = Color(0xFF7EC8E3) +private val RdgAccessBackground = Color(0x1A7EC8E3) +private val RdgAccessBorder = Color(0x667EC8E3) + +@Composable +fun RdgAccessBadge( + path: String, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier + .border(1.dp, RdgAccessBorder, RoundedCornerShape(4.dp)), + shape = RoundedCornerShape(4.dp), + color = RdgAccessBackground, + ) { + Text( + text = path, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + color = RdgAccessText, + fontSize = 11.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.3.sp, + ) + } +} diff --git a/app/src/main/java/ru/kalinamall/seaca/ui/components/RdgQwinstaDialog.kt b/app/src/main/java/ru/kalinamall/seaca/ui/components/RdgQwinstaDialog.kt index 7023e6c..48c14cc 100644 --- a/app/src/main/java/ru/kalinamall/seaca/ui/components/RdgQwinstaDialog.kt +++ b/app/src/main/java/ru/kalinamall/seaca/ui/components/RdgQwinstaDialog.kt @@ -39,7 +39,7 @@ import ru.kalinamall.seaca.data.SacRepository import ru.kalinamall.seaca.ui.util.QwinstaSessionRow import ru.kalinamall.seaca.ui.util.agentCommandError import ru.kalinamall.seaca.ui.util.parseQwinstaSessions -import ru.kalinamall.seaca.ui.util.rdgFlapQwinstaEventId +import ru.kalinamall.seaca.ui.util.rdgQwinstaEventId class RdgQwinstaController(private val repo: SacRepository) { var open by mutableStateOf(false) @@ -61,7 +61,7 @@ class RdgQwinstaController(private val repo: SacRepository) { private var actorUser: String? = null fun openFor(event: EventSummary, scope: kotlinx.coroutines.CoroutineScope) { - val targetId = rdgFlapQwinstaEventId(event) ?: return + val targetId = rdgQwinstaEventId(event) ?: return targetEventId = targetId actorUser = event.actorUser open = true @@ -69,7 +69,7 @@ class RdgQwinstaController(private val repo: SacRepository) { } fun openFor(event: EventDetail, scope: kotlinx.coroutines.CoroutineScope) { - val targetId = rdgFlapQwinstaEventId(event) ?: return + val targetId = rdgQwinstaEventId(event) ?: return targetEventId = targetId actorUser = event.actorUser open = true @@ -191,7 +191,7 @@ fun RdgQwinstaDialog( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(10.dp), ) { - Text("qwinsta / logoff", style = MaterialTheme.typography.titleLarge) + Text("Оборвать сессию", style = MaterialTheme.typography.titleLarge) controller.meta?.let { Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } @@ -329,6 +329,6 @@ fun RdgQwinstaButton( onClick = onClick, modifier = modifier, ) { - Text(if (controller.loading) "qwinsta…" else "qwinsta") + Text(if (controller.loading) "…" else "Оборвать сессию") } } diff --git a/app/src/main/java/ru/kalinamall/seaca/ui/screens/DetailScreens.kt b/app/src/main/java/ru/kalinamall/seaca/ui/screens/DetailScreens.kt index faf5885..b16ff3f 100644 --- a/app/src/main/java/ru/kalinamall/seaca/ui/screens/DetailScreens.kt +++ b/app/src/main/java/ru/kalinamall/seaca/ui/screens/DetailScreens.kt @@ -68,7 +68,7 @@ import ru.kalinamall.seaca.ui.components.rememberRdgQwinstaController import ru.kalinamall.seaca.ui.components.EventSessionTerminateButton import ru.kalinamall.seaca.ui.components.HostSessionsSection import ru.kalinamall.seaca.ui.util.hasRdgFlapUi -import ru.kalinamall.seaca.ui.util.rdgFlapQwinstaEventId +import ru.kalinamall.seaca.ui.util.rdgQwinstaEventId import ru.kalinamall.seaca.ui.util.eventSupportsSessionTerminate import ru.kalinamall.seaca.ui.util.sessionTerminateLabel import ru.kalinamall.seaca.push.NotificationHelper @@ -115,16 +115,19 @@ fun EventDetailScreen(repo: SacRepository, id: Long, onBack: () -> Unit) { DetailFrame(title = data?.title ?: "Событие", loading, error, onBack) { val d = data ?: return@DetailFrame + if (!d.rdgAccessPath.isNullOrBlank()) { + Field("Путь RDS", d.rdgAccessPath) + } if (hasRdgFlapUi(d)) { Field("RDG flap", "302→303 — возможна зависшая сессия на ПК пользователя") d.rdgFlapPairEventId?.let { Field("Пара", "#$it") } - rdgFlapQwinstaEventId(d)?.let { - RdgQwinstaButton( - controller = qwinsta, - onClick = { qwinsta.openFor(d, scope) }, - modifier = Modifier.padding(vertical = 4.dp), - ) - } + } + rdgQwinstaEventId(d)?.let { + RdgQwinstaButton( + controller = qwinsta, + onClick = { qwinsta.openFor(d, scope) }, + modifier = Modifier.padding(vertical = 4.dp), + ) } if (eventSupportsSessionTerminate(d)) { EventSessionTerminateButton( diff --git a/app/src/main/java/ru/kalinamall/seaca/ui/screens/ListScreens.kt b/app/src/main/java/ru/kalinamall/seaca/ui/screens/ListScreens.kt index e030095..76fb396 100644 --- a/app/src/main/java/ru/kalinamall/seaca/ui/screens/ListScreens.kt +++ b/app/src/main/java/ru/kalinamall/seaca/ui/screens/ListScreens.kt @@ -53,6 +53,7 @@ import ru.kalinamall.seaca.data.ProblemSummary import ru.kalinamall.seaca.data.SacRepository import ru.kalinamall.seaca.ui.components.BackArrowButton import ru.kalinamall.seaca.ui.components.EventSessionTerminateButton +import ru.kalinamall.seaca.ui.components.RdgAccessBadge import ru.kalinamall.seaca.ui.components.RdgFlapBadge import ru.kalinamall.seaca.ui.components.RdgQwinstaButton import ru.kalinamall.seaca.ui.components.RdgQwinstaDialog @@ -60,7 +61,7 @@ import ru.kalinamall.seaca.ui.components.rememberRdgQwinstaController import ru.kalinamall.seaca.ui.components.ForwardArrowButton import ru.kalinamall.seaca.ui.util.SacDateTimes import ru.kalinamall.seaca.ui.util.hasRdgFlapUi -import ru.kalinamall.seaca.ui.util.rdgFlapQwinstaEventId +import ru.kalinamall.seaca.ui.util.rdgQwinstaEventId import ru.kalinamall.seaca.ui.util.eventSupportsSessionTerminate import ru.kalinamall.seaca.ui.util.sessionTerminateLabel @@ -138,6 +139,9 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) { verticalAlignment = Alignment.Top, ) { Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + if (!e.rdgAccessPath.isNullOrBlank()) { + RdgAccessBadge(path = e.rdgAccessPath) + } if (hasRdgFlapUi(e)) { RdgFlapBadge(pairEventId = e.rdgFlapPairEventId) } @@ -146,7 +150,7 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) { style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface, ) - if (rdgFlapQwinstaEventId(e) != null) { + rdgQwinstaEventId(e)?.let { RdgQwinstaButton( controller = qwinsta, onClick = { qwinsta.openFor(e, scope) }, @@ -258,9 +262,10 @@ fun EventsScreen(repo: SacRepository, onOpen: (Long) -> Unit) { itemKey = EventSummary::id, itemLabel = { "${it.severity} · ${it.title}" }, itemSub = { "${it.hostname} · ${SacDateTimes.format(it.occurredAt, dateMode)}" }, + itemRdgAccessPath = { it.rdgAccessPath }, itemRdgFlap = { hasRdgFlapUi(it) }, itemRdgFlapPairId = { it.rdgFlapPairEventId }, - itemQwinsta = { rdgFlapQwinstaEventId(it) != null }, + itemQwinsta = { rdgQwinstaEventId(it) != null }, onQwinsta = { qwinsta.openFor(it, scope) }, qwinstaLoading = qwinsta.loading, itemTerminate = { eventSupportsSessionTerminate(it) }, @@ -406,6 +411,7 @@ private fun PagedListScreen( itemSub: (T) -> String, onOpen: (Long) -> Unit, severities: List, + itemRdgAccessPath: ((T) -> String?)? = null, itemRdgFlap: ((T) -> Boolean)? = null, itemRdgFlapPairId: ((T) -> Long?)? = null, itemQwinsta: ((T) -> Boolean)? = null, @@ -476,6 +482,7 @@ private fun PagedListScreen( title = itemLabel(item), subtitle = itemSub(item), onClick = { onOpen(itemKey(item)) }, + rdgAccessPath = itemRdgAccessPath?.invoke(item), rdgFlap = itemRdgFlap?.invoke(item) == true, rdgFlapPairId = itemRdgFlapPairId?.invoke(item), showQwinsta = itemQwinsta?.invoke(item) == true, @@ -497,6 +504,7 @@ private fun ListRow( title: String, subtitle: String, onClick: () -> Unit, + rdgAccessPath: String? = null, rdgFlap: Boolean = false, rdgFlapPairId: Long? = null, showQwinsta: Boolean = false, @@ -522,6 +530,9 @@ private fun ListRow( .clickable(onClick = onClick), verticalArrangement = Arrangement.spacedBy(4.dp), ) { + if (!rdgAccessPath.isNullOrBlank()) { + RdgAccessBadge(path = rdgAccessPath) + } if (rdgFlap) { RdgFlapBadge(pairEventId = rdgFlapPairId) } @@ -532,7 +543,7 @@ private fun ListRow( OutlinedButton( enabled = !qwinstaLoading, onClick = onQwinstaClick, - ) { Text(if (qwinstaLoading) "qwinsta…" else "qwinsta / logoff") } + ) { Text(if (qwinstaLoading) "…" else "Оборвать сессию") } } if (showTerminate && onTerminateClick != null) { EventSessionTerminateButton( diff --git a/app/src/main/java/ru/kalinamall/seaca/ui/util/RdgQwinsta.kt b/app/src/main/java/ru/kalinamall/seaca/ui/util/RdgQwinsta.kt index a85d1d3..b26720b 100644 --- a/app/src/main/java/ru/kalinamall/seaca/ui/util/RdgQwinsta.kt +++ b/app/src/main/java/ru/kalinamall/seaca/ui/util/RdgQwinsta.kt @@ -13,14 +13,31 @@ data class QwinstaSessionRow( fun hasRdgFlapUi(event: EventSummary): Boolean = event.rdgFlap || event.rdgFlapQwinstaEventId != null -fun rdgFlapQwinstaEventId(event: EventSummary): Long? = - event.rdgFlapQwinstaEventId ?: if (event.rdgFlap) event.id else null - fun hasRdgFlapUi(event: EventDetail): Boolean = event.rdgFlap || event.rdgFlapQwinstaEventId != null -fun rdgFlapQwinstaEventId(event: EventDetail): Long? = - event.rdgFlapQwinstaEventId ?: if (event.rdgFlap) event.id else null +fun hasRdgAccessPath(event: EventSummary): Boolean = + !event.rdgAccessPath.isNullOrBlank() + +fun hasRdgAccessPath(event: EventDetail): Boolean = + !event.rdgAccessPath.isNullOrBlank() + +/** Как SAC rdgQwinstaEventId: кнопка только при rdg_qwinsta_enabled от API. */ +fun rdgQwinstaEventId(event: EventSummary): Long? { + if (!event.rdgQwinstaEnabled) return null + return event.rdgFlapQwinstaEventId ?: event.id +} + +fun rdgQwinstaEventId(event: EventDetail): Long? { + if (!event.rdgQwinstaEnabled) return null + return event.rdgFlapQwinstaEventId ?: event.id +} + +/** @deprecated use rdgQwinstaEventId */ +fun rdgFlapQwinstaEventId(event: EventSummary): Long? = rdgQwinstaEventId(event) + +/** @deprecated use rdgQwinstaEventId */ +fun rdgFlapQwinstaEventId(event: EventDetail): Long? = rdgQwinstaEventId(event) fun parseQwinstaSessions(stdout: String, actorUser: String?): List { val lines = stdout.lines().map { it.trim() }.filter { it.isNotEmpty() }