Files
SmartUp/frontend/src/api/index.ts
T
SmartUp Developer 8a6ed249be fix: complete remaining 8 optimization items
- HTTP connection pooling: UpstreamClient & WebsiteClient reuse httpx.Client
- Deduplicate decimal_string into shared app/utils/number.py
- Split scheduler transaction: snapshot write → webhook/website sync in separate sessions
- Remove hardcoded 170.106.100.210 migration from database.py
- Reset consecutive_failures on upstream update
- Healthcheck: install curl, replace python -c with curl -f
- Add .dockerignore to reduce build context
- Frontend: add axios-retry with exponential backoff (5xx/network errors only)
2026-05-17 11:09:35 +08:00

326 lines
9.8 KiB
TypeScript

import axios from 'axios'
import axiosRetry from 'axios-retry'
import router from '@/router'
import { authStorageKeys } from '@/authStorage'
export const api = axios.create({
baseURL: '/',
timeout: 30000,
})
axiosRetry(api, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (err) => {
// Retry on network errors or 5xx, but never on 401/403/404/4xx
if (!err.response) return true
return err.response.status >= 500 && err.response.status < 600
},
onRetry: (_retryCount, _err, _requestConfig) => {
// no-op — could log in dev
},
})
api.interceptors.response.use(
(r) => r,
(err) => {
if (err.response?.status === 401) {
localStorage.removeItem(authStorageKeys.token)
localStorage.removeItem(authStorageKeys.email)
router.push('/login')
}
return Promise.reject(err)
}
)
// ——— Auth ———
export const authApi = {
login: (email: string, password: string) =>
api.post<{ access_token: string }>('/api/auth/login', { email, password }),
me: () => api.get<{ email: string }>('/api/auth/me'),
}
// ——— Upstreams ———
export interface UpstreamData {
id: number
name: string
base_url: string
api_prefix: string
auth_type: string
auth_config_masked: Record<string, any>
rate_endpoint: string
groups_endpoint: string
enabled: boolean
check_interval_seconds: number
timeout_seconds: number
last_status: string
last_checked_at: string | null
last_error: string | null
created_at: string
updated_at: string
}
export interface UpstreamForm {
name: string
base_url: string
api_prefix: string
auth_type: string
auth_config: Record<string, any>
rate_endpoint: string
groups_endpoint: string
enabled: boolean
check_interval_seconds: number
timeout_seconds: number
}
export const upstreamsApi = {
list: () => api.get<UpstreamData[]>('/api/upstreams'),
create: (data: UpstreamForm) => api.post<UpstreamData>('/api/upstreams', data),
update: (id: number, data: Partial<UpstreamForm>) => api.put<UpstreamData>(`/api/upstreams/${id}`, data),
delete: (id: number) => api.delete(`/api/upstreams/${id}`),
test: (id: number) => api.post<{ success: boolean; message: string; detail?: string }>(`/api/upstreams/${id}/test`),
checkNow: (id: number) => api.post<{ success: boolean; message: string }>(`/api/upstreams/${id}/check-now`),
latestSnapshot: (id: number) => api.get(`/api/upstreams/${id}/snapshots/latest`),
listSnapshots: (id: number, limit = 20, offset = 0) =>
api.get<any[]>(`/api/upstreams/${id}/snapshots`, { params: { limit, offset } }),
}
// ——— Websites ———
export interface WebsiteData {
id: number
name: string
site_type: string
base_url: string
api_prefix: string
auth_type: string
auth_config_masked: Record<string, any>
groups_endpoint: string
group_update_endpoint: string
enabled: boolean
auto_sync_enabled: boolean
timeout_seconds: number
last_status: string
last_checked_at: string | null
last_error: string | null
created_at: string
updated_at: string
}
export interface WebsiteForm {
name: string
site_type: string
base_url: string
api_prefix: string
auth_type: string
auth_config: Record<string, any>
groups_endpoint: string
group_update_endpoint: string
enabled: boolean
auto_sync_enabled: boolean
timeout_seconds: number
}
export interface WebsiteGroup {
id: string
name: string
rate_multiplier: string | null
raw: Record<string, any>
}
export interface BindingSourceGroup {
upstream_id: number
group_id: string
upstream_name: string
group_name: string
}
export interface GroupBindingData {
id: number
website_id: number
website_name: string
target_group_id: string
target_group_name: string
source_groups: BindingSourceGroup[]
percent: number
algorithm: string
enabled: boolean
created_at: string
updated_at: string
}
export interface GroupBindingForm {
website_id: number
target_group_id: string
target_group_name: string
source_groups: BindingSourceGroup[]
percent: number
algorithm: string
enabled: boolean
}
export interface WebsiteSyncLog {
id: number
website_id: number
binding_id: number | null
target_group_id: string
target_group_name: string
algorithm: string
percent: number
source_rates: Array<Record<string, any>>
old_rate: string | null
new_rate: string | null
status: string
message: string
created_at: string
}
export const websitesApi = {
list: () => api.get<WebsiteData[]>('/api/websites'),
create: (data: WebsiteForm) => api.post<WebsiteData>('/api/websites', data),
update: (id: number, data: Partial<WebsiteForm>) => api.put<WebsiteData>(`/api/websites/${id}`, data),
delete: (id: number) => api.delete(`/api/websites/${id}`),
test: (id: number) => api.post<{ success: boolean; message: string; detail?: string }>(`/api/websites/${id}/test`),
groups: (id: number) => api.get<WebsiteGroup[]>(`/api/websites/${id}/groups`),
listBindings: () => api.get<GroupBindingData[]>('/api/group-bindings'),
createBinding: (data: GroupBindingForm) => api.post<GroupBindingData>('/api/group-bindings', data),
updateBinding: (id: number, data: Partial<GroupBindingForm>) => api.put<GroupBindingData>(`/api/group-bindings/${id}`, data),
deleteBinding: (id: number) => api.delete(`/api/group-bindings/${id}`),
syncNow: (id: number) => api.post<WebsiteSyncLog>(`/api/group-bindings/${id}/sync-now`),
logs: (params?: { website_id?: number; binding_id?: number; limit?: number; offset?: number }) =>
api.get<WebsiteSyncLog[]>('/api/website-sync-logs', { params }),
}
// ——— Webhooks ———
export interface WebhookData {
id: number
name: string
type: string
url: string
secret_masked: string
enabled: boolean
events: string[]
created_at: string
updated_at: string
}
export interface WebhookForm {
name: string
type: string
url: string
secret: string
enabled: boolean
events: string[]
}
export const webhooksApi = {
list: () => api.get<WebhookData[]>('/api/webhooks'),
create: (data: WebhookForm) => api.post<WebhookData>('/api/webhooks', data),
update: (id: number, data: Partial<WebhookForm>) => api.put<WebhookData>(`/api/webhooks/${id}`, data),
delete: (id: number) => api.delete(`/api/webhooks/${id}`),
test: (id: number) => api.post<{ success: boolean; message: string }>(`/api/webhooks/${id}/test`),
}
// ——— Logs ———
export interface LogData {
id: number
webhook_config_id: number
webhook_name: string
event_type: string
payload: Record<string, any>
status: string
response_text: string | null
created_at: string
}
export const logsApi = {
list: (params?: { status?: string; event_type?: string; limit?: number; offset?: number }) =>
api.get<LogData[]>('/api/notification-logs', { params }),
}
// ——— Custom Pages ———
export type CustomPageAccessMode = 'direct' | 'proxy' | 'remote_browser'
export interface CustomPageData {
id: number
name: string
url: string
icon: string
sort_order: number
enabled: boolean
use_proxy: boolean
access_mode: CustomPageAccessMode
description: string | null
login_username: string | null
login_username_selector: string | null
login_password_selector: string | null
login_submit_selector: string | null
login_autofill_enabled: boolean
login_password_configured: boolean
created_at: string
updated_at: string
}
export interface CustomPageForm {
name: string
url: string
icon: string
sort_order: number
enabled: boolean
use_proxy: boolean
access_mode: CustomPageAccessMode
description?: string
login_username?: string
login_password?: string
login_username_selector?: string
login_password_selector?: string
login_submit_selector?: string
login_autofill_enabled?: boolean
login_password_clear?: boolean
}
export const customPagesApi = {
list: () => api.get<CustomPageData[]>('/api/custom-pages'),
listPublic: () => axios.get<CustomPageData[]>('/api/custom-pages/public'),
create: (data: CustomPageForm) => api.post<CustomPageData>('/api/custom-pages', data),
update: (id: number, data: Partial<CustomPageForm>) => api.put<CustomPageData>(`/api/custom-pages/${id}`, data),
delete: (id: number) => api.delete(`/api/custom-pages/${id}`),
}
// ——— Remote browser sessions ———
export interface BrowserSessionData {
id: string
custom_page_id: number
url: string
title: string
}
export type BrowserEventPayload =
| { type: 'click' | 'dblclick' | 'mousemove' | 'mousedown' | 'mouseup'; x: number; y: number; button?: 'left' | 'right' | 'middle' }
| { type: 'type'; text: string }
| { type: 'key'; key: string }
| { type: 'scroll'; delta_x: number; delta_y: number; x?: number; y?: number }
| { type: 'reload' | 'back' | 'forward' }
| { type: 'resize'; width: number; height: number }
export const browserSessionsApi = {
create: (data: { custom_page_id: number; width: number; height: number }) =>
api.post<BrowserSessionData>('/api/browser-sessions', data),
get: (id: string) => api.get<BrowserSessionData>(`/api/browser-sessions/${id}`),
event: (id: string, data: BrowserEventPayload) =>
api.post<BrowserSessionData>(`/api/browser-sessions/${id}/events`, data),
selection: (id: string) => api.get<{ text: string }>(`/api/browser-sessions/${id}/selection`),
close: (id: string) => api.delete(`/api/browser-sessions/${id}`),
screenshotUrl: (id: string, token?: string) => {
const params = new URLSearchParams({ t: String(Date.now()) })
if (token) params.set('token', token)
return `/api/browser-sessions/${id}/screenshot?${params.toString()}`
},
/** Build a WebSocket URL for the streaming endpoint. */
wsUrl: (id: string, token?: string) => {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
const params = new URLSearchParams()
if (token) params.set('token', token)
return `${proto}//${location.host}/api/browser-sessions/${id}/ws?${params.toString()}`
},
}