240 lines
6.7 KiB
TypeScript
240 lines
6.7 KiB
TypeScript
const API_BASE = '/api';
|
|
|
|
export interface DashboardStats {
|
|
total_clients: number;
|
|
online_clients: number;
|
|
offline_clients: number;
|
|
total_jobs: number;
|
|
backups_today_count: number;
|
|
backups_today_success: number;
|
|
backups_today_failed: number;
|
|
active_uploads_count: number;
|
|
storage: {
|
|
total_bytes: number;
|
|
used_bytes: number;
|
|
free_bytes: number;
|
|
usage_percent: number;
|
|
storage_root: string;
|
|
};
|
|
}
|
|
|
|
export interface ClientItem {
|
|
id: number;
|
|
client_code: string;
|
|
name: string;
|
|
alias?: string;
|
|
hostname?: string;
|
|
os_info?: string;
|
|
ip_address?: string;
|
|
agent_version: string;
|
|
status: string;
|
|
storage_used_bytes: number;
|
|
storage_quota_bytes: number;
|
|
last_seen_at?: string;
|
|
last_backup_at?: string;
|
|
is_active: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface BackupJobItem {
|
|
id: number;
|
|
job_code: string;
|
|
client_id: number;
|
|
name: string;
|
|
source_path: string;
|
|
file_patterns: string;
|
|
schedule_cron: string;
|
|
is_active: boolean;
|
|
keep_daily: number;
|
|
keep_weekly: number;
|
|
keep_monthly: number;
|
|
min_stable_time_seconds: number;
|
|
status: string;
|
|
last_run_at?: string;
|
|
next_run_at?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface BackupFileItem {
|
|
id: number;
|
|
client_id: number;
|
|
job_id?: number;
|
|
session_id?: number;
|
|
filename: string;
|
|
relative_path: string;
|
|
file_size: number;
|
|
sha256: string;
|
|
retention_tag: string;
|
|
is_active: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface EventLogItem {
|
|
id: number;
|
|
timestamp: string;
|
|
event_type: string;
|
|
severity: string;
|
|
client_id?: number;
|
|
job_id?: number;
|
|
user_email?: string;
|
|
ip_address?: string;
|
|
message: string;
|
|
}
|
|
|
|
export interface SystemSettingsResponse {
|
|
storage_root: string;
|
|
global_quota_gb: number;
|
|
default_client_quota_gb: number;
|
|
default_keep_daily: number;
|
|
default_keep_weekly: number;
|
|
default_keep_monthly: number;
|
|
}
|
|
|
|
export const getAuthToken = (): string | null => {
|
|
return localStorage.getItem('oed_token');
|
|
};
|
|
|
|
export const getCurrentUser = (): any | null => {
|
|
const saved = localStorage.getItem('oed_user');
|
|
if (saved) {
|
|
try {
|
|
return JSON.parse(saved);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
export const setAuthToken = (token: string | null) => {
|
|
if (token) {
|
|
localStorage.setItem('oed_token', token);
|
|
} else {
|
|
localStorage.removeItem('oed_token');
|
|
localStorage.removeItem('oed_user');
|
|
}
|
|
};
|
|
|
|
const request = async <T>(endpoint: string, options: RequestInit = {}): Promise<T> => {
|
|
const token = getAuthToken();
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(options.headers as Record<string, string>),
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
...options,
|
|
headers,
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
setAuthToken(null);
|
|
window.dispatchEvent(new Event('oed_unauthorized'));
|
|
const errorData = await response.json().catch(() => ({ detail: 'Authentication required' }));
|
|
throw new Error(errorData.detail || 'Authentication required');
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ detail: 'Network error' }));
|
|
throw new Error(errorData.detail || `Request failed with status ${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
};
|
|
|
|
export const api = {
|
|
// Auth
|
|
login: (email: string, password: string) =>
|
|
request<{ access_token: string; user: any }>('/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password }),
|
|
}),
|
|
|
|
// Stats
|
|
getStats: () => request<DashboardStats>('/stats'),
|
|
|
|
// Clients
|
|
getClients: () => request<ClientItem[]>('/clients'),
|
|
createRegistrationCode: (clientNameHint?: string) =>
|
|
request<{ code: string; expires_at: string }>('/clients/registration-code', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ client_name_hint: clientNameHint, expires_in_hours: 48 }),
|
|
}),
|
|
revokeClient: (clientId: number) =>
|
|
request<{ message: string }>(`/clients/${clientId}/revoke`, { method: 'POST' }),
|
|
generateReRegisterCode: (clientId: number) =>
|
|
request<{ code: string; expires_at: string; client_id: number }>(`/clients/${clientId}/re-register`, { method: 'POST' }),
|
|
deleteClient: (clientId: number) =>
|
|
request<{ message: string }>(`/clients/${clientId}`, { method: 'DELETE' }),
|
|
updateClient: (clientId: number, data: { alias?: string; storage_quota_bytes?: number }) =>
|
|
request<ClientItem>(`/clients/${clientId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
getSettings: () => request<SystemSettingsResponse>('/settings'),
|
|
updateSettings: (data: Partial<SystemSettingsResponse>) =>
|
|
request<SystemSettingsResponse>('/settings', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
// Jobs
|
|
getJobs: (clientId?: number) =>
|
|
request<BackupJobItem[]>(clientId ? `/jobs?client_id=${clientId}` : '/jobs'),
|
|
createJob: (jobData: {
|
|
client_id: number;
|
|
name: string;
|
|
source_path: string;
|
|
file_patterns: string;
|
|
schedule_cron: string;
|
|
keep_daily: number;
|
|
keep_weekly: number;
|
|
keep_monthly: number;
|
|
min_stable_time_seconds: number;
|
|
}) =>
|
|
request<BackupJobItem>('/jobs', {
|
|
method: 'POST',
|
|
body: JSON.stringify(jobData),
|
|
}),
|
|
triggerJob: (jobId: number) =>
|
|
request<{ message: string }>(`/jobs/${jobId}/trigger`, { method: 'POST' }),
|
|
deleteJob: (jobId: number) =>
|
|
request<{ message: string }>(`/jobs/${jobId}`, { method: 'DELETE' }),
|
|
|
|
// Backups
|
|
getBackups: (clientId?: number, jobId?: number) => {
|
|
const params = new URLSearchParams();
|
|
if (clientId) params.append('client_id', clientId.toString());
|
|
if (jobId) params.append('job_id', jobId.toString());
|
|
return request<BackupFileItem[]>(`/backups?${params.toString()}`);
|
|
},
|
|
downloadBackup: async (backup: BackupFileItem) => {
|
|
const token = getAuthToken();
|
|
const response = await fetch(`${API_BASE}/backups/${backup.id}/download`, {
|
|
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
|
});
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ detail: 'Network error' }));
|
|
throw new Error(errorData.detail || `Download failed with status ${response.status}`);
|
|
}
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = backup.filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
},
|
|
deleteBackup: (backupId: number) =>
|
|
request<{ message: string }>(`/backups/${backupId}`, { method: 'DELETE' }),
|
|
|
|
// Events
|
|
getEvents: (limit: number = 50) => request<EventLogItem[]>(`/events?limit=${limit}`),
|
|
};
|