feat: RDG 302/303 display path labels and RDS session break (0.3.1)

Show RDS access via RDG-Comp or Haproxy-RDG-Comp, enable qwinsta/logoff on any RDG event with internal_ip.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-22 19:52:22 +10:00
parent 4eb41608ea
commit 24ca87a47a
19 changed files with 468 additions and 32 deletions
+2
View File
@@ -187,6 +187,8 @@ export interface EventSummary {
rdg_flap?: boolean;
rdg_flap_pair_event_id?: number | null;
rdg_flap_qwinsta_event_id?: number | null;
rdg_access_path?: string | null;
rdg_qwinsta_enabled?: boolean;
}
export interface AgentCommandResponse {
@@ -0,0 +1,40 @@
<template>
<span class="rdg-access-badge" :title="tooltip">{{ event.rdg_access_path }}</span>
</template>
<script setup lang="ts">
import { computed } from "vue";
import type { EventSummary } from "../api";
const props = defineProps<{
event: EventSummary;
}>();
const tooltip = computed(() => {
const path = props.event.rdg_access_path;
if (path === "Haproxy-RDG-Comp") {
return "Вход на рабочий ПК через HAProxy → RD Gateway";
}
if (path === "RDG-Comp") {
return "Прямой вход на рабочий ПК через RD Gateway";
}
return "Путь входа RDS через RD Gateway";
});
</script>
<style scoped>
.rdg-access-badge {
display: inline-block;
margin-right: 0.45rem;
padding: 0.1rem 0.45rem;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
color: #7ec8e3;
background: rgba(126, 200, 227, 0.1);
border: 1px solid rgba(126, 200, 227, 0.4);
border-radius: 4px;
vertical-align: middle;
white-space: nowrap;
}
</style>
+2 -1
View File
@@ -6,6 +6,7 @@ import {
type AgentCommandResponse,
type EventSummary,
} from "../api";
import { rdgQwinstaEventId } from "../utils/rdgFlap";
export type QwinstaSessionRow = {
sessionName: string;
@@ -114,7 +115,7 @@ export function useRdgQwinsta() {
}
async function runQwinsta(event: EventSummary) {
const targetId = event.rdg_flap_qwinsta_event_id ?? event.id;
const targetId = rdgQwinstaEventId(event) ?? event.id;
qwinstaLoadingId.value = event.id;
qwinstaModal.value = {
open: true,
+21 -2
View File
@@ -4,12 +4,31 @@ export function hasRdgFlapUi(event: EventSummary): boolean {
return Boolean(event.rdg_flap || event.rdg_flap_qwinsta_event_id);
}
export function rdgFlapQwinstaEventId(event: EventSummary): number | null {
export function hasRdgAccessPath(event: EventSummary): boolean {
return Boolean(event.rdg_access_path);
}
export function rdgAccessPathLabel(event: EventSummary): string {
return event.rdg_access_path || "";
}
export function rdgQwinstaEventId(event: EventSummary): number | null {
if (event.rdg_flap_qwinsta_event_id) return event.rdg_flap_qwinsta_event_id;
return event.rdg_flap ? event.id : null;
if (event.rdg_qwinsta_enabled) return event.id;
if (event.rdg_flap) return event.id;
return null;
}
/** @deprecated use rdgQwinstaEventId */
export function rdgFlapQwinstaEventId(event: EventSummary): number | null {
return rdgQwinstaEventId(event);
}
export function rdgFlapPairLabel(event: EventSummary): string {
if (!event.rdg_flap_pair_event_id) return "";
return event.type === "rdg.connection.success" ? "→" : "←";
}
export function isRdgConnectionEvent(event: EventSummary): boolean {
return event.type.startsWith("rdg.connection.");
}
+1 -1
View File
@@ -1,4 +1,4 @@
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
export const APP_NAME = "Security Alert Center";
export const APP_VERSION = "0.3.0";
export const APP_VERSION = "0.3.1";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
+6 -3
View File
@@ -282,6 +282,7 @@
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
<td>
<RdgAccessBadge v-if="e.rdg_access_path" :event="e" />
<RdgFlapBadge v-if="hasRdgFlapUi(e)" :event="e" />
{{ e.title }}
</td>
@@ -289,13 +290,14 @@
<td class="dash-actions">
<button
v-if="rdgFlapQwinstaEventId(e)"
v-if="rdgQwinstaEventId(e)"
type="button"
class="secondary dash-qwinsta-btn"
:disabled="qwinstaLoadingId === e.id"
title="qwinsta и завершение сеанса на рабочем ПК"
@click="runQwinsta(e)"
>
qwinsta
Обрыв RDS
</button>
<button
v-if="eventSupportsSessionTerminate(e)"
@@ -346,12 +348,13 @@ import {
type DashboardSummary,
type EventSummary,
} from "../api";
import RdgAccessBadge from "../components/RdgAccessBadge.vue";
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
import { useSacVersion } from "../composables/useSacVersion";
import { formatServerName } from "../utils/hostDisplay";
import { hasRdgFlapUi, rdgFlapQwinstaEventId } from "../utils/rdgFlap";
import { hasRdgFlapUi, rdgQwinstaEventId } from "../utils/rdgFlap";
import { eventSupportsSessionTerminate } from "../utils/sessionActions";
const { appName, appVersion } = useSacVersion();
+9 -3
View File
@@ -7,18 +7,23 @@
<p v-else-if="loading">Загрузка</p>
<template v-else-if="event">
<h1>{{ event.title }}</h1>
<p v-if="event.rdg_access_path" class="event-rdg-access-banner">
<RdgAccessBadge :event="event" />
<span class="muted">Вход на рабочий ПК через RD Gateway.</span>
</p>
<p v-if="hasRdgFlapUi(event)" class="event-rdg-flap-banner">
<RdgFlapBadge :event="event" :detail-link-query="backToEventsQuery" />
<span class="muted">302303 за несколько секунд возможна зависшая сессия на ПК пользователя.</span>
</p>
<p v-if="rdgFlapQwinstaEventId(event)" class="event-rdg-actions">
<p v-if="rdgQwinstaEventId(event)" class="event-rdg-actions">
<button
type="button"
class="secondary"
:disabled="qwinstaLoadingId === event.id"
title="qwinsta и завершение сеанса на рабочем ПК"
@click="runQwinsta(event)"
>
qwinsta (RDG flap)
Обрыв RDS
</button>
</p>
<RdgQwinstaModal
@@ -93,11 +98,12 @@
import { computed, onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { apiFetch, type EventDetail } from "../api";
import RdgAccessBadge from "../components/RdgAccessBadge.vue";
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
import ReportBodyCard from "../components/ReportBodyCard.vue";
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
import { hasRdgFlapUi, rdgFlapQwinstaEventId } from "../utils/rdgFlap";
import { hasRdgFlapUi, rdgQwinstaEventId } from "../utils/rdgFlap";
import { eventsBackQueryFromDetail } from "../utils/eventsListQuery";
import { dailyReportTypeLabel, isDailyReportType } from "../utils/reportDisplay";
+6 -3
View File
@@ -54,18 +54,20 @@
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
<td><code>{{ e.type }}</code></td>
<td>
<RdgAccessBadge v-if="e.rdg_access_path" :event="e" />
<RdgFlapBadge v-if="hasRdgFlapUi(e)" :event="e" :detail-link-query="detailLinkQuery" />
{{ e.title }}
</td>
<td class="events-actions">
<button
v-if="rdgFlapQwinstaEventId(e)"
v-if="rdgQwinstaEventId(e)"
type="button"
class="secondary events-qwinsta-btn"
:disabled="qwinstaLoadingId === e.id"
title="qwinsta и завершение сеанса на рабочем ПК"
@click="runQwinsta(e)"
>
qwinsta
Обрыв RDS
</button>
<button
v-if="eventSupportsSessionTerminate(e)"
@@ -105,10 +107,11 @@ import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useSacLiveEventStream } from "../composables/useSacLiveEventStream";
import { apiFetch, postEventTerminateSession, type EventListResponse, type EventSummary } from "../api";
import RdgAccessBadge from "../components/RdgAccessBadge.vue";
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
import { hasRdgFlapUi, rdgFlapQwinstaEventId } from "../utils/rdgFlap";
import { hasRdgFlapUi, rdgQwinstaEventId } from "../utils/rdgFlap";
import {
apiParamsFromState,
buildEventsListQuery,