feat: Initial commit for OnEver Drive centralized backup system with Windows PyQt6 agent and Proxmox LXC deployment
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
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;
|
||||
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 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' }),
|
||||
deleteClient: (clientId: number) =>
|
||||
request<{ message: string }>(`/clients/${clientId}`, { method: 'DELETE' }),
|
||||
|
||||
// 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()}`);
|
||||
},
|
||||
deleteBackup: (backupId: number) =>
|
||||
request<{ message: string }>(`/backups/${backupId}`, { method: 'DELETE' }),
|
||||
|
||||
// Events
|
||||
getEvents: (limit: number = 50) => request<EventLogItem[]>(`/events?limit=${limit}`),
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
export type WebSocketCallback = (event: { type: string; data: any }) => void;
|
||||
|
||||
class WebSocketClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private listeners: Set<WebSocketCallback> = new Set();
|
||||
private reconnectInterval = 3000;
|
||||
private isConnected = false;
|
||||
|
||||
public connect() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host;
|
||||
const wsUrl = `${protocol}//${host}/ws/telemetry`;
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.isConnected = true;
|
||||
this.notify({ type: 'WS_CONNECTED', data: { status: true } });
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const parsed = JSON.parse(event.data);
|
||||
this.notify(parsed);
|
||||
} catch {
|
||||
// Ignored
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.isConnected = false;
|
||||
this.notify({ type: 'WS_DISCONNECTED', data: { status: false } });
|
||||
setTimeout(() => this.connect(), this.reconnectInterval);
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
this.ws?.close();
|
||||
};
|
||||
} catch {
|
||||
setTimeout(() => this.connect(), this.reconnectInterval);
|
||||
}
|
||||
}
|
||||
|
||||
public subscribe(callback: WebSocketCallback) {
|
||||
this.listeners.add(callback);
|
||||
return () => {
|
||||
this.listeners.delete(callback);
|
||||
};
|
||||
}
|
||||
|
||||
private notify(payload: { type: string; data: any }) {
|
||||
this.listeners.forEach((cb) => {
|
||||
try {
|
||||
cb(payload);
|
||||
} catch (err) {
|
||||
console.error('WebSocket subscriber error:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getStatus() {
|
||||
return this.isConnected;
|
||||
}
|
||||
}
|
||||
|
||||
export const wsClient = new WebSocketClient();
|
||||
Reference in New Issue
Block a user