diff --git a/CLAUDE.md b/CLAUDE.md index 8a89cef..f9c9a35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ - **Provider Adapters** — Telegram (бот), Email (SMTP), Push (web-push), Internal Log (БД — деградация и аудит). - **System-to-System Delivery** — доставка в Navi (swarm-инстансы) и другие сервисы через их API/webhook. - **Persistence** — PostgreSQL: пользователи, типы уведомлений, настройки маршрутизации, логи доставки со статусами и ретраями. SQLite допустим только для локальных быстрых тестов, основной путь — Postgres. -- **Web UI / Админ-панель** — SPA на **Vue 3** (PWA на Gnexus UI Kit), раздаётся как статика из того же контейнера FastAPI (отдельного фронтенд-контейнера нет). Это не только просмотр, но и основной способ **конфигурирования Synapse**: пользователи, типы уведомлений, маршрутизация, API-ключи, шаблоны — всё управляется из админки. Обзор — события, статусы, логи доставки. +- **Web UI / Админ-панель** — SPA на **Vue 3** (PWA на Gnexus UI Kit), раздаётся как статика из того же контейнера FastAPI (отдельного фронтенд-контейнера нет). Это не только просмотр, но и основной способ **конфигурирования Synapse**: пользователи, типы уведомлений, маршрутизация, API-ключи, шаблоны — всё управляется из админки. Поток смотреть в разделе «Доставки» (отдельной страницы-обзора нет — убрана как бесполезная). **Стек**: Python + FastAPI, SQLAlchemy + Alembic, Celery + Redis, PostgreSQL, Docker + docker-compose. Frontend админки — SPA на Vue 3 + Gnexus UI Kit. diff --git a/app/api/admin_routes.py b/app/api/admin_routes.py index 4c4fe36..7604dcf 100644 --- a/app/api/admin_routes.py +++ b/app/api/admin_routes.py @@ -54,7 +54,6 @@ from app.i18n_server import locale_from_request, localize_settings from app.settings_registry import EDITABLE, is_secret, validate as validate_setting from app.settings_store import default_of, get_setting_row, set_setting -from app.worker.celery_app import celery_app router = APIRouter(prefix="/api/v1/admin", tags=["admin"]) @@ -854,10 +853,12 @@ ) -# --- обзор (дашборд) --- +# --- сводка для MCP-тула stats_get --- +# HTTP-эндпоинта нет: страница «Обзор» убрана (решение владельца), сводку +# читает только тул. Живость БД/воркера — забота system_status (docs/07). def dashboard_stats(db: Session) -> dict: - """Агрегаты справочников и потока (одно место на SPA-обзор и тул stats_get).""" + """Агрегаты справочников и потока — единственный источник для stats_get.""" ev = dict(db.execute(select(Event.status, func.count(Event.id)).group_by(Event.status)).all()) dv = dict( db.execute(select(Delivery.status, func.count(Delivery.id)).group_by(Delivery.status)).all() @@ -875,23 +876,3 @@ ), "rules": db.scalar(select(func.count(RoutingRule.id)).where(RoutingRule.deleted_at.is_(None))), } - - -@router.get("/stats") -def stats( - user: AuthenticatedUser = Depends(require_admin), - db: Session = Depends(get_db), -) -> dict: - """Сводка для «Обзора» админки: агрегаты, живость БД/воркера, SSO. - - Недоступный воркер не валит эндпоинт (ping с коротким таймаутом) — - на карточке будет «unavailable», как в system_status тула docs/07. - """ - out = dashboard_stats(db) - out["database"] = "ok" # дошли — БД отвечает - try: - out["worker"] = celery_app.send_task("synapse.ping").get(timeout=3) - except Exception: # noqa: BLE001 — недоступный воркер не ломает обзор - out["worker"] = "unavailable" - out["sso_configured"] = get_settings().gauth_configured() - return out diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2b98e0b..1672e8a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -27,7 +27,6 @@ // Всё подписанно t() через computed (i18n.md «грабли»: module-level массив // с уже вычисленным t() застывает). const adminItems = computed(() => [ - { to: "/dashboard", label: t("nav.dashboard"), icon: "ph-activity" }, { to: "/sources", label: t("nav.sources"), icon: "ph-broadcast" }, { to: "/types", label: t("nav.types"), icon: "ph-squares-four" }, { to: "/targets", label: t("nav.targets"), icon: "ph-target" }, @@ -37,7 +36,6 @@ { to: "/settings", label: t("nav.settings"), icon: "ph-gear-six" }, ]); const adminCurrents = computed(() => ({ - "/dashboard": t("nav.dashboard"), "/sources": t("nav.sources"), "/types": t("nav.types"), "/targets": t("nav.targets"), diff --git a/frontend/src/api.js b/frontend/src/api.js index c2b3fd2..255d70b 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -119,20 +119,11 @@ return response.json(); } -export function healthz() { - return apiFetch("/api/healthz"); -} - /** Профиль текущего пользователя. 401 — токен невалиден, 403 — роль ниже admin. */ export function adminMe() { return apiFetch("/api/v1/admin/me"); } -/** Сводка для «Обзора»: агрегаты справочников/потока, БД, воркер, SSO (только admin). */ -export function adminStats() { - return apiFetch("/api/v1/admin/stats"); -} - // --- Личный кабинет (app/api/me_routes.py): любой аутентифицированный. --- /** Профиль: без ролевого гейта, role в ответе (system_role) — с этого гейт роутера. */ diff --git a/frontend/src/i18n/messages/en.js b/frontend/src/i18n/messages/en.js index 5b6b1ce..8b86d5c 100644 --- a/frontend/src/i18n/messages/en.js +++ b/frontend/src/i18n/messages/en.js @@ -6,7 +6,6 @@ "common.signOut": "Sign out", - "nav.dashboard": "Overview", "nav.sources": "Sources", "nav.types": "Types", "nav.targets": "Targets", @@ -19,13 +18,6 @@ "settings.languageAuto": "Auto", "settings.languageHelp": "“Auto” means the gnexus-auth account language; a choice here overrides it for Synapse only and survives sign-out.", "settings.languageSaved": "Language changed", - "dashboard.title": "Overview", - "dashboard.subtitle": "Notification hub status", - "dashboard.loaderChecking": "Checking the service…", - "dashboard.errorUnavailable": "Service unavailable: {message}", - "dashboard.metricService": "Service", - "dashboard.getStartTitle": "Where to start", - "dashboard.getStartText": "Setup order: Sources — register a service and issue it an API key; Targets — where to deliver (internal log, a service over s2s); Rules — which event goes to which target. The Deliveries view shows what actually went out.", "deliveries.title": "Deliveries", "deliveries.subtitle": "Attempt log: channel, target, status, retries and the text actually sent to the channel", "deliveries.filterAll": "all", @@ -386,15 +378,4 @@ "mcpKeys.cancel": "Cancel", 'push.unsupported': 'The browser does not support web-push', 'push.swNotReady': 'Service worker is not ready yet (reload the page)', - 'dashboard.metricSourcesMeta': 'active', - 'dashboard.systemOkTitle': 'All systems normal', - 'dashboard.systemOkText': 'Database and worker are responding', - 'dashboard.ssoOk': 'SSO configured', - 'dashboard.ssoMissing': 'SSO not configured (OAuth client is empty)', - 'dashboard.systemWarnTitle': 'Problems detected', - 'dashboard.dbDown': 'Database is not responding', - 'dashboard.workerDown': 'worker unavailable — events will pile up in the queue', - 'dashboard.flowEvents': 'Events', - 'dashboard.flowDeliveries': 'Deliveries', - 'dashboard.flowEmpty': 'Nothing yet — no events so far', }; diff --git a/frontend/src/i18n/messages/ru.js b/frontend/src/i18n/messages/ru.js index 13dcf10..166df05 100644 --- a/frontend/src/i18n/messages/ru.js +++ b/frontend/src/i18n/messages/ru.js @@ -7,7 +7,6 @@ "common.signOut": "Выйти", - "nav.dashboard": "Обзор", "nav.sources": "Источники", "nav.types": "Типы", "nav.targets": "Цели", @@ -20,13 +19,6 @@ "settings.languageAuto": "Авто", "settings.languageHelp": "«Авто» — язык аккаунта gnexus-auth; выбор перекрывает его только для Synapse и переживает выход.", "settings.languageSaved": "Язык изменён", - "dashboard.title": "Обзор", - "dashboard.subtitle": "Состояние хаба уведомлений", - "dashboard.loaderChecking": "Опрашиваю сервис…", - "dashboard.errorUnavailable": "Сервис недоступен: {message}", - "dashboard.metricService": "Сервис", - "dashboard.getStartTitle": "С чего начать", - "dashboard.getStartText": "Порядок настройки: Источники — зарегистрировать сервис и выпустить ему API-ключ; Цели — куда доставлять (журнал, сервис через s2s); Правила — какое событие и в какую цель уходит. Доставки покажут, что реально ушло.", "deliveries.title": "Доставки", "deliveries.subtitle": "Лог попыток: канал, цель, статус, ретраи и текст, реально ушедший в канал", "deliveries.filterAll": "все", @@ -387,15 +379,4 @@ "mcpKeys.cancel": "Отмена", 'push.unsupported': 'Браузер не поддерживает web-push', 'push.swNotReady': 'Service worker ещё не готов (обновите страницу)', - 'dashboard.metricSourcesMeta': 'активные', - 'dashboard.systemOkTitle': 'Сервис здоров', - 'dashboard.systemOkText': 'База данных и воркер отвечают', - 'dashboard.ssoOk': 'SSO настроен', - 'dashboard.ssoMissing': 'SSO не настроен (OAuth-клиент пуст)', - 'dashboard.systemWarnTitle': 'Есть проблемы', - 'dashboard.dbDown': 'База данных не отвечает', - 'dashboard.workerDown': 'воркер недоступен — события будут копиться в очереди', - 'dashboard.flowEvents': 'События', - 'dashboard.flowDeliveries': 'Доставки', - 'dashboard.flowEmpty': 'Пока пусто — событий ещё не было', }; diff --git a/frontend/src/i18n/messages/uk.js b/frontend/src/i18n/messages/uk.js index f51236c..aa58852 100644 --- a/frontend/src/i18n/messages/uk.js +++ b/frontend/src/i18n/messages/uk.js @@ -6,7 +6,6 @@ "common.signOut": "Вийти", - "nav.dashboard": "Огляд", "nav.sources": "Джерела", "nav.types": "Типи", "nav.targets": "Цілі", @@ -19,13 +18,6 @@ "settings.languageAuto": "Авто", "settings.languageHelp": "«Авто» — мова акаунта gnexus-auth; вибір перекриває її лише для Synapse і переживає вихід.", "settings.languageSaved": "Мову змінено", - "dashboard.title": "Огляд", - "dashboard.subtitle": "Стан хаба сповіщень", - "dashboard.loaderChecking": "Опитую сервіс…", - "dashboard.errorUnavailable": "Сервіс недоступний: {message}", - "dashboard.metricService": "Сервіс", - "dashboard.getStartTitle": "З чого почати", - "dashboard.getStartText": "Порядок налаштування: Джерела — зареєструвати сервіс і видати йому API-ключ; Цілі — куди доставляти (журнал, сервіс через s2s); Правила — яка подія та в яку ціль іде. Доставки покажуть, що реально пішло.", "deliveries.title": "Доставки", "deliveries.subtitle": "Лог спроб: канал, ціль, статус, ретраї та текст, реально надісланий у канал", "deliveries.filterAll": "всі", @@ -386,15 +378,4 @@ "mcpKeys.cancel": "Скасувати", 'push.unsupported': 'Браузер не підтримує web-push', 'push.swNotReady': 'Service worker ще не готовий (оновіть сторінку)', - 'dashboard.metricSourcesMeta': 'активні', - 'dashboard.systemOkTitle': 'Сервіс справний', - 'dashboard.systemOkText': 'База даних і воркер відповідають', - 'dashboard.ssoOk': 'SSO налаштовано', - 'dashboard.ssoMissing': 'SSO не налаштовано (OAuth-клієнт порожній)', - 'dashboard.systemWarnTitle': 'Є проблеми', - 'dashboard.dbDown': 'База даних не відповідає', - 'dashboard.workerDown': 'воркер недоступний — події накопичуватимуться в черзі', - 'dashboard.flowEvents': 'Події', - 'dashboard.flowDeliveries': 'Доставки', - 'dashboard.flowEmpty': 'Поки порожньо — подій ще не було', }; diff --git a/frontend/src/router.js b/frontend/src/router.js index f9dab9c..e0bb554 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -1,7 +1,6 @@ import { createRouter, createWebHistory } from "vue-router"; import { clearToken, getToken, getSession } from "./api.js"; import LoginView from "./views/LoginView.vue"; -import DashboardView from "./views/DashboardView.vue"; import DeniedView from "./views/DeniedView.vue"; import MyEventsView from "./views/MyEventsView.vue"; import McpKeysView from "./views/McpKeysView.vue"; @@ -34,11 +33,9 @@ { path: "/my-events", component: MyEventsView, meta: { userOk: true } }, { path: "/mcp-keys", component: McpKeysView, meta: { userOk: true } }, { path: "/settings", component: SettingsView, meta: { userOk: true } }, - { - path: "/dashboard", - component: DashboardView, - meta: { requiresAdmin: true }, - }, + // «Обзор» убран (решение владельца: страница была бесполезной). Старые + // ссылки/закладки на /dashboard ведём в первый раздел админки. + { path: "/dashboard", redirect: "/sources" }, { path: "/sources", component: SourcesView, meta: { requiresAdmin: true } }, { path: "/sources/:id", component: SourceDetailView, meta: { requiresAdmin: true } }, { path: "/types", component: TypesView, meta: { requiresAdmin: true } }, @@ -63,7 +60,9 @@ } const isAdmin = (session?.system_role || "") === "admin" || (session?.system_role || "") === "superadmin"; - if (to.path === "/") return isAdmin ? "/dashboard" : "/my-events"; + // Куда попадает админ после входа: первый раздел админки (порядок настройки + // — источник → цели → правила → доставки; «Обзор» убран). + if (to.path === "/") return isAdmin ? "/sources" : "/my-events"; // Админские пути — admin+; роли user путь подменяется на личный раздел // (сервер всё равно держит админ-API под 403, это двойная защита). if (!to.meta.userOk && !isAdmin) return "/my-events"; diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue deleted file mode 100644 index 08eaeeb..0000000 --- a/frontend/src/views/DashboardView.vue +++ /dev/null @@ -1,189 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/views/OauthCallbackView.vue b/frontend/src/views/OauthCallbackView.vue index 9330d26..f7a19f6 100644 --- a/frontend/src/views/OauthCallbackView.vue +++ b/frontend/src/views/OauthCallbackView.vue @@ -25,7 +25,8 @@ } // Чистим фрагмент из адресной строки сразу. history.replaceState(null, "", window.location.pathname + window.location.search); - router.replace(access ? "/dashboard" : "/login"); + // "/" разводит роут-гейт по роли (админ — /sources, user — /my-events). + router.replace(access ? "/" : "/login"); });