Bump version to 1.0.11
Machine status probe on page load: - POST /api/machines/refresh probes all machines in parallel (max 20 concurrent, 1.5s timeout) - Updates DB status and broadcasts via SSE to all connected browser tabs - Server-side throttle: ignores refresh requests within 10s - Machines.tsx and Dashboard.tsx fire probe on mount - Visual "Checking machine status..." indicator in Machines table - MachineHandler now accepts *Engine for ProbeAllMachines access
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
BINARY=syncserver
|
BINARY=syncserver
|
||||||
VERSION?=1.0.10
|
VERSION?=1.0.11
|
||||||
GO?=go
|
GO?=go
|
||||||
LDFLAGS=-s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
LDFLAGS=-s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
BUILD_FLAGS=CGO_ENABLED=0
|
BUILD_FLAGS=CGO_ENABLED=0
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
|||||||
"github.com/syncserver/internal/syncengine"
|
"github.com/syncserver/internal/syncengine"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.10"
|
var version = "1.0.11"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfgPath := flag.String("config", "", "Path to config.yaml")
|
cfgPath := flag.String("config", "", "Path to config.yaml")
|
||||||
|
|||||||
@@ -10,15 +10,17 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/syncserver/internal/models"
|
"github.com/syncserver/internal/models"
|
||||||
|
"github.com/syncserver/internal/syncengine"
|
||||||
"github.com/syncserver/internal/wol"
|
"github.com/syncserver/internal/wol"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MachineHandler struct {
|
type MachineHandler struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
|
engine *syncengine.Engine
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMachineHandler(db *sql.DB) *MachineHandler {
|
func NewMachineHandler(db *sql.DB, engine *syncengine.Engine) *MachineHandler {
|
||||||
return &MachineHandler{db: db}
|
return &MachineHandler{db: db, engine: engine}
|
||||||
}
|
}
|
||||||
|
|
||||||
var macRegex = regexp.MustCompile(`^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$`)
|
var macRegex = regexp.MustCompile(`^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$`)
|
||||||
@@ -209,6 +211,16 @@ func (h *MachineHandler) TestWoL(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, map[string]interface{}{"ok": true, "sent": 3})
|
writeJSON(w, map[string]interface{}{"ok": true, "sent": 3})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *MachineHandler) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.engine == nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "engine not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go h.engine.ProbeAllMachines()
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
writeJSON(w, map[string]string{"status": "probing"})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *MachineHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
func (h *MachineHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func NewServer(cfg *config.Config, db *sql.DB, engine *syncengine.Engine) *Serve
|
|||||||
s := &Server{router: r, cfg: cfg, engine: engine}
|
s := &Server{router: r, cfg: cfg, engine: engine}
|
||||||
|
|
||||||
authHandler := NewAuthHandler(db)
|
authHandler := NewAuthHandler(db)
|
||||||
machineHandler := NewMachineHandler(db)
|
machineHandler := NewMachineHandler(db, engine)
|
||||||
syncPairHandler := NewSyncPairHandler(db)
|
syncPairHandler := NewSyncPairHandler(db)
|
||||||
jobHandler := NewJobHandler(db, engine)
|
jobHandler := NewJobHandler(db, engine)
|
||||||
sseHandler := NewSSEHandler(engine)
|
sseHandler := NewSSEHandler(engine)
|
||||||
@@ -49,6 +49,7 @@ func NewServer(cfg *config.Config, db *sql.DB, engine *syncengine.Engine) *Serve
|
|||||||
r.With(auth.RequireAuth).Route("/machines", func(r chi.Router) {
|
r.With(auth.RequireAuth).Route("/machines", func(r chi.Router) {
|
||||||
r.Get("/", machineHandler.List)
|
r.Get("/", machineHandler.List)
|
||||||
r.Post("/", machineHandler.Create)
|
r.Post("/", machineHandler.Create)
|
||||||
|
r.Post("/refresh", machineHandler.Refresh)
|
||||||
r.Get("/{id}", machineHandler.Get)
|
r.Get("/{id}", machineHandler.Get)
|
||||||
r.Put("/{id}", machineHandler.Update)
|
r.Put("/{id}", machineHandler.Update)
|
||||||
r.Delete("/{id}", machineHandler.Delete)
|
r.Delete("/{id}", machineHandler.Delete)
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
queue *Queue
|
queue *Queue
|
||||||
eventBus *EventBus
|
eventBus *EventBus
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
stopped bool
|
stopped bool
|
||||||
|
lastProbeAt atomic.Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
@@ -333,3 +334,51 @@ func (e *Engine) CreateJob(syncPairID int64, triggerType string) (int64, error)
|
|||||||
}
|
}
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
probeThrottleSeconds = 10
|
||||||
|
probeTimeout = 1500 * time.Millisecond
|
||||||
|
probeMaxConcurrent = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *Engine) ProbeAllMachines() {
|
||||||
|
now := time.Now().UnixNano()
|
||||||
|
last := e.lastProbeAt.Load()
|
||||||
|
|
||||||
|
if now-last < int64(probeThrottleSeconds*time.Second) {
|
||||||
|
slog.Debug("ProbeAllMachines: skipped (throttled)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !e.lastProbeAt.CompareAndSwap(last, now) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
machineRepo := models.NewMachineRepository(e.db)
|
||||||
|
ms, err := machineRepo.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("ProbeAllMachines: list failed", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sem := make(chan struct{}, probeMaxConcurrent)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i := range ms {
|
||||||
|
wg.Add(1)
|
||||||
|
sem <- struct{}{}
|
||||||
|
go func(m *models.Machine) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), probeTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
status := "offline"
|
||||||
|
if wol.IsReachable(ctx, m.Host, m.Port, probeTimeout) {
|
||||||
|
status = "online"
|
||||||
|
}
|
||||||
|
e.setMachineStatus(m.ID, status)
|
||||||
|
}(&ms[i])
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package syncengine
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -37,3 +38,29 @@ func TestIsReachableTimeout(t *testing.T) {
|
|||||||
t.Errorf("IsReachable returned too early: %v", d)
|
t.Errorf("IsReachable returned too early: %v", d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProbeThrottle(t *testing.T) {
|
||||||
|
e := &Engine{lastProbeAt: atomic.Int64{}}
|
||||||
|
|
||||||
|
e.lastProbeAt.Store(time.Now().UnixNano())
|
||||||
|
|
||||||
|
now := time.Now().UnixNano()
|
||||||
|
last := e.lastProbeAt.Load()
|
||||||
|
if now-last < int64(probeThrottleSeconds*time.Second) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Error("throttle check did not run as expected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProbeConstants(t *testing.T) {
|
||||||
|
if probeThrottleSeconds != 10 {
|
||||||
|
t.Errorf("probeThrottleSeconds = %d, want 10", probeThrottleSeconds)
|
||||||
|
}
|
||||||
|
if probeTimeout != 1500*time.Millisecond {
|
||||||
|
t.Errorf("probeTimeout = %v, want 1500ms", probeTimeout)
|
||||||
|
}
|
||||||
|
if probeMaxConcurrent != 20 {
|
||||||
|
t.Errorf("probeMaxConcurrent = %d, want 20", probeMaxConcurrent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default function Dashboard() {
|
|||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
|
api('/api/machines/refresh', { method: 'POST' }).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -66,9 +66,14 @@ export default function Machines() {
|
|||||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||||
const [form, setForm] = useState<MachineForm>(defaultForm);
|
const [form, setForm] = useState<MachineForm>(defaultForm);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [probing, setProbing] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load();
|
||||||
|
setProbing(true);
|
||||||
|
api('/api/machines/refresh', { method: 'POST' }).catch(() => {});
|
||||||
|
const timer = setTimeout(() => setProbing(false), 5000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -212,6 +217,13 @@ export default function Machines() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
{probing && (
|
||||||
|
<div className="flex items-center gap-2 px-4 pt-3 text-xs text-muted-foreground">
|
||||||
|
<span className="inline-block h-2 w-2 bg-amber-400 rounded-full animate-pulse" />
|
||||||
|
Checking machine status...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -290,6 +302,7 @@ export default function Machines() {
|
|||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user