Route guards, WS lifecycle, ticket stats. Refs EventHub/EventHubFrontAdmin#3

This commit is contained in:
2026-07-07 21:03:46 +03:00
parent 896e3bcd35
commit ecfa1a66bb
18 changed files with 1331 additions and 955 deletions
+71 -69
View File
@@ -1,70 +1,72 @@
import { create } from 'zustand';
import { Admin } from '../types/api';
import { authApi } from '../api/authApi';
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from '../utils/constants';
interface AuthState {
user: Admin | null;
accessToken: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
isInitialized: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
checkAuth: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
accessToken: localStorage.getItem(ACCESS_TOKEN_KEY),
refreshToken: localStorage.getItem(REFRESH_TOKEN_KEY),
isAuthenticated: false,
isInitialized: false,
login: async (email: string, password: string) => {
const { token, refresh_token, user } = await authApi.login(email, password);
localStorage.setItem(ACCESS_TOKEN_KEY, token);
localStorage.setItem(REFRESH_TOKEN_KEY, refresh_token);
set({
accessToken: token,
refreshToken: refresh_token,
user,
isAuthenticated: true,
isInitialized: true,
});
},
logout: async () => {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
set({
user: null,
accessToken: null,
refreshToken: null,
isAuthenticated: false,
isInitialized: true,
});
},
checkAuth: async () => {
const token = localStorage.getItem(ACCESS_TOKEN_KEY);
if (!token) {
set({ isInitialized: true });
return;
}
try {
const user = await authApi.getMe();
set({ user, isAuthenticated: true, isInitialized: true });
} catch {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
set({
user: null,
accessToken: null,
refreshToken: null,
isAuthenticated: false,
isInitialized: true,
});
}
},
import { create } from 'zustand';
import { Admin } from '../types/api';
import { authApi } from '../api/authApi';
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from '../utils/constants';
import { useMetricsStore } from './metricsStore';
interface AuthState {
user: Admin | null;
accessToken: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
isInitialized: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
checkAuth: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
accessToken: localStorage.getItem(ACCESS_TOKEN_KEY),
refreshToken: localStorage.getItem(REFRESH_TOKEN_KEY),
isAuthenticated: false,
isInitialized: false,
login: async (email: string, password: string) => {
const { token, refresh_token, user } = await authApi.login(email, password);
localStorage.setItem(ACCESS_TOKEN_KEY, token);
localStorage.setItem(REFRESH_TOKEN_KEY, refresh_token);
set({
accessToken: token,
refreshToken: refresh_token,
user,
isAuthenticated: true,
isInitialized: true,
});
},
logout: async () => {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
useMetricsStore.getState().reset();
set({
user: null,
accessToken: null,
refreshToken: null,
isAuthenticated: false,
isInitialized: true,
});
},
checkAuth: async () => {
const token = localStorage.getItem(ACCESS_TOKEN_KEY);
if (!token) {
set({ isInitialized: true });
return;
}
try {
const user = await authApi.getMe();
set({ user, isAuthenticated: true, isInitialized: true });
} catch {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
set({
user: null,
accessToken: null,
refreshToken: null,
isAuthenticated: false,
isInitialized: true,
});
}
},
}));
+89
View File
@@ -0,0 +1,89 @@
import { create } from 'zustand';
import dayjs from 'dayjs';
export interface NodeMetric {
active_sessions: number;
io_in: number;
io_out: number;
memory_atom: number;
memory_binary: number;
memory_ets: number;
memory_processes: number;
memory_total: number;
mnesia_commits: number;
mnesia_failures: number;
node: string;
process_count: number;
run_queue: number;
table_sizes: Record<string, number>;
timestamp: string;
ws_connections: number;
cpu_utilization: number;
memory_available: number;
}
interface MetricsState {
current: NodeMetric | null;
previous: NodeMetric | null;
allHistory: NodeMetric[];
visibleHistory: NodeMetric[];
setCurrent: (metric: NodeMetric) => void;
setAllHistory: (history: NodeMetric[]) => void;
addToAllHistory: (metric: NodeMetric) => void;
filterVisibleHistory: (minutes: number) => void;
reset: () => void;
}
const MAX_HISTORY = 500; // жесткий лимит, чтобы избежать ошибок памяти
export const useMetricsStore = create<MetricsState>((set, get) => ({
current: null,
previous: null,
allHistory: [],
visibleHistory: [],
setCurrent: (metric) =>
set((state) => ({
previous: state.current,
current: metric,
})),
setAllHistory: (history) =>
set({
allHistory: history.slice(-MAX_HISTORY),
visibleHistory: history.slice(-MAX_HISTORY),
}),
addToAllHistory: (metric) =>
set((state) => {
const newAll = [...state.allHistory, metric].slice(-MAX_HISTORY);
return { allHistory: newAll };
}),
filterVisibleHistory: (minutes) => {
const { allHistory } = get();
if (allHistory.length === 0) return;
const now = dayjs();
const from = now.subtract(minutes, 'minute');
const filtered = allHistory.filter((m) => {
const t = dayjs(m.timestamp);
return t.isAfter(from);
});
let step = 1;
if (filtered.length > 300) step = Math.ceil(filtered.length / 300);
const thinned = filtered.filter((_, i) => i % step === 0);
set({ visibleHistory: thinned });
},
reset: () =>
set({
current: null,
previous: null,
allHistory: [],
visibleHistory: [],
}),
}));