diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue index 17161b5..5a1654f 100644 --- a/meshchatx/src/frontend/components/App.vue +++ b/meshchatx/src/frontend/components/App.vue @@ -1212,6 +1212,24 @@ export default { } break; } + if (json.ingest_type === "docs_view") { + const dq = json.docs_query; + const rel = dq && typeof dq.reticulum === "string" ? dq.reticulum.trim() : ""; + if (rel) { + await this.$router.push({ + name: "documentation", + query: { reticulum: encodeURIComponent(rel) }, + }); + } else { + await this.$router.push({ name: "documentation" }); + } + if (json.status === "error") { + ToastUtils.error(json.message); + } else if (json.message) { + ToastUtils.info(json.message); + } + break; + } if (json.status === "success") { ToastUtils.success(json.message); } else if (json.status === "error") { @@ -1766,6 +1784,33 @@ export default { this.$router.push({ name: "call", query: { tab: "phone" } }); return; } + try { + const u = new URL(normalizedUrl); + const proto = u.protocol.toLowerCase(); + const host = u.hostname.toLowerCase(); + if ((proto === "meshchatx:" || proto === "meshchat:") && host === "docs") { + let rel = u.searchParams.get("reticulum") ?? u.searchParams.get("path") ?? ""; + rel = String(rel).trim(); + if (!rel && u.pathname && u.pathname !== "/") { + try { + rel = decodeURIComponent(u.pathname.replace(/^\/+/, "")); + } catch { + rel = u.pathname.replace(/^\/+/, ""); + } + } + if (rel) { + this.$router.push({ + name: "documentation", + query: { reticulum: encodeURIComponent(rel) }, + }); + } else { + this.$router.push({ name: "documentation" }); + } + return; + } + } catch { + /* not a valid URL; continue */ + } if (/^(meshchatx|meshchat):\/\/map\b/i.test(normalizedUrl)) { WebSocketConnection.send( JSON.stringify({ diff --git a/meshchatx/src/frontend/components/TutorialModal.vue b/meshchatx/src/frontend/components/TutorialModal.vue index 33e5749..2b6690c 100644 --- a/meshchatx/src/frontend/components/TutorialModal.vue +++ b/meshchatx/src/frontend/components/TutorialModal.vue @@ -280,6 +280,41 @@

{{ $t("tutorial.bootstrap_desc") }}

+
+ +
+
+ {{ $t("tutorial.bootstrap_selected_nodes_heading") }} +
+ +
+
- +

@@ -605,114 +640,197 @@

-
-
- -
-
- {{ $t("tutorial.documentation") }} -
-
- {{ $t("tutorial.documentation_desc") }} -
- -
-
- -
- -
-
- {{ $t("tutorial.micron_editor") }} -
-
- {{ $t("tutorial.micron_editor_desc") }} -
- -
-
- -
+
+
- + +
+
+ {{ $t("tutorial.documentation") }} +
+
+ {{ $t("tutorial.documentation_desc") }} +
+ +
+
+ +
+ +
+
+ {{ $t("tutorial.micron_editor") }} +
+
+ {{ $t("tutorial.micron_editor_desc") }} +
+
+ + +
+
+
+ +
+ +
+
+ {{ $t("tutorial.identities_card_title") }} +
+
+ {{ $t("tutorial.identities_card_desc") }} +
+
+
+ +
+
-
+
+ {{ $t("tutorial.archiver") }} +
+
+ {{ $t("tutorial.archiver_desc") }} +
+
+
+
+ +

+ {{ $t("tutorial.learn_create_more") }} +

+ +
+
+ +
+
{{ $t("tutorial.paper_messages") }}
-
+
{{ $t("tutorial.paper_messages_desc") }}
- -
-
+ +
+
{{ $t("tutorial.send_messages") }}
-
+
{{ $t("tutorial.send_messages_desc") }}
- -
-
+ +
+
{{ $t("tutorial.explore_nodes") }}
-
+
{{ $t("tutorial.explore_nodes_desc") }}
- -
-
+ +
+
{{ $t("tutorial.voice_calls") }}
-
+
{{ $t("tutorial.voice_calls_desc") }}
@@ -1063,6 +1181,41 @@

{{ $t("tutorial.bootstrap_desc_page") }}

+
+ +
+
+ {{ $t("tutorial.bootstrap_selected_nodes_heading") }} +
+
    +
  • + {{ label }} +
  • +
+
+
- +

@@ -1393,116 +1546,243 @@

-
-
- -
-
- {{ $t("tutorial.documentation") }} +
+
+
+
+ +
+
+ {{ $t("tutorial.documentation") }} +
+

+ {{ $t("tutorial.documentation_desc_page") }} +

+ +
-

- {{ $t("tutorial.documentation_desc_page") }} -

- + +

+ {{ $t("tutorial.learn_create_more") }} +

+ +
+ -
- -
-
- {{ $t("tutorial.micron_editor") }} -
-

- {{ $t("tutorial.micron_editor_desc_page") }} -

- -
-
-
- -
-
- -
-
- {{ $t("tutorial.paper_messages") }} -
-
- {{ $t("tutorial.paper_messages_desc") }} +
+ +
+
+ {{ $t("tutorial.send_messages") }} +
+
+ {{ $t("tutorial.send_messages_desc") }} +
-
-
- -
-
- {{ $t("tutorial.send_messages") }} -
-
- {{ $t("tutorial.send_messages_desc") }} +
+ +
+
+ {{ $t("tutorial.explore_nodes") }} +
+
+ {{ $t("tutorial.explore_nodes_desc") }} +
-
-
- -
-
- {{ $t("tutorial.explore_nodes") }} -
-
- {{ $t("tutorial.explore_nodes_desc") }} -
-
-
- -
- -
-
- {{ $t("tutorial.voice_calls") }} -
-
- {{ $t("tutorial.voice_calls_desc") }} +
+ +
+
+ {{ $t("tutorial.voice_calls") }} +
+
+ {{ $t("tutorial.voice_calls_desc") }} +
@@ -1632,6 +1912,8 @@ export default { bootstrapListSearch: "", bootstrapDiscoveredSectionOpen: true, bootstrapCommunitySectionOpen: true, + bootstrapAutoPickDone: false, + pickingRandomBootstraps: false, }; }, computed: { @@ -1696,6 +1978,22 @@ export default { reticulumBundledDocsUrl() { return bundledReticulumDocsUrl(this.$i18n.locale); }, + bootstrapSelectedLabels() { + return this.selectedBootstrapKeys.map((k) => this.bootstrapDisplayLabelForKey(k)).filter(Boolean); + }, + }, + watch: { + communityInterfaces() { + this.$nextTick(() => void this.maybeAutoPickBootstrapTcp()); + }, + discoveredInterfaces() { + this.$nextTick(() => void this.maybeAutoPickBootstrapTcp()); + }, + currentStep(val) { + if (val === 3) { + this.$nextTick(() => void this.maybeAutoPickBootstrapTcp()); + } + }, }, beforeUnmount() { if (this.onWindowResize) { @@ -1752,6 +2050,7 @@ export default { this.bootstrapListSearch = ""; this.bootstrapDiscoveredSectionOpen = true; this.bootstrapCommunitySectionOpen = true; + this.bootstrapAutoPickDone = false; await this.loadDiscoveryBootstrapDefaults(); await this.loadCommunityInterfaces(); await this.loadDiscoveredInterfaces(); @@ -1834,6 +2133,9 @@ export default { this.bootstrapListSearch = ""; this.bootstrapDiscoveredSectionOpen = true; this.bootstrapCommunitySectionOpen = true; + await this.loadCommunityInterfaces(); + await this.loadDiscoveredInterfaces(); + await this.maybeAutoPickBootstrapTcp(); } catch (e) { console.error("Failed to enable discovery:", e); ToastUtils.error(this.$t("tutorial.failed_enable_discovery")); @@ -1879,6 +2181,176 @@ export default { this.selectedBootstrapKeys.push(key); } }, + bootstrapDisplayLabelForKey(key) { + if (!key) { + return ""; + } + if (key.startsWith("comm:")) { + const name = key.slice(5); + const iface = this.communityInterfaces.find((c) => c.name === name); + return iface?.name || name; + } + if (key.startsWith("disc:")) { + const suffix = key.slice(5); + const iface = this.discoveredInterfaces.find((d) => String(d.discovery_hash || d.name) === suffix); + return iface?.name || suffix; + } + return key; + }, + communityBootstrapExcludedFromRandom(iface) { + const name = String(iface.name || ""); + const desc = String(iface.description || ""); + const host = String(iface.target_host || "").trim(); + const hay = `${name} ${desc}`.toLowerCase(); + if (hay.includes("yggdrasil")) { + return true; + } + if (/\bygg\b/.test(hay) || hay.includes("-ygg") || hay.includes(" ygg") || hay.includes("(ygg")) { + return true; + } + if (/^(200|201|202|203):[0-9a-f:]+$/i.test(host)) { + return true; + } + return false; + }, + pickEligibleCommunityTcpBootstrapForRandom() { + const out = []; + for (const iface of this.communityInterfaces) { + const t = iface.type; + if (t !== "TCPClientInterface" && t !== "BackboneInterface") { + continue; + } + const host = String(iface.target_host || "").trim(); + const port = iface.target_port; + if (!host || port === undefined || port === null || port === "") { + continue; + } + if (this.communityBootstrapExcludedFromRandom(iface)) { + continue; + } + out.push({ + key: `comm:${iface.name}`, + kind: "community", + iface, + dedupe: `${host.toLowerCase()}:${Number(port)}`, + }); + } + return out; + }, + pickEligibleTcpBootstrapEntries() { + const out = []; + for (const iface of this.communityInterfaces) { + const t = iface.type; + if (t !== "TCPClientInterface" && t !== "BackboneInterface") { + continue; + } + const host = String(iface.target_host || "").trim(); + const port = iface.target_port; + if (!host || port === undefined || port === null || port === "") { + continue; + } + out.push({ + key: `comm:${iface.name}`, + kind: "community", + iface, + dedupe: `${host.toLowerCase()}:${Number(port)}`, + }); + } + for (const iface of this.discoveredInterfaces) { + const host = String(iface.reachable_on || "").trim(); + const port = iface.port; + if (!host || port === undefined || port === null || port === "") { + continue; + } + const typ = iface.type || ""; + if (typ && typ !== "BackboneInterface" && typ !== "TCPClientInterface") { + continue; + } + out.push({ + key: `disc:${iface.discovery_hash || iface.name}`, + kind: "discovered", + iface, + dedupe: `${host.toLowerCase()}:${Number(port)}`, + }); + } + return out; + }, + dedupeBootstrapEntries(entries) { + const seen = new Set(); + const deduped = []; + for (const e of entries) { + if (seen.has(e.dedupe)) { + continue; + } + seen.add(e.dedupe); + deduped.push(e); + } + return deduped; + }, + shuffleArrayInPlace(arr) { + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + }, + async pickRandomTcpBootstraps(options = {}) { + const silent = options.silent === true; + const auto = options.auto === true; + if (!silent && !auto) { + this.pickingRandomBootstraps = true; + } + await Promise.resolve(); + await new Promise((resolve) => { + if (typeof requestAnimationFrame !== "undefined") { + requestAnimationFrame(() => resolve()); + } else { + setTimeout(resolve, 0); + } + }); + try { + let entries = this.pickEligibleCommunityTcpBootstrapForRandom(); + entries = this.dedupeBootstrapEntries(entries); + if (entries.length === 0) { + if (!silent && !auto) { + ToastUtils.warning(this.$t("tutorial.bootstrap_pick_random_none")); + } + return; + } + this.shuffleArrayInPlace(entries); + const take = Math.min(3, entries.length); + this.selectedBootstrapKeys = entries.slice(0, take).map((e) => e.key); + const labels = this.selectedBootstrapKeys.map((k) => this.bootstrapDisplayLabelForKey(k)); + if (!silent && !auto) { + ToastUtils.success( + this.$t("tutorial.bootstrap_pick_random_done", { + count: take, + names: labels.join(", "), + }) + ); + } + } finally { + if (!silent && !auto) { + this.pickingRandomBootstraps = false; + } + } + }, + async maybeAutoPickBootstrapTcp() { + if (this.bootstrapAutoPickDone) { + return; + } + if (this.currentStep !== 3 || this.connectionMode !== "discovery") { + return; + } + if (this.selectedBootstrapKeys.length > 0) { + return; + } + const entries = this.dedupeBootstrapEntries(this.pickEligibleCommunityTcpBootstrapForRandom()); + if (entries.length === 0) { + return; + } + await this.pickRandomTcpBootstraps({ silent: true, auto: true }); + this.bootstrapAutoPickDone = true; + }, buildBootstrapPayload(item) { if (item.kind === "discovered") { const iface = item.iface; diff --git a/meshchatx/src/frontend/components/docs/DocsPage.vue b/meshchatx/src/frontend/components/docs/DocsPage.vue index b2eefb3..deb7be4 100644 --- a/meshchatx/src/frontend/components/docs/DocsPage.vue +++ b/meshchatx/src/frontend/components/docs/DocsPage.vue @@ -587,11 +587,20 @@ export default { if (!this.status.has_docs) return []; return this.allLanguages.filter((l) => l.code !== this.currentLang); }, + reticulumDocsQueryParam() { + return this.$route?.query?.reticulum; + }, + }, + watch: { + reticulumDocsQueryParam() { + this.applyDocumentationRouteQuery(); + }, }, mounted() { this.fetchStatus(); this.fetchMeshChatXDocs(); this.statusInterval = setInterval(this.fetchStatus, 2000); + this.applyDocumentationRouteQuery(); }, beforeUnmount() { if (this.statusInterval) { @@ -612,6 +621,9 @@ export default { } catch (error) { console.error("Failed to fetch docs status:", error); } + if (this.reticulumDocsQueryParam) { + this.applyDocumentationRouteQuery(); + } }, dismissError() { this.status = { ...this.status, last_error: null }; @@ -758,6 +770,28 @@ export default { this.searchQuery = ""; this.searchResults = []; }, + applyDocumentationRouteQuery() { + const q = this.reticulumDocsQueryParam; + if (q === undefined || q === null || q === "") { + return; + } + const raw = Array.isArray(q) ? q[0] : q; + if (typeof raw !== "string" || !raw.trim()) { + return; + } + let path = raw.trim(); + try { + path = decodeURIComponent(path); + } catch { + return; + } + path = path.replace(/^\/?(?:reticulum-docs\/)?/, ""); + if (!path) { + return; + } + this.activeTab = "reticulum"; + this.selectedReticulumPath = path; + }, navigateTo(path) { if (path.startsWith("/meshchatx-docs/")) { this.activeTab = "meshchatx"; diff --git a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue index 0da939a..7f9ddf4 100644 --- a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue +++ b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue @@ -163,6 +163,9 @@ +
@@ -241,9 +244,9 @@ $t("interfaces.discovery_default_bootstrap_only") }} -

- {{ $t("interfaces.discovery_default_bootstrap_only_hint") }} -

+
@@ -312,13 +315,20 @@ />
- Transport identity (hex) + {{ + $t("interfaces.backbone_transport_identity_label") + }} +

+ {{ $t("interfaces.backbone_transport_identity_hint") }} +

-

- {{ $t("interfaces.discovery_default_bootstrap_only_hint") }} -

+
@@ -524,12 +534,30 @@ >
- - Connect over network (IP) +
+ + Connect over network (IP) +
+
+ + {{ $t("interfaces.rnode_ble_toggle") }} +
+
+ {{ + $t("interfaces.rnode_ble_peer_label") + }} + +

+ {{ $t("interfaces.rnode_ble_hint") }} +

+
Serial Port
@@ -1027,6 +1077,59 @@ />
+ + +
+
+ {{ $t("interfaces.loopback_local_title") }} +
+

+ {{ $t("interfaces.loopback_local_body") }} +

+ +
+ + +
+

+ {{ $t("interfaces.custom_external_intro") }} +

+
+ {{ + $t("interfaces.custom_external_type_label") + }} + +
+
+ {{ + $t("interfaces.custom_external_json_label") + }} + +
+ +
@@ -1232,9 +1335,7 @@ {{ $t("interfaces.discovery_default_bootstrap_only") }} -

- {{ $t("interfaces.discovery_default_bootstrap_only_hint") }} -

+
@@ -1566,6 +1667,8 @@ import FormLabel from "../forms/FormLabel.vue"; import Toggle from "../forms/Toggle.vue"; import GlobalState from "../../js/GlobalState"; import MaterialDesignIcon from "../MaterialDesignIcon.vue"; +import BundledDocsHint from "./BundledDocsHint.vue"; +import { RETICULUM_MANUAL_INTERFACES_OVERVIEW_REL } from "../../js/reticulumDocsEntryUrl.js"; export default { name: "AddInterfacePage", @@ -1574,6 +1677,7 @@ export default { FormLabel, ExpandingSection, Toggle, + BundledDocsHint, }, data() { return { @@ -1582,6 +1686,10 @@ export default { isSaving: false, isEditingInterface: false, + customExternalTypeName: "", + customExternalOptionsJson: "{}", + docsReticulumInterfacesOverview: RETICULUM_MANUAL_INTERFACES_OVERVIEW_REL, + config: null, communityInterfaces: [], @@ -1673,6 +1781,8 @@ export default { newInterfacePort: null, newInterfaceRNodeUseIP: false, + newInterfaceRNodeUseBle: false, + newInterfaceRNodeBlePeer: "", newInterfaceRNodeIPHost: "localhost", newInterfaceRNodeIPPort: "7633", RNodeGHzValue: 0, @@ -1876,6 +1986,28 @@ export default { console.log(e); } }, + effectiveRNodeBlePort() { + let p = (this.newInterfaceRNodeBlePeer || "").trim(); + if (!p) { + return "ble://"; + } + if (p.toLowerCase().startsWith("ble://")) { + return p; + } + return `ble://${p}`; + }, + setRNodeTransportIp(v) { + this.newInterfaceRNodeUseIP = Boolean(v); + if (this.newInterfaceRNodeUseIP) { + this.newInterfaceRNodeUseBle = false; + } + }, + setRNodeTransportBle(v) { + this.newInterfaceRNodeUseBle = Boolean(v); + if (this.newInterfaceRNodeUseBle) { + this.newInterfaceRNodeUseIP = false; + } + }, async loadCommunityInterfaces() { try { const response = await window.api.get(`/api/v1/community-interfaces`); @@ -1916,6 +2048,25 @@ export default { this.newInterfaceName = interfaceName; this.newInterfaceType = iface.type; + this.customExternalTypeName = ""; + this.customExternalOptionsJson = "{}"; + if (!this.isDedicatedFormInterfaceType(iface.type)) { + this.newInterfaceType = "__external__"; + this.customExternalTypeName = iface.type; + const skip = new Set(["type", "name", "interface_enabled", "enabled"]); + const flat = {}; + for (const [k, v] of Object.entries(iface)) { + if (skip.has(k)) { + continue; + } + if (v !== null && typeof v === "object" && !Array.isArray(v)) { + continue; + } + flat[k] = v; + } + this.customExternalOptionsJson = JSON.stringify(flat, null, 2); + } + this.newInterfaceTargetHost = iface.target_host ?? iface.remote ?? null; this.newInterfaceTargetPort = iface.target_port ?? null; this.newInterfaceTransportIdentity = iface.transport_identity ?? null; @@ -1982,7 +2133,12 @@ export default { this.newInterfacePort = iface.port; this.newInterfaceRNodeUseIP = false; - if (iface.port && String(iface.port).startsWith("tcp://")) { + this.newInterfaceRNodeUseBle = false; + this.newInterfaceRNodeBlePeer = ""; + if (iface.port && String(iface.port).toLowerCase().startsWith("ble://")) { + this.newInterfaceRNodeUseBle = true; + this.newInterfaceRNodeBlePeer = String(iface.port); + } else if (iface.port && String(iface.port).startsWith("tcp://")) { const addr = String(iface.port).replace("tcp://", ""); const parts = addr.split(":"); this.newInterfaceRNodeIPHost = parts[0] || "localhost"; @@ -2103,7 +2259,12 @@ export default { if (config.forward_port) this.newInterfaceForwardPort = Number(config.forward_port); if (config.port) { this.newInterfacePort = config.port; - if (config.port.startsWith("tcp://")) { + this.newInterfaceRNodeUseBle = false; + this.newInterfaceRNodeUseIP = false; + if (String(config.port).toLowerCase().startsWith("ble://")) { + this.newInterfaceRNodeUseBle = true; + this.newInterfaceRNodeBlePeer = config.port; + } else if (config.port.startsWith("tcp://")) { const addr = config.port.replace("tcp://", ""); const [host, port] = addr.split(":"); this.newInterfaceRNodeIPHost = host; @@ -2391,6 +2552,73 @@ export default { const discoveryEnabled = this.discovery.discoverable === true; const freqHz = Math.round(this.calculateFrequencyInHz()); + if (this.newInterfaceType === "RNodeInterface" && this.newInterfaceRNodeUseBle) { + const raw = (this.newInterfaceRNodeBlePeer || "").trim(); + const inner = raw.toLowerCase().startsWith("ble://") ? raw.slice(6).trim() : raw; + if (!inner) { + ToastUtils.warning(this.$t("interfaces.rnode_ble_peer_required")); + return; + } + } + + if (this.newInterfaceType === "__external__") { + const typeStr = (this.customExternalTypeName || "").trim(); + if (!typeStr) { + ToastUtils.error(this.$t("interfaces.custom_external_type_required")); + return; + } + let extra = {}; + try { + extra = JSON.parse((this.customExternalOptionsJson || "").trim() || "{}"); + } catch { + ToastUtils.error(this.$t("interfaces.custom_external_json_invalid")); + return; + } + if (extra !== null && typeof extra !== "object") { + ToastUtils.error(this.$t("interfaces.custom_external_json_invalid")); + return; + } + const payload = { + allow_overwriting_interface: this.isEditingInterface, + name: this.newInterfaceName, + type: typeStr, + extra_config: extra, + discoverable: discoveryEnabled ? "yes" : null, + discovery_name: discoveryEnabled ? this.discovery.discovery_name : null, + announce_interval: discoveryEnabled + ? (this.numOrNull(this.discovery.announce_interval) ?? 360) + : null, + reachable_on: discoveryEnabled ? this.discovery.reachable_on : null, + discovery_stamp_value: discoveryEnabled + ? (this.numOrNull(this.discovery.discovery_stamp_value) ?? 14) + : null, + discovery_encrypt: discoveryEnabled ? this.discovery.discovery_encrypt === true : null, + publish_ifac: discoveryEnabled ? this.discovery.publish_ifac === true : null, + latitude: discoveryEnabled ? this.numOrNull(this.discovery.latitude) : null, + longitude: discoveryEnabled ? this.numOrNull(this.discovery.longitude) : null, + height: discoveryEnabled ? this.numOrNull(this.discovery.height) : null, + discovery_frequency: discoveryEnabled + ? this.numOrNull(this.discovery.discovery_frequency) + : null, + discovery_bandwidth: discoveryEnabled + ? this.numOrNull(this.discovery.discovery_bandwidth) + : null, + discovery_modulation: discoveryEnabled + ? this.numOrNull(this.discovery.discovery_modulation) + : null, + mode: this.sharedInterfaceSettings.mode || null, + bitrate: this.sharedInterfaceSettings.bitrate, + network_name: this.sharedInterfaceSettings.network_name, + passphrase: this.sharedInterfaceSettings.passphrase, + }; + const response = await window.api.post(`/api/v1/reticulum/interfaces/add`, payload); + if (response.data.message) ToastUtils.success(response.data.message); + GlobalState.hasPendingInterfaceChanges = true; + GlobalState.modifiedInterfaceNames.add(this.newInterfaceName); + this.$router.push({ name: "interfaces" }); + return; + } + const i2pPeers = this.newInterfaceType === "I2PInterface" ? (this.I2PSettings.newInterfacePeers || []).map((p) => String(p).trim()).filter(Boolean) @@ -2433,7 +2661,9 @@ export default { this.newInterfaceType === "I2PInterface" ? this.newInterfaceConnectable === true : null, port: this.newInterfaceRNodeUseIP ? `tcp://${this.newInterfaceRNodeIPHost}:${this.newInterfaceRNodeIPPort}` - : this.newInterfacePort, + : this.newInterfaceRNodeUseBle + ? this.effectiveRNodeBlePort() + : this.newInterfacePort, frequency: freqHz, bandwidth: this.newInterfaceBandwidth, txpower: this.newInterfaceTxpower, @@ -2555,6 +2785,25 @@ export default { removeSubInterface(idx) { this.RNodeMultiInterface.subInterfaces.splice(idx, 1); }, + isDedicatedFormInterfaceType(t) { + const builtin = new Set([ + "TCPClientInterface", + "BackboneInterface", + "I2PInterface", + "TCPServerInterface", + "UDPInterface", + "RNodeInterface", + "RNodeIPInterface", + "RNodeMultiInterface", + "SerialInterface", + "KISSInterface", + "AX25KISSInterface", + "PipeInterface", + "AutoInterface", + "LocalInterface", + ]); + return builtin.has(t); + }, }, }; diff --git a/meshchatx/src/frontend/components/interfaces/BundledDocsHint.vue b/meshchatx/src/frontend/components/interfaces/BundledDocsHint.vue new file mode 100644 index 0000000..a9a3de5 --- /dev/null +++ b/meshchatx/src/frontend/components/interfaces/BundledDocsHint.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue index f8ccb48..b9790e0 100644 --- a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue +++ b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue @@ -656,7 +656,9 @@ {{ $t("interfaces.discovery_default_bootstrap_only") }}
- {{ $t("interfaces.discovery_default_bootstrap_only_hint") }} +