diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 94918f1..d2b6de1 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -13,18 +13,28 @@ // подсветку активного пункта навигации. import { computed, onUnmounted, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; -import { GnIdentity, GnIconButton, GnNavigationShell } from "gnexus-ui-kit/vue"; +import { GnIdentity, GnIconButton, GnNavigationShell, GnToastProvider } from "gnexus-ui-kit/vue"; import { adminMe, clearToken, logout } from "./api.js"; const route = useRoute(); const router = useRouter(); -// Пока в Synapse одна секция — «Обзор»; новые секции добавляются сюда и в router.js. +// Секции админки; иконки — только Phosphor (правила кита). const items = [ { to: "/dashboard", label: "Обзор", icon: "ph-pulse" }, + { to: "/sources", label: "Источники", icon: "ph-broadcast" }, + { to: "/types", label: "Типы", icon: "ph-shapes" }, + { to: "/targets", label: "Цели", icon: "ph-target" }, + { to: "/rules", label: "Правила", icon: "ph-funnel" }, + { to: "/deliveries", label: "Доставки", icon: "ph-paper-plane-tilt" }, ]; const currents = { "/dashboard": "Обзор", + "/sources": "Источники", + "/types": "Типы", + "/targets": "Цели", + "/rules": "Правила", + "/deliveries": "Доставки", }; const current = computed(() => currents[route.path] || ""); const bare = computed(() => Boolean(route.meta.bare)); @@ -150,7 +160,10 @@ diff --git a/frontend/src/api.js b/frontend/src/api.js index 19d90dc..f9b67d7 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -109,6 +109,40 @@ return apiFetch("/api/v1/admin/me"); } +// --- Admin API (app/api/admin_routes.py). Все эндпоинты под require_admin. --- + +export const api = { + // Источники и ключи + listSources: () => apiFetch("/api/v1/admin/sources"), + createSource: (data) => apiFetch("/api/v1/admin/sources", { method: "POST", body: JSON.stringify(data) }), + deleteSource: (id) => apiFetch(`/api/v1/admin/sources/${id}`, { method: "DELETE" }), + listKeys: (sourceId) => apiFetch(`/api/v1/admin/sources/${sourceId}/keys`), + createKey: (sourceId, data) => + apiFetch(`/api/v1/admin/sources/${sourceId}/keys`, { method: "POST", body: JSON.stringify(data) }), + revokeKey: (sourceId, keyId) => + apiFetch(`/api/v1/admin/sources/${sourceId}/keys/${keyId}/revoke`, { method: "POST" }), + + // Типы + listTypes: () => apiFetch("/api/v1/admin/types"), + createType: (data) => apiFetch("/api/v1/admin/types", { method: "POST", body: JSON.stringify(data) }), + deleteType: (id) => apiFetch(`/api/v1/admin/types/${id}`, { method: "DELETE" }), + + // Цели каналов + listTargets: () => apiFetch("/api/v1/admin/targets"), + createTarget: (data) => apiFetch("/api/v1/admin/targets", { method: "POST", body: JSON.stringify(data) }), + deleteTarget: (id) => apiFetch(`/api/v1/admin/targets/${id}`, { method: "DELETE" }), + + // Правила + listRules: () => apiFetch("/api/v1/admin/rules"), + createRule: (data) => apiFetch("/api/v1/admin/rules", { method: "POST", body: JSON.stringify(data) }), + patchRule: (id, data) => apiFetch(`/api/v1/admin/rules/${id}`, { method: "PATCH", body: JSON.stringify(data) }), + deleteRule: (id) => apiFetch(`/api/v1/admin/rules/${id}`, { method: "DELETE" }), + + // Поток (только чтение) + listEvents: (query = {}) => apiFetch(`/api/v1/admin/events?${new URLSearchParams(query)}`), + listDeliveries: (query = {}) => apiFetch(`/api/v1/admin/deliveries?${new URLSearchParams(query)}`), +}; + /** Логаут: revoke всех входов пользователя (наш + на gnexus-auth). */ export async function logout() { try { diff --git a/frontend/src/router.js b/frontend/src/router.js index b2d2732..ee5ccba 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -4,6 +4,11 @@ import DashboardView from "./views/DashboardView.vue"; import DeniedView from "./views/DeniedView.vue"; import OauthCallbackView from "./views/OauthCallbackView.vue"; +import SourcesView from "./views/SourcesView.vue"; +import TypesView from "./views/TypesView.vue"; +import TargetsView from "./views/TargetsView.vue"; +import RulesView from "./views/RulesView.vue"; +import DeliveriesView from "./views/DeliveriesView.vue"; // Гейт ролей: /dashboard и далее — только system_role admin и выше. // Проверка роли происходит на сервере (require_admin), фронт лишь заранее @@ -21,6 +26,11 @@ component: DashboardView, meta: { requiresAuth: true }, }, + { path: "/sources", component: SourcesView, meta: { requiresAuth: true } }, + { path: "/types", component: TypesView, meta: { requiresAuth: true } }, + { path: "/targets", component: TargetsView, meta: { requiresAuth: true } }, + { path: "/rules", component: RulesView, meta: { requiresAuth: true } }, + { path: "/deliveries", component: DeliveriesView, meta: { requiresAuth: true } }, ], }); diff --git a/frontend/src/ui.js b/frontend/src/ui.js new file mode 100644 index 0000000..21cee9c --- /dev/null +++ b/frontend/src/ui.js @@ -0,0 +1,45 @@ +/** Общие мелочи для админ-разделов: бейджи статусов, формат времени, сокращение. */ + +/** Разрешённые варианты кита: primary, secondary, accent, success, warning, danger, error, info. */ +export const statusVariant = { + // delivery + delivered: "success", + pending: "info", + failed: "danger", + skipped: "warning", + // event + queued: "info", + processing: "info", + done: "success", +}; + +export function badgeFor(status) { + return { label: status, variant: statusVariant[status] || "secondary" }; +} + +export function fmtTime(iso) { + if (!iso) return "—"; + return new Date(iso).toLocaleString("ru-RU", { + day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", + }); +} + +export function truncate(text, n = 80) { + if (!text) return ""; + return text.length > n ? `${text.slice(0, n - 1)}…` : text; +} + +/** JSON в одну строку для ячейки таблицы. */ +export function shortJson(value, n = 60) { + if (!value || (typeof value === "object" && !Object.keys(value).length)) return "—"; + const text = JSON.stringify(value); + return truncate(text, n); +} + +/** Разбор строки с запятыми/пробелами в массив непустых токенов. */ +export function parseList(text) { + return (text || "") + .split(/[,\n]+/) + .map((s) => s.trim()) + .filter(Boolean); +} \ No newline at end of file diff --git a/frontend/src/views/DeliveriesView.vue b/frontend/src/views/DeliveriesView.vue new file mode 100644 index 0000000..e46df4f --- /dev/null +++ b/frontend/src/views/DeliveriesView.vue @@ -0,0 +1,121 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/RulesView.vue b/frontend/src/views/RulesView.vue new file mode 100644 index 0000000..8fe364b --- /dev/null +++ b/frontend/src/views/RulesView.vue @@ -0,0 +1,333 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/SourcesView.vue b/frontend/src/views/SourcesView.vue new file mode 100644 index 0000000..64b6787 --- /dev/null +++ b/frontend/src/views/SourcesView.vue @@ -0,0 +1,278 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/TargetsView.vue b/frontend/src/views/TargetsView.vue new file mode 100644 index 0000000..18daab5 --- /dev/null +++ b/frontend/src/views/TargetsView.vue @@ -0,0 +1,178 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/TypesView.vue b/frontend/src/views/TypesView.vue new file mode 100644 index 0000000..37fd3aa --- /dev/null +++ b/frontend/src/views/TypesView.vue @@ -0,0 +1,165 @@ + + + + + \ No newline at end of file