import type { Component } from 'solid-js'; import { Show, For, createMemo } from 'solid-js'; import { ProxmoxSectionNav } from '@/components/Proxmox/ProxmoxSectionNav'; import { useWebSocket } from '@/App'; import { Card } from '@/components/shared/Card'; import { EmptyState } from '@/components/shared/EmptyState'; import { formatAbsoluteTime, formatRelativeTime } from '@/utils/format'; import type { ReplicationJob } from '@/types/api'; function formatDuration(durationSeconds?: number, durationHuman?: string): string { if (durationHuman && durationHuman.trim()) return durationHuman; if (!durationSeconds || durationSeconds <= 0) return ''; const hours = Math.floor(durationSeconds / 3600) .toString() .padStart(2, '0'); const minutes = Math.floor((durationSeconds % 3600) / 60) .toString() .padStart(2, '0'); const seconds = Math.floor(durationSeconds % 60) .toString() .padStart(2, '0'); return `${hours}:${minutes}:${seconds}`; } function getStatusBadge(job: ReplicationJob) { const status = (job.status || job.state || '').toLowerCase(); const lastStatus = (job.lastSyncStatus || '').toLowerCase(); if (status.includes('error') || lastStatus.includes('error')) { return { tone: 'danger' as const, label: status || lastStatus || 'Error', }; } if (status.includes('sync')) { return { tone: 'warning' as const, label: job.status || job.state || 'Syncing', }; } return { tone: 'success' as const, label: job.status || job.state || 'Idle', }; } function formatRate(limit?: number): string { if (!limit || limit <= 0) return '—'; return `${limit.toFixed(0)} MB/s`; } const Replication: Component = () => { const { state, connected } = useWebSocket(); const replicationJobs = createMemo(() => { const jobs = state.replicationJobs ?? []; return [...jobs].sort((a, b) => { if (a.instance !== b.instance) return a.instance.localeCompare(b.instance); if ((a.guestName || '') !== (b.guestName || '')) { return (a.guestName || '').localeCompare(b.guestName || ''); } return (a.jobId || '').localeCompare(b.jobId || ''); }); }); return (
} title="Connection lost" description="Unable to connect to the backend server. Attempting to reconnect..." tone="danger" /> }> 0} fallback={ } title="No replication jobs detected" description="Replication jobs will appear here once configured in Proxmox." /> } >
{(job) => { const badge = getStatusBadge(job); return ( ); }}
Guest Job Source → Target Last Sync Next Sync Status Failures Rate
{job.guestName || `VM ${job.guestId ?? ''}`}
{job.instance} · ID {job.guestId ?? '—'} · {job.guestNode || job.sourceNode || 'Unknown node'}
{job.jobId || '—'}
{job.type ? `${job.type.toUpperCase()} · ` : ''}{job.schedule || '*/15'}
{job.sourceNode || '—'} {job.targetNode || '—'}
{job.sourceStorage || 'local'} → {job.targetStorage || 'remote'}
{formatAbsoluteTime(job.lastSyncTime!)}
{formatRelativeTime(job.lastSyncTime!)} · {formatDuration(job.lastSyncDurationSeconds, job.lastSyncDurationHuman)}
Never
{formatAbsoluteTime(job.nextSyncTime!)}
{formatRelativeTime(job.nextSyncTime!)}
{badge.label}
{job.error}
{job.failCount ?? 0} {formatRate(job.rateLimitMbps)}
); }; export default Replication;