Показать понятный аудит в консоли оператора
Названия событий, акторов, параметров и значений вынесены в ресурсы ru.js (operator.event/actor/field/value) с заделом под языки. Таблица аудита показывает понятное название действия, сырой ключ вторично, изменения «было → стало» и сырой JSON под спойлером. В колонке пользователя основное — имя, id мелко под ним. Термин «тенант» в UI оператора заменён на «Пользователь».
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
// Фильтры лент аудита/действий (общие для разделов «Аудит» и «Аналитика»).
|
||||
import { t } from '@/i18n/index.js'
|
||||
import { AUDIT_EVENT_TYPES, ACTOR_TYPES } from '../../store/operator.js'
|
||||
import SelectInput from '../ui/SelectInput.vue'
|
||||
import TextInput from '../ui/TextInput.vue'
|
||||
@@ -10,8 +11,20 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:filters'])
|
||||
|
||||
const eventOptions = AUDIT_EVENT_TYPES.map((t) => ({ value: t, label: t }))
|
||||
const actorOptions = ACTOR_TYPES.map((t) => ({ value: t, label: t }))
|
||||
// Локализует код по ключу, возвращая сам код при отсутствии перевода.
|
||||
function translate(key, fallback) {
|
||||
const label = t(key)
|
||||
return label === key ? fallback : label
|
||||
}
|
||||
|
||||
const eventOptions = AUDIT_EVENT_TYPES.map((code) => ({
|
||||
value: code,
|
||||
label: translate(`operator.event.${code}`, code),
|
||||
}))
|
||||
const actorOptions = ACTOR_TYPES.map((code) => ({
|
||||
value: code,
|
||||
label: translate(`operator.actor.${code}`, code),
|
||||
}))
|
||||
|
||||
function set(key, value) {
|
||||
emit('update:filters', { ...props.filters, [key]: value })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { t } from '@/i18n/index.js'
|
||||
// Таблица записей аудита (AuditRecordDto): общая для /audit и analytics/activity.
|
||||
// detailJson — строка JSON; раскрывается по клику на тип события.
|
||||
// Таблица записей аудита (AuditRecordViewDto): общая для /audit и analytics/activity.
|
||||
// Названия событий/акторов/параметров — из ресурсов; сырой ключ события — вторично.
|
||||
import { ref } from 'vue'
|
||||
import { fmtDateTimeCompact } from '../../store/core.js'
|
||||
import Badge from '../ui/Badge.vue'
|
||||
@@ -21,12 +21,43 @@ const COLUMNS = [
|
||||
{ key: 'at', label: t('operator.vremya'), align: 'right' },
|
||||
]
|
||||
|
||||
const expanded = ref(-1)
|
||||
|
||||
const ACTOR_TONES = { operator: 'brand', tenant: 'neutral', system: 'warn' }
|
||||
|
||||
const expanded = ref(-1)
|
||||
const jsonOpen = ref(-1)
|
||||
|
||||
function toggle(index) {
|
||||
expanded.value = expanded.value === index ? -1 : index
|
||||
if (expanded.value !== index) jsonOpen.value = -1
|
||||
}
|
||||
|
||||
function toggleJson(index) {
|
||||
jsonOpen.value = jsonOpen.value === index ? -1 : index
|
||||
}
|
||||
|
||||
// Локализует код по ключу, возвращая сам код при отсутствии перевода.
|
||||
function translate(key, fallback) {
|
||||
const label = t(key)
|
||||
return label === key ? fallback : label
|
||||
}
|
||||
|
||||
const eventLabel = (code) => translate(`operator.event.${code}`, code)
|
||||
const actorLabel = (code) => translate(`operator.actor.${code}`, code)
|
||||
const fieldLabel = (field) => translate(`operator.field.${field}`, field)
|
||||
|
||||
function valueLabel(field, raw) {
|
||||
if (raw === null || raw === undefined || raw === '') return '—'
|
||||
if (raw === 'true') return t('operator.da')
|
||||
if (raw === 'false') return t('operator.net')
|
||||
return translate(`operator.value.${field}.${raw}`, raw)
|
||||
}
|
||||
|
||||
function changesOf(row) {
|
||||
return Array.isArray(row.changes) ? row.changes : []
|
||||
}
|
||||
|
||||
function hasFrom(change) {
|
||||
return change.from !== null && change.from !== undefined
|
||||
}
|
||||
|
||||
function pretty(json) {
|
||||
@@ -52,19 +83,47 @@ function pretty(json) {
|
||||
<span class="inline-block text-[10px]" :class="expanded === index ? 'rotate-90' : ''">▶</span>
|
||||
</button>
|
||||
<div class="min-w-0">
|
||||
<div class="font-mono text-[12px] text-hi">{{ row.eventType }}</div>
|
||||
<pre
|
||||
v-if="expanded === index && row.detailJson"
|
||||
class="mt-1.5 max-w-[420px] max-h-40 overflow-auto rounded-md bg-ink/70 border border-white/8 p-2 text-[10.5px] text-mid"
|
||||
>{{ pretty(row.detailJson) }}</pre>
|
||||
<div class="text-[12.5px] text-hi">{{ eventLabel(row.eventType) }}</div>
|
||||
<div class="font-mono text-[10.5px] text-low mt-0.5">{{ row.eventType }}</div>
|
||||
<div
|
||||
v-if="expanded === index"
|
||||
class="mt-1.5 max-w-[460px] rounded-md bg-ink/70 border border-white/8 p-2"
|
||||
>
|
||||
<div v-if="changesOf(row).length" class="space-y-0.5">
|
||||
<div v-for="(change, i) in changesOf(row)" :key="i" class="text-[11px] text-mid">
|
||||
<span class="text-low">{{ fieldLabel(change.field) }}:</span>
|
||||
<template v-if="hasFrom(change)">
|
||||
<span class="text-low">{{ valueLabel(change.field, change.from) }}</span>
|
||||
<span class="text-low/60"> → </span>
|
||||
</template>
|
||||
<span class="text-hi">{{ valueLabel(change.field, change.to) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-[11px] text-low">{{ $t('operator.dopolnitelnyh-parametrov-net') }}</div>
|
||||
<button
|
||||
v-if="row.detailJson"
|
||||
class="mt-1.5 text-[10.5px] text-low hover:text-mid transition-colors"
|
||||
@click.stop="toggleJson(index)"
|
||||
>
|
||||
{{ jsonOpen === index ? $t('operator.skryt-json') : $t('operator.pokazat-json') }}
|
||||
</button>
|
||||
<pre
|
||||
v-if="jsonOpen === index"
|
||||
class="mt-1 max-w-[420px] max-h-40 overflow-auto rounded-md bg-ink/70 border border-white/8 p-2 text-[10.5px] text-mid"
|
||||
>{{ pretty(row.detailJson) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-actorType="{ row }">
|
||||
<Badge :tone="ACTOR_TONES[row.actorType] || 'neutral'">{{ row.actorType }}</Badge>
|
||||
<Badge :tone="ACTOR_TONES[row.actorType] || 'neutral'">{{ actorLabel(row.actorType) }}</Badge>
|
||||
</template>
|
||||
<template #cell-tenantId="{ row }">
|
||||
<span class="font-mono text-[11px] text-mid">{{ row.tenantId || '—' }}</span>
|
||||
<div v-if="row.tenantName" class="min-w-0">
|
||||
<div class="text-[12px] text-hi truncate">{{ row.tenantName }}</div>
|
||||
<div class="font-mono text-[10.5px] text-low truncate">{{ row.tenantId }}</div>
|
||||
</div>
|
||||
<span v-else class="font-mono text-[11px] text-low">{{ row.tenantId || '—' }}</span>
|
||||
</template>
|
||||
<template #cell-ip="{ row }">
|
||||
<span class="font-mono text-[11px] text-low">{{ row.ip || '—' }}</span>
|
||||
|
||||
@@ -773,7 +773,7 @@ export const ru = {
|
||||
"nedostupna": "недоступна",
|
||||
"da": "да",
|
||||
"net": "нет",
|
||||
"tenant": "Тенант",
|
||||
"tenant": "Пользователь",
|
||||
"razreshen": "Разрешён",
|
||||
"zamorozhen": "Заморожен",
|
||||
"operator": "оператор",
|
||||
@@ -784,7 +784,7 @@ export const ru = {
|
||||
"vse-sobytiya": "Все события",
|
||||
"aktor": "Актор",
|
||||
"vse-aktory": "Все акторы",
|
||||
"id-tenanta": "ID тенанта",
|
||||
"id-tenanta": "ID пользователя",
|
||||
"id-aktora": "ID актора",
|
||||
"s": "С",
|
||||
"po": "По",
|
||||
@@ -796,10 +796,10 @@ export const ru = {
|
||||
"tokeny": "Токены",
|
||||
"dejstviya": "Действия",
|
||||
"po-dnyam": "По дням",
|
||||
"po-tenantam": "По тенантам",
|
||||
"po-tenantam": "По пользователям",
|
||||
"po-provajderam": "По провайдерам",
|
||||
"po-modelyam": "По моделям",
|
||||
"tenanty": "Тенанты",
|
||||
"tenanty": "Пользователи",
|
||||
"aktivnyh-vsego": "активных / всего",
|
||||
"vsego-tokenov": "Всего токенов",
|
||||
"tokeny-zaprosa": "Токены запроса",
|
||||
@@ -814,7 +814,7 @@ export const ru = {
|
||||
"period": "Период",
|
||||
"rashod-tokenov": "Расход токенов",
|
||||
"gruppirovka": "Группировка",
|
||||
"id-tenanta-filtr": "ID тенанта (фильтр)",
|
||||
"id-tenanta-filtr": "ID пользователя (фильтр)",
|
||||
"itogo-tokenov": "Итого токенов",
|
||||
"zapros": "запрос",
|
||||
"otvet": "·\r\n ответ",
|
||||
@@ -826,6 +826,89 @@ export const ru = {
|
||||
"audit": "Аудит",
|
||||
"zhurnal-dejstvij-polzovatelej-operatorov": "Журнал действий пользователей, операторов и системы",
|
||||
"sobytij-po-filtru-net": "Событий по фильтру нет",
|
||||
"pokazat-json": "Показать JSON",
|
||||
"skryt-json": "Скрыть JSON",
|
||||
"dopolnitelnyh-parametrov-net": "Дополнительных параметров нет",
|
||||
"actor": {
|
||||
"operator": "Оператор",
|
||||
"tenant": "Пользователь",
|
||||
"system": "Система"
|
||||
},
|
||||
"event": {
|
||||
"tenant_login_ok": "Вход пользователя",
|
||||
"tenant_login_failed": "Неудачный вход пользователя",
|
||||
"tenant_logout": "Выход пользователя",
|
||||
"operator_login_ok": "Вход оператора",
|
||||
"operator_login_failed": "Неудачный вход оператора",
|
||||
"operator_logout": "Выход оператора",
|
||||
"invite_joined": "Регистрация по приглашению",
|
||||
"invite_created": "Создано приглашение",
|
||||
"invite_revoked": "Отозвано приглашение",
|
||||
"invite_activated": "Приглашение активировано",
|
||||
"card_created": "Создана карточка",
|
||||
"card_moved": "Карточка перемещена",
|
||||
"card_trashed": "Карточка в корзине",
|
||||
"card_restored": "Карточка восстановлена",
|
||||
"card_deleted": "Карточка удалена",
|
||||
"card_comment_added": "Комментарий к карточке",
|
||||
"card_reclassified": "Карточки переклассифицированы",
|
||||
"container_created": "Создана колонка",
|
||||
"container_updated": "Изменена колонка",
|
||||
"container_deleted": "Удалена колонка",
|
||||
"settings_updated": "Изменены настройки",
|
||||
"channel_enabled": "Включён мониторинг каналов",
|
||||
"channel_created": "Канал добавлен",
|
||||
"telegram_linked": "Подключён Telegram",
|
||||
"telegram_keys_changed": "Изменены ключи Telegram",
|
||||
"tenant_created": "Создан пользователь",
|
||||
"tenant_status_changed": "Изменён статус пользователя",
|
||||
"tenant_limit_changed": "Изменён лимит ИИ",
|
||||
"impersonation_started": "Вход от имени пользователя",
|
||||
"impersonation_stopped": "Завершён вход от имени пользователя"
|
||||
},
|
||||
"field": {
|
||||
"login": "Логин",
|
||||
"email": "Email",
|
||||
"codeHash": "Код (хэш)",
|
||||
"budget": "Бюджет токенов",
|
||||
"period": "Период",
|
||||
"apiId": "api_id",
|
||||
"apiHashSet": "api_hash задан",
|
||||
"name": "Имя",
|
||||
"status": "Статус",
|
||||
"targetLogin": "Вход",
|
||||
"cardId": "Карточка",
|
||||
"to": "Куда",
|
||||
"col": "Колонка",
|
||||
"containerId": "Колонка",
|
||||
"attempted": "Обработано",
|
||||
"reclassified": "Переклассифицировано",
|
||||
"moved": "Перемещено",
|
||||
"trashed": "Отправлено в корзину",
|
||||
"fields": "Изменённые поля",
|
||||
"all": "Все каналы",
|
||||
"count": "Каналов",
|
||||
"phase": "Этап",
|
||||
"dialogId": "Диалог"
|
||||
},
|
||||
"value": {
|
||||
"status": {
|
||||
"active": "Активен",
|
||||
"suspended": "Приостановлен"
|
||||
},
|
||||
"period": {
|
||||
"month": "Месяц",
|
||||
"day": "День"
|
||||
},
|
||||
"phase": {
|
||||
"ready": "Готово",
|
||||
"idle": "Не подключён",
|
||||
"qr": "Ожидает QR",
|
||||
"code": "Ожидает код",
|
||||
"password": "Ожидает пароль",
|
||||
"phone": "Ожидает телефон"
|
||||
}
|
||||
},
|
||||
"v-poryadke": "в порядке",
|
||||
"nezdorov": "нездоров",
|
||||
"nedostupen": "недоступен",
|
||||
@@ -849,7 +932,7 @@ export const ru = {
|
||||
"priglasheniya": "Приглашения",
|
||||
"sozdat-priglashenie": "Создать приглашение",
|
||||
"priglashenij-poka-net": "Приглашений пока нет",
|
||||
"novyj-tenant": "Новый тенант",
|
||||
"novyj-tenant": "Новый пользователь",
|
||||
"ssylka": "Ссылка",
|
||||
"novoe-priglashenie": "Новое приглашение",
|
||||
"kod-dejstvuet-72-chasa-ssylka-vedet-na": "Код действует 72 часа. Ссылка ведёт на страницу активации.",
|
||||
@@ -857,7 +940,7 @@ export const ru = {
|
||||
"sozdano-2": "создано.",
|
||||
"ssylka-aktivacii": "Ссылка активации",
|
||||
"email-priglashennogo": "Email приглашённого",
|
||||
"sozdat-novyj-tenant-pri-aktivacii": "Создать новый тенант при активации",
|
||||
"sozdat-novyj-tenant-pri-aktivacii": "Создать нового пользователя при активации",
|
||||
"sozdat": "Создать",
|
||||
"mesyac": "Месяц",
|
||||
"den": "День",
|
||||
@@ -876,32 +959,32 @@ export const ru = {
|
||||
"perejti-v-osnovnoe-prilozhenie": "Перейти в основное приложение",
|
||||
"v-prilozhenie": "В приложение",
|
||||
"nevernyj-login-ili-parol-operatora": "Неверный логин или пароль оператора",
|
||||
"upravlenie-tenantami-priglasheniyami": "Управление тенантами, приглашениями, лимитами и аналитикой",
|
||||
"upravlenie-tenantami-priglasheniyami": "Управление пользователями, приглашениями, лимитами и аналитикой",
|
||||
"dev-defolt": "Dev-дефолт:",
|
||||
"polzovatelej": "Пользователей",
|
||||
"sozdan": "Создан",
|
||||
"priostanovit-tenant": "Приостановить тенант?",
|
||||
"vozobnovit-tenant": "Возобновить тенант?",
|
||||
"sozdat-tenanta": "Создать тенанта",
|
||||
"tenantov-poka-net": "Тенантов пока нет",
|
||||
"priostanovit-tenant": "Приостановить пользователя?",
|
||||
"vozobnovit-tenant": "Возобновить пользователя?",
|
||||
"sozdat-tenanta": "Создать пользователя",
|
||||
"tenantov-poka-net": "Пользователей пока нет",
|
||||
"detali": "Детали",
|
||||
"email-vladelca-opcionalen-bez-nego": "Email владельца опционален: без него владелец заводится через приглашение.",
|
||||
"tenant-2": "Тенант «",
|
||||
"tenant-2": "Пользователь «",
|
||||
"sozdan-2": "» создан.",
|
||||
"odnorazovyj-parol-vladelca-pokazhetsya": "Одноразовый пароль владельца — покажется только сейчас",
|
||||
"login": "Логин:",
|
||||
"parol": "Пароль:",
|
||||
"gotovo": "Готово",
|
||||
"imya-tenanta": "Имя тенанта",
|
||||
"imya-tenanta": "Имя пользователя",
|
||||
"ooo-romashka": "ООО «Ромашка»",
|
||||
"email-vladelca": "Email владельца",
|
||||
"esli-ukazat-vladelec-s-odnorazovym": "Если указать — владелец с одноразовым паролем создаётся сразу",
|
||||
"polzovateli": "Пользователи (",
|
||||
"v-tenante-net-polzovatelej": "В тенанте нет пользователей.",
|
||||
"vojti-pod-etim-polzovatelem-impersonation": "Войти под этим пользователем (impersonation)",
|
||||
"vhod-ot-imeni-polzovatelya": "Вход от имени пользователя",
|
||||
"polzovatel": "Пользователь:",
|
||||
"servernaya-sessiya-tenant-polzovatelya": "Серверная сессия тенант-пользователя активна в этом браузере. Выход из неё — обычная кнопка\r\n выхода в приложении.",
|
||||
"polzovateli": "Учётные записи (",
|
||||
"v-tenante-net-polzovatelej": "Нет связанных учётных записей.",
|
||||
"vojti-pod-etim-polzovatelem-impersonation": "Войти под этой учётной записью (impersonation)",
|
||||
"vhod-ot-imeni-polzovatelya": "Вход от имени учётной записи",
|
||||
"polzovatel": "Учётная запись:",
|
||||
"servernaya-sessiya-tenant-polzovatelya": "Серверная сессия учётной записи активна в этом браузере. Выход из неё — обычная кнопка\r\n выхода в приложении.",
|
||||
"perejti-v-prilozhenie": "Перейти в приложение",
|
||||
"tokensLine": "запрос {prompt} · ответ {completion} · событий {events}",
|
||||
"servicesCount": "Автономные сервисы: {count}",
|
||||
@@ -926,14 +1009,14 @@ export const ru = {
|
||||
"suspicious-failed-logins-ip": "Всплеск неудачных входов с одного IP",
|
||||
"suspicious-failed-logins-login": "Всплеск неудачных входов по одному логину",
|
||||
"suspicious-many-ips-actor": "Входы одного актора с множества IP",
|
||||
"suspicious-auth-failures-tenant": "Повторные неудачные входы по тенанту",
|
||||
"suspicious-auth-failures-tenant": "Повторные неудачные входы по пользователю",
|
||||
"telegram": "Telegram",
|
||||
"telegram-desc": "Глобальные ключи приложения Telegram для подключения аккаунтов тенантов",
|
||||
"telegram-desc": "Глобальные ключи приложения Telegram для подключения аккаунтов пользователей",
|
||||
"globalnye-klyuchi-telegram": "Глобальные ключи приложения",
|
||||
"globalnye-klyuchi-telegram-desc": "Едины для всех тенантов: тенант подключает аккаунт, не видя сами ключи",
|
||||
"globalnye-klyuchi-telegram-desc": "Едины для всех пользователей: пользователь подключает аккаунт, не видя сами ключи",
|
||||
"klyuchi-zadany": "Ключи заданы",
|
||||
"klyuchi-ne-zadany": "Ключи не заданы",
|
||||
"klyuchi-ne-zadany-poyasnenie": "Пока оператор не задаст ключи, подключение аккаунтов Telegram у тенантов недоступно.",
|
||||
"klyuchi-ne-zadany-poyasnenie": "Пока оператор не задаст ключи, подключение аккаунтов Telegram у пользователей недоступно.",
|
||||
"api-id-placeholder": "числовой api_id из my.telegram.org",
|
||||
"api-hash-placeholder": "буквенно-цифровой api_hash",
|
||||
"api-id-hint": "5–9 цифр — идентификатор приложения из my.telegram.org",
|
||||
|
||||
Reference in New Issue
Block a user