feat: RDG session break aligned with SAC rdg_qwinsta_enabled (0.5.14)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-23 19:08:41 +10:00
parent 477e5ea6ed
commit c0842eb84b
7 changed files with 98 additions and 24 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "ru.kalinamall.seaca" applicationId = "ru.kalinamall.seaca"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 27 versionCode = 28
versionName = "0.5.13" versionName = "0.5.14"
} }
buildTypes { buildTypes {
@@ -61,6 +61,8 @@ data class EventSummary(
@SerialName("rdg_flap") val rdgFlap: Boolean = false, @SerialName("rdg_flap") val rdgFlap: Boolean = false,
@SerialName("rdg_flap_pair_event_id") val rdgFlapPairEventId: Long? = null, @SerialName("rdg_flap_pair_event_id") val rdgFlapPairEventId: Long? = null,
@SerialName("rdg_flap_qwinsta_event_id") val rdgFlapQwinstaEventId: 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 @Serializable
@@ -87,6 +89,8 @@ data class EventDetail(
@SerialName("rdg_flap") val rdgFlap: Boolean = false, @SerialName("rdg_flap") val rdgFlap: Boolean = false,
@SerialName("rdg_flap_pair_event_id") val rdgFlapPairEventId: Long? = null, @SerialName("rdg_flap_pair_event_id") val rdgFlapPairEventId: Long? = null,
@SerialName("rdg_flap_qwinsta_event_id") val rdgFlapQwinstaEventId: 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 details: JsonElement? = null,
val raw: JsonElement? = null, val raw: JsonElement? = null,
val payload: JsonElement? = null, val payload: JsonElement? = null,
@@ -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,
)
}
}
@@ -39,7 +39,7 @@ import ru.kalinamall.seaca.data.SacRepository
import ru.kalinamall.seaca.ui.util.QwinstaSessionRow import ru.kalinamall.seaca.ui.util.QwinstaSessionRow
import ru.kalinamall.seaca.ui.util.agentCommandError import ru.kalinamall.seaca.ui.util.agentCommandError
import ru.kalinamall.seaca.ui.util.parseQwinstaSessions 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) { class RdgQwinstaController(private val repo: SacRepository) {
var open by mutableStateOf(false) var open by mutableStateOf(false)
@@ -61,7 +61,7 @@ class RdgQwinstaController(private val repo: SacRepository) {
private var actorUser: String? = null private var actorUser: String? = null
fun openFor(event: EventSummary, scope: kotlinx.coroutines.CoroutineScope) { fun openFor(event: EventSummary, scope: kotlinx.coroutines.CoroutineScope) {
val targetId = rdgFlapQwinstaEventId(event) ?: return val targetId = rdgQwinstaEventId(event) ?: return
targetEventId = targetId targetEventId = targetId
actorUser = event.actorUser actorUser = event.actorUser
open = true open = true
@@ -69,7 +69,7 @@ class RdgQwinstaController(private val repo: SacRepository) {
} }
fun openFor(event: EventDetail, scope: kotlinx.coroutines.CoroutineScope) { fun openFor(event: EventDetail, scope: kotlinx.coroutines.CoroutineScope) {
val targetId = rdgFlapQwinstaEventId(event) ?: return val targetId = rdgQwinstaEventId(event) ?: return
targetEventId = targetId targetEventId = targetId
actorUser = event.actorUser actorUser = event.actorUser
open = true open = true
@@ -191,7 +191,7 @@ fun RdgQwinstaDialog(
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
Text("qwinsta / logoff", style = MaterialTheme.typography.titleLarge) Text("Оборвать сессию", style = MaterialTheme.typography.titleLarge)
controller.meta?.let { controller.meta?.let {
Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
@@ -329,6 +329,6 @@ fun RdgQwinstaButton(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
) { ) {
Text(if (controller.loading) "qwinsta" else "qwinsta") Text(if (controller.loading) "" else "Оборвать сессию")
} }
} }
@@ -68,7 +68,7 @@ import ru.kalinamall.seaca.ui.components.rememberRdgQwinstaController
import ru.kalinamall.seaca.ui.components.EventSessionTerminateButton import ru.kalinamall.seaca.ui.components.EventSessionTerminateButton
import ru.kalinamall.seaca.ui.components.HostSessionsSection import ru.kalinamall.seaca.ui.components.HostSessionsSection
import ru.kalinamall.seaca.ui.util.hasRdgFlapUi 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.eventSupportsSessionTerminate
import ru.kalinamall.seaca.ui.util.sessionTerminateLabel import ru.kalinamall.seaca.ui.util.sessionTerminateLabel
import ru.kalinamall.seaca.push.NotificationHelper 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) { DetailFrame(title = data?.title ?: "Событие", loading, error, onBack) {
val d = data ?: return@DetailFrame val d = data ?: return@DetailFrame
if (!d.rdgAccessPath.isNullOrBlank()) {
Field("Путь RDS", d.rdgAccessPath)
}
if (hasRdgFlapUi(d)) { if (hasRdgFlapUi(d)) {
Field("RDG flap", "302→303 — возможна зависшая сессия на ПК пользователя") Field("RDG flap", "302→303 — возможна зависшая сессия на ПК пользователя")
d.rdgFlapPairEventId?.let { Field("Пара", "#$it") } d.rdgFlapPairEventId?.let { Field("Пара", "#$it") }
rdgFlapQwinstaEventId(d)?.let { }
RdgQwinstaButton( rdgQwinstaEventId(d)?.let {
controller = qwinsta, RdgQwinstaButton(
onClick = { qwinsta.openFor(d, scope) }, controller = qwinsta,
modifier = Modifier.padding(vertical = 4.dp), onClick = { qwinsta.openFor(d, scope) },
) modifier = Modifier.padding(vertical = 4.dp),
} )
} }
if (eventSupportsSessionTerminate(d)) { if (eventSupportsSessionTerminate(d)) {
EventSessionTerminateButton( EventSessionTerminateButton(
@@ -53,6 +53,7 @@ import ru.kalinamall.seaca.data.ProblemSummary
import ru.kalinamall.seaca.data.SacRepository import ru.kalinamall.seaca.data.SacRepository
import ru.kalinamall.seaca.ui.components.BackArrowButton import ru.kalinamall.seaca.ui.components.BackArrowButton
import ru.kalinamall.seaca.ui.components.EventSessionTerminateButton 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.RdgFlapBadge
import ru.kalinamall.seaca.ui.components.RdgQwinstaButton import ru.kalinamall.seaca.ui.components.RdgQwinstaButton
import ru.kalinamall.seaca.ui.components.RdgQwinstaDialog 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.components.ForwardArrowButton
import ru.kalinamall.seaca.ui.util.SacDateTimes import ru.kalinamall.seaca.ui.util.SacDateTimes
import ru.kalinamall.seaca.ui.util.hasRdgFlapUi 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.eventSupportsSessionTerminate
import ru.kalinamall.seaca.ui.util.sessionTerminateLabel import ru.kalinamall.seaca.ui.util.sessionTerminateLabel
@@ -138,6 +139,9 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) {
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.Top,
) { ) {
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
if (!e.rdgAccessPath.isNullOrBlank()) {
RdgAccessBadge(path = e.rdgAccessPath)
}
if (hasRdgFlapUi(e)) { if (hasRdgFlapUi(e)) {
RdgFlapBadge(pairEventId = e.rdgFlapPairEventId) RdgFlapBadge(pairEventId = e.rdgFlapPairEventId)
} }
@@ -146,7 +150,7 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) {
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
if (rdgFlapQwinstaEventId(e) != null) { rdgQwinstaEventId(e)?.let {
RdgQwinstaButton( RdgQwinstaButton(
controller = qwinsta, controller = qwinsta,
onClick = { qwinsta.openFor(e, scope) }, onClick = { qwinsta.openFor(e, scope) },
@@ -258,9 +262,10 @@ fun EventsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
itemKey = EventSummary::id, itemKey = EventSummary::id,
itemLabel = { "${it.severity} · ${it.title}" }, itemLabel = { "${it.severity} · ${it.title}" },
itemSub = { "${it.hostname} · ${SacDateTimes.format(it.occurredAt, dateMode)}" }, itemSub = { "${it.hostname} · ${SacDateTimes.format(it.occurredAt, dateMode)}" },
itemRdgAccessPath = { it.rdgAccessPath },
itemRdgFlap = { hasRdgFlapUi(it) }, itemRdgFlap = { hasRdgFlapUi(it) },
itemRdgFlapPairId = { it.rdgFlapPairEventId }, itemRdgFlapPairId = { it.rdgFlapPairEventId },
itemQwinsta = { rdgFlapQwinstaEventId(it) != null }, itemQwinsta = { rdgQwinstaEventId(it) != null },
onQwinsta = { qwinsta.openFor(it, scope) }, onQwinsta = { qwinsta.openFor(it, scope) },
qwinstaLoading = qwinsta.loading, qwinstaLoading = qwinsta.loading,
itemTerminate = { eventSupportsSessionTerminate(it) }, itemTerminate = { eventSupportsSessionTerminate(it) },
@@ -406,6 +411,7 @@ private fun <T> PagedListScreen(
itemSub: (T) -> String, itemSub: (T) -> String,
onOpen: (Long) -> Unit, onOpen: (Long) -> Unit,
severities: List<String?>, severities: List<String?>,
itemRdgAccessPath: ((T) -> String?)? = null,
itemRdgFlap: ((T) -> Boolean)? = null, itemRdgFlap: ((T) -> Boolean)? = null,
itemRdgFlapPairId: ((T) -> Long?)? = null, itemRdgFlapPairId: ((T) -> Long?)? = null,
itemQwinsta: ((T) -> Boolean)? = null, itemQwinsta: ((T) -> Boolean)? = null,
@@ -476,6 +482,7 @@ private fun <T> PagedListScreen(
title = itemLabel(item), title = itemLabel(item),
subtitle = itemSub(item), subtitle = itemSub(item),
onClick = { onOpen(itemKey(item)) }, onClick = { onOpen(itemKey(item)) },
rdgAccessPath = itemRdgAccessPath?.invoke(item),
rdgFlap = itemRdgFlap?.invoke(item) == true, rdgFlap = itemRdgFlap?.invoke(item) == true,
rdgFlapPairId = itemRdgFlapPairId?.invoke(item), rdgFlapPairId = itemRdgFlapPairId?.invoke(item),
showQwinsta = itemQwinsta?.invoke(item) == true, showQwinsta = itemQwinsta?.invoke(item) == true,
@@ -497,6 +504,7 @@ private fun ListRow(
title: String, title: String,
subtitle: String, subtitle: String,
onClick: () -> Unit, onClick: () -> Unit,
rdgAccessPath: String? = null,
rdgFlap: Boolean = false, rdgFlap: Boolean = false,
rdgFlapPairId: Long? = null, rdgFlapPairId: Long? = null,
showQwinsta: Boolean = false, showQwinsta: Boolean = false,
@@ -522,6 +530,9 @@ private fun ListRow(
.clickable(onClick = onClick), .clickable(onClick = onClick),
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
if (!rdgAccessPath.isNullOrBlank()) {
RdgAccessBadge(path = rdgAccessPath)
}
if (rdgFlap) { if (rdgFlap) {
RdgFlapBadge(pairEventId = rdgFlapPairId) RdgFlapBadge(pairEventId = rdgFlapPairId)
} }
@@ -532,7 +543,7 @@ private fun ListRow(
OutlinedButton( OutlinedButton(
enabled = !qwinstaLoading, enabled = !qwinstaLoading,
onClick = onQwinstaClick, onClick = onQwinstaClick,
) { Text(if (qwinstaLoading) "qwinsta" else "qwinsta / logoff") } ) { Text(if (qwinstaLoading) "" else "Оборвать сессию") }
} }
if (showTerminate && onTerminateClick != null) { if (showTerminate && onTerminateClick != null) {
EventSessionTerminateButton( EventSessionTerminateButton(
@@ -13,14 +13,31 @@ data class QwinstaSessionRow(
fun hasRdgFlapUi(event: EventSummary): Boolean = fun hasRdgFlapUi(event: EventSummary): Boolean =
event.rdgFlap || event.rdgFlapQwinstaEventId != null event.rdgFlap || event.rdgFlapQwinstaEventId != null
fun rdgFlapQwinstaEventId(event: EventSummary): Long? =
event.rdgFlapQwinstaEventId ?: if (event.rdgFlap) event.id else null
fun hasRdgFlapUi(event: EventDetail): Boolean = fun hasRdgFlapUi(event: EventDetail): Boolean =
event.rdgFlap || event.rdgFlapQwinstaEventId != null event.rdgFlap || event.rdgFlapQwinstaEventId != null
fun rdgFlapQwinstaEventId(event: EventDetail): Long? = fun hasRdgAccessPath(event: EventSummary): Boolean =
event.rdgFlapQwinstaEventId ?: if (event.rdgFlap) event.id else null !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<QwinstaSessionRow> { fun parseQwinstaSessions(stdout: String, actorUser: String?): List<QwinstaSessionRow> {
val lines = stdout.lines().map { it.trim() }.filter { it.isNotEmpty() } val lines = stdout.lines().map { it.trim() }.filter { it.isNotEmpty() }