From bf7c83c40824d76f843328a2c5b97109947c456c Mon Sep 17 00:00:00 2001 From: PTah Date: Sun, 21 Jun 2026 20:13:40 +1000 Subject: [PATCH] feat: hide event types from SAC UI via Settings checkbox (0.20.23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-type «Показывать в событиях» in severity settings; filters lists, dashboard, hosts, and problems. --- .../versions/023_event_type_visibility.py | 34 +++++++++ backend/app/api/v1/dashboards.py | 47 +++++++----- backend/app/api/v1/events.py | 11 ++- backend/app/api/v1/hosts.py | 21 +++-- backend/app/api/v1/problems.py | 5 +- backend/app/api/v1/settings.py | 48 +++++++----- backend/app/models/__init__.py | 4 +- backend/app/models/event_type_visibility.py | 21 +++++ .../app/services/event_severity_overrides.py | 8 +- backend/app/services/event_type_visibility.py | 58 ++++++++++++++ backend/app/version.py | 2 +- backend/tests/test_event_type_visibility.py | 76 +++++++++++++++++++ frontend/package.json | 4 +- frontend/src/version.ts | 2 +- frontend/src/views/SettingsView.vue | 32 ++++++-- 15 files changed, 313 insertions(+), 60 deletions(-) create mode 100644 backend/alembic/versions/023_event_type_visibility.py create mode 100644 backend/app/models/event_type_visibility.py create mode 100644 backend/app/services/event_type_visibility.py create mode 100644 backend/tests/test_event_type_visibility.py diff --git a/backend/alembic/versions/023_event_type_visibility.py b/backend/alembic/versions/023_event_type_visibility.py new file mode 100644 index 0000000..fbb6015 --- /dev/null +++ b/backend/alembic/versions/023_event_type_visibility.py @@ -0,0 +1,34 @@ +"""event type visibility settings + +Revision ID: 023_event_type_visibility +Revises: 022_host_remote_action +Create Date: 2026-06-21 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = "023_event_type_visibility" +down_revision = "022_host_remote_action" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "event_type_visibility", + sa.Column("event_type", sa.String(length=128), nullable=False), + sa.Column("show_in_events", sa.Boolean(), nullable=False), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.PrimaryKeyConstraint("event_type"), + ) + + +def downgrade() -> None: + op.drop_table("event_type_visibility") diff --git a/backend/app/api/v1/dashboards.py b/backend/app/api/v1/dashboards.py index c0724e4..0cdf049 100644 --- a/backend/app/api/v1/dashboards.py +++ b/backend/app/api/v1/dashboards.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, Query from pydantic import BaseModel @@ -11,6 +11,7 @@ from app.database import get_db from app.models import Event, Host, Problem from app.schemas.list_models import EventSummary from app.services.event_summary import event_to_summary +from app.services.event_type_visibility import get_hidden_event_types, visibility_type_filter from app.services.host_health import DAILY_REPORT_TYPES, HEARTBEAT_TYPE, count_stale_hosts router = APIRouter(prefix="/dashboards", tags=["dashboards"]) @@ -18,13 +19,18 @@ router = APIRouter(prefix="/dashboards", tags=["dashboards"]) def fetch_recent_events(db: Session, *, limit: int = 8) -> list[EventSummary]: """Последние события по времени приёма ingest (received_at).""" - rows = db.scalars( + hidden = get_hidden_event_types(db) + type_filter = visibility_type_filter(hidden) + stmt = ( select(Event) .join(Host) .options(joinedload(Event.host)) .order_by(Event.received_at.desc()) .limit(limit) - ).all() + ) + if type_filter is not None: + stmt = stmt.where(type_filter) + rows = db.scalars(stmt).all() return [event_to_summary(e, db) for e in rows] @@ -61,10 +67,13 @@ def dashboard_summary( _user: str = Depends(get_current_user), ) -> DashboardSummary: since = datetime.now(timezone.utc) - timedelta(hours=24) + hidden = get_hidden_event_types(db) + type_filter = visibility_type_filter(hidden) - events_24h = db.scalar( - select(func.count()).select_from(Event).where(Event.received_at >= since) - ) or 0 + events_24h_stmt = select(func.count()).select_from(Event).where(Event.received_at >= since) + if type_filter is not None: + events_24h_stmt = events_24h_stmt.where(type_filter) + events_24h = db.scalar(events_24h_stmt) or 0 hosts_total = db.scalar(select(func.count()).select_from(Host)) or 0 settings = get_settings() hosts_stale = count_stale_hosts(db, settings.sac_heartbeat_stale_minutes) @@ -100,12 +109,16 @@ def dashboard_summary( or 0 ) - top_host_rows = db.execute( + top_host_stmt = ( select(Host.id, Host.hostname, Host.display_name, func.count()) .select_from(Event) .join(Host, Event.host_id == Host.id) .where(Event.received_at >= since) - .group_by(Host.id, Host.hostname, Host.display_name) + ) + if type_filter is not None: + top_host_stmt = top_host_stmt.where(type_filter) + top_host_rows = db.execute( + top_host_stmt.group_by(Host.id, Host.hostname, Host.display_name) .order_by(func.count().desc()) .limit(10) ).all() @@ -113,20 +126,18 @@ def dashboard_summary( TopHostItem(host_id=row[0], hostname=row[1], display_name=row[2], count=row[3]) for row in top_host_rows ] + top_type_stmt = select(Event.type, func.count()).where(Event.received_at >= since) + if type_filter is not None: + top_type_stmt = top_type_stmt.where(type_filter) top_type_rows = db.execute( - select(Event.type, func.count()) - .where(Event.received_at >= since) - .group_by(Event.type) - .order_by(func.count().desc()) - .limit(10) + top_type_stmt.group_by(Event.type).order_by(func.count().desc()).limit(10) ).all() top_event_types = [TopTypeItem(type=row[0], count=row[1]) for row in top_type_rows] - severity_rows = db.execute( - select(Event.severity, func.count()) - .where(Event.received_at >= since) - .group_by(Event.severity) - ).all() + severity_stmt = select(Event.severity, func.count()).where(Event.received_at >= since) + if type_filter is not None: + severity_stmt = severity_stmt.where(type_filter) + severity_rows = db.execute(severity_stmt.group_by(Event.severity)).all() severity_24h = {row[0]: row[1] for row in severity_rows} recent_events = fetch_recent_events(db, limit=8) diff --git a/backend/app/api/v1/events.py b/backend/app/api/v1/events.py index 00b74a3..c862d50 100644 --- a/backend/app/api/v1/events.py +++ b/backend/app/api/v1/events.py @@ -1,4 +1,4 @@ -import logging +import logging from datetime import datetime, timezone from typing import Any @@ -22,6 +22,7 @@ from app.services.agent_commands import ( from app.services.rdg_winrm_actions import execute_logoff_via_winrm, execute_qwinsta_via_winrm from app.services.ingest import ingest_event from app.services.event_summary import event_to_summary +from app.services.event_type_visibility import get_hidden_event_types, visibility_type_filter from app.services.problems import maybe_create_problem from app.services.schema_validate import validate_event_payload from app.services.notify_dispatch import ( @@ -134,8 +135,13 @@ def list_events( db: Session = Depends(get_db), _user: str = Depends(get_current_user), ) -> EventListResponse: + hidden = get_hidden_event_types(db) + type_filter = visibility_type_filter(hidden) stmt = select(Event).join(Host).options(joinedload(Event.host)) count_stmt = select(func.count()).select_from(Event).join(Host) + if type_filter is not None: + stmt = stmt.where(type_filter) + count_stmt = count_stmt.where(type_filter) if severity: stmt = stmt.where(Event.severity == severity) @@ -261,11 +267,14 @@ def get_event( db: Session = Depends(get_db), _user: str = Depends(get_current_user), ) -> EventDetail: + hidden = get_hidden_event_types(db) event = db.scalar( select(Event).where(Event.id == event_db_id).options(joinedload(Event.host)) ) if event is None: raise HTTPException(status_code=404, detail="Event not found") + if event.type in hidden: + raise HTTPException(status_code=404, detail="Event not found") base = event_to_summary(event, db) return EventDetail( **base.model_dump(), diff --git a/backend/app/api/v1/hosts.py b/backend/app/api/v1/hosts.py index c6a49b8..1d06fbf 100644 --- a/backend/app/api/v1/hosts.py +++ b/backend/app/api/v1/hosts.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.orm import Session @@ -56,10 +56,19 @@ from app.services.host_health import ( max_daily_report_time_by_host, max_event_time_by_host, ) +from app.services.event_type_visibility import get_hidden_event_types, visibility_type_filter router = APIRouter(prefix="/hosts", tags=["hosts"]) +def _count_visible_events(db: Session, host_id: int, hidden: frozenset[str]) -> int: + stmt = select(func.count()).select_from(Event).where(Event.host_id == host_id) + type_filter = visibility_type_filter(hidden) + if type_filter is not None: + stmt = stmt.where(type_filter) + return int(db.scalar(stmt) or 0) + + @router.get("", response_model=HostListResponse) def list_hosts( page: int = Query(1, ge=1), @@ -108,12 +117,11 @@ def list_hosts( latest_map, git_versions=git_release.versions, ) + hidden = get_hidden_event_types(db) items: list[HostSummary] = [] for host in rows: - event_count = db.scalar( - select(func.count()).select_from(Event).where(Event.host_id == host.id) - ) + event_count = _count_visible_events(db, host.id, hidden) last_hb = hb_map.get(host.id) items.append( HostSummary( @@ -161,9 +169,8 @@ def _host_detail_from_model( ) -> HostDetail: hb_map = max_event_time_by_host(db, HEARTBEAT_TYPE) report_map = max_daily_report_time_by_host(db) - event_count = db.scalar( - select(func.count()).select_from(Event).where(Event.host_id == host.id) - ) + hidden = get_hidden_event_types(db) + event_count = _count_visible_events(db, host.id, hidden) last_hb = hb_map.get(host.id) latest_map = latest_agent_versions_by_product(db) update_cfg = get_effective_agent_update_config(db) diff --git a/backend/app/api/v1/problems.py b/backend/app/api/v1/problems.py index 1e35aa9..183be8e 100644 --- a/backend/app/api/v1/problems.py +++ b/backend/app/api/v1/problems.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta, timezone @@ -17,6 +17,7 @@ from app.auth.jwt_auth import get_current_user from app.database import get_db from app.models import Event, Host, Problem, ProblemEvent +from app.services.event_type_visibility import get_hidden_event_types @@ -269,6 +270,8 @@ def get_problem( .order_by(Event.occurred_at.desc()) ).all() + hidden = get_hidden_event_types(db) + event_rows = [event for event in event_rows if event.type not in hidden] diff --git a/backend/app/api/v1/settings.py b/backend/app/api/v1/settings.py index 33d64f6..258a819 100644 --- a/backend/app/api/v1/settings.py +++ b/backend/app/api/v1/settings.py @@ -1,4 +1,4 @@ -from dataclasses import replace +from dataclasses import replace from datetime import datetime @@ -37,6 +37,7 @@ from app.services.event_severity_overrides import ( list_severity_override_items, replace_severity_overrides, ) +from app.services.event_type_visibility import replace_event_visibility from app.services.ui_settings import ( get_effective_ui_settings, upsert_ui_settings, @@ -375,6 +376,7 @@ class EventSeverityOverrideRow(BaseModel): event_type: str default_severity: str override_severity: str | None = None + show_in_events: bool = True class EventSeverityOverridesResponse(BaseModel): @@ -384,8 +386,25 @@ class EventSeverityOverridesResponse(BaseModel): class EventSeverityOverridesUpdate(BaseModel): overrides: dict[str, str | None] = Field( + default_factory=dict, description="event_type → severity; null или пустая строка сбрасывает override", ) + visibility: dict[str, bool | None] = Field( + default_factory=dict, + description="event_type → show_in_events; null или true сбрасывает скрытие (показывать)", + ) + + +def _severity_override_rows(items) -> list[EventSeverityOverrideRow]: + return [ + EventSeverityOverrideRow( + event_type=i.event_type, + default_severity=i.default_severity, + override_severity=i.override_severity, + show_in_events=i.show_in_events, + ) + for i in items + ] @router.get("/notifications/severity-overrides", response_model=EventSeverityOverridesResponse) @@ -395,14 +414,7 @@ def get_severity_overrides( ) -> EventSeverityOverridesResponse: items = list_severity_override_items(db) return EventSeverityOverridesResponse( - items=[ - EventSeverityOverrideRow( - event_type=i.event_type, - default_severity=i.default_severity, - override_severity=i.override_severity, - ) - for i in items - ], + items=_severity_override_rows(items), source=SOURCE_DB, ) @@ -414,19 +426,19 @@ def update_severity_overrides( _user=Depends(require_admin), ) -> EventSeverityOverridesResponse: try: - items = replace_severity_overrides(db, body.overrides) + items = list_severity_override_items(db) + if body.overrides: + items = replace_severity_overrides(db, body.overrides) + if body.visibility: + replace_event_visibility(db, body.visibility) + items = list_severity_override_items(db) + if not body.overrides and not body.visibility: + raise HTTPException(status_code=422, detail="Нет изменений для сохранения") db.commit() except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc return EventSeverityOverridesResponse( - items=[ - EventSeverityOverrideRow( - event_type=i.event_type, - default_severity=i.default_severity, - override_severity=i.override_severity, - ) - for i in items - ], + items=_severity_override_rows(items), source=SOURCE_DB, ) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 8539a22..a4152aa 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,4 +1,4 @@ -from app.models.agent_command import AgentCommand +from app.models.agent_command import AgentCommand from app.models.api_key import ApiKey from app.models.event import Event from app.models.host import Host @@ -9,6 +9,7 @@ from app.models.problem import Problem, ProblemEvent from app.models.login_attempt import LoginAttempt from app.models.ui_settings import UiSettings from app.models.event_severity_override import EventSeverityOverride +from app.models.event_type_visibility import EventTypeVisibility from app.models.user import User from app.models.mobile_settings import MobileSettings from app.models.mobile_enrollment_code import MobileEnrollmentCode @@ -27,6 +28,7 @@ __all__ = [ "ProblemEvent", "User", "EventSeverityOverride", + "EventTypeVisibility", "LoginAttempt", "UiSettings", "MobileSettings", diff --git a/backend/app/models/event_type_visibility.py b/backend/app/models/event_type_visibility.py new file mode 100644 index 0000000..8426232 --- /dev/null +++ b/backend/app/models/event_type_visibility.py @@ -0,0 +1,21 @@ +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, String, func +from sqlalchemy.orm import Mapped, mapped_column + +from app.database import Base + + +class EventTypeVisibility(Base): + """Per event-type UI visibility (default: show in events lists).""" + + __tablename__ = "event_type_visibility" + + event_type: Mapped[str] = mapped_column(String(128), primary_key=True) + show_in_events: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) diff --git a/backend/app/services/event_severity_overrides.py b/backend/app/services/event_severity_overrides.py index 770fd9c..b5fc9d6 100644 --- a/backend/app/services/event_severity_overrides.py +++ b/backend/app/services/event_severity_overrides.py @@ -1,4 +1,4 @@ -"""Per event-type severity overrides (admin Settings UI).""" +"""Per event-type severity overrides (admin Settings UI).""" from __future__ import annotations @@ -9,6 +9,7 @@ from sqlalchemy.orm import Session from app.constants.event_types import DEFAULT_EVENT_SEVERITIES from app.models.event_severity_override import EventSeverityOverride +from app.services.event_type_visibility import get_visibility_map from app.services.notification_settings import VALID_SEVERITIES SOURCE_DB = "db" @@ -19,6 +20,7 @@ class EventSeverityOverrideItem: event_type: str default_severity: str override_severity: str | None + show_in_events: bool = True def get_override_map(db: Session) -> dict[str, str]: @@ -48,7 +50,8 @@ def apply_severity_override(payload: dict, db: Session) -> dict: def list_severity_override_items(db: Session) -> list[EventSeverityOverrideItem]: override_map = get_override_map(db) - types = set(DEFAULT_EVENT_SEVERITIES) | set(override_map) + visibility_map = get_visibility_map(db) + types = set(DEFAULT_EVENT_SEVERITIES) | set(override_map) | set(visibility_map) items: list[EventSeverityOverrideItem] = [] for event_type in sorted(types): default = DEFAULT_EVENT_SEVERITIES.get(event_type, "info") @@ -57,6 +60,7 @@ def list_severity_override_items(db: Session) -> list[EventSeverityOverrideItem] event_type=event_type, default_severity=default, override_severity=override_map.get(event_type), + show_in_events=visibility_map.get(event_type, True), ) ) return items diff --git a/backend/app/services/event_type_visibility.py b/backend/app/services/event_type_visibility.py new file mode 100644 index 0000000..cf3c0d8 --- /dev/null +++ b/backend/app/services/event_type_visibility.py @@ -0,0 +1,58 @@ +"""Hide selected event types from SAC UI/API event lists.""" + +from __future__ import annotations + +from sqlalchemy import delete, select +from sqlalchemy.orm import Session +from sqlalchemy.sql import ColumnElement + +from app.models.event import Event +from app.models.event_type_visibility import EventTypeVisibility + + +def get_hidden_event_types(db: Session) -> frozenset[str]: + rows = db.scalars( + select(EventTypeVisibility.event_type).where(EventTypeVisibility.show_in_events.is_(False)) + ).all() + return frozenset(rows) + + +def get_visibility_map(db: Session) -> dict[str, bool]: + rows = db.scalars(select(EventTypeVisibility)).all() + return {row.event_type: row.show_in_events for row in rows} + + +def show_in_events_for(event_type: str, *, hidden: frozenset[str]) -> bool: + return event_type not in hidden + + +def visibility_type_filter(hidden: frozenset[str]) -> ColumnElement[bool] | None: + if not hidden: + return None + return Event.type.not_in(tuple(sorted(hidden))) + + +def replace_event_visibility(db: Session, visibility: dict[str, bool | None]) -> dict[str, bool]: + stored = get_visibility_map(db) + for event_type, show in visibility.items(): + normalized_type = event_type.strip() + if not normalized_type: + raise ValueError("event_type is required") + if show is None or show is True: + db.execute( + delete(EventTypeVisibility).where(EventTypeVisibility.event_type == normalized_type) + ) + stored.pop(normalized_type, None) + continue + if show is False: + row = db.get(EventTypeVisibility, normalized_type) + if row is None: + row = EventTypeVisibility(event_type=normalized_type, show_in_events=False) + db.add(row) + else: + row.show_in_events = False + stored[normalized_type] = False + else: + raise ValueError("show_in_events must be true, false, or null") + db.flush() + return stored diff --git a/backend/app/version.py b/backend/app/version.py index e7cb194..28d1fca 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.20.22" +APP_VERSION = "0.20.23" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/backend/tests/test_event_type_visibility.py b/backend/tests/test_event_type_visibility.py new file mode 100644 index 0000000..d1b27c8 --- /dev/null +++ b/backend/tests/test_event_type_visibility.py @@ -0,0 +1,76 @@ +"""Tests for hiding event types from SAC UI.""" + +import uuid + +from app.models.event_type_visibility import EventTypeVisibility + +VALID_EVENT = { + "schema_version": "1.0", + "event_id": "550e8400-e29b-41d4-a716-446655440000", + "occurred_at": "2026-05-27T10:00:00+03:00", + "source": {"product": "rdp-login-monitor", "product_version": "1.2.3-SAC"}, + "host": {"hostname": "test-host", "os_family": "windows"}, + "category": "auth", + "type": "agent.heartbeat", + "severity": "info", + "title": "Heartbeat", + "summary": "pytest", +} + +LOGIN_EVENT = { + **VALID_EVENT, + "type": "rdp.login.success", + "title": "RDP login", + "summary": "login pytest", +} + + +def _ingest(client, auth_headers, payload): + event_id = str(uuid.uuid4()) + r = client.post("/api/v1/events", json={**payload, "event_id": event_id}, headers=auth_headers) + assert r.status_code == 201 + return event_id + + +def test_hidden_event_type_excluded_from_list(client, auth_headers, jwt_headers, db_session): + db_session.add(EventTypeVisibility(event_type="agent.heartbeat", show_in_events=False)) + db_session.commit() + + hb_id = _ingest(client, auth_headers, VALID_EVENT) + login_id = _ingest(client, auth_headers, LOGIN_EVENT) + + listed = client.get("/api/v1/events", headers=jwt_headers) + assert listed.status_code == 200 + ids = {item["event_id"] for item in listed.json()["items"]} + assert hb_id not in ids + assert login_id in ids + + +def test_hidden_event_type_returns_404_on_detail(client, auth_headers, jwt_headers, db_session): + db_session.add(EventTypeVisibility(event_type="agent.heartbeat", show_in_events=False)) + db_session.commit() + + _ingest(client, auth_headers, VALID_EVENT) + listed = client.get("/api/v1/events", headers=jwt_headers, params={"type": "agent.heartbeat"}) + assert listed.json()["total"] == 0 + + +def test_visibility_settings_api(client, jwt_headers, db_session): + put = client.put( + "/api/v1/settings/notifications/severity-overrides", + headers=jwt_headers, + json={"visibility": {"agent.heartbeat": False}}, + ) + assert put.status_code == 200 + items = {i["event_type"]: i for i in put.json()["items"]} + assert items["agent.heartbeat"]["show_in_events"] is False + assert items["rdp.login.success"]["show_in_events"] is True + + reset = client.put( + "/api/v1/settings/notifications/severity-overrides", + headers=jwt_headers, + json={"visibility": {"agent.heartbeat": None}}, + ) + assert reset.status_code == 200 + items = {i["event_type"]: i for i in reset.json()["items"]} + assert items["agent.heartbeat"]["show_in_events"] is True diff --git a/frontend/package.json b/frontend/package.json index 43a903c..2ba0745 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ -{ +{ "name": "sac-ui", "private": true, - "version": "0.11.0", + "version": "0.11.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/version.ts b/frontend/src/version.ts index c91beaf..61ecfb2 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1,4 +1,4 @@ /** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */ export const APP_NAME = "Security Alert Center"; -export const APP_VERSION = "0.20.22"; +export const APP_VERSION = "0.20.23"; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`; diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index a52e1cb..32ab1a3 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -397,6 +397,7 @@

Переопределение значимости для новых событий после ingest. Исходное severity агента сохраняется в details.severity_agent. Пустое значение — как прислал агент (см. «По умолчанию»). + Снимите «Показывать в событиях», чтобы скрыть тип из списков SAC (например agent.heartbeat).

@@ -411,6 +412,7 @@ Тип события По умолчанию Effective + Показывать в событиях @@ -426,6 +428,12 @@ + + + @@ -678,10 +686,12 @@ interface SeverityOverrideRowApi { event_type: string; default_severity: string; override_severity: string | null; + show_in_events: boolean; } interface SeverityOverrideRowUi extends SeverityOverrideRowApi { selected: string; + override_show_in_events: boolean; } const loading = ref(true); @@ -868,6 +878,7 @@ function mapSeverityRows(items: SeverityOverrideRowApi[]): SeverityOverrideRowUi return items.map((row) => ({ ...row, selected: row.override_severity ?? "", + override_show_in_events: row.show_in_events, })); } @@ -884,25 +895,30 @@ async function saveSeverityOverrides() { success.value = ""; try { const overrides: Record = {}; + const visibility: Record = {}; for (const row of severityRows.value) { - const prev = row.override_severity ?? ""; - const next = row.selected.trim(); - if (next === prev) continue; - overrides[row.event_type] = next || null; + const prevSeverity = row.override_severity ?? ""; + const nextSeverity = row.selected.trim(); + if (nextSeverity !== prevSeverity) { + overrides[row.event_type] = nextSeverity || null; + } + if (row.show_in_events !== (row.override_show_in_events ?? true)) { + visibility[row.event_type] = row.show_in_events ? null : false; + } } - if (Object.keys(overrides).length === 0) { - error.value = "Нет изменений severity для сохранения"; + if (Object.keys(overrides).length === 0 && Object.keys(visibility).length === 0) { + error.value = "Нет изменений для сохранения"; return; } const data = await apiFetch<{ items: SeverityOverrideRowApi[] }>( "/api/v1/settings/notifications/severity-overrides", { method: "PUT", - body: JSON.stringify({ overrides }), + body: JSON.stringify({ overrides, visibility }), }, ); severityRows.value = mapSeverityRows(data.items); - success.value = "Severity по типам событий сохранён."; + success.value = "Severity и видимость событий сохранены."; } catch (e) { error.value = e instanceof Error ? e.message : "Ошибка сохранения severity"; } finally {