fix: show logoff actions in RDG qwinsta modal (0.20.7)

Put session table before raw output, add manual session ID logoff, improve qwinsta line parsing.
This commit is contained in:
2026-06-20 19:20:52 +10:00
parent fc9d630263
commit 115289ef35
4 changed files with 119 additions and 36 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
"""Единый источник версии SAC (API, health, логи, OpenAPI).""" """Единый источник версии SAC (API, health, логи, OpenAPI)."""
APP_NAME = "Security Alert Center" APP_NAME = "Security Alert Center"
APP_VERSION = "0.20.6" APP_VERSION = "0.20.7"
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
+88 -5
View File
@@ -1,14 +1,16 @@
<template> <template>
<div v-if="modal.open" class="modal-backdrop" @click.self="emit('close')"> <div v-if="modal.open" class="modal-backdrop" @click.self="emit('close')">
<div class="modal-card qwinsta-modal" role="dialog" aria-modal="true"> <div class="modal-card qwinsta-modal" role="dialog" aria-modal="true">
<h3>qwinsta {{ modal.clientHostname || modal.target || `#${modal.eventId}` }}</h3> <h3>qwinsta / logoff {{ modal.clientHostname || modal.target || `#${modal.eventId}` }}</h3>
<p v-if="modal.internalIp" class="muted qwinsta-meta">Клиентский ПК: {{ modal.internalIp }}</p> <p v-if="modal.internalIp" class="muted qwinsta-meta">Клиентский ПК: {{ modal.internalIp }}</p>
<p v-if="modal.error" class="error">{{ modal.error }}</p> <p v-if="modal.error" class="error">{{ modal.error }}</p>
<p v-else-if="modal.loading">Ожидание ответа агента</p> <p v-else-if="modal.loading">Ожидание ответа</p>
<template v-else> <template v-else>
<pre v-if="modal.stdout" class="qwinsta-raw">{{ modal.stdout }}</pre> <template v-if="modal.sessions.length">
<p v-else class="muted">Нет вывода</p> <p class="qwinsta-hint">
<table v-if="modal.sessions.length" class="dash-table"> Сеансы на ПК пользователя нажмите <strong>logoff</strong>, чтобы оборвать зависшую сессию:
</p>
<table class="dash-table qwinsta-sessions-table">
<thead> <thead>
<tr> <tr>
<th>SESSION</th> <th>SESSION</th>
@@ -38,6 +40,35 @@
</tbody> </tbody>
</table> </table>
</template> </template>
<div v-else-if="modal.stdout" class="qwinsta-manual">
<p class="muted">
Сеансы не распознаны автоматически. Введите ID из вывода qwinsta ниже:
</p>
<div class="qwinsta-manual-row">
<input
v-model="manualSessionId"
type="text"
inputmode="numeric"
class="qwinsta-manual-input"
placeholder="ID сеанса"
/>
<button
type="button"
class="danger"
:disabled="!manualSessionId.trim() || modal.logoffId !== null"
@click="submitManualLogoff"
>
logoff
</button>
</div>
</div>
<p v-else class="muted">Нет вывода</p>
<details v-if="modal.stdout" class="qwinsta-raw-details">
<summary>Вывод qwinsta</summary>
<pre class="qwinsta-raw">{{ modal.stdout }}</pre>
</details>
</template>
<div class="modal-actions"> <div class="modal-actions">
<button type="button" class="secondary" @click="emit('close')">Закрыть</button> <button type="button" class="secondary" @click="emit('close')">Закрыть</button>
</div> </div>
@@ -46,6 +77,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue";
import type { QwinstaSessionRow } from "../composables/useRdgQwinsta"; import type { QwinstaSessionRow } from "../composables/useRdgQwinsta";
defineProps<{ defineProps<{
@@ -67,6 +99,14 @@ const emit = defineEmits<{
close: []; close: [];
logoff: [sessionId: number]; logoff: [sessionId: number];
}>(); }>();
const manualSessionId = ref("");
function submitManualLogoff() {
const id = Number.parseInt(manualSessionId.value.trim(), 10);
if (!Number.isFinite(id)) return;
emit("logoff", id);
}
</script> </script>
<style scoped> <style scoped>
@@ -97,6 +137,48 @@ const emit = defineEmits<{
font-size: 0.9rem; font-size: 0.9rem;
} }
.qwinsta-hint {
margin: 0 0 0.75rem;
font-size: 0.95rem;
color: #e6edf3;
}
.qwinsta-sessions-table {
margin-bottom: 1rem;
}
.qwinsta-manual {
margin-bottom: 1rem;
}
.qwinsta-manual-row {
display: flex;
gap: 0.5rem;
align-items: center;
margin-top: 0.5rem;
}
.qwinsta-manual-input {
flex: 1;
min-width: 0;
padding: 0.45rem 0.6rem;
border-radius: 6px;
border: 1px solid #3d4f63;
background: #0f1419;
color: #e6edf3;
}
.qwinsta-raw-details {
margin-top: 0.5rem;
}
.qwinsta-raw-details summary {
cursor: pointer;
color: #9aa4b2;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.qwinsta-raw { .qwinsta-raw {
font-size: 0.8rem; font-size: 0.8rem;
max-height: 12rem; max-height: 12rem;
@@ -105,6 +187,7 @@ const emit = defineEmits<{
background: #0d1117; background: #0d1117;
padding: 0.75rem; padding: 0.75rem;
border-radius: 4px; border-radius: 4px;
margin: 0;
} }
.modal-actions { .modal-actions {
+2 -2
View File
@@ -21,7 +21,7 @@ function parseQwinstaSessions(stdout: string, actorUser: string): QwinstaSession
if (/^SESSION/i.test(line) || /^---/.test(line)) continue; if (/^SESSION/i.test(line) || /^---/.test(line)) continue;
const parts = line.split(/\s+/); const parts = line.split(/\s+/);
if (parts.length < 4) continue; if (parts.length < 4) continue;
const sessionName = parts[0]; const sessionName = parts[0].replace(/^>/, "");
const userName = parts[1]; const userName = parts[1];
const id = Number.parseInt(parts[2], 10); const id = Number.parseInt(parts[2], 10);
const state = parts.slice(3).join(" "); const state = parts.slice(3).join(" ");
@@ -39,7 +39,7 @@ function parseQwinstaSessions(stdout: string, actorUser: string): QwinstaSession
const id = Number.parseInt(parts[2], 10); const id = Number.parseInt(parts[2], 10);
if (!Number.isFinite(id)) continue; if (!Number.isFinite(id)) continue;
rows.push({ rows.push({
sessionName: parts[0], sessionName: parts[0].replace(/^>/, ""),
userName: parts[1], userName: parts[1],
id, id,
state: parts.slice(3).join(" "), state: parts.slice(3).join(" "),
+1 -1
View File
@@ -1,4 +1,4 @@
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */ /** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
export const APP_NAME = "Security Alert Center"; export const APP_NAME = "Security Alert Center";
export const APP_VERSION = "0.20.6"; export const APP_VERSION = "0.20.7";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;