mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
fix(presence): clarify post-migration offline state and emit peer_online events.
After RustDesk→BetterDesk migration, HTTP heartbeats can update last seen while UDP signal registration is still missing. Show a distinct "No signal" badge, document the two-channel presence model, and publish peer_online on registration.
This commit is contained in:
@@ -65,6 +65,19 @@ func signalLimiterKey(kind, clientHost, peerID string) string {
|
||||
return fmt.Sprintf("%s:%s:%s", kind, clientHost, peerID)
|
||||
}
|
||||
|
||||
func (s *Server) publishPeerOnline(id string) {
|
||||
if s.eventBus == nil {
|
||||
return
|
||||
}
|
||||
s.eventBus.Publish(events.Event{
|
||||
Type: events.EventPeerOnline,
|
||||
Data: map[string]string{
|
||||
"id": id,
|
||||
"status": "online",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) allowRegistration(clientHost, peerID string, knownPeer bool) bool {
|
||||
if s.limiter == nil {
|
||||
return true
|
||||
@@ -319,6 +332,7 @@ func (s *Server) handleRegisterPeer(msg *pb.RegisterPeer, raddr *net.UDPAddr) {
|
||||
|
||||
log.Printf("[signal] New peer registered: %s from %s (pk_loaded=%v)", id, raddr, len(entry.PK) > 0)
|
||||
s.db.UpdatePeerStatus(id, "ONLINE", raddr.IP.String())
|
||||
s.publishPeerOnline(id)
|
||||
}
|
||||
|
||||
// handleRegisterPk processes a public key registration.
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/unitronix/betterdesk-server/config"
|
||||
"github.com/unitronix/betterdesk-server/db"
|
||||
"github.com/unitronix/betterdesk-server/events"
|
||||
"github.com/unitronix/betterdesk-server/peer"
|
||||
pb "github.com/unitronix/betterdesk-server/proto"
|
||||
"github.com/unitronix/betterdesk-server/ratelimit"
|
||||
@@ -363,3 +364,27 @@ func TestOrgPolicyForcesRelayOnPunchHole(t *testing.T) {
|
||||
t.Fatal("org block_direct_p2p should force relay")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishPeerOnlineEmitsEvent(t *testing.T) {
|
||||
srv, _ := newTestSignalServer(t, config.EnrollmentModeOpen)
|
||||
|
||||
sub := srv.eventBus.Subscribe(events.EventPeerOnline)
|
||||
defer srv.eventBus.Unsubscribe(sub)
|
||||
|
||||
srv.publishPeerOnline("PEERON1")
|
||||
|
||||
select {
|
||||
case evt := <-sub.Ch:
|
||||
if evt.Type != events.EventPeerOnline {
|
||||
t.Fatalf("event type = %s, want %s", evt.Type, events.EventPeerOnline)
|
||||
}
|
||||
if evt.Data["id"] != "PEERON1" {
|
||||
t.Fatalf("event id = %q, want PEERON1", evt.Data["id"])
|
||||
}
|
||||
if evt.Data["status"] != "online" {
|
||||
t.Fatalf("event status = %q, want online", evt.Data["status"])
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for peer_online event")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +336,7 @@ func (s *Server) handleRegisterPeerWS(msg *pb.RegisterPeer, remoteAddr string) *
|
||||
|
||||
log.Printf("[signal] New WS peer registered: %s from %s", id, remoteAddr)
|
||||
s.db.UpdatePeerStatus(id, "ONLINE", remoteAddr)
|
||||
s.publishPeerOnline(id)
|
||||
|
||||
return &pb.RendezvousMessage{
|
||||
Union: &pb.RendezvousMessage_RegisterPeerResponse{
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# RustDesk → BetterDesk server migration — client presence
|
||||
|
||||
## Symptom
|
||||
|
||||
After replacing a RustDesk Community server (hbbs/hbbr) with BetterDesk while keeping the same IP, keys, and client URLs:
|
||||
|
||||
- Remote sessions may work via the relay server.
|
||||
- The console shows **Last seen** as recent (e.g. “6 minutes ago”).
|
||||
- The **Online** badge stays **Offline** until each client is restarted.
|
||||
|
||||
## Why this happens
|
||||
|
||||
BetterDesk tracks presence in two separate channels:
|
||||
|
||||
| Channel | Client config | Server port | Updates | Console column |
|
||||
|---------|---------------|-------------|---------|----------------|
|
||||
| HTTP heartbeat | `api_server` | 21114 (`/api/heartbeat`) | DB `last_online`, DB `status` | **Last seen** |
|
||||
| UDP registration | `rendezvous_server` / `id_server` | **21116** (`RegisterPeer`) | In-memory `live_online` | **Online / Offline** |
|
||||
|
||||
The devices table uses **`live_online`** (signal-server memory map), not the DB `status` field alone.
|
||||
|
||||
After migration or a BetterDesk server restart:
|
||||
|
||||
1. The in-memory peer table starts **empty** (`SetAllOffline()` on boot).
|
||||
2. RustDesk clients often resume **HTTP** heartbeats immediately (same `api_server` URL).
|
||||
3. **UDP** registration to port 21116 may stay stale until the client process or service is restarted — even when the hostname/IP did not change.
|
||||
|
||||
So “recent last seen + offline” is expected until clients re-register on UDP 21116.
|
||||
|
||||
## What to do
|
||||
|
||||
1. **Restart** the RustDesk client or its background service on affected machines (or reboot).
|
||||
2. No need to regenerate keys if connections already work.
|
||||
3. For large fleets, plan a rolling client/service restart after the server backend swap.
|
||||
|
||||
## Verify on the server
|
||||
|
||||
```bash
|
||||
# Signal registration after a client restart
|
||||
journalctl -u betterdesk-server -f | grep "New peer registered"
|
||||
|
||||
# Compare DB last seen vs live online for one peer
|
||||
curl -s -H "X-API-Key: YOUR_KEY" http://127.0.0.1:21114/api/peers/DEVICE_ID | jq '{live_online, last_online, status}'
|
||||
```
|
||||
|
||||
Checklist:
|
||||
|
||||
- [ ] UDP **21116** reachable from clients (firewall, NAT, Docker port mapping).
|
||||
- [ ] Enrollment mode is `open`, or migrated peers already exist in the database (`managed` / `locked` can block unknown IDs).
|
||||
- [ ] After restart, `live_online` becomes `true` within ~30 seconds.
|
||||
|
||||
## Enrollment modes
|
||||
|
||||
| Mode | Re-registration after migration |
|
||||
|------|-------------------------------|
|
||||
| `open` | Any peer with valid keys can register on UDP 21116. |
|
||||
| `managed` | Peers already in the DB re-register freely; unknown IDs need approval. |
|
||||
| `locked` | Only peers with a valid device token can register. |
|
||||
|
||||
## Related
|
||||
|
||||
- Go signal handler: `betterdesk-server/signal/handler.go` (UDP `RegisterPeer`)
|
||||
- HTTP heartbeat (DB only): `betterdesk-server/api/client_api_handlers.go`
|
||||
- Panel mapping: `web-nodejs/services/betterdeskApi.js` (`online: !!peer.live_online`)
|
||||
@@ -310,7 +310,8 @@
|
||||
"group_deleted": "تم حذف مجموعة الأجهزة",
|
||||
"folder_group_readonly": "تتم إدارة المجموعات المدعومة بالمجلدات من المجلدات.",
|
||||
"manage_user_groups": "إدارة مجموعات المستخدمين",
|
||||
"restore_failed": "فشل استعادة الجهاز"
|
||||
"restore_failed": "فشل استعادة الجهاز",
|
||||
"no_signal_tooltip": "اتصل العميل بـ API مؤخرًا لكنه غير مسجل على خادم الإشارة (UDP :21116). أعد تشغيل العميل بعد ترحيل الخادم."
|
||||
},
|
||||
"keys": {
|
||||
"title": "مفاتيح الخادم",
|
||||
@@ -732,7 +733,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "جارٍ التحقق...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "لا إشارة"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "Skupina zařízení odstraněna",
|
||||
"folder_group_readonly": "Skupiny založené na složkách se spravují ve složkách.",
|
||||
"manage_user_groups": "Spravovat skupiny uživatelů",
|
||||
"restore_failed": "Ne udalo sie przywrocic urzadzenia"
|
||||
"restore_failed": "Ne udalo sie przywrocic urzadzenia",
|
||||
"no_signal_tooltip": "Klient nedávno kontaktoval API, ale není zaregistrován na signal serveru (UDP :21116). Po migraci serveru restartujte klienta."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Klíče serveru",
|
||||
@@ -725,7 +726,8 @@
|
||||
"stopped": "Zastaveno",
|
||||
"unknown": "Neznámý",
|
||||
"checking": "Kontroluji...",
|
||||
"label": "Stav"
|
||||
"label": "Stav",
|
||||
"no_signal": "Bez signálu"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Obnovit",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "Enhedsgruppen blev slettet",
|
||||
"folder_group_readonly": "Mappestøttede grupper administreres fra mapper.",
|
||||
"manage_user_groups": "Administrer brugergrupper",
|
||||
"restore_failed": "Enhed konnte nicht wiederhergestellt werden"
|
||||
"restore_failed": "Enhed konnte nicht wiederhergestellt werden",
|
||||
"no_signal_tooltip": "Klienten kontaktede API'et for nylig, men er ikke registreret på signalserveren (UDP :21116). Genstart klienten efter servermigration."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Servernøgler",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "Tjekker...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Intet signal"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "Gerätegruppe gelöscht",
|
||||
"folder_group_readonly": "Ordnerbasierte Gruppen werden über Ordner verwaltet.",
|
||||
"manage_user_groups": "Benutzergruppen verwalten",
|
||||
"restore_failed": "Gerät konnte nicht wiederhergestellt werden"
|
||||
"restore_failed": "Gerät konnte nicht wiederhergestellt werden",
|
||||
"no_signal_tooltip": "Der Client hat kürzlich die API kontaktiert, ist aber nicht auf dem Signalserver registriert (UDP :21116). Starten Sie den Client nach der Servermigration neu."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Dashboard",
|
||||
@@ -725,7 +726,8 @@
|
||||
"stopped": "Gestoppt",
|
||||
"unknown": "Unbekannt",
|
||||
"checking": "Prüfe...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Kein Signal"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Aktualisieren",
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
"filter_online": "Online",
|
||||
"filter_offline": "Offline",
|
||||
"filter_banned": "Banned",
|
||||
"no_signal_tooltip": "Client contacted the API recently but is not registered on the signal server (UDP :21116). Restart the client after server migration.",
|
||||
"selected": "selected",
|
||||
"showing": "Showing",
|
||||
"of": "of",
|
||||
@@ -730,6 +731,7 @@
|
||||
"status": {
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"no_signal": "No signal",
|
||||
"banned": "Banned",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "Grupo de dispositivos eliminado",
|
||||
"folder_group_readonly": "Los grupos basados en carpetas se administran desde Carpetas.",
|
||||
"manage_user_groups": "Administrar grupos de usuarios",
|
||||
"restore_failed": "Error al restaurar el dispositivo"
|
||||
"restore_failed": "Error al restaurar el dispositivo",
|
||||
"no_signal_tooltip": "El cliente contactó la API recientemente, pero no está registrado en el servidor de señal (UDP :21116). Reinicie el cliente tras la migración del servidor."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Panel",
|
||||
@@ -725,7 +726,8 @@
|
||||
"stopped": "Detenido",
|
||||
"unknown": "Desconocido",
|
||||
"checking": "Verificando...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Sin señal"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Actualizar",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "Laiteryhmä poistettu",
|
||||
"folder_group_readonly": "Kansiota tukevia ryhmiä hallitaan kansioista.",
|
||||
"manage_user_groups": "Hallinnoi käyttäjäryhmiä",
|
||||
"restore_failed": "Laite konnte nicht wiederhergestellt werden"
|
||||
"restore_failed": "Laite konnte nicht wiederhergestellt werden",
|
||||
"no_signal_tooltip": "Asiakas otti hiljattain yhteyttä API:in, mutta ei ole rekisteröitynyt signal-palvelimeen (UDP :21116). Käynnistä asiakas uudelleen palvelinsiirron jälkeen."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Palvelimen avaimet",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "Tarkistetaan...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Ei signaalia"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "Groupe d’appareils supprimé",
|
||||
"folder_group_readonly": "Les groupes associés aux dossiers sont gérés depuis les dossiers.",
|
||||
"manage_user_groups": "Gérer les groupes d’utilisateurs",
|
||||
"restore_failed": "Échec de la restauration de l'appareil"
|
||||
"restore_failed": "Échec de la restauration de l'appareil",
|
||||
"no_signal_tooltip": "Le client a contacté l'API récemment mais n'est pas enregistré sur le serveur de signal (UDP :21116). Redémarrez le client après la migration du serveur."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Tableau de bord",
|
||||
@@ -725,7 +726,8 @@
|
||||
"stopped": "Arrêté",
|
||||
"unknown": "Inconnu",
|
||||
"checking": "Vérification...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Pas de signal"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Actualiser",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "डिवाइस समूह हटा दिया गया",
|
||||
"folder_group_readonly": "फ़ोल्डर-समर्थित समूहों को फ़ोल्डरों से प्रबंधित किया जाता है।",
|
||||
"manage_user_groups": "उपयोगकर्ता समूह प्रबंधित करें",
|
||||
"restore_failed": "डिवाइस पुनर्स्थापित करने में विफल"
|
||||
"restore_failed": "डिवाइस पुनर्स्थापित करने में विफल",
|
||||
"no_signal_tooltip": "क्लाइंट ने हाल ही में API से संपर्क किया, लेकिन सिग्नल सर्वर (UDP :21116) पर पंजीकृत नहीं है। सर्वर माइग्रेशन के बाद क्लाइंट को पुनः प्रारंभ करें।"
|
||||
},
|
||||
"keys": {
|
||||
"title": "सर्वर कुंजी",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "जाँच हो रही है...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "कोई सिग्नल नहीं"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "Eszközcsoport törölve",
|
||||
"folder_group_readonly": "A mappaalapú csoportok kezelése mappákból történik.",
|
||||
"manage_user_groups": "Felhasználói csoportok kezelése",
|
||||
"restore_failed": "Eszköz konnte nicht wiederhergestellt werden"
|
||||
"restore_failed": "Eszköz konnte nicht wiederhergestellt werden",
|
||||
"no_signal_tooltip": "Az ügyfél nemrég elérte az API-t, de nincs regisztrálva a jelzőszerveren (UDP :21116). A szerver migrációja után indítsa újra az ügyfelet."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Szerverkulcsok",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "Ellenőrzés...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Nincs jel"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "Grup perangkat dihapus",
|
||||
"folder_group_readonly": "Grup yang didukung folder dikelola dari folder.",
|
||||
"manage_user_groups": "Kelola grup pengguna",
|
||||
"restore_failed": "Gagal memulihkan perangkat"
|
||||
"restore_failed": "Gagal memulihkan perangkat",
|
||||
"no_signal_tooltip": "Klien baru saja menghubungi API tetapi tidak terdaftar di server sinyal (UDP :21116). Mulai ulang klien setelah migrasi server."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Kunci Server",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "Memeriksa...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Tanpa sinyal"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "Gruppo dispositivi eliminato",
|
||||
"folder_group_readonly": "I gruppi basati su cartelle sono gestiti tramite Cartelle.",
|
||||
"manage_user_groups": "Gestisci gruppi utente",
|
||||
"restore_failed": "Èchec de la restauration de il dispositivo"
|
||||
"restore_failed": "Èchec de la restauration de il dispositivo",
|
||||
"no_signal_tooltip": "Il client ha contattato l'API di recente ma non è registrato sul server di segnale (UDP :21116). Riavvia il client dopo la migrazione del server."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Chiavi del server",
|
||||
@@ -725,7 +726,8 @@
|
||||
"stopped": "Fermato",
|
||||
"unknown": "Sconosciuto",
|
||||
"checking": "Verifica...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Nessun segnale"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Aggiorna",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "デバイスグループが削除されました",
|
||||
"folder_group_readonly": "フォルダーベースのグループはフォルダーから管理されます。",
|
||||
"manage_user_groups": "ユーザーグループの管理",
|
||||
"restore_failed": "デバイスの復元に失敗しました"
|
||||
"restore_failed": "デバイスの復元に失敗しました",
|
||||
"no_signal_tooltip": "クライアントは最近 API に接続しましたが、シグナルサーバー (UDP :21116) に登録されていません。サーバー移行後にクライアントを再起動してください。"
|
||||
},
|
||||
"keys": {
|
||||
"title": "サーバーキー",
|
||||
@@ -725,7 +726,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "確認中...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "シグナルなし"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "기기 그룹이 삭제되었습니다.",
|
||||
"folder_group_readonly": "폴더 지원 그룹은 폴더에서 관리됩니다.",
|
||||
"manage_user_groups": "사용자 그룹 관리",
|
||||
"restore_failed": "기기 복원에 실패했습니다"
|
||||
"restore_failed": "기기 복원에 실패했습니다",
|
||||
"no_signal_tooltip": "클라이언트가 최근 API에 연락했지만 시그널 서버(UDP :21116)에 등록되지 않았습니다. 서버 마이그레이션 후 클라이언트를 재시작하세요."
|
||||
},
|
||||
"keys": {
|
||||
"title": "서버 키",
|
||||
@@ -725,7 +726,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "확인 중...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "신호 없음"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "Enhetsgruppe slettet",
|
||||
"folder_group_readonly": "Mappestøttede grupper administreres fra mapper.",
|
||||
"manage_user_groups": "Administrer brukergrupper",
|
||||
"restore_failed": "Enhet konnte nicht wiederhergestellt werden"
|
||||
"restore_failed": "Enhet konnte nicht wiederhergestellt werden",
|
||||
"no_signal_tooltip": "Klienten kontaktet API-et nylig, men er ikke registrert på signalserveren (UDP :21116). Start klienten på nytt etter servermigrering."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Servernøkler",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "Sjekker...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Intet signal"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "Apparaatgroep verwijderd",
|
||||
"folder_group_readonly": "Mapgebaseerde groepen worden via Mappen beheerd.",
|
||||
"manage_user_groups": "Gebruikersgroepen beheren",
|
||||
"restore_failed": "Gerat konnte nicht wiederhergestellt werden"
|
||||
"restore_failed": "Gerat konnte nicht wiederhergestellt werden",
|
||||
"no_signal_tooltip": "De client heeft onlangs de API bereikt maar is niet geregistreerd op de signalserver (UDP :21116). Herstart de client na servermigratie."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Serversleutels",
|
||||
@@ -725,7 +726,8 @@
|
||||
"stopped": "Gestopt",
|
||||
"unknown": "Onbekend",
|
||||
"checking": "Controleren...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Geen signaal"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Vernieuwen",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"no_data": "Brak danych o sprzęcie",
|
||||
"title": "Informacje sprzętowe",
|
||||
"gpu": "Karta graficzna"
|
||||
}
|
||||
},
|
||||
"no_signal_tooltip": "Klient niedawno kontaktował API, ale nie jest zarejestrowany na serwerze sygnałowym (UDP :21116). Uruchom ponownie klienta po migracji serwera."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Klucze serwera",
|
||||
@@ -735,7 +736,8 @@
|
||||
"stopped": "Zatrzymany",
|
||||
"unknown": "Nieznany",
|
||||
"checking": "Sprawdzanie...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Brak sygnału"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Odśwież",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "Grupo de dispositivos excluído",
|
||||
"folder_group_readonly": "Grupos baseados em pastas são gerenciados em Pastas.",
|
||||
"manage_user_groups": "Gerenciar grupos de usuários",
|
||||
"restore_failed": "Erro al restaurar o dispositivo"
|
||||
"restore_failed": "Erro al restaurar o dispositivo",
|
||||
"no_signal_tooltip": "O cliente contactou a API recentemente, mas não está registado no servidor de sinal (UDP :21116). Reinicie o cliente após a migração do servidor."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Chaves do servidor",
|
||||
@@ -725,7 +726,8 @@
|
||||
"stopped": "Parado",
|
||||
"unknown": "Desconhecido",
|
||||
"checking": "A verificar...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Sem sinal"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Atualizar",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "Grupul de dispozitive a fost șters",
|
||||
"folder_group_readonly": "Grupurile susținute de foldere sunt gestionate din dosare.",
|
||||
"manage_user_groups": "Gestionați grupurile de utilizatori",
|
||||
"restore_failed": "Dispozitiv konnte nicht wiederhergestellt werden"
|
||||
"restore_failed": "Dispozitiv konnte nicht wiederhergestellt werden",
|
||||
"no_signal_tooltip": "Clientul a contactat API-ul recent, dar nu este înregistrat pe serverul de semnal (UDP :21116). Reporniți clientul după migrarea serverului."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Cheile serverului",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "Se verifică...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Fără semnal"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "Enhetsgruppen har tagits bort",
|
||||
"folder_group_readonly": "Mappstödda grupper hanteras från mappar.",
|
||||
"manage_user_groups": "Hantera användargrupper",
|
||||
"restore_failed": "Enhet konnte nicht wiederhergestellt werden"
|
||||
"restore_failed": "Enhet konnte nicht wiederhergestellt werden",
|
||||
"no_signal_tooltip": "Klienten kontaktade API:et nyligen men är inte registrerad på signalservern (UDP :21116). Starta om klienten efter servermigrering."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Servernycklar",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "Kontrollerar...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Ingen signal"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "ลบกลุ่มอุปกรณ์แล้ว",
|
||||
"folder_group_readonly": "กลุ่มที่สำรองโฟลเดอร์ได้รับการจัดการจากโฟลเดอร์",
|
||||
"manage_user_groups": "จัดการกลุ่มผู้ใช้",
|
||||
"restore_failed": "กู้คืนอุปกรณ์ไม่สำเร็จ"
|
||||
"restore_failed": "กู้คืนอุปกรณ์ไม่สำเร็จ",
|
||||
"no_signal_tooltip": "ไคลเอนต์ติดต่อ API เมื่อเร็ว ๆ นี้ แต่ยังไม่ได้ลงทะเบียนบนเซิร์ฟเวอร์สัญญาณ (UDP :21116) โปรดรีสตาร์ทไคลเอนต์หลังย้ายเซิร์ฟเวอร์"
|
||||
},
|
||||
"keys": {
|
||||
"title": "คีย์เซิร์ฟเวอร์",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "กำลังตรวจสอบ...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "ไม่มีสัญญาณ"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "Cihaz grubu silindi",
|
||||
"folder_group_readonly": "Klasör destekli gruplar klasörlerden yönetilir.",
|
||||
"manage_user_groups": "Kullanıcı gruplarını yönet",
|
||||
"restore_failed": "Cihaz geri yükleme başarısız"
|
||||
"restore_failed": "Cihaz geri yükleme başarısız",
|
||||
"no_signal_tooltip": "İstemci yakın zamanda API ile iletişim kurdu ancak sinyal sunucusuna (UDP :21116) kayıtlı değil. Sunucu geçişinden sonra istemciyi yeniden başlatın."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Sunucu Anahtarları",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "Kontrol ediliyor...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Sinyal yok"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "Групу пристроїв видалено",
|
||||
"folder_group_readonly": "Групи з папками керуються з папок.",
|
||||
"manage_user_groups": "Керуйте групами користувачів",
|
||||
"restore_failed": "Пристрій konnte nicht wiederhergestellt werden"
|
||||
"restore_failed": "Пристрій konnte nicht wiederhergestellt werden",
|
||||
"no_signal_tooltip": "Клієнт нещодавно звернувся до API, але не зареєстрований на сигнальному сервері (UDP :21116). Перезапустіть клієнт після міграції сервера."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Ключі сервера",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "Перевірка...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Немає сигналу"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"group_deleted": "Đã xóa nhóm thiết bị",
|
||||
"folder_group_readonly": "Các nhóm được hỗ trợ bởi thư mục được quản lý từ các thư mục.",
|
||||
"manage_user_groups": "Quản lý nhóm người dùng",
|
||||
"restore_failed": "Không thể khôi phục thiết bị"
|
||||
"restore_failed": "Không thể khôi phục thiết bị",
|
||||
"no_signal_tooltip": "Máy khách vừa liên hệ API nhưng chưa đăng ký trên máy chủ tín hiệu (UDP :21116). Khởi động lại máy khách sau khi di chuyển máy chủ."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Khóa máy chủ",
|
||||
@@ -731,7 +732,8 @@
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown",
|
||||
"checking": "Đang kiểm tra...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "Không có tín hiệu"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "裝置群組已刪除",
|
||||
"folder_group_readonly": "資料夾關聯的群組由資料夾管理。",
|
||||
"manage_user_groups": "管理使用者群組",
|
||||
"restore_failed": "還原裝置失敗"
|
||||
"restore_failed": "還原裝置失敗",
|
||||
"no_signal_tooltip": "用戶端最近聯絡了 API,但未在訊號伺服器(UDP :21116)上註冊。伺服器遷移後請重新啟動用戶端。"
|
||||
},
|
||||
"keys": {
|
||||
"title": "伺服器金鑰",
|
||||
@@ -735,7 +736,8 @@
|
||||
"stopped": "已停止",
|
||||
"unknown": "未知",
|
||||
"checking": "檢查中...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "無信號"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "重新整理",
|
||||
|
||||
@@ -303,7 +303,8 @@
|
||||
"group_deleted": "设备分组已删除",
|
||||
"folder_group_readonly": "文件夹关联的分组由文件夹管理。",
|
||||
"manage_user_groups": "管理用户组",
|
||||
"restore_failed": "恢复设备失败"
|
||||
"restore_failed": "恢复设备失败",
|
||||
"no_signal_tooltip": "客户端最近联系了 API,但未在信号服务器(UDP :21116)上注册。服务器迁移后请重启客户端。"
|
||||
},
|
||||
"keys": {
|
||||
"title": "服务器密钥",
|
||||
@@ -725,7 +726,8 @@
|
||||
"stopped": "已停止",
|
||||
"unknown": "未知",
|
||||
"checking": "检查中...",
|
||||
"label": "Status"
|
||||
"label": "Status",
|
||||
"no_signal": "无信号"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "刷新",
|
||||
|
||||
@@ -177,6 +177,15 @@
|
||||
background: var(--text-tertiary, #6e7681);
|
||||
}
|
||||
|
||||
.device-panel-status-badge.no_signal {
|
||||
background: var(--accent-yellow-muted, rgba(210, 153, 34, 0.15));
|
||||
color: var(--accent-yellow, #d29922);
|
||||
}
|
||||
|
||||
.device-panel-status-badge.no_signal .status-dot {
|
||||
background: var(--accent-yellow, #d29922);
|
||||
}
|
||||
|
||||
.device-panel-status-badge.banned {
|
||||
background: var(--accent-red-muted, rgba(248, 81, 73, 0.15));
|
||||
color: var(--accent-red, #f85149);
|
||||
|
||||
@@ -638,6 +638,10 @@ td.column-hidden {
|
||||
background: var(--text-tertiary, #6e7681);
|
||||
}
|
||||
|
||||
.device-status-dot.no_signal {
|
||||
background: var(--accent-yellow, #d29922);
|
||||
}
|
||||
|
||||
.device-status-dot.banned {
|
||||
background: var(--accent-red, #f85149);
|
||||
}
|
||||
@@ -717,6 +721,15 @@ td.column-hidden {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.status-badge.no_signal {
|
||||
background: var(--accent-yellow-muted, rgba(210, 153, 34, 0.15));
|
||||
color: var(--accent-yellow, #d29922);
|
||||
}
|
||||
|
||||
.status-badge.no_signal .status-dot {
|
||||
background: var(--accent-yellow, #d29922);
|
||||
}
|
||||
|
||||
.status-badge.banned {
|
||||
background: var(--accent-red-muted);
|
||||
color: var(--accent-red);
|
||||
|
||||
@@ -143,15 +143,35 @@ const DeviceDetail = (function () {
|
||||
|
||||
// ── Header ──
|
||||
|
||||
function _resolveDeviceStatus(d) {
|
||||
if (d.banned) {
|
||||
return { className: 'banned', label: _('status.banned'), title: '' };
|
||||
}
|
||||
if (d.online) {
|
||||
const tier = (d.status_tier || 'online').toLowerCase();
|
||||
if (tier === 'degraded' || tier === 'critical') {
|
||||
return { className: tier, label: _('status.' + tier), title: '' };
|
||||
}
|
||||
return { className: 'online', label: _('status.online'), title: '' };
|
||||
}
|
||||
if (d.no_signal) {
|
||||
return {
|
||||
className: 'no_signal',
|
||||
label: _('status.no_signal'),
|
||||
title: _('devices.no_signal_tooltip')
|
||||
};
|
||||
}
|
||||
return { className: 'offline', label: _('status.offline'), title: '' };
|
||||
}
|
||||
|
||||
function _headerHTML() {
|
||||
const d = device;
|
||||
const platformLabel = d.platform || d.os || '-';
|
||||
const platformIcon = Utils.getPlatformIcon(d.platform || d.os);
|
||||
const statusRaw = (d.status_tier || (d.online ? 'online' : 'offline')).toLowerCase();
|
||||
const statusClass = d.banned ? 'banned' : statusRaw;
|
||||
const statusLabel = d.banned
|
||||
? _('status.banned')
|
||||
: (_('status.' + statusRaw));
|
||||
const statusInfo = _resolveDeviceStatus(d);
|
||||
const statusClass = statusInfo.className;
|
||||
const statusLabel = statusInfo.label;
|
||||
const statusTitle = statusInfo.title;
|
||||
|
||||
return `
|
||||
<div class="device-panel-header">
|
||||
@@ -174,7 +194,7 @@ const DeviceDetail = (function () {
|
||||
</button>
|
||||
</div>
|
||||
<div class="device-panel-status-bar">
|
||||
<span class="device-panel-status-badge ${statusClass}">
|
||||
<span class="device-panel-status-badge ${statusClass}"${statusTitle ? ` title="${Utils.escapeHtml(statusTitle)}"` : ''}>
|
||||
<span class="status-dot"></span>${statusLabel}
|
||||
</span>
|
||||
<span class="device-panel-platform-badge">
|
||||
@@ -1301,15 +1321,16 @@ const DeviceDetail = (function () {
|
||||
// Only re-render header status (minimal update, avoid losing form state)
|
||||
const statusBar = overlayEl.querySelector('.device-panel-status-bar');
|
||||
if (statusBar) {
|
||||
const statusRaw2 = (device.status_tier || (device.online ? 'online' : 'offline')).toLowerCase();
|
||||
const statusClass = device.banned ? 'banned' : statusRaw2;
|
||||
const statusLabel = device.banned
|
||||
? _('status.banned')
|
||||
: (_('status.' + statusRaw2));
|
||||
const statusInfo = _resolveDeviceStatus(device);
|
||||
const badge = statusBar.querySelector('.device-panel-status-badge');
|
||||
if (badge) {
|
||||
badge.className = 'device-panel-status-badge ' + statusClass;
|
||||
badge.innerHTML = '<span class="status-dot"></span>' + statusLabel;
|
||||
badge.className = 'device-panel-status-badge ' + statusInfo.className;
|
||||
if (statusInfo.title) {
|
||||
badge.title = statusInfo.title;
|
||||
} else {
|
||||
badge.removeAttribute('title');
|
||||
}
|
||||
badge.innerHTML = '<span class="status-dot"></span>' + statusInfo.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,23 @@
|
||||
return '#808080';
|
||||
}
|
||||
|
||||
function deviceStatusInfo(d) {
|
||||
if (d.banned) {
|
||||
return { className: 'banned', label: _('status.banned'), title: '' };
|
||||
}
|
||||
if (d.online) {
|
||||
return { className: 'online', label: _('status.online'), title: '' };
|
||||
}
|
||||
if (d.no_signal) {
|
||||
return {
|
||||
className: 'no_signal',
|
||||
label: _('status.no_signal'),
|
||||
title: _('devices.no_signal_tooltip')
|
||||
};
|
||||
}
|
||||
return { className: 'offline', label: _('status.offline'), title: '' };
|
||||
}
|
||||
|
||||
// Map device_type to Material Icons
|
||||
function getDeviceTypeIcon(type) {
|
||||
switch ((type || '').toLowerCase()) {
|
||||
@@ -147,7 +164,7 @@
|
||||
if (!row) return;
|
||||
|
||||
const normalizedStatus = String(status || '').toLowerCase();
|
||||
const statusClassName = ['online', 'offline', 'degraded', 'critical'].includes(normalizedStatus)
|
||||
const statusClassName = ['online', 'offline', 'no_signal', 'degraded', 'critical'].includes(normalizedStatus)
|
||||
? normalizedStatus
|
||||
: 'offline';
|
||||
const statusText = _('status.' + statusClassName);
|
||||
@@ -173,6 +190,7 @@
|
||||
dev.live_status = statusClassName;
|
||||
dev.live_online = statusClassName === 'online';
|
||||
dev.online = statusClassName === 'online';
|
||||
dev.no_signal = statusClassName === 'no_signal';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,12 +490,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const statusClass = (d) => d.banned ? 'banned' : d.online ? 'online' : 'offline';
|
||||
const statusLabel = (d) => d.banned ? _('status.banned') : d.online ? _('status.online') : _('status.offline');
|
||||
|
||||
tableBody.innerHTML = pageDevices.map(device => {
|
||||
const eid = Utils.escapeHtml(device.id);
|
||||
const sc = statusClass(device);
|
||||
const statusInfo = deviceStatusInfo(device);
|
||||
const sc = statusInfo.className;
|
||||
return `
|
||||
<tr data-id="${eid}" class="${device.banned ? 'banned-row' : ''}" draggable="true">
|
||||
<td data-column="id">
|
||||
@@ -506,7 +522,7 @@
|
||||
<span class="last-seen-text" title="${Utils.formatDate(device.last_online)}">${Utils.formatRelativeTime(device.last_online)}</span>
|
||||
</td>
|
||||
<td data-column="status">
|
||||
<span class="status-badge ${sc}"><span class="status-dot"></span>${statusLabel(device)}</span>
|
||||
<span class="status-badge ${sc}"${statusInfo.title ? ` title="${Utils.escapeHtml(statusInfo.title)}"` : ''}><span class="status-dot"></span>${statusInfo.label}</span>
|
||||
</td>
|
||||
<td data-column="tags">${renderTagsCell(device)}</td>
|
||||
<td data-column="actions">
|
||||
@@ -905,7 +921,7 @@
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">${_('status.label')}:</span>
|
||||
<span class="detail-value">${device.banned ? _('status.banned') : device.online ? _('status.online') : _('status.offline')}</span>
|
||||
<span class="detail-value">${deviceStatusInfo(device).label}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">${_('devices.last_online')}:</span>
|
||||
|
||||
@@ -462,9 +462,21 @@ async function syncOnlineStatus(/* db */) {
|
||||
* online (bool), banned (bool), created_at, last_online, ban_reason,
|
||||
* folder_id, tags[], status_tier, uuid, disabled, os, version
|
||||
*/
|
||||
const NO_SIGNAL_THRESHOLD_MS = 5 * 60 * 1000;
|
||||
|
||||
function isRecentLastOnline(lastOnline) {
|
||||
if (!lastOnline) return false;
|
||||
const t = new Date(lastOnline).getTime();
|
||||
return Number.isFinite(t) && (Date.now() - t) < NO_SIGNAL_THRESHOLD_MS;
|
||||
}
|
||||
|
||||
function normalisePeer(peer) {
|
||||
if (!peer) return peer;
|
||||
|
||||
const liveOnline = !!(peer.live_online);
|
||||
const banned = !!(peer.banned);
|
||||
const lastOnline = peer.last_online || '';
|
||||
|
||||
// Parse tags: Go server sends comma-separated string or JSON array
|
||||
let tags = [];
|
||||
if (Array.isArray(peer.tags)) {
|
||||
@@ -488,10 +500,11 @@ function normalisePeer(peer) {
|
||||
version: peer.version || '',
|
||||
ip: peer.ip || '',
|
||||
note: peer.note || '',
|
||||
online: !!(peer.live_online),
|
||||
banned: !!(peer.banned),
|
||||
online: liveOnline,
|
||||
banned,
|
||||
no_signal: !liveOnline && !banned && isRecentLastOnline(lastOnline),
|
||||
created_at: peer.created_at || '',
|
||||
last_online: peer.last_online || '',
|
||||
last_online: lastOnline,
|
||||
ban_reason: peer.ban_reason || '',
|
||||
banned_at: peer.banned_at || null,
|
||||
folder_id: peer.folder_id || null,
|
||||
|
||||
Reference in New Issue
Block a user