-
+
{{ session.name || $t("rnsh.unnamed_session") }}
{{ statusLabel(session) }}
-
+
{{ session.mode === "listen" ? $t("rnsh.listen_mode") : session.destination || "-" }}
-
+
{{ $t("rnsh.no_sessions") }}
-
-
-
-
- {{ selectedSession?.name || $t("rnsh.session_output") }}
-
-
- {{ selectedSession?.last_command || $t("rnsh.no_command_yet") }}
-
-
-
-
-
-
-
-
-
-
-
- {{ selectedOutput }}
-
-
-
+
+
+
diff --git a/meshchatx/src/frontend/js/relayHostModalClasses.js b/meshchatx/src/frontend/js/relayHostModalClasses.js
new file mode 100644
index 00000000..a4fca4ac
--- /dev/null
+++ b/meshchatx/src/frontend/js/relayHostModalClasses.js
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: 0BSD
+
+/** Create-hub dialog overlay and panel (compact modal). */
+
+export const RELAY_HOST_MODAL_OVERLAY =
+ "fixed inset-0 z-[60] flex flex-col overflow-hidden bg-black/50 backdrop-blur-[2px] dark:bg-black/70 p-3 sm:items-center sm:justify-center sm:p-4 pt-[max(0.75rem,env(safe-area-inset-top))] pb-[max(0.75rem,env(safe-area-inset-bottom))]";
+
+export const RELAY_HOST_MODAL_PANEL_COMPACT =
+ "mx-auto flex w-full max-w-md shrink-0 flex-col overflow-hidden rounded-2xl border border-sem-border-card bg-sem-canvas p-5 text-sem-fg shadow-2xl ring-1 ring-inset ring-sem-border/25";
diff --git a/meshchatx/src/frontend/js/relayHostModerationClasses.js b/meshchatx/src/frontend/js/relayHostModerationClasses.js
new file mode 100644
index 00000000..250f7f85
--- /dev/null
+++ b/meshchatx/src/frontend/js/relayHostModerationClasses.js
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: 0BSD
+
+/** Relay Chat host moderation page layout (full-height, no modal shell). */
+
+export const RELAY_HOST_PAGE_HEADER =
+ "flex shrink-0 flex-wrap items-center gap-2 border-b border-sem-border bg-sem-canvas px-3 py-2.5 sm:px-4";
+
+export const RELAY_HOST_PAGE_TABS = "flex shrink-0 gap-1.5 border-b border-sem-border bg-sem-canvas px-3 py-2 sm:px-4";
+
+export const RELAY_HOST_PAGE_TAB =
+ "inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors";
+
+export const RELAY_HOST_PAGE_TAB_ACTIVE = "bg-sem-surface-raised text-sem-fg shadow-sm ring-1 ring-sem-border";
+
+export const RELAY_HOST_PAGE_TAB_IDLE = "text-sem-fg-muted hover:bg-sem-surface-raised/60 hover:text-sem-fg";
+
+export const RELAY_HOST_PAGE_BODY = "flex min-h-0 flex-1 flex-col overflow-hidden lg:flex-row";
+
+export const RELAY_HOST_PAGE_LIST =
+ "flex min-h-0 flex-col border-sem-border lg:w-80 lg:shrink-0 lg:border-r lg:bg-sem-surface-muted/20";
+
+export const RELAY_HOST_PAGE_DETAIL = "flex min-h-0 min-w-0 flex-1 flex-col bg-sem-canvas";
+
+export const RELAY_HOST_LIST_ITEM = "rounded-xl border border-sem-border bg-sem-surface-raised/50 transition-colors";
+
+export const RELAY_HOST_LIST_ITEM_SELECTED = "border-sem-accent bg-sem-accent/15";
+
+export const RELAY_HOST_LIST_ITEM_IDLE = "hover:bg-sem-surface-raised";
+
+export const RELAY_HOST_DETAIL_HEADER =
+ "shrink-0 border-b border-sem-border bg-sem-surface-muted/30 px-3 py-2.5 sm:px-4";
+
+export const RELAY_HOST_MESSAGE =
+ "rounded-lg border border-sem-border bg-sem-surface-raised px-3 py-2 text-sm text-sem-fg";
+
+export const RELAY_HOST_ICON_BTN =
+ "rounded-lg p-1.5 text-sem-fg-muted transition-colors hover:bg-sem-surface-raised hover:text-sem-fg";
diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index 2eb70d88..6f07fbb0 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -2162,7 +2162,14 @@
"failed_to_stop_session": "Failed to stop RNSH session",
"failed_to_remove_session": "Failed to remove RNSH session",
"failed_to_clear_output": "Failed to clear RNSH output",
- "failed_to_send_input": "Failed to send RNSH input"
+ "failed_to_send_input": "Failed to send RNSH input",
+ "tab_sessions_short": "Sessions",
+ "tab_connect_short": "Connect",
+ "tab_listen_short": "Listen",
+ "fullscreen": "Full screen",
+ "exit_fullscreen": "Exit full screen",
+ "show_sessions": "Sessions",
+ "hide_sessions": "Back to terminal"
},
"rnprobe": {
"network_diagnostics": "Netzwerkdiagnose",
@@ -2832,6 +2839,9 @@
"copy_hash": "Ziel-Hash kopieren",
"hash_copied": "Ziel-Hash kopiert",
"host_start": "Starten",
+ "host_status_running": "Läuft",
+ "host_status_stopped": "Gestoppt",
+ "host_hub_not_running": "Starten Sie den Hub, bevor Sie Räume oder Mitglieder verwalten.",
"host_stop": "Stoppen",
"host_announce": "Jetzt ankündigen",
"host_announced": "Angekündigt",
@@ -2853,8 +2863,8 @@
"host_room_created": "Raum erstellt",
"host_delete_room_confirm": "Diesen Raum löschen?",
"host_room_deleted": "Raum gelöscht",
- "host_members_all_title": "Verbundene Clients — {hub}",
- "host_members_room_title": "#{room} — {hub}",
+ "host_members_all_title": "Verbundene Clients: {hub}",
+ "host_members_room_title": "#{room}: {hub}",
"host_member_rooms": "Räume",
"hub_icon": "Hub-Symbol",
"hub_icon_choose": "Symbol wählen",
@@ -2883,7 +2893,12 @@
"show_members": "Mitglieder anzeigen",
"hide_members": "Mitglieder ausblenden",
"discovery_refreshed": "Entdeckung aktualisiert",
- "host_members_search": "Mitglieder nach Name, Hash oder Raum suchen...",
+ "host_members_search": "Mitglieder suchen...",
+ "host_rooms_search": "Räume suchen...",
+ "host_rooms_search_empty": "Keine Räume passen zur Suche.",
+ "host_no_rooms": "Noch keine Räume.",
+ "host_moderation_uptime": "{time} Laufzeit",
+ "host_moderation_members": "{count} Mitglieder",
"host_members_select": "Mitglied auswählen, um Nachrichten anzuzeigen und zu moderieren",
"host_no_messages": "Noch keine Nachrichten von diesem Benutzer im Protokoll",
"host_ban_hub": "Vom Hub sperren",
@@ -2895,7 +2910,14 @@
"host_ban_confirm": "{name} von diesem Hub sperren?",
"host_room_ban_confirm": "{name} aus #{room} sperren?",
"host_manage_rooms": "Räume verwalten",
- "host_rooms_modal_title": "Räume — {hub}",
+ "host_moderate": "Hub moderieren",
+ "host_moderation_title": "Moderation",
+ "host_moderation_title_hub": "Moderation: {hub}",
+ "host_moderation_title_room": "Moderation: {hub} (#{room})",
+ "host_moderation_tab_rooms": "Räume",
+ "host_moderation_tab_members": "Mitglieder",
+ "host_moderation_hub_missing": "Hub nicht gefunden.",
+ "host_rooms_modal_title": "Räume: {hub}",
"host_rooms_select": "Raum auswählen, um Aktivität anzuzeigen",
"host_room_activity": "Letzte Aktivität",
"host_no_activity": "Keine kürzliche Aktivität in diesem Raum",
@@ -2926,5 +2948,22 @@
"search_no_results": "Keine Nachrichten entsprechen der Suche",
"popout_channel": "In neuem Fenster öffnen",
"new_message_toast": "Neue Nachricht in #{room}"
+ },
+ "android_storage": {
+ "setup_title": "Choose where MeshChatX stores data",
+ "setup_desc": "File-manager storage is recommended. You can browse and back up your identity and messages under Android/data/com.meshchatx/files/. The app restarts when you change this during setup.",
+ "setup_external_title": "File-manager storage (recommended)",
+ "setup_external_desc": "Android/data/com.meshchatx/files/meshchatx — visible in many file managers.",
+ "setup_internal_title": "Private app storage",
+ "setup_internal_desc": "Hidden from file managers; only this app can access it.",
+ "setup_continue": "Continue",
+ "upgrade_title": "Move data to file-manager storage?",
+ "upgrade_desc": "Your MeshChatX data is in private app storage. Copy it to the file-manager folder so you can browse and back up files, then restart the app.",
+ "upgrade_copy": "Copy and restart",
+ "upgrade_stay_internal": "Stay on private storage",
+ "working": "Preparing…",
+ "copy_restart_hint": "Copy scheduled. Closing the app — open MeshChatX again to finish.",
+ "restart_to_apply": "Restart the app to apply your storage choice.",
+ "failed": "Could not update storage location."
}
}
diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index 95d05f04..0d21fed7 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -2210,6 +2210,9 @@
"copy_hash": "Copy destination hash",
"hash_copied": "Destination hash copied",
"host_start": "Start",
+ "host_status_running": "Running",
+ "host_status_stopped": "Stopped",
+ "host_hub_not_running": "Start the hub before managing rooms or members.",
"host_stop": "Stop",
"host_announce": "Announce now",
"host_announced": "Announced",
@@ -2231,10 +2234,15 @@
"host_room_created": "Room created",
"host_delete_room_confirm": "Delete this room?",
"host_room_deleted": "Room deleted",
- "host_members_all_title": "Connected clients — {hub}",
- "host_members_room_title": "#{room} — {hub}",
+ "host_members_all_title": "Connected clients: {hub}",
+ "host_members_room_title": "#{room}: {hub}",
"host_member_rooms": "Rooms",
- "host_members_search": "Search members by name, hash, or room...",
+ "host_members_search": "Search members...",
+ "host_rooms_search": "Search rooms...",
+ "host_rooms_search_empty": "No rooms match your search.",
+ "host_no_rooms": "No rooms yet.",
+ "host_moderation_uptime": "{time} uptime",
+ "host_moderation_members": "{count} members",
"host_members_select": "Select a member to view messages and moderate",
"host_no_messages": "No messages from this user in the log yet",
"host_ban_hub": "Ban from hub",
@@ -2246,7 +2254,14 @@
"host_ban_confirm": "Ban {name} from this hub?",
"host_room_ban_confirm": "Ban {name} from #{room}?",
"host_manage_rooms": "Manage rooms",
- "host_rooms_modal_title": "Rooms — {hub}",
+ "host_moderate": "Moderate hub",
+ "host_moderation_title": "Moderation",
+ "host_moderation_title_hub": "Moderation: {hub}",
+ "host_moderation_title_room": "Moderation: {hub} (#{room})",
+ "host_moderation_tab_rooms": "Rooms",
+ "host_moderation_tab_members": "Members",
+ "host_moderation_hub_missing": "Hub not found.",
+ "host_rooms_modal_title": "Rooms: {hub}",
"host_rooms_select": "Select a room to view activity",
"host_room_activity": "Recent activity",
"host_no_activity": "No recent activity in this room",
@@ -2361,8 +2376,15 @@
"usage_title": "Usage",
"usage_hint": "Create connect or listen sessions, then manage them on the Sessions tab. Output streams live while MeshChatX is running.",
"tab_sessions": "Sessions",
+ "tab_sessions_short": "Sessions",
"tab_connect": "Connect",
+ "tab_connect_short": "Connect",
"tab_listen": "Listen",
+ "tab_listen_short": "Listen",
+ "fullscreen": "Full screen",
+ "exit_fullscreen": "Exit full screen",
+ "show_sessions": "Sessions",
+ "hide_sessions": "Back to terminal",
"sessions": "Sessions",
"refresh": "Refresh",
"name": "Session name",
@@ -2721,6 +2743,23 @@
"contact_updated": "Contact updated",
"failed_update_contact": "Failed to update contact"
},
+ "android_storage": {
+ "setup_title": "Choose where MeshChatX stores data",
+ "setup_desc": "File-manager storage is recommended. You can browse and back up your identity and messages under Android/data/com.meshchatx/files/. The app restarts when you change this during setup.",
+ "setup_external_title": "File-manager storage (recommended)",
+ "setup_external_desc": "Android/data/com.meshchatx/files/meshchatx — visible in many file managers.",
+ "setup_internal_title": "Private app storage",
+ "setup_internal_desc": "Hidden from file managers; only this app can access it.",
+ "setup_continue": "Continue",
+ "upgrade_title": "Move data to file-manager storage?",
+ "upgrade_desc": "Your MeshChatX data is in private app storage. Copy it to the file-manager folder so you can browse and back up files, then restart the app.",
+ "upgrade_copy": "Copy and restart",
+ "upgrade_stay_internal": "Stay on private storage",
+ "working": "Preparing…",
+ "copy_restart_hint": "Copy scheduled. Closing the app — open MeshChatX again to finish.",
+ "restart_to_apply": "Restart the app to apply your storage choice.",
+ "failed": "Could not update storage location."
+ },
"tutorial": {
"title": "Getting Started",
"welcome": "Welcome to",
diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index 22807184..4eb3bc51 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -2251,7 +2251,14 @@
"failed_to_stop_session": "Failed to stop RNSH session",
"failed_to_remove_session": "Failed to remove RNSH session",
"failed_to_clear_output": "Failed to clear RNSH output",
- "failed_to_send_input": "Failed to send RNSH input"
+ "failed_to_send_input": "Failed to send RNSH input",
+ "tab_sessions_short": "Sessions",
+ "tab_connect_short": "Connect",
+ "tab_listen_short": "Listen",
+ "fullscreen": "Full screen",
+ "exit_fullscreen": "Exit full screen",
+ "show_sessions": "Sessions",
+ "hide_sessions": "Back to terminal"
},
"rnprobe": {
"network_diagnostics": "Diagnósticos de red",
@@ -2832,6 +2839,9 @@
"copy_hash": "Copiar hash de destino",
"hash_copied": "Hash de destino copiado",
"host_start": "Iniciar",
+ "host_status_running": "En ejecución",
+ "host_status_stopped": "Detenido",
+ "host_hub_not_running": "Inicie el hub antes de administrar salas o miembros.",
"host_stop": "Detener",
"host_announce": "Anunciar ahora",
"host_announced": "Anunciado",
@@ -2853,8 +2863,8 @@
"host_room_created": "Sala creada",
"host_delete_room_confirm": "¿Eliminar esta sala?",
"host_room_deleted": "Sala eliminada",
- "host_members_all_title": "Clientes conectados — {hub}",
- "host_members_room_title": "#{room} — {hub}",
+ "host_members_all_title": "Clientes conectados: {hub}",
+ "host_members_room_title": "#{room}: {hub}",
"host_member_rooms": "Salas",
"hub_icon": "Icono del hub",
"hub_icon_choose": "Elegir icono",
@@ -2883,7 +2893,12 @@
"show_members": "Mostrar miembros",
"hide_members": "Ocultar miembros",
"discovery_refreshed": "Descubrimiento actualizado",
- "host_members_search": "Buscar miembros por nombre, hash o sala...",
+ "host_members_search": "Buscar miembros...",
+ "host_rooms_search": "Buscar salas...",
+ "host_rooms_search_empty": "Ninguna sala coincide con la búsqueda.",
+ "host_no_rooms": "Aún no hay salas.",
+ "host_moderation_uptime": "{time} en ejecución",
+ "host_moderation_members": "{count} miembros",
"host_members_select": "Selecciona un miembro para ver mensajes y moderar",
"host_no_messages": "Aún no hay mensajes de este usuario en el registro",
"host_ban_hub": "Prohibir en el hub",
@@ -2895,7 +2910,14 @@
"host_ban_confirm": "¿Prohibir a {name} en este hub?",
"host_room_ban_confirm": "¿Prohibir a {name} en #{room}?",
"host_manage_rooms": "Gestionar salas",
- "host_rooms_modal_title": "Salas — {hub}",
+ "host_moderate": "Moderar hub",
+ "host_moderation_title": "Moderación",
+ "host_moderation_title_hub": "Moderación: {hub}",
+ "host_moderation_title_room": "Moderación: {hub} (#{room})",
+ "host_moderation_tab_rooms": "Salas",
+ "host_moderation_tab_members": "Miembros",
+ "host_moderation_hub_missing": "Hub no encontrado.",
+ "host_rooms_modal_title": "Salas: {hub}",
"host_rooms_select": "Selecciona una sala para ver la actividad",
"host_room_activity": "Actividad reciente",
"host_no_activity": "No hay actividad reciente en esta sala",
@@ -2926,5 +2948,22 @@
"search_no_results": "Ningún mensaje coincide con la búsqueda",
"popout_channel": "Abrir en una ventana nueva",
"new_message_toast": "Nuevo mensaje en #{room}"
+ },
+ "android_storage": {
+ "setup_title": "Choose where MeshChatX stores data",
+ "setup_desc": "File-manager storage is recommended. You can browse and back up your identity and messages under Android/data/com.meshchatx/files/. The app restarts when you change this during setup.",
+ "setup_external_title": "File-manager storage (recommended)",
+ "setup_external_desc": "Android/data/com.meshchatx/files/meshchatx — visible in many file managers.",
+ "setup_internal_title": "Private app storage",
+ "setup_internal_desc": "Hidden from file managers; only this app can access it.",
+ "setup_continue": "Continue",
+ "upgrade_title": "Move data to file-manager storage?",
+ "upgrade_desc": "Your MeshChatX data is in private app storage. Copy it to the file-manager folder so you can browse and back up files, then restart the app.",
+ "upgrade_copy": "Copy and restart",
+ "upgrade_stay_internal": "Stay on private storage",
+ "working": "Preparing…",
+ "copy_restart_hint": "Copy scheduled. Closing the app — open MeshChatX again to finish.",
+ "restart_to_apply": "Restart the app to apply your storage choice.",
+ "failed": "Could not update storage location."
}
}
diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index 976855db..97e67a19 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -2251,7 +2251,14 @@
"failed_to_stop_session": "Failed to stop RNSH session",
"failed_to_remove_session": "Failed to remove RNSH session",
"failed_to_clear_output": "Failed to clear RNSH output",
- "failed_to_send_input": "Failed to send RNSH input"
+ "failed_to_send_input": "Failed to send RNSH input",
+ "tab_sessions_short": "Sessions",
+ "tab_connect_short": "Connect",
+ "tab_listen_short": "Listen",
+ "fullscreen": "Full screen",
+ "exit_fullscreen": "Exit full screen",
+ "show_sessions": "Sessions",
+ "hide_sessions": "Back to terminal"
},
"rnprobe": {
"network_diagnostics": "Diagnostics réseau",
@@ -2832,6 +2839,9 @@
"copy_hash": "Copier le hash de destination",
"hash_copied": "Hash de destination copié",
"host_start": "Démarrer",
+ "host_status_running": "En cours",
+ "host_status_stopped": "Arrêté",
+ "host_hub_not_running": "Démarrez le hub avant de gérer les salons ou les membres.",
"host_stop": "Arrêter",
"host_announce": "Annoncer maintenant",
"host_announced": "Annoncé",
@@ -2853,8 +2863,8 @@
"host_room_created": "Salon créé",
"host_delete_room_confirm": "Supprimer ce salon ?",
"host_room_deleted": "Salon supprimé",
- "host_members_all_title": "Clients connectés — {hub}",
- "host_members_room_title": "#{room} — {hub}",
+ "host_members_all_title": "Clients connectés : {hub}",
+ "host_members_room_title": "#{room} : {hub}",
"host_member_rooms": "Salons",
"hub_icon": "Icône du hub",
"hub_icon_choose": "Choisir une icône",
@@ -2883,7 +2893,12 @@
"show_members": "Afficher les membres",
"hide_members": "Masquer les membres",
"discovery_refreshed": "Découverte actualisée",
- "host_members_search": "Rechercher des membres par nom, hash ou salon...",
+ "host_members_search": "Rechercher des membres...",
+ "host_rooms_search": "Rechercher des salons...",
+ "host_rooms_search_empty": "Aucun salon ne correspond à la recherche.",
+ "host_no_rooms": "Aucun salon pour l'instant.",
+ "host_moderation_uptime": "{time} de disponibilité",
+ "host_moderation_members": "{count} membres",
"host_members_select": "Sélectionnez un membre pour voir les messages et modérer",
"host_no_messages": "Aucun message de cet utilisateur dans le journal pour l'instant",
"host_ban_hub": "Bannir du hub",
@@ -2895,7 +2910,14 @@
"host_ban_confirm": "Bannir {name} de ce hub ?",
"host_room_ban_confirm": "Bannir {name} de #{room} ?",
"host_manage_rooms": "Gérer les salons",
- "host_rooms_modal_title": "Salons — {hub}",
+ "host_moderate": "Modérer le hub",
+ "host_moderation_title": "Modération",
+ "host_moderation_title_hub": "Modération : {hub}",
+ "host_moderation_title_room": "Modération : {hub} (#{room})",
+ "host_moderation_tab_rooms": "Salons",
+ "host_moderation_tab_members": "Membres",
+ "host_moderation_hub_missing": "Hub introuvable.",
+ "host_rooms_modal_title": "Salons : {hub}",
"host_rooms_select": "Sélectionnez un salon pour voir l'activité",
"host_room_activity": "Activité récente",
"host_no_activity": "Aucune activité récente dans ce salon",
@@ -2926,5 +2948,22 @@
"search_no_results": "Aucun message ne correspond à la recherche",
"popout_channel": "Ouvrir dans une nouvelle fenêtre",
"new_message_toast": "Nouveau message dans #{room}"
+ },
+ "android_storage": {
+ "setup_title": "Choose where MeshChatX stores data",
+ "setup_desc": "File-manager storage is recommended. You can browse and back up your identity and messages under Android/data/com.meshchatx/files/. The app restarts when you change this during setup.",
+ "setup_external_title": "File-manager storage (recommended)",
+ "setup_external_desc": "Android/data/com.meshchatx/files/meshchatx — visible in many file managers.",
+ "setup_internal_title": "Private app storage",
+ "setup_internal_desc": "Hidden from file managers; only this app can access it.",
+ "setup_continue": "Continue",
+ "upgrade_title": "Move data to file-manager storage?",
+ "upgrade_desc": "Your MeshChatX data is in private app storage. Copy it to the file-manager folder so you can browse and back up files, then restart the app.",
+ "upgrade_copy": "Copy and restart",
+ "upgrade_stay_internal": "Stay on private storage",
+ "working": "Preparing…",
+ "copy_restart_hint": "Copy scheduled. Closing the app — open MeshChatX again to finish.",
+ "restart_to_apply": "Restart the app to apply your storage choice.",
+ "failed": "Could not update storage location."
}
}
diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index 52e776cd..d38ece22 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -2303,7 +2303,14 @@
"failed_to_stop_session": "Failed to stop RNSH session",
"failed_to_remove_session": "Failed to remove RNSH session",
"failed_to_clear_output": "Failed to clear RNSH output",
- "failed_to_send_input": "Failed to send RNSH input"
+ "failed_to_send_input": "Failed to send RNSH input",
+ "tab_sessions_short": "Sessions",
+ "tab_connect_short": "Connect",
+ "tab_listen_short": "Listen",
+ "fullscreen": "Full screen",
+ "exit_fullscreen": "Exit full screen",
+ "show_sessions": "Sessions",
+ "hide_sessions": "Back to terminal"
},
"rnprobe": {
"network_diagnostics": "Diagnostica di Rete",
@@ -2832,6 +2839,9 @@
"copy_hash": "Copia hash di destinazione",
"hash_copied": "Hash di destinazione copiato",
"host_start": "Avvia",
+ "host_status_running": "In esecuzione",
+ "host_status_stopped": "Arrestato",
+ "host_hub_not_running": "Avvia l'hub prima di gestire stanze o membri.",
"host_stop": "Ferma",
"host_announce": "Annuncia ora",
"host_announced": "Annunciato",
@@ -2853,8 +2863,8 @@
"host_room_created": "Stanza creata",
"host_delete_room_confirm": "Eliminare questa stanza?",
"host_room_deleted": "Stanza eliminata",
- "host_members_all_title": "Client connessi — {hub}",
- "host_members_room_title": "#{room} — {hub}",
+ "host_members_all_title": "Client connessi: {hub}",
+ "host_members_room_title": "#{room}: {hub}",
"host_member_rooms": "Stanze",
"hub_icon": "Icona hub",
"hub_icon_choose": "Scegli icona",
@@ -2883,7 +2893,12 @@
"show_members": "Mostra membri",
"hide_members": "Nascondi membri",
"discovery_refreshed": "Scoperta aggiornata",
- "host_members_search": "Cerca membri per nome, hash o stanza...",
+ "host_members_search": "Cerca membri...",
+ "host_rooms_search": "Cerca stanze...",
+ "host_rooms_search_empty": "Nessuna stanza corrisponde alla ricerca.",
+ "host_no_rooms": "Nessuna stanza ancora.",
+ "host_moderation_uptime": "{time} di attività",
+ "host_moderation_members": "{count} membri",
"host_members_select": "Seleziona un membro per vedere i messaggi e moderare",
"host_no_messages": "Nessun messaggio da questo utente nel registro",
"host_ban_hub": "Banna dall'hub",
@@ -2895,7 +2910,14 @@
"host_ban_confirm": "Bannare {name} da questo hub?",
"host_room_ban_confirm": "Bannare {name} da #{room}?",
"host_manage_rooms": "Gestisci stanze",
- "host_rooms_modal_title": "Stanze — {hub}",
+ "host_moderate": "Modera hub",
+ "host_moderation_title": "Moderazione",
+ "host_moderation_title_hub": "Moderazione: {hub}",
+ "host_moderation_title_room": "Moderazione: {hub} (#{room})",
+ "host_moderation_tab_rooms": "Stanze",
+ "host_moderation_tab_members": "Membri",
+ "host_moderation_hub_missing": "Hub non trovato.",
+ "host_rooms_modal_title": "Stanze: {hub}",
"host_rooms_select": "Seleziona una stanza per vedere l'attività",
"host_room_activity": "Attività recente",
"host_no_activity": "Nessuna attività recente in questa stanza",
@@ -2926,5 +2948,22 @@
"search_no_results": "Nessun messaggio corrisponde alla ricerca",
"popout_channel": "Apri in una nuova finestra",
"new_message_toast": "Nuovo messaggio in #{room}"
+ },
+ "android_storage": {
+ "setup_title": "Choose where MeshChatX stores data",
+ "setup_desc": "File-manager storage is recommended. You can browse and back up your identity and messages under Android/data/com.meshchatx/files/. The app restarts when you change this during setup.",
+ "setup_external_title": "File-manager storage (recommended)",
+ "setup_external_desc": "Android/data/com.meshchatx/files/meshchatx — visible in many file managers.",
+ "setup_internal_title": "Private app storage",
+ "setup_internal_desc": "Hidden from file managers; only this app can access it.",
+ "setup_continue": "Continue",
+ "upgrade_title": "Move data to file-manager storage?",
+ "upgrade_desc": "Your MeshChatX data is in private app storage. Copy it to the file-manager folder so you can browse and back up files, then restart the app.",
+ "upgrade_copy": "Copy and restart",
+ "upgrade_stay_internal": "Stay on private storage",
+ "working": "Preparing…",
+ "copy_restart_hint": "Copy scheduled. Closing the app — open MeshChatX again to finish.",
+ "restart_to_apply": "Restart the app to apply your storage choice.",
+ "failed": "Could not update storage location."
}
}
diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 19b7d022..daa815e4 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -2251,7 +2251,14 @@
"failed_to_stop_session": "Failed to stop RNSH session",
"failed_to_remove_session": "Failed to remove RNSH session",
"failed_to_clear_output": "Failed to clear RNSH output",
- "failed_to_send_input": "Failed to send RNSH input"
+ "failed_to_send_input": "Failed to send RNSH input",
+ "tab_sessions_short": "Sessions",
+ "tab_connect_short": "Connect",
+ "tab_listen_short": "Listen",
+ "fullscreen": "Full screen",
+ "exit_fullscreen": "Exit full screen",
+ "show_sessions": "Sessions",
+ "hide_sessions": "Back to terminal"
},
"rnprobe": {
"network_diagnostics": "Netwerkdiagnostiek",
@@ -2832,6 +2839,9 @@
"copy_hash": "Bestemmingshash kopiëren",
"hash_copied": "Bestemmingshash gekopieerd",
"host_start": "Starten",
+ "host_status_running": "Actief",
+ "host_status_stopped": "Gestopt",
+ "host_hub_not_running": "Start de hub voordat je kamers of leden beheert.",
"host_stop": "Stoppen",
"host_announce": "Nu aankondigen",
"host_announced": "Aangekondigd",
@@ -2853,8 +2863,8 @@
"host_room_created": "Kamer gemaakt",
"host_delete_room_confirm": "Deze kamer verwijderen?",
"host_room_deleted": "Kamer verwijderd",
- "host_members_all_title": "Verbonden clients — {hub}",
- "host_members_room_title": "#{room} — {hub}",
+ "host_members_all_title": "Verbonden clients: {hub}",
+ "host_members_room_title": "#{room}: {hub}",
"host_member_rooms": "Kamers",
"hub_icon": "Hub-pictogram",
"hub_icon_choose": "Pictogram kiezen",
@@ -2883,7 +2893,12 @@
"show_members": "Leden tonen",
"hide_members": "Leden verbergen",
"discovery_refreshed": "Ontdekking vernieuwd",
- "host_members_search": "Zoek leden op naam, hash of kamer...",
+ "host_members_search": "Leden zoeken...",
+ "host_rooms_search": "Kamers zoeken...",
+ "host_rooms_search_empty": "Geen kamers komen overeen met je zoekopdracht.",
+ "host_no_rooms": "Nog geen kamers.",
+ "host_moderation_uptime": "{time} uptime",
+ "host_moderation_members": "{count} leden",
"host_members_select": "Selecteer een lid om berichten te bekijken en te modereren",
"host_no_messages": "Nog geen berichten van deze gebruiker in het logboek",
"host_ban_hub": "Verbannen van hub",
@@ -2895,7 +2910,14 @@
"host_ban_confirm": "{name} van deze hub verbannen?",
"host_room_ban_confirm": "{name} uit #{room} verbannen?",
"host_manage_rooms": "Kamers beheren",
- "host_rooms_modal_title": "Kamers — {hub}",
+ "host_moderate": "Hub modereren",
+ "host_moderation_title": "Moderatie",
+ "host_moderation_title_hub": "Moderatie: {hub}",
+ "host_moderation_title_room": "Moderatie: {hub} (#{room})",
+ "host_moderation_tab_rooms": "Kamers",
+ "host_moderation_tab_members": "Leden",
+ "host_moderation_hub_missing": "Hub niet gevonden.",
+ "host_rooms_modal_title": "Kamers: {hub}",
"host_rooms_select": "Selecteer een kamer om activiteit te bekijken",
"host_room_activity": "Recente activiteit",
"host_no_activity": "Geen recente activiteit in deze kamer",
@@ -2926,5 +2948,22 @@
"search_no_results": "Geen berichten komen overeen met je zoekopdracht",
"popout_channel": "Openen in nieuw venster",
"new_message_toast": "Nieuw bericht in #{room}"
+ },
+ "android_storage": {
+ "setup_title": "Choose where MeshChatX stores data",
+ "setup_desc": "File-manager storage is recommended. You can browse and back up your identity and messages under Android/data/com.meshchatx/files/. The app restarts when you change this during setup.",
+ "setup_external_title": "File-manager storage (recommended)",
+ "setup_external_desc": "Android/data/com.meshchatx/files/meshchatx — visible in many file managers.",
+ "setup_internal_title": "Private app storage",
+ "setup_internal_desc": "Hidden from file managers; only this app can access it.",
+ "setup_continue": "Continue",
+ "upgrade_title": "Move data to file-manager storage?",
+ "upgrade_desc": "Your MeshChatX data is in private app storage. Copy it to the file-manager folder so you can browse and back up files, then restart the app.",
+ "upgrade_copy": "Copy and restart",
+ "upgrade_stay_internal": "Stay on private storage",
+ "working": "Preparing…",
+ "copy_restart_hint": "Copy scheduled. Closing the app — open MeshChatX again to finish.",
+ "restart_to_apply": "Restart the app to apply your storage choice.",
+ "failed": "Could not update storage location."
}
}
diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 9bedff97..17e3850a 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -2162,7 +2162,14 @@
"failed_to_stop_session": "Failed to stop RNSH session",
"failed_to_remove_session": "Failed to remove RNSH session",
"failed_to_clear_output": "Failed to clear RNSH output",
- "failed_to_send_input": "Failed to send RNSH input"
+ "failed_to_send_input": "Failed to send RNSH input",
+ "tab_sessions_short": "Sessions",
+ "tab_connect_short": "Connect",
+ "tab_listen_short": "Listen",
+ "fullscreen": "Full screen",
+ "exit_fullscreen": "Exit full screen",
+ "show_sessions": "Sessions",
+ "hide_sessions": "Back to terminal"
},
"rnprobe": {
"network_diagnostics": "Диагностика сети",
@@ -2832,6 +2839,9 @@
"copy_hash": "Копировать хеш назначения",
"hash_copied": "Хеш назначения скопирован",
"host_start": "Запустить",
+ "host_status_running": "Работает",
+ "host_status_stopped": "Остановлен",
+ "host_hub_not_running": "Запустите хаб перед управлением комнатами или участниками.",
"host_stop": "Остановить",
"host_announce": "Объявить сейчас",
"host_announced": "Объявлено",
@@ -2853,8 +2863,8 @@
"host_room_created": "Комната создана",
"host_delete_room_confirm": "Удалить эту комнату?",
"host_room_deleted": "Комната удалена",
- "host_members_all_title": "Подключённые клиенты — {hub}",
- "host_members_room_title": "#{room} — {hub}",
+ "host_members_all_title": "Подключённые клиенты: {hub}",
+ "host_members_room_title": "#{room}: {hub}",
"host_member_rooms": "Комнаты",
"hub_icon": "Иконка хаба",
"hub_icon_choose": "Выбрать иконку",
@@ -2883,7 +2893,12 @@
"show_members": "Показать участников",
"hide_members": "Скрыть участников",
"discovery_refreshed": "Обзор обновлён",
- "host_members_search": "Поиск участников по имени, хешу или комнате...",
+ "host_members_search": "Поиск участников...",
+ "host_rooms_search": "Поиск комнат...",
+ "host_rooms_search_empty": "Нет комнат по вашему запросу.",
+ "host_no_rooms": "Пока нет комнат.",
+ "host_moderation_uptime": "Аптайм {time}",
+ "host_moderation_members": "{count} участников",
"host_members_select": "Выберите участника, чтобы просмотреть сообщения и модерировать",
"host_no_messages": "В журнале пока нет сообщений от этого пользователя",
"host_ban_hub": "Забанить на хабе",
@@ -2895,7 +2910,14 @@
"host_ban_confirm": "Забанить {name} на этом хабе?",
"host_room_ban_confirm": "Забанить {name} в #{room}?",
"host_manage_rooms": "Управление комнатами",
- "host_rooms_modal_title": "Комнаты — {hub}",
+ "host_moderate": "Модерация хаба",
+ "host_moderation_title": "Модерация",
+ "host_moderation_title_hub": "Модерация: {hub}",
+ "host_moderation_title_room": "Модерация: {hub} (#{room})",
+ "host_moderation_tab_rooms": "Комнаты",
+ "host_moderation_tab_members": "Участники",
+ "host_moderation_hub_missing": "Хаб не найден.",
+ "host_rooms_modal_title": "Комнаты: {hub}",
"host_rooms_select": "Выберите комнату для просмотра активности",
"host_room_activity": "Недавняя активность",
"host_no_activity": "Нет недавней активности в этой комнате",
@@ -2926,5 +2948,22 @@
"search_no_results": "Сообщения по запросу не найдены",
"popout_channel": "Открыть в новом окне",
"new_message_toast": "Новое сообщение в #{room}"
+ },
+ "android_storage": {
+ "setup_title": "Choose where MeshChatX stores data",
+ "setup_desc": "File-manager storage is recommended. You can browse and back up your identity and messages under Android/data/com.meshchatx/files/. The app restarts when you change this during setup.",
+ "setup_external_title": "File-manager storage (recommended)",
+ "setup_external_desc": "Android/data/com.meshchatx/files/meshchatx — visible in many file managers.",
+ "setup_internal_title": "Private app storage",
+ "setup_internal_desc": "Hidden from file managers; only this app can access it.",
+ "setup_continue": "Continue",
+ "upgrade_title": "Move data to file-manager storage?",
+ "upgrade_desc": "Your MeshChatX data is in private app storage. Copy it to the file-manager folder so you can browse and back up files, then restart the app.",
+ "upgrade_copy": "Copy and restart",
+ "upgrade_stay_internal": "Stay on private storage",
+ "working": "Preparing…",
+ "copy_restart_hint": "Copy scheduled. Closing the app — open MeshChatX again to finish.",
+ "restart_to_apply": "Restart the app to apply your storage choice.",
+ "failed": "Could not update storage location."
}
}
diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index e7abeec6..e4081360 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -2251,7 +2251,14 @@
"failed_to_stop_session": "Failed to stop RNSH session",
"failed_to_remove_session": "Failed to remove RNSH session",
"failed_to_clear_output": "Failed to clear RNSH output",
- "failed_to_send_input": "Failed to send RNSH input"
+ "failed_to_send_input": "Failed to send RNSH input",
+ "tab_sessions_short": "Sessions",
+ "tab_connect_short": "Connect",
+ "tab_listen_short": "Listen",
+ "fullscreen": "Full screen",
+ "exit_fullscreen": "Exit full screen",
+ "show_sessions": "Sessions",
+ "hide_sessions": "Back to terminal"
},
"rnprobe": {
"network_diagnostics": "网络诊断",
@@ -2832,6 +2839,9 @@
"copy_hash": "复制目标哈希",
"hash_copied": "已复制目标哈希",
"host_start": "启动",
+ "host_status_running": "运行中",
+ "host_status_stopped": "已停止",
+ "host_hub_not_running": "请先启动中心,再管理房间或成员。",
"host_stop": "停止",
"host_announce": "立即通告",
"host_announced": "已通告",
@@ -2853,8 +2863,8 @@
"host_room_created": "房间已创建",
"host_delete_room_confirm": "删除此房间?",
"host_room_deleted": "房间已删除",
- "host_members_all_title": "已连接客户端 — {hub}",
- "host_members_room_title": "#{room} — {hub}",
+ "host_members_all_title": "已连接客户端:{hub}",
+ "host_members_room_title": "#{room}:{hub}",
"host_member_rooms": "房间",
"hub_icon": "中心图标",
"hub_icon_choose": "选择图标",
@@ -2883,7 +2893,12 @@
"show_members": "显示成员",
"hide_members": "隐藏成员",
"discovery_refreshed": "已刷新发现",
- "host_members_search": "按名称、哈希或房间搜索成员...",
+ "host_members_search": "搜索成员...",
+ "host_rooms_search": "搜索房间...",
+ "host_rooms_search_empty": "没有匹配的房间。",
+ "host_no_rooms": "暂无房间。",
+ "host_moderation_uptime": "运行 {time}",
+ "host_moderation_members": "{count} 名成员",
"host_members_select": "选择成员以查看消息并进行管理",
"host_no_messages": "日志中尚无此用户的消息",
"host_ban_hub": "从中心封禁",
@@ -2895,7 +2910,14 @@
"host_ban_confirm": "在中心封禁 {name}?",
"host_room_ban_confirm": "在 #{room} 封禁 {name}?",
"host_manage_rooms": "管理房间",
- "host_rooms_modal_title": "房间 — {hub}",
+ "host_moderate": "管理 Hub",
+ "host_moderation_title": "管理",
+ "host_moderation_title_hub": "管理:{hub}",
+ "host_moderation_title_room": "管理:{hub}(#{room})",
+ "host_moderation_tab_rooms": "房间",
+ "host_moderation_tab_members": "成员",
+ "host_moderation_hub_missing": "未找到 Hub。",
+ "host_rooms_modal_title": "房间:{hub}",
"host_rooms_select": "选择房间以查看活动",
"host_room_activity": "最近活动",
"host_no_activity": "此房间暂无最近活动",
@@ -2926,5 +2948,22 @@
"search_no_results": "没有匹配搜索的消息",
"popout_channel": "在新窗口中打开",
"new_message_toast": "#{room} 中有新消息"
+ },
+ "android_storage": {
+ "setup_title": "Choose where MeshChatX stores data",
+ "setup_desc": "File-manager storage is recommended. You can browse and back up your identity and messages under Android/data/com.meshchatx/files/. The app restarts when you change this during setup.",
+ "setup_external_title": "File-manager storage (recommended)",
+ "setup_external_desc": "Android/data/com.meshchatx/files/meshchatx — visible in many file managers.",
+ "setup_internal_title": "Private app storage",
+ "setup_internal_desc": "Hidden from file managers; only this app can access it.",
+ "setup_continue": "Continue",
+ "upgrade_title": "Move data to file-manager storage?",
+ "upgrade_desc": "Your MeshChatX data is in private app storage. Copy it to the file-manager folder so you can browse and back up files, then restart the app.",
+ "upgrade_copy": "Copy and restart",
+ "upgrade_stay_internal": "Stay on private storage",
+ "working": "Preparing…",
+ "copy_restart_hint": "Copy scheduled. Closing the app — open MeshChatX again to finish.",
+ "restart_to_apply": "Restart the app to apply your storage choice.",
+ "failed": "Could not update storage location."
}
}
diff --git a/tests/backend/test_android_rnode.py b/tests/backend/test_android_rnode.py
index 05f38101..81e4d960 100644
--- a/tests/backend/test_android_rnode.py
+++ b/tests/backend/test_android_rnode.py
@@ -7,6 +7,7 @@ from meshchatx.meshchat import ReticulumMeshChat
def test_disable_rnode_interfaces_on_android(tmp_path):
+ """Unconditional disable helper still clears RNode interfaces on Android."""
config_path = tmp_path / "config"
config_path.write_text(
"""[reticulum]
diff --git a/tests/backend/test_lxmf_propagation_full.py b/tests/backend/test_lxmf_propagation_full.py
index 9147626f..2b6c45a3 100644
--- a/tests/backend/test_lxmf_propagation_full.py
+++ b/tests/backend/test_lxmf_propagation_full.py
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: 0BSD
+import asyncio
import json
import shutil
import tempfile
@@ -121,6 +122,7 @@ async def test_lxmf_sync_flow(mock_app):
await route.handler(None)
break
+ await asyncio.sleep(0.05)
mock_router.request_messages_from_propagation_node.assert_called_once()
# Check status (Receiving)
@@ -152,6 +154,7 @@ async def test_lxmf_sync_requests_path_before_sync(mock_app):
with patch("meshchatx.meshchat.RNS.Transport.has_path", return_value=False):
with patch("meshchatx.meshchat.RNS.Transport.request_path") as mock_request:
await sync_handler(None)
+ await asyncio.sleep(0.05)
mock_request.assert_called_with(outbound)
mock_router.request_messages_from_propagation_node.assert_called_with(
mock_app.current_context.identity
@@ -327,6 +330,7 @@ async def test_user_provided_node_hash(mock_app):
if r.path == "/api/v1/lxmf/propagation-node/sync"
)
await sync_handler(None)
+ await asyncio.sleep(0.05)
# Verify the router was told to sync for our identity
mock_app.current_context.message_router.request_messages_from_propagation_node.assert_called_with(
diff --git a/tests/backend/test_lxmf_propagation_sync_integration.py b/tests/backend/test_lxmf_propagation_sync_integration.py
index c7af2e0d..eeb0d8a9 100644
--- a/tests/backend/test_lxmf_propagation_sync_integration.py
+++ b/tests/backend/test_lxmf_propagation_sync_integration.py
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: 0BSD
+import asyncio
import json
import shutil
import tempfile
@@ -145,6 +146,7 @@ async def test_remote_propagation_sync_transitions_path_requested_to_complete(
):
first_sync = await sync_handler(None)
assert first_sync.status == 200
+ await asyncio.sleep(0.05)
first_status = json.loads((await status_handler(None)).body)[
"propagation_node_status"
@@ -153,6 +155,7 @@ async def test_remote_propagation_sync_transitions_path_requested_to_complete(
second_sync = await sync_handler(None)
assert second_sync.status == 200
+ await asyncio.sleep(0.05)
second_status = json.loads((await status_handler(None)).body)[
"propagation_node_status"
diff --git a/tests/backend/test_lxmf_sync.py b/tests/backend/test_lxmf_sync.py
index 32166566..7b194b1c 100644
--- a/tests/backend/test_lxmf_sync.py
+++ b/tests/backend/test_lxmf_sync.py
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: 0BSD
+import asyncio
import json
import shutil
import tempfile
@@ -104,6 +105,7 @@ async def test_lxmf_sync_endpoints(mock_app):
response = await sync_handler(None)
assert response.status == 200
+ await asyncio.sleep(0.05)
mock_app.current_context.message_router.request_messages_from_propagation_node.assert_called_once()
# 3. Test status change to complete
@@ -143,6 +145,7 @@ async def test_specific_node_hash_validation(mock_app):
mock_app.current_context.message_router.get_outbound_propagation_node.return_value = expected_bytes
await sync_handler(None)
+ await asyncio.sleep(0.05)
mock_app.current_context.message_router.request_messages_from_propagation_node.assert_called_once()
@@ -175,6 +178,7 @@ async def test_status_includes_sync_storage_and_confirmation_metrics(mock_app):
),
):
await sync_handler(None)
+ await asyncio.sleep(0.05)
response = await status_handler(None)
data = json.loads(response.body)["propagation_node_status"]
@@ -229,6 +233,7 @@ async def test_status_hidden_metric_is_clamped_to_zero(mock_app):
),
):
await sync_handler(None)
+ await asyncio.sleep(0.05)
response = await status_handler(None)
data = json.loads(response.body)["propagation_node_status"]
diff --git a/tests/backend/test_propagation_sync_cancel.py b/tests/backend/test_propagation_sync_cancel.py
new file mode 100644
index 00000000..5eb31f1e
--- /dev/null
+++ b/tests/backend/test_propagation_sync_cancel.py
@@ -0,0 +1,92 @@
+# SPDX-License-Identifier: 0BSD
+
+import asyncio
+import threading
+from unittest.mock import MagicMock, patch
+
+import LXMF
+import pytest
+
+from meshchatx.meshchat import ReticulumMeshChat
+
+PR_IDLE = LXMF.LXMRouter.PR_IDLE
+PR_LINK_ESTABLISHING = LXMF.LXMRouter.PR_LINK_ESTABLISHING
+PR_PATH_REQUESTED = LXMF.LXMRouter.PR_PATH_REQUESTED
+
+
+@pytest.mark.asyncio
+async def test_request_propagation_node_messages_handles_eof_error():
+ app = ReticulumMeshChat.__new__(ReticulumMeshChat)
+ ctx = MagicMock()
+ router = MagicMock()
+ router.PR_IDLE = PR_IDLE
+ router.propagation_transfer_state = PR_LINK_ESTABLISHING
+ router.request_messages_from_propagation_node.side_effect = EOFError()
+ ctx.message_router = router
+ ctx.identity = MagicMock()
+
+ with patch.object(app, "send_config_to_websocket_clients", return_value=None):
+ await app._request_propagation_node_messages(context=ctx)
+
+ assert router.propagation_transfer_state == PR_IDLE
+ assert router.propagation_transfer_progress == 0.0
+
+
+def test_stop_propagation_node_sync_forces_idle_when_cancel_leaves_active_state():
+ app = ReticulumMeshChat.__new__(ReticulumMeshChat)
+ ctx = MagicMock()
+ router = MagicMock()
+ router.PR_IDLE = PR_IDLE
+ router.PR_PATH_REQUESTED = PR_PATH_REQUESTED
+ router.PR_LINK_ESTABLISHING = PR_LINK_ESTABLISHING
+ router.PR_LINK_ESTABLISHED = LXMF.LXMRouter.PR_LINK_ESTABLISHED
+ router.PR_REQUEST_SENT = LXMF.LXMRouter.PR_REQUEST_SENT
+ router.PR_RECEIVING = LXMF.LXMRouter.PR_RECEIVING
+ router.PR_RESPONSE_RECEIVED = LXMF.LXMRouter.PR_RESPONSE_RECEIVED
+ router.propagation_transfer_state = PR_PATH_REQUESTED
+ ctx.message_router = router
+
+ app.stop_propagation_node_sync(context=ctx)
+
+ router.cancel_propagation_node_requests.assert_called_once()
+ assert router.propagation_transfer_state == PR_IDLE
+ assert router.propagation_transfer_progress == 0.0
+
+
+@pytest.mark.asyncio
+async def test_sync_propagation_nodes_returns_before_request_thread_finishes():
+ app = ReticulumMeshChat.__new__(ReticulumMeshChat)
+ ctx = MagicMock()
+ router = MagicMock()
+ router.PR_IDLE = PR_IDLE
+ router.PR_COMPLETE = LXMF.LXMRouter.PR_COMPLETE
+ router.propagation_transfer_state = PR_IDLE
+ router.get_outbound_propagation_node.return_value = b"\x22" * 16
+ router.propagation_destination = MagicMock(hash=b"\x11" * 16)
+ ctx.message_router = router
+ ctx.identity = MagicMock()
+ ctx.config = MagicMock()
+ ctx.database = MagicMock()
+ ctx.database.messages.count_lxmf_messages.return_value = 0
+ ctx.database.messages.count_lxmf_messages_by_state.return_value = 0
+
+ request_started = threading.Event()
+ request_finished = threading.Event()
+
+ def slow_request(_identity):
+ request_started.set()
+ request_finished.wait(timeout=5.0)
+
+ router.request_messages_from_propagation_node.side_effect = slow_request
+
+ with (
+ patch.object(app, "_begin_propagation_sync_metrics"),
+ patch.object(app, "send_config_to_websocket_clients", return_value=None),
+ ):
+ await app.sync_propagation_nodes(context=ctx, force=False)
+ await asyncio.sleep(0.05)
+ assert request_started.is_set()
+ assert not request_finished.is_set()
+
+ request_finished.set()
+ await asyncio.sleep(0.05)
diff --git a/tests/backend/test_rnode_support.py b/tests/backend/test_rnode_support.py
new file mode 100644
index 00000000..f54bad2d
--- /dev/null
+++ b/tests/backend/test_rnode_support.py
@@ -0,0 +1,44 @@
+# SPDX-License-Identifier: 0BSD
+
+
+from meshchatx.src.backend import rnode_support
+
+
+def test_guard_disables_rnode_when_usbserial4a_missing(tmp_path, monkeypatch):
+ config_path = tmp_path / "config"
+ config_path.write_text(
+ """[interfaces]
+ [[RNode Serial]]
+ type = RNodeInterface
+ interface_enabled = True
+ port = ble://aa:bb:cc:dd:ee:ff
+""",
+ encoding="utf-8",
+ )
+ monkeypatch.setattr(rnode_support, "_is_chaquopy_android", lambda: True)
+ monkeypatch.setattr(rnode_support, "android_usbserial4a_available", lambda: False)
+
+ assert rnode_support.guard_rnode_interfaces_on_android(str(config_path)) is True
+ assert "interface_enabled = false" in config_path.read_text(encoding="utf-8")
+
+
+def test_guard_keeps_rnode_when_usbserial4a_available(tmp_path, monkeypatch):
+ config_path = tmp_path / "config"
+ config_path.write_text(
+ """[interfaces]
+ [[RNode Serial]]
+ type = RNodeInterface
+ interface_enabled = True
+""",
+ encoding="utf-8",
+ )
+ monkeypatch.setattr(rnode_support, "_is_chaquopy_android", lambda: True)
+ monkeypatch.setattr(rnode_support, "android_usbserial4a_available", lambda: True)
+
+ assert rnode_support.guard_rnode_interfaces_on_android(str(config_path)) is False
+ assert "interface_enabled = True" in config_path.read_text(encoding="utf-8")
+
+
+def test_rnode_serial_supported_on_desktop(monkeypatch):
+ monkeypatch.setattr(rnode_support, "_is_chaquopy_android", lambda: False)
+ assert rnode_support.rnode_serial_supported() is True
diff --git a/tests/backend/test_rrc_server.py b/tests/backend/test_rrc_server.py
index fa69b7c9..d30b0eca 100644
--- a/tests/backend/test_rrc_server.py
+++ b/tests/backend/test_rrc_server.py
@@ -2,6 +2,8 @@
"""Unit tests for the RRC hub server (hosting) layer."""
+import time
+
from meshchatx.src.backend.rrc import protocol as proto
from meshchatx.src.backend.rrc.manager import RRCManager, RRCHub
from meshchatx.src.backend.rrc.server import (
@@ -277,6 +279,13 @@ def test_ping_returns_pong():
assert out[0][1][proto.K_BODY] == 123
+def test_uptime_seconds_in_summary():
+ server = make_running_server()
+ assert server.to_dict()["uptime_seconds"] == 0
+ server._started_at = time.time() - 90
+ assert server.to_dict()["uptime_seconds"] >= 90
+
+
def test_register_and_unregister_room():
server = make_server()
server.register_room("Lobby", topic="hi")
diff --git a/tests/frontend/ConversationViewer.outboundPropagationStatus.test.js b/tests/frontend/ConversationViewer.outboundPropagationStatus.test.js
index 76e87eb0..9f44a8c0 100644
--- a/tests/frontend/ConversationViewer.outboundPropagationStatus.test.js
+++ b/tests/frontend/ConversationViewer.outboundPropagationStatus.test.js
@@ -77,6 +77,17 @@ describe("ConversationViewer outbound propagation status", () => {
expect(wrapper.vm.outboundSentStatusTitle(null)).toBe("");
});
+ it("outboundTransferProgressPercent and label track resource transfer", () => {
+ const wrapper = mountViewer();
+ expect(wrapper.vm.outboundTransferProgressPercent({ state: "sending", progress: 42.5 })).toBe(43);
+ expect(wrapper.vm.outboundSendingProgressLabel({ state: "sending", progress: 42.5 })).toBe("43%");
+ expect(wrapper.vm.outboundTransferProgressPercent({ state: "sending", progress: 0 })).toBe(0);
+ expect(wrapper.vm.outboundTransferProgressPercent({ state: "outbound", progress: 50 })).toBe(50);
+ expect(wrapper.vm.outboundTransferProgressPercent({ state: "outbound", progress: 0 })).toBeNull();
+ expect(wrapper.vm.outboundTransferProgressPercent({ state: "sending", _pendingPathfinding: true })).toBeNull();
+ expect(wrapper.vm.outboundSendingProgressLabel(null)).toBeNull();
+ });
+
it("outboundSendingStatusTooltip uses propagation pending strings for propagated method", () => {
const wrapper = mountViewer();
const withProgress = wrapper.vm.outboundSendingStatusTooltip({
diff --git a/tests/frontend/RNSHManagerPage.test.js b/tests/frontend/RNSHManagerPage.test.js
index 71189d99..8a1b47e0 100644
--- a/tests/frontend/RNSHManagerPage.test.js
+++ b/tests/frontend/RNSHManagerPage.test.js
@@ -108,4 +108,28 @@ describe("RNSHManagerPage.vue", () => {
expect(wrapper.vm.outputsBySession[SESSION_ID]).toContain("line2");
});
+
+ it("toggles session fullscreen and closes mobile sessions drawer on narrow screens", async () => {
+ const wrapper = mount(RNSHManagerPage, { global: mountToolsPageGlobals() });
+ await vi.waitFor(() => expect(wrapper.vm.sessions.length).toBe(1));
+
+ wrapper.vm.isNarrowScreen = true;
+ wrapper.vm.mobileSessionsOpen = true;
+ wrapper.vm.toggleSessionFullscreen();
+ expect(wrapper.vm.sessionFullscreen).toBe(true);
+ expect(wrapper.vm.mobileSessionsOpen).toBe(false);
+
+ wrapper.vm.toggleSessionFullscreen();
+ expect(wrapper.vm.sessionFullscreen).toBe(false);
+ });
+
+ it("selectSession closes mobile sessions list on narrow screens", async () => {
+ const wrapper = mount(RNSHManagerPage, { global: mountToolsPageGlobals() });
+ await vi.waitFor(() => expect(wrapper.vm.sessions.length).toBe(1));
+
+ wrapper.vm.isNarrowScreen = true;
+ wrapper.vm.mobileSessionsOpen = true;
+ wrapper.vm.selectSession(SESSION_ID);
+ expect(wrapper.vm.mobileSessionsOpen).toBe(false);
+ });
});
diff --git a/tests/frontend/RelayChatPage.test.js b/tests/frontend/RelayChatPage.test.js
index cbb777ad..ff2a67d5 100644
--- a/tests/frontend/RelayChatPage.test.js
+++ b/tests/frontend/RelayChatPage.test.js
@@ -368,23 +368,23 @@ describe("RelayChatPage.vue", () => {
);
});
- it("loads hosted hub members when opening the members dialog", async () => {
+ it("loads hosted hub members when opening the moderation page", async () => {
const wrapper = mountPage();
await vi.waitFor(() => expect(wrapper.vm.serverHubs.length).toBe(1));
const hub = wrapper.vm.serverHubs[0];
- wrapper.vm.openHostMembers(hub, "lobby");
+ wrapper.vm.view = "host";
+ wrapper.vm.openHostModeration(hub, { tab: "members", room: "lobby" });
await wrapper.vm.$nextTick();
- const modal = wrapper.findComponent({ name: "RelayHostMembersModal" });
- await vi.waitFor(() => expect(modal.vm.members).toHaveLength(1));
+ const page = wrapper.findComponent({ name: "RelayHostModerationPage" });
+ await vi.waitFor(() => expect(page.vm.members).toHaveLength(1));
expect(axiosMock.get).toHaveBeenCalledWith(`/api/v1/rrc/servers/${HOSTED_HUB_ID}/members`, {
params: { room: "lobby" },
});
- expect(wrapper.vm.hostMembersModal.open).toBe(true);
- expect(wrapper.vm.hostMembersModal.hub.id).toBe(HOSTED_HUB_ID);
- expect(modal.vm.members[0].name).toBe("alice");
+ expect(wrapper.vm.hostModeration.hub.id).toBe(HOSTED_HUB_ID);
+ expect(page.vm.members[0].name).toBe("alice");
});
it("refreshes hosted hubs on a server change websocket event", async () => {
diff --git a/tests/frontend/RelayHostMembersModal.test.js b/tests/frontend/RelayHostModerationPage.test.js
similarity index 77%
rename from tests/frontend/RelayHostMembersModal.test.js
rename to tests/frontend/RelayHostModerationPage.test.js
index dfa0b41c..7122737b 100644
--- a/tests/frontend/RelayHostMembersModal.test.js
+++ b/tests/frontend/RelayHostModerationPage.test.js
@@ -1,7 +1,7 @@
import { mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
-import RelayHostMembersModal from "@/components/relay/RelayHostMembersModal.vue";
+import RelayHostModerationPage from "@/components/relay/RelayHostModerationPage.vue";
import DialogUtils from "@/js/DialogUtils";
import ToastUtils from "@/js/ToastUtils";
import { mountToolsPageGlobals } from "./testI18n.js";
@@ -26,7 +26,7 @@ const PEER_HASH = "00112233445566778899aabbccddeeff";
const LOCAL_HASH = "ffeeddccbbaa99887766554433221100";
function makeHub() {
- return { id: HUB_ID, name: "Hosted" };
+ return { id: HUB_ID, name: "Hosted", running: true };
}
function makeMember(overrides = {}) {
@@ -38,7 +38,7 @@ function makeMember(overrides = {}) {
};
}
-describe("RelayHostMembersModal.vue", () => {
+describe("RelayHostModerationPage.vue", () => {
beforeEach(() => {
vi.clearAllMocks();
DialogUtils.confirm.mockResolvedValue(true);
@@ -50,25 +50,28 @@ describe("RelayHostMembersModal.vue", () => {
if (url.includes("/members")) {
return { data: { members: [makeMember()] } };
}
+ if (url.includes("/activity")) {
+ return { data: { rooms: [], recent: [] } };
+ }
return { data: {} };
}),
post: vi.fn(async () => ({ data: { message: "ok" } })),
};
});
- const mountModal = (props = {}) =>
- mount(RelayHostMembersModal, {
+ const mountPage = (props = {}) =>
+ mount(RelayHostModerationPage, {
props: {
- open: true,
hub: makeHub(),
- room: null,
+ initialTab: "members",
+ roomFilter: null,
...props,
},
global: mountToolsPageGlobals(),
});
- it("kicks using the member room when the modal has no room filter", async () => {
- const wrapper = mountModal();
+ it("kicks using the member room when there is no room filter", async () => {
+ const wrapper = mountPage();
await wrapper.vm.fetchMembers();
await wrapper.vm.moderate(makeMember(), "kick");
@@ -80,8 +83,8 @@ describe("RelayHostMembersModal.vue", () => {
});
});
- it("uses the modal room filter when set", async () => {
- const wrapper = mountModal({ room: "ops" });
+ it("uses the room filter when set", async () => {
+ const wrapper = mountPage({ roomFilter: "ops" });
await wrapper.vm.fetchMembers();
await wrapper.vm.moderate(makeMember({ rooms: ["lobby", "ops"] }), "kick");
@@ -94,7 +97,7 @@ describe("RelayHostMembersModal.vue", () => {
});
it("blocks moderating the local identity", async () => {
- const wrapper = mountModal();
+ const wrapper = mountPage();
await wrapper.vm.ensureLocalIdentity();
await wrapper.vm.moderate(makeMember({ hash: LOCAL_HASH }), "kick");