feat: Initial commit for OnEver Drive centralized backup system with Windows PyQt6 agent and Proxmox LXC deployment

This commit is contained in:
2026-08-13 19:33:44 -03:00
commit 1bfb808c79
77 changed files with 10675 additions and 0 deletions
+197
View File
@@ -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}`),
};
+67
View File
@@ -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();