diff --git a/backend/app/api/v1/events.py b/backend/app/api/v1/events.py index 13aacd4..847558b 100644 --- a/backend/app/api/v1/events.py +++ b/backend/app/api/v1/events.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime +from datetime import datetime, timezone from typing import Any from fastapi import APIRouter, Body, Depends, HTTPException, Query @@ -100,10 +100,18 @@ def post_event( return JSONResponse(status_code=status_code, content=body.model_dump(mode="json")) -def _parse_optional_dt(value: str | None) -> datetime | None: +def _parse_optional_dt(value: str | None, *, end_of_day: bool = False) -> datetime | None: if not value: return None - return datetime.fromisoformat(value.replace("Z", "+00:00")) + raw = value.strip() + if len(raw) == 10 and raw[4] == "-" and raw[7] == "-": + dt = datetime.fromisoformat(raw) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + if end_of_day: + return dt.replace(hour=23, minute=59, second=59, microsecond=999999) + return dt.replace(hour=0, minute=0, second=0, microsecond=0) + return datetime.fromisoformat(raw.replace("Z", "+00:00")) @router.get("", response_model=EventListResponse) @@ -137,7 +145,7 @@ def list_events( stmt = stmt.where(Host.hostname.ilike(like) | Host.display_name.ilike(like)) count_stmt = count_stmt.where(Host.hostname.ilike(like) | Host.display_name.ilike(like)) dt_from = _parse_optional_dt(from_time) - dt_to = _parse_optional_dt(to_time) + dt_to = _parse_optional_dt(to_time, end_of_day=True) if dt_from: stmt = stmt.where(Event.occurred_at >= dt_from) count_stmt = count_stmt.where(Event.occurred_at >= dt_from) diff --git a/backend/app/version.py b/backend/app/version.py index 37fd662..5759b2c 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.7.0" +APP_VERSION = "0.7.1" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py index 3ecb3ba..a4e941e 100644 --- a/backend/tests/test_health.py +++ b/backend/tests/test_health.py @@ -4,6 +4,6 @@ from app.version import APP_NAME, APP_VERSION, APP_VERSION_LABEL def test_version_constants(): - assert APP_VERSION == "0.7.0" + assert APP_VERSION == "0.7.1" assert APP_NAME == "Security Alert Center" - assert APP_VERSION_LABEL == "Security Alert Center v.0.7.0" + assert APP_VERSION_LABEL == "Security Alert Center v.0.7.1" diff --git a/frontend/package.json b/frontend/package.json index 3fd9200..5cf835a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "sac-ui", "private": true, - "version": "0.7.0", + "version": "0.7.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/utils/eventsListQuery.ts b/frontend/src/utils/eventsListQuery.ts new file mode 100644 index 0000000..4ce30d1 --- /dev/null +++ b/frontend/src/utils/eventsListQuery.ts @@ -0,0 +1,80 @@ +/** Query params for /events list — preserved when opening event detail and going back. */ + +import type { LocationQuery } from "vue-router"; + +export interface EventsListState { + page: number; + q: string; + severity: string; + type: string; + hostname: string; + dateFrom: string; + dateTo: string; +} + +const LIST_KEYS = ["page", "q", "severity", "type", "hostname", "from", "to"] as const; + +function queryString(value: unknown): string { + if (typeof value === "string") return value; + if (Array.isArray(value) && typeof value[0] === "string") return value[0]; + return ""; +} + +export function parseEventsListQuery(query: LocationQuery): EventsListState { + const pageRaw = parseInt(queryString(query.page), 10); + return { + page: Number.isFinite(pageRaw) && pageRaw > 0 ? pageRaw : 1, + q: queryString(query.q), + severity: queryString(query.severity), + type: queryString(query.type), + hostname: queryString(query.hostname), + dateFrom: queryString(query.from), + dateTo: queryString(query.to), + }; +} + +export function buildEventsListQuery(state: EventsListState): Record { + const q: Record = {}; + if (state.page > 1) q.page = String(state.page); + if (state.q.trim()) q.q = state.q.trim(); + if (state.severity) q.severity = state.severity; + if (state.type.trim()) q.type = state.type.trim(); + if (state.hostname.trim()) q.hostname = state.hostname.trim(); + if (state.dateFrom) q.from = state.dateFrom; + if (state.dateTo) q.to = state.dateTo; + return q; +} + +/** Subset of detail route query to restore the events list (excludes `from=reports`). */ +export function eventsBackQueryFromDetail(query: LocationQuery): Record { + const out: Record = {}; + for (const key of LIST_KEYS) { + const v = queryString(query[key]); + if (v) out[key] = v; + } + return out; +} + +export function apiParamsFromState(state: EventsListState, pageSize: number): URLSearchParams { + const params = new URLSearchParams({ + page: String(state.page), + page_size: String(pageSize), + }); + if (state.severity) params.set("severity", state.severity); + if (state.type.trim()) params.set("type", state.type.trim()); + if (state.hostname.trim()) params.set("hostname", state.hostname.trim()); + if (state.q.trim()) params.set("q", state.q.trim()); + if (state.dateFrom) params.set("from", state.dateFrom); + if (state.dateTo) params.set("to", state.dateTo); + return params; +} + +export function eventsListQueryMatches(state: EventsListState, query: LocationQuery): boolean { + const built = buildEventsListQuery(state); + const fromRoute = buildEventsListQuery(parseEventsListQuery(query)); + const keys = new Set([...Object.keys(built), ...Object.keys(fromRoute)]); + for (const key of keys) { + if ((built[key] ?? "") !== (fromRoute[key] ?? "")) return false; + } + return true; +} diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 36d05c4..ce77a57 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1,3 +1,3 @@ export const APP_NAME = "Security Alert Center"; -export const APP_VERSION = "0.7.0"; +export const APP_VERSION = "0.7.1"; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`; diff --git a/frontend/src/views/EventDetailView.vue b/frontend/src/views/EventDetailView.vue index c717395..638378c 100644 --- a/frontend/src/views/EventDetailView.vue +++ b/frontend/src/views/EventDetailView.vue @@ -4,7 +4,7 @@ ← Отчёты - ← События + ← События

@@ -105,6 +105,7 @@ import { apiFetch, type EventDetail } from "../api"; import ReportBodyCard from "../components/ReportBodyCard.vue"; +import { eventsBackQueryFromDetail } from "../utils/eventsListQuery"; import { isDailyReportType } from "../utils/reportDisplay"; @@ -134,6 +135,8 @@ const backToReportsQuery = computed(() => { return q; }); +const backToEventsQuery = computed(() => eventsBackQueryFromDetail(route.query)); + function formatDt(iso: string) { diff --git a/frontend/src/views/EventsView.vue b/frontend/src/views/EventsView.vue index be4d350..1015d76 100644 --- a/frontend/src/views/EventsView.vue +++ b/frontend/src/views/EventsView.vue @@ -1,17 +1,26 @@ + +