diff --git a/betterdesk-server/signal/handler.go b/betterdesk-server/signal/handler.go index e7a6442d..232a4359 100644 --- a/betterdesk-server/signal/handler.go +++ b/betterdesk-server/signal/handler.go @@ -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. diff --git a/betterdesk-server/signal/handler_test.go b/betterdesk-server/signal/handler_test.go index b65f0382..76b071de 100644 --- a/betterdesk-server/signal/handler_test.go +++ b/betterdesk-server/signal/handler_test.go @@ -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") + } +} diff --git a/betterdesk-server/signal/ws.go b/betterdesk-server/signal/ws.go index 1fc942f7..3cbcd84e 100644 --- a/betterdesk-server/signal/ws.go +++ b/betterdesk-server/signal/ws.go @@ -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{ diff --git a/docs/troubleshooting/SERVER_MIGRATION.md b/docs/troubleshooting/SERVER_MIGRATION.md new file mode 100644 index 00000000..685eca63 --- /dev/null +++ b/docs/troubleshooting/SERVER_MIGRATION.md @@ -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`) diff --git a/web-nodejs/lang/ar.json b/web-nodejs/lang/ar.json index 52e16466..7804c7b9 100644 --- a/web-nodejs/lang/ar.json +++ b/web-nodejs/lang/ar.json @@ -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", diff --git a/web-nodejs/lang/cs.json b/web-nodejs/lang/cs.json index 145f992a..eb14b9a3 100644 --- a/web-nodejs/lang/cs.json +++ b/web-nodejs/lang/cs.json @@ -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", diff --git a/web-nodejs/lang/da.json b/web-nodejs/lang/da.json index f13f01e9..4d47c210 100644 --- a/web-nodejs/lang/da.json +++ b/web-nodejs/lang/da.json @@ -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", diff --git a/web-nodejs/lang/de.json b/web-nodejs/lang/de.json index 57598f14..c1fba024 100644 --- a/web-nodejs/lang/de.json +++ b/web-nodejs/lang/de.json @@ -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", diff --git a/web-nodejs/lang/en.json b/web-nodejs/lang/en.json index 5891ed32..0e0ae891 100644 --- a/web-nodejs/lang/en.json +++ b/web-nodejs/lang/en.json @@ -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", diff --git a/web-nodejs/lang/es.json b/web-nodejs/lang/es.json index 03a04ae2..ba89ce51 100644 --- a/web-nodejs/lang/es.json +++ b/web-nodejs/lang/es.json @@ -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", diff --git a/web-nodejs/lang/fi.json b/web-nodejs/lang/fi.json index f7f93a3c..81efe0a6 100644 --- a/web-nodejs/lang/fi.json +++ b/web-nodejs/lang/fi.json @@ -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", diff --git a/web-nodejs/lang/fr.json b/web-nodejs/lang/fr.json index f61d8213..0cabecde 100644 --- a/web-nodejs/lang/fr.json +++ b/web-nodejs/lang/fr.json @@ -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", diff --git a/web-nodejs/lang/hi.json b/web-nodejs/lang/hi.json index 9612e8ac..cd12cff8 100644 --- a/web-nodejs/lang/hi.json +++ b/web-nodejs/lang/hi.json @@ -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", diff --git a/web-nodejs/lang/hu.json b/web-nodejs/lang/hu.json index 4e27e672..dec15e02 100644 --- a/web-nodejs/lang/hu.json +++ b/web-nodejs/lang/hu.json @@ -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", diff --git a/web-nodejs/lang/id.json b/web-nodejs/lang/id.json index 4ae3f574..f2fbbbfa 100644 --- a/web-nodejs/lang/id.json +++ b/web-nodejs/lang/id.json @@ -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", diff --git a/web-nodejs/lang/it.json b/web-nodejs/lang/it.json index ef9e6fb9..698af435 100644 --- a/web-nodejs/lang/it.json +++ b/web-nodejs/lang/it.json @@ -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", diff --git a/web-nodejs/lang/ja.json b/web-nodejs/lang/ja.json index d3337f1c..eaf9891d 100644 --- a/web-nodejs/lang/ja.json +++ b/web-nodejs/lang/ja.json @@ -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", diff --git a/web-nodejs/lang/ko.json b/web-nodejs/lang/ko.json index 1854dcb4..f5ee0bd9 100644 --- a/web-nodejs/lang/ko.json +++ b/web-nodejs/lang/ko.json @@ -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", diff --git a/web-nodejs/lang/nb.json b/web-nodejs/lang/nb.json index 9a37e4f5..c233b41d 100644 --- a/web-nodejs/lang/nb.json +++ b/web-nodejs/lang/nb.json @@ -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", diff --git a/web-nodejs/lang/nl.json b/web-nodejs/lang/nl.json index ead69160..d7db86e4 100644 --- a/web-nodejs/lang/nl.json +++ b/web-nodejs/lang/nl.json @@ -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", diff --git a/web-nodejs/lang/pl.json b/web-nodejs/lang/pl.json index 403ecb42..f8db7e66 100644 --- a/web-nodejs/lang/pl.json +++ b/web-nodejs/lang/pl.json @@ -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ż", diff --git a/web-nodejs/lang/pt.json b/web-nodejs/lang/pt.json index 797e6523..651745a4 100644 --- a/web-nodejs/lang/pt.json +++ b/web-nodejs/lang/pt.json @@ -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", diff --git a/web-nodejs/lang/ro.json b/web-nodejs/lang/ro.json index f2915747..24175ca6 100644 --- a/web-nodejs/lang/ro.json +++ b/web-nodejs/lang/ro.json @@ -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", diff --git a/web-nodejs/lang/sv.json b/web-nodejs/lang/sv.json index 2284ab2f..02f95624 100644 --- a/web-nodejs/lang/sv.json +++ b/web-nodejs/lang/sv.json @@ -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", diff --git a/web-nodejs/lang/th.json b/web-nodejs/lang/th.json index 37789072..ae4b1364 100644 --- a/web-nodejs/lang/th.json +++ b/web-nodejs/lang/th.json @@ -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", diff --git a/web-nodejs/lang/tr.json b/web-nodejs/lang/tr.json index a457584e..961c6814 100644 --- a/web-nodejs/lang/tr.json +++ b/web-nodejs/lang/tr.json @@ -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", diff --git a/web-nodejs/lang/uk.json b/web-nodejs/lang/uk.json index cf9c2ee5..2310db99 100644 --- a/web-nodejs/lang/uk.json +++ b/web-nodejs/lang/uk.json @@ -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", diff --git a/web-nodejs/lang/vi.json b/web-nodejs/lang/vi.json index 17db31dd..3dd934f0 100644 --- a/web-nodejs/lang/vi.json +++ b/web-nodejs/lang/vi.json @@ -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", diff --git a/web-nodejs/lang/zh-TW.json b/web-nodejs/lang/zh-TW.json index 98656a41..f0be7144 100644 --- a/web-nodejs/lang/zh-TW.json +++ b/web-nodejs/lang/zh-TW.json @@ -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": "重新整理", diff --git a/web-nodejs/lang/zh.json b/web-nodejs/lang/zh.json index f8c7d6df..d1ebf77a 100644 --- a/web-nodejs/lang/zh.json +++ b/web-nodejs/lang/zh.json @@ -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": "刷新", diff --git a/web-nodejs/public/css/device-detail.css b/web-nodejs/public/css/device-detail.css index 400bbf61..9a2672a0 100644 --- a/web-nodejs/public/css/device-detail.css +++ b/web-nodejs/public/css/device-detail.css @@ -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); diff --git a/web-nodejs/public/css/devices.css b/web-nodejs/public/css/devices.css index 614e033d..96690ead 100644 --- a/web-nodejs/public/css/devices.css +++ b/web-nodejs/public/css/devices.css @@ -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); diff --git a/web-nodejs/public/js/deviceDetail.js b/web-nodejs/public/js/deviceDetail.js index 0b17cba3..2aa0cb29 100644 --- a/web-nodejs/public/js/deviceDetail.js +++ b/web-nodejs/public/js/deviceDetail.js @@ -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 `