fix: show RDG flap badge on 302 and 303 events in SAC UI (0.20.5)
Seaca surfaced flap via Problems; the web UI only flagged 303 in DB and had no visible label. Resolve flap pairs at read time and show badge plus qwinsta on both sides of 302→303.
This commit is contained in:
@@ -170,6 +170,8 @@ export interface EventSummary {
|
||||
summary: string;
|
||||
actor_user?: string | null;
|
||||
rdg_flap?: boolean;
|
||||
rdg_flap_pair_event_id?: number | null;
|
||||
rdg_flap_qwinsta_event_id?: number | null;
|
||||
}
|
||||
|
||||
export interface AgentCommandResponse {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<span class="rdg-flap-badge" :title="tooltip">
|
||||
RDG flap
|
||||
<template v-if="event.rdg_flap_pair_event_id">
|
||||
{{ pairArrow }}
|
||||
<RouterLink :to="pairLink">{{ event.rdg_flap_pair_event_id }}</RouterLink>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { EventSummary } from "../api";
|
||||
import { rdgFlapPairLabel } from "../utils/rdgFlap";
|
||||
|
||||
const props = defineProps<{
|
||||
event: EventSummary;
|
||||
detailLinkQuery?: Record<string, string>;
|
||||
}>();
|
||||
|
||||
const pairArrow = computed(() => rdgFlapPairLabel(props.event));
|
||||
const tooltip = computed(
|
||||
() =>
|
||||
"302→303 за 1–10 с. Возможна зависшая сессия на ПК пользователя — qwinsta/logoff.",
|
||||
);
|
||||
const pairLink = computed(() => ({
|
||||
path: `/events/${props.event.rdg_flap_pair_event_id}`,
|
||||
query: props.detailLinkQuery,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rdg-flap-badge {
|
||||
display: inline-block;
|
||||
margin-right: 0.45rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: #f0c674;
|
||||
background: rgba(240, 198, 116, 0.12);
|
||||
border: 1px solid rgba(240, 198, 116, 0.45);
|
||||
border-radius: 4px;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rdg-flap-badge a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
margin-left: 0.15rem;
|
||||
}
|
||||
</style>
|
||||
@@ -114,10 +114,11 @@ export function useRdgQwinsta() {
|
||||
}
|
||||
|
||||
async function runQwinsta(event: EventSummary) {
|
||||
const targetId = event.rdg_flap_qwinsta_event_id ?? event.id;
|
||||
qwinstaLoadingId.value = event.id;
|
||||
qwinstaModal.value = {
|
||||
open: true,
|
||||
eventId: event.id,
|
||||
eventId: targetId,
|
||||
actorUser: event.actor_user || "",
|
||||
commandUuid: "",
|
||||
target: "",
|
||||
@@ -130,13 +131,13 @@ export function useRdgQwinsta() {
|
||||
logoffId: null,
|
||||
};
|
||||
try {
|
||||
const cmd = await postEventQwinsta(event.id);
|
||||
const cmd = await postEventQwinsta(targetId);
|
||||
applyCommandMeta(qwinstaModal.value, cmd);
|
||||
qwinstaModal.value.commandUuid = cmd.command_uuid;
|
||||
if (applyCommandResult(event.id, event.actor_user || "", cmd)) {
|
||||
if (applyCommandResult(targetId, event.actor_user || "", cmd)) {
|
||||
return;
|
||||
}
|
||||
pollQwinstaCommand(event.id, cmd.command_uuid, event.actor_user || "");
|
||||
pollQwinstaCommand(targetId, cmd.command_uuid, event.actor_user || "");
|
||||
} catch (e) {
|
||||
qwinstaModal.value.loading = false;
|
||||
qwinstaModal.value.error = e instanceof Error ? e.message : "Не удалось отправить qwinsta";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { EventSummary } from "../api";
|
||||
|
||||
export function hasRdgFlapUi(event: EventSummary): boolean {
|
||||
return Boolean(event.rdg_flap || event.rdg_flap_qwinsta_event_id);
|
||||
}
|
||||
|
||||
export function rdgFlapQwinstaEventId(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;
|
||||
}
|
||||
|
||||
export function rdgFlapPairLabel(event: EventSummary): string {
|
||||
if (!event.rdg_flap_pair_event_id) return "";
|
||||
return event.type === "rdg.connection.success" ? "→" : "←";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||
export const APP_NAME = "Security Alert Center";
|
||||
export const APP_VERSION = "0.20.4";
|
||||
export const APP_VERSION = "0.20.5";
|
||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||
|
||||
@@ -281,12 +281,15 @@
|
||||
|
||||
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
||||
|
||||
<td>{{ e.title }}</td>
|
||||
<td>
|
||||
<RdgFlapBadge v-if="hasRdgFlapUi(e)" :event="e" />
|
||||
{{ e.title }}
|
||||
</td>
|
||||
|
||||
<td class="dash-actions">
|
||||
|
||||
<button
|
||||
v-if="e.rdg_flap"
|
||||
v-if="rdgFlapQwinstaEventId(e)"
|
||||
type="button"
|
||||
class="secondary dash-qwinsta-btn"
|
||||
:disabled="qwinstaLoadingId === e.id"
|
||||
@@ -333,10 +336,12 @@ import {
|
||||
type DashboardSummary,
|
||||
type EventSummary,
|
||||
} from "../api";
|
||||
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";
|
||||
|
||||
const { appName, appVersion } = useSacVersion();
|
||||
const { qwinstaLoadingId, qwinstaModal, closeQwinstaModal, runQwinsta, runLogoff } = useRdgQwinsta();
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
<p v-else-if="loading">Загрузка…</p>
|
||||
<template v-else-if="event">
|
||||
<h1>{{ event.title }}</h1>
|
||||
<p v-if="event.rdg_flap" class="event-rdg-actions">
|
||||
<p v-if="hasRdgFlapUi(event)" class="event-rdg-flap-banner">
|
||||
<RdgFlapBadge :event="event" :detail-link-query="backToEventsQuery" />
|
||||
<span class="muted">302→303 за несколько секунд — возможна зависшая сессия на ПК пользователя.</span>
|
||||
</p>
|
||||
<p v-if="rdgFlapQwinstaEventId(event)" class="event-rdg-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
@@ -89,9 +93,11 @@
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { apiFetch, type EventDetail } from "../api";
|
||||
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 { eventsBackQueryFromDetail } from "../utils/eventsListQuery";
|
||||
import { dailyReportTypeLabel, isDailyReportType } from "../utils/reportDisplay";
|
||||
|
||||
@@ -138,3 +144,13 @@ async function load() {
|
||||
onMounted(load);
|
||||
watch(() => props.id, load);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.event-rdg-flap-banner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem 0.75rem;
|
||||
margin: 0.5rem 0 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,10 +53,13 @@
|
||||
<td>{{ e.product_version || "—" }}</td>
|
||||
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
||||
<td><code>{{ e.type }}</code></td>
|
||||
<td>{{ e.title }}</td>
|
||||
<td>
|
||||
<RdgFlapBadge v-if="hasRdgFlapUi(e)" :event="e" :detail-link-query="detailLinkQuery" />
|
||||
{{ e.title }}
|
||||
</td>
|
||||
<td class="events-actions">
|
||||
<button
|
||||
v-if="e.rdg_flap"
|
||||
v-if="rdgFlapQwinstaEventId(e)"
|
||||
type="button"
|
||||
class="secondary events-qwinsta-btn"
|
||||
:disabled="qwinstaLoadingId === e.id"
|
||||
@@ -92,8 +95,10 @@
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { apiFetch, type EventListResponse } from "../api";
|
||||
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
|
||||
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||
import { hasRdgFlapUi, rdgFlapQwinstaEventId } from "../utils/rdgFlap";
|
||||
import {
|
||||
apiParamsFromState,
|
||||
buildEventsListQuery,
|
||||
|
||||
Reference in New Issue
Block a user