Fix status page bugs (#19)

* Fix status page bugs

* simplify
This commit is contained in:
Günter Grodotzki 2026-04-23 22:45:38 +02:00 committed by GitHub
parent 95e767a85d
commit cae3dabf1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 39 additions and 22 deletions

View File

@ -4,7 +4,6 @@ import (
"encoding/base64"
"fmt"
"net/http"
"sort"
"strings"
"time"
@ -25,6 +24,10 @@ import (
// connectedThreshold defines how recently a peer must have handshaked to be considered connected
var connectedThreshold = 3 * time.Minute
func isConnected(lastHandshake time.Time) bool {
return !lastHandshake.IsZero() && time.Since(lastHandshake) < connectedThreshold
}
func connectedPeerKeys() map[string]bool {
keys := make(map[string]bool)
wgClient, err := wgctrl.New()
@ -42,7 +45,7 @@ func connectedPeerKeys() map[string]bool {
for _, dev := range devices {
for _, peer := range dev.Peers {
if time.Since(peer.LastHandshakeTime) < connectedThreshold {
if isConnected(peer.LastHandshakeTime) {
keys[peer.PublicKey.String()] = true
}
}
@ -89,7 +92,6 @@ func APIListClients(db store.IStore) echo.HandlerFunc {
filtered := make([]model.ClientData, 0, len(clientDataList))
for _, clientData := range clientDataList {
clientData = util.FillClientSubnetRange(clientData)
cl := clientData.Client
// Non-admin users can only see clients matching their email
@ -121,7 +123,7 @@ func APIListClients(db store.IStore) echo.HandlerFunc {
}
}
filtered = append(filtered, clientData)
filtered = append(filtered, util.FillClientSubnetRange(clientData))
}
return c.JSON(http.StatusOK, filtered)
}
@ -193,7 +195,10 @@ func APICreateClient(db store.IStore, cw *ConfigWriter) echo.HandlerFunc {
}
// validate name + public key uniqueness in one pass
existingClients, _ := db.GetClients(false)
existingClients, err := db.GetClients(false)
if err != nil {
return apiInternalError(c, "Cannot check for duplicates")
}
for _, ec := range existingClients {
if strings.EqualFold(ec.Client.Name, client.Name) {
return apiBadRequest(c, "A client with this name already exists")
@ -300,7 +305,10 @@ func APIUpdateClient(db store.IStore, cw *ConfigWriter) echo.HandlerFunc {
nameChanged := !strings.EqualFold(_client.Name, client.Name)
pubKeyChanged := _client.PublicKey != "" && client.PublicKey != _client.PublicKey
if nameChanged || pubKeyChanged {
existingClients, _ := db.GetClients(false)
existingClients, err := db.GetClients(false)
if err != nil {
return apiInternalError(c, "Cannot check for duplicates")
}
for _, ec := range existingClients {
if ec.Client.ID == client.ID {
continue
@ -645,7 +653,6 @@ func APIServerStatus(db store.IStore) echo.HandlerFunc {
}
}
conv := map[bool]int{true: 1, false: 0}
for i := range devices {
dev := DeviceStatus{Name: devices[i].Name}
for j := range devices[i].Peers {
@ -669,7 +676,7 @@ func APIServerStatus(db store.IStore) echo.HandlerFunc {
LastHandshakeRel: handshakeRel,
AllocatedIP: allocatedIPs,
}
p.Connected = p.LastHandshakeRel < connectedThreshold
p.Connected = isConnected(handshakeTime)
if devices[i].Peers[j].Endpoint != nil {
p.Endpoint = devices[i].Peers[j].Endpoint.String()
@ -681,8 +688,6 @@ func APIServerStatus(db store.IStore) echo.HandlerFunc {
}
dev.Peers = append(dev.Peers, p)
}
sort.SliceStable(dev.Peers, func(a, b int) bool { return dev.Peers[a].Name < dev.Peers[b].Name })
sort.SliceStable(dev.Peers, func(a, b int) bool { return conv[dev.Peers[a].Connected] > conv[dev.Peers[b].Connected] })
devicesStatus = append(devicesStatus, dev)
}
}

View File

@ -2071,3 +2071,22 @@ func TestAPIServerStatus(t *testing.T) {
// On systems without, it returns 500
assert.Contains(t, []int{http.StatusOK, http.StatusInternalServerError}, rec.Code)
}
// Regression: peers that never connected (zero handshake time) must not be
// reported as connected. Both connectedPeerKeys and APIServerStatus use
// isConnected() which guards against time.Since(zero).
func TestIsConnected_ZeroHandshake(t *testing.T) {
assert.False(t, isConnected(time.Time{}), "zero time must not be connected")
}
func TestIsConnected_RecentHandshake(t *testing.T) {
assert.True(t, isConnected(time.Now().Add(-30*time.Second)), "30s ago must be connected")
}
func TestIsConnected_OldHandshake(t *testing.T) {
assert.False(t, isConnected(time.Now().Add(-10*time.Minute)), "10min ago must be disconnected")
}
func TestIsConnected_ExactlyAtThreshold(t *testing.T) {
assert.False(t, isConnected(time.Now().Add(-connectedThreshold)), "exactly at threshold must be disconnected")
}

View File

@ -8,7 +8,6 @@ import (
"github.com/labstack/gommon/log"
"github.com/DigitalTolk/wireguard-ui/store"
"github.com/DigitalTolk/wireguard-ui/util"
)
// APIListUsers returns all users (read-only, managed via SSO)
@ -48,11 +47,6 @@ func APIPatchUserAdmin(db store.IStore) echo.HandlerFunc {
return apiInternalError(c, "Cannot update user")
}
// update CRC32 cache so existing sessions reflect the change
util.DBUsersToCRC32Mutex.Lock()
util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
util.DBUsersToCRC32Mutex.Unlock()
action := "user.demote"
if body.Admin {
action = "user.promote"

View File

@ -42,8 +42,7 @@ func RegisterAPIv1(g *echo.Group, db store.IStore, mailer emailer.Emailer, cw *h
settings.GET("", handler.APIGetSettings(db))
settings.PUT("", handler.APIUpdateSettings(db, cw), handler.ContentTypeJson)
// Users (admin only for list/create/delete)
// Users (read-only — managed via SSO)
// Users (admin only)
users := g.Group("/users", handler.APIAuth, handler.APIAdmin)
users.GET("", handler.APIListUsers(db))
users.GET("/:username", handler.APIGetUser(db))

View File

@ -97,7 +97,7 @@ function QrCodeDialog({ client, onClose }: { client: ClientData | null; onClose:
queryKey: ["client-qr", client?.Client.id],
queryFn: () => apiGet<{ qr_code: string }>(`/clients/${client!.Client.id}/qrcode`),
enabled: !!client,
staleTime: Infinity,
staleTime: 60_000,
});
return (
@ -570,7 +570,7 @@ export function ClientsPage() {
<DialogHeader>
<DialogTitle>New Client</DialogTitle>
</DialogHeader>
<div className="grid gap-5 py-4 sm:grid-cols-2" onChange={() => setCreateTouched(true)}>
<div className="grid items-start gap-5 py-4 sm:grid-cols-2" onChange={() => setCreateTouched(true)}>
<div className="grid gap-2">
<Label htmlFor="new-name">Name</Label>
<Input
@ -739,7 +739,7 @@ export function ClientsPage() {
<DialogHeader>
<DialogTitle>Edit Client</DialogTitle>
</DialogHeader>
<div className="grid gap-5 py-4 sm:grid-cols-2">
<div className="grid items-start gap-5 py-4 sm:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="edit-name">Name</Label>
<Input

View File

@ -48,7 +48,7 @@ type SortKey =
| "endpoint";
type SortDir = "asc" | "desc";
function getSortValue(peer: PeerStatus, key: SortKey): string | number | boolean {
function getSortValue(peer: PeerStatus, key: SortKey): string | number {
switch (key) {
case "name":
return (peer.name || "").toLowerCase();