diff --git a/.gitignore b/.gitignore index af28cb0..c3b67e9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ dist/ downloads/ eggs/ .eggs/ -lib/ +backend/lib/ lib64/ parts/ sdist/ diff --git a/backend/requirements.txt b/backend/requirements.txt index 412bfb7..8361db3 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,7 +7,7 @@ pydantic-settings==2.1.0 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 python-multipart==0.0.9 -psutil==5.9.8 +psutil==6.1.0 aiosqlite==0.19.0 websockets==12.0 python-dotenv==1.0.1 diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts new file mode 100644 index 0000000..989d466 --- /dev/null +++ b/frontend/lib/api/client.ts @@ -0,0 +1,48 @@ +/** + * API client for making requests to the backend. + */ + +import axios from "axios"; +import type { ApiResponse } from "@/types/api"; + +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + +export const apiClient = axios.create({ + baseURL: API_BASE_URL, + headers: { + "Content-Type": "application/json", + }, + timeout: 10000, +}); + +// Request interceptor for adding auth token +apiClient.interceptors.request.use( + (config) => { + const token = localStorage.getItem("access_token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response interceptor for handling errors +apiClient.interceptors.response.use( + (response) => response, + async (error) => { + if (error.response?.status === 401) { + // Handle token refresh or redirect to login + localStorage.removeItem("access_token"); + localStorage.removeItem("refresh_token"); + // Optionally redirect to login page + // window.location.href = '/login'; + } + return Promise.reject(error); + } +); + +export default apiClient; diff --git a/frontend/lib/hooks/useBots.ts b/frontend/lib/hooks/useBots.ts new file mode 100644 index 0000000..61cdfee --- /dev/null +++ b/frontend/lib/hooks/useBots.ts @@ -0,0 +1,181 @@ +/** + * Hooks for fetching and managing bots. + */ + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import apiClient from "@/lib/api/client"; +import type { + Bot, + BotListResponse, + BotCreate, + BotUpdate, + BotStatusResponse, +} from "@/types/bot"; +import type { BotFilters } from "@/types/api"; + +/** + * Hook to fetch list of bots with optional filters. + */ +export function useBots(filters?: BotFilters) { + return useQuery({ + queryKey: ["bots", filters], + queryFn: async () => { + const params = new URLSearchParams(); + + if (filters?.status) { + params.append("status", filters.status); + } + if (filters?.search) { + params.append("search", filters.search); + } + if (filters?.page) { + params.append("page", filters.page.toString()); + } + if (filters?.page_size) { + params.append("page_size", filters.page_size.toString()); + } + + const response = await apiClient.get( + `/bots?${params.toString()}` + ); + return response.data; + }, + refetchInterval: 10000, // Refetch every 10 seconds + }); +} + +/** + * Hook to fetch a single bot by ID. + */ +export function useBot(botId: string) { + return useQuery({ + queryKey: ["bots", botId], + queryFn: async () => { + const response = await apiClient.get(`/bots/${botId}`); + return response.data; + }, + enabled: !!botId, + }); +} + +/** + * Hook to fetch bot status. + */ +export function useBotStatus(botId: string) { + return useQuery({ + queryKey: ["bots", botId, "status"], + queryFn: async () => { + const response = await apiClient.get( + `/bots/${botId}/status` + ); + return response.data; + }, + refetchInterval: 5000, + enabled: !!botId, + }); +} + +/** + * Hook to create a new bot. + */ +export function useCreateBot() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (botData: BotCreate) => { + const response = await apiClient.post("/bots", botData); + return response.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["bots"] }); + }, + }); +} + +/** + * Hook to update a bot. + */ +export function useUpdateBot(botId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (botData: BotUpdate) => { + const response = await apiClient.put(`/bots/${botId}`, botData); + return response.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["bots", botId] }); + queryClient.invalidateQueries({ queryKey: ["bots"] }); + }, + }); +} + +/** + * Hook to delete a bot. + */ +export function useDeleteBot() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (botId: string) => { + await apiClient.delete(`/bots/${botId}`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["bots"] }); + }, + }); +} + +/** + * Hook to start a bot. + */ +export function useStartBot() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (botId: string) => { + const response = await apiClient.post(`/bots/${botId}/start`); + return response.data; + }, + onSuccess: (_, botId) => { + queryClient.invalidateQueries({ queryKey: ["bots", botId] }); + queryClient.invalidateQueries({ queryKey: ["bots"] }); + }, + }); +} + +/** + * Hook to stop a bot. + */ +export function useStopBot() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (botId: string) => { + const response = await apiClient.post(`/bots/${botId}/stop`); + return response.data; + }, + onSuccess: (_, botId) => { + queryClient.invalidateQueries({ queryKey: ["bots", botId] }); + queryClient.invalidateQueries({ queryKey: ["bots"] }); + }, + }); +} + +/** + * Hook to restart a bot. + */ +export function useRestartBot() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (botId: string) => { + const response = await apiClient.post(`/bots/${botId}/restart`); + return response.data; + }, + onSuccess: (_, botId) => { + queryClient.invalidateQueries({ queryKey: ["bots", botId] }); + queryClient.invalidateQueries({ queryKey: ["bots"] }); + }, + }); +} diff --git a/frontend/lib/hooks/useStats.ts b/frontend/lib/hooks/useStats.ts new file mode 100644 index 0000000..3c022df --- /dev/null +++ b/frontend/lib/hooks/useStats.ts @@ -0,0 +1,50 @@ +/** + * Hooks for fetching statistics data. + */ + +import { useQuery } from "@tanstack/react-query"; +import apiClient from "@/lib/api/client"; +import type { SystemStats, BotStats, AggregateStats } from "@/types/stats"; + +/** + * Hook to fetch system statistics. + */ +export function useSystemStats() { + return useQuery({ + queryKey: ["stats", "system"], + queryFn: async () => { + const response = await apiClient.get("/stats/system"); + return response.data; + }, + refetchInterval: 5000, // Refetch every 5 seconds + }); +} + +/** + * Hook to fetch bot statistics by bot ID. + */ +export function useBotStats(botId: string) { + return useQuery({ + queryKey: ["stats", "bot", botId], + queryFn: async () => { + const response = await apiClient.get(`/stats/bots/${botId}`); + return response.data; + }, + refetchInterval: 5000, + enabled: !!botId, + }); +} + +/** + * Hook to fetch aggregate statistics for all bots. + */ +export function useAggregateStats() { + return useQuery({ + queryKey: ["stats", "bots", "aggregate"], + queryFn: async () => { + const response = await apiClient.get("/stats/bots"); + return response.data; + }, + refetchInterval: 5000, + }); +}