feat: complete SyncServer implementation
Full-stack Go monolith with embedded React frontend for orchestrating rsync-over-SSH file synchronization with Wake-on-LAN support. Features: - JWT auth (HS256) with bcrypt password hashing - CRUD for machines (with WoL config) and sync_pairs - Ed25519 SSH key generation and known_hosts management - WoL magic packet sender + TCP-connect waiter with backoff - Sync engine: rsync subprocess, per-pair job queue, progress parsing - Homebrew cron parser for scheduled syncs - SSE stream for live job status (queued/waking_up/running/success/failed) - React+TS+Vite+Tailwind SPA embedded via embed.FS - Debian packaging with systemd unit, postinst/prerm/postrm Tech stack: - Go 1.22+ (CGO_ENABLED=0, pure SQLite via modernc.org/sqlite) - chi router for HTTP API - TypeScript + React 18 + Tailwind CSS frontend - Cross-compiled to Linux amd64 for Proxmox LXC deployment Tests: wol (MAC parsing, magic packet), syncengine/queue, scheduler/cron
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api, Machine, Job } from '../api/client';
|
||||
|
||||
export default function Dashboard() {
|
||||
const [machines, setMachines] = useState<Machine[]>([]);
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
api<Machine[]>('/api/machines'),
|
||||
api<Job[]>('/api/jobs?limit=5'),
|
||||
]).then(([m, j]) => {
|
||||
setMachines(m);
|
||||
setJobs(j);
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const online = machines.filter(m => m.status.startsWith('online')).length;
|
||||
const todayJobs = jobs.filter(j => {
|
||||
if (!j.started_at) return false;
|
||||
return j.started_at.startsWith(new Date().toISOString().split('T')[0]);
|
||||
}).length;
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||
<div className="grid grid-cols-3 gap-4 mb-8">
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<div className="text-gray-400 text-sm">Machines</div>
|
||||
<div className="text-3xl font-bold">{machines.length}</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<div className="text-gray-400 text-sm">Online</div>
|
||||
<div className="text-3xl font-bold text-green-500">{online}</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<div className="text-gray-400 text-sm">Jobs Today</div>
|
||||
<div className="text-3xl font-bold text-blue-500">{todayJobs}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold mb-3">Recent Jobs</h2>
|
||||
{jobs.length === 0 ? <p className="text-gray-500">No jobs yet</p> : (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-gray-400 border-b border-gray-700">
|
||||
<th className="pb-2">ID</th>
|
||||
<th className="pb-2">Sync Pair</th>
|
||||
<th className="pb-2">Status</th>
|
||||
<th className="pb-2">Started</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{jobs.map(j => (
|
||||
<tr key={j.id} className="border-b border-gray-700/50">
|
||||
<td className="py-2">{j.id}</td>
|
||||
<td className="py-2">{j.sync_pair_id}</td>
|
||||
<td className="py-2">
|
||||
<StatusBadge status={j.status} />
|
||||
</td>
|
||||
<td className="py-2">{j.started_at ? new Date(j.started_at).toLocaleString() : '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex gap-4">
|
||||
<Link to="/machines" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">Machines</Link>
|
||||
<Link to="/sync-pairs" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">Sync Pairs</Link>
|
||||
<Link to="/jobs" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">All Jobs</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const colors: Record<string, string> = {
|
||||
queued: 'bg-gray-600',
|
||||
waking_up: 'bg-yellow-600',
|
||||
running: 'bg-blue-600',
|
||||
success: 'bg-green-600',
|
||||
failed: 'bg-red-600',
|
||||
cancelled: 'bg-gray-600',
|
||||
};
|
||||
return (
|
||||
<span className={`${colors[status] || 'bg-gray-600'} text-white text-xs px-2 py-0.5 rounded`}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user