diff --git a/core/archipelago/src/mesh/mod.rs b/core/archipelago/src/mesh/mod.rs index ee5d3cae..0a58b3d9 100644 --- a/core/archipelago/src/mesh/mod.rs +++ b/core/archipelago/src/mesh/mod.rs @@ -1116,7 +1116,14 @@ impl MeshService { .map(|p| !p.reachable && p.arch_pubkey_hex.is_some()) .unwrap_or(false) }; - if is_federation_synthetic || exceeds_lora || radio_federated_unreachable { + let mesh_only_mode = load_config(&self.data_dir) + .await + .ok() + .and_then(|cfg| cfg.mesh_only_mode) + .unwrap_or(false); + if !mesh_only_mode + && (is_federation_synthetic || exceeds_lora || radio_federated_unreachable) + { // Resolve the peer's pubkey/did. Prefer the live mesh peer table, // but fall back to federation storage for federation-synthetic ids // that were never seeded into `state.peers` — e.g. a radio-less @@ -1185,6 +1192,17 @@ impl MeshService { // (`send_dm_via_channel` in listener/session.rs) handles both // single-frame and chunked transmission internally; we must NOT // pre-chunk here as well or the receiver sees garbage. + } else if mesh_only_mode + && (is_federation_synthetic || exceeds_lora || radio_federated_unreachable) + { + tracing::info!( + contact_id, + bytes = wire.len(), + is_federation_synthetic, + exceeds_lora, + radio_federated_unreachable, + "Off-grid mode active; forcing mesh message over LoRa only" + ); } self.send_raw_payload(contact_id, wire).await?; Ok(self diff --git a/neode-ui/src/stores/transport.ts b/neode-ui/src/stores/transport.ts index 8ec1f881..4141317d 100644 --- a/neode-ui/src/stores/transport.ts +++ b/neode-ui/src/stores/transport.ts @@ -3,7 +3,7 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { rpcClient } from '@/api/rpc-client' -export type TransportKind = 'mesh' | 'lan' | 'tor' +export type TransportKind = 'mesh' | 'lan' | 'fips' | 'tor' export interface TransportInfo { kind: TransportKind diff --git a/neode-ui/src/views/Mesh.vue b/neode-ui/src/views/Mesh.vue index e45e577c..d1337a2c 100644 --- a/neode-ui/src/views/Mesh.vue +++ b/neode-ui/src/views/Mesh.vue @@ -1477,7 +1477,7 @@ function isImageMime(mime?: string): boolean { OFF-GRID - Tor disabled — mesh only + Tor/FIPS disabled - LoRa only diff --git a/neode-ui/src/views/mesh/mesh-styles.css b/neode-ui/src/views/mesh/mesh-styles.css index e7f7383b..fea1568d 100644 --- a/neode-ui/src/views/mesh/mesh-styles.css +++ b/neode-ui/src/views/mesh/mesh-styles.css @@ -119,10 +119,10 @@ .mesh-signal-bar.active { background: #4ade80; } .mesh-unread-badge { background: #fb923c; color: #000; font-size: 0.65rem; font-weight: 700; min-width: 18px; height: 18px; border-radius: 9px; display: flex; align-items: center; justify-content: center; padding: 0 5px; flex-shrink: 0; } .mesh-chat-card { padding: 0; flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; } -.mesh-chat-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.3); gap: 8px; padding: 40px; } -.mesh-chat-empty-icon { font-size: 3rem; opacity: 0.4; } +.mesh-chat-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.14); gap: 8px; padding: 40px; } +.mesh-chat-empty-icon { font-size: 3rem; opacity: 0.18; } .mesh-chat-empty p { margin: 0; font-size: 0.9rem; } -.mesh-chat-empty-sub { font-size: 0.75rem !important; color: rgba(255, 255, 255, 0.2); } +.mesh-chat-empty-sub { font-size: 0.75rem !important; color: rgba(255, 255, 255, 0.1); } .mesh-chat-header { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.06); flex-shrink: 0; } /* Floating mobile back button (Teleported to body). Hidden by default; only shown in the single-column mobile mesh layout (see media query below). */