diff --git a/README.md b/README.md index e2a48fd..0c3ab8b 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ docker compose up -d ``` ### Docker Permissions Note + If you encounter a `PermissionError` when running the Docker container, it's likely because the container's user (UID 1000) doesn't have permission to write to your host's `./meshchat-config` folder. You can fix this by running: ```bash @@ -246,36 +247,36 @@ MeshChatX can be configured via command-line arguments or environment variables. We use [Task](https://taskfile.dev/) for automation. -| Task | Description | -| :----------------------------- | :--------------------------------------------- | -| `task install` | Install all dependencies | -| `task run` | Run the application | -| `task dev` | Run the application in development mode | -| `task lint:all` | Run all linters (Python & Frontend) | -| `task lint:be` | Lint Python code only | -| `task lint:fe` | Lint frontend code only | -| `task fmt:all` | Format all code (Python & Frontend) | -| `task fmt:be` | Format Python code only | -| `task fmt:fe` | Format frontend code only | -| `task test:all` | Run all tests | -| `task test:cov` | Run tests with coverage reports | -| `task test:be` | Run Python tests only | -| `task test:fe` | Run frontend tests only | -| `task build:all` | Build frontend and backend | -| `task build:fe` | Build only the frontend | -| `task build:wheel` | Build Python wheel package | -| `task compile` | Compile Python code to check for syntax errors | -| `task docker:build` | Build Docker image using buildx | -| `task docker:run` | Run Docker container using docker-compose | -| `task dist:linux:appimage` | Build Linux AppImage | -| `task dist:win:exe` | Build Windows portable executable | -| `task dist:win:wine` | Build Windows portable (Wine cross-build) | -| `task dist:all` | Build all Electron apps | -| `task dist:all:wine` | Build all Electron apps (Wine cross-build) | -| `task android:prepare` | Prepare Android build | -| `task android:build` | Build Android APK | -| `task dist:fe:flatpak` | Build Flatpak package | -| `task clean` | Clean build artifacts and dependencies | +| Task | Description | +| :------------------------- | :--------------------------------------------- | +| `task install` | Install all dependencies | +| `task run` | Run the application | +| `task dev` | Run the application in development mode | +| `task lint:all` | Run all linters (Python & Frontend) | +| `task lint:be` | Lint Python code only | +| `task lint:fe` | Lint frontend code only | +| `task fmt:all` | Format all code (Python & Frontend) | +| `task fmt:be` | Format Python code only | +| `task fmt:fe` | Format frontend code only | +| `task test:all` | Run all tests | +| `task test:cov` | Run tests with coverage reports | +| `task test:be` | Run Python tests only | +| `task test:fe` | Run frontend tests only | +| `task build:all` | Build frontend and backend | +| `task build:fe` | Build only the frontend | +| `task build:wheel` | Build Python wheel package | +| `task compile` | Compile Python code to check for syntax errors | +| `task docker:build` | Build Docker image using buildx | +| `task docker:run` | Run Docker container using docker-compose | +| `task dist:linux:appimage` | Build Linux AppImage | +| `task dist:win:exe` | Build Windows portable executable | +| `task dist:win:wine` | Build Windows portable (Wine cross-build) | +| `task dist:all` | Build all Electron apps | +| `task dist:all:wine` | Build all Electron apps (Wine cross-build) | +| `task android:prepare` | Prepare Android build | +| `task android:build` | Build Android APK | +| `task dist:fe:flatpak` | Build Flatpak package | +| `task clean` | Clean build artifacts and dependencies | ## Security diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue index 05ec0c1..9e65820 100644 --- a/meshchatx/src/frontend/components/App.vue +++ b/meshchatx/src/frontend/components/App.vue @@ -1095,8 +1095,7 @@ export default { const status = this.propagationNodeStatus?.state; const messagesReceived = this.propagationNodeStatus?.messages_received ?? 0; const messagesStored = this.propagationNodeStatus?.messages_stored ?? 0; - const deliveryConfirmations = - this.propagationNodeStatus?.delivery_confirmations ?? 0; + const deliveryConfirmations = this.propagationNodeStatus?.delivery_confirmations ?? 0; const messagesHidden = this.propagationNodeStatus?.messages_hidden ?? 0; if (status === "complete" || status === "idle") { const base = this.$t("app.sync_complete", { count: messagesReceived }); diff --git a/meshchatx/src/frontend/components/interfaces/Interface.vue b/meshchatx/src/frontend/components/interfaces/Interface.vue index 75d2615..1cd6c07 100644 --- a/meshchatx/src/frontend/components/interfaces/Interface.vue +++ b/meshchatx/src/frontend/components/interfaces/Interface.vue @@ -38,7 +38,9 @@
-
{{ iface._name }}
+
+ {{ iface._name }} +
{{ iface.type }} {{ isInterfaceEnabled(iface) ? $t("app.enabled") : $t("app.disabled") @@ -48,34 +50,40 @@
{{ description }}
-
- {{ $t("interface.bitrate") }} {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }} - {{ $t("interface.tx") }} {{ formatBytes(iface._stats?.txb ?? 0) }} - {{ $t("interface.rx") }} {{ formatBytes(iface._stats?.rxb ?? 0) }} - {{ $t("interface.noise") }} {{ iface._stats?.noise_floor }} dBm - {{ $t("interface.clients") }} {{ iface._stats?.clients }} -
-
- {{ iface._stats.ifac_size * 8 }}-bit IFAC - • {{ iface._stats.ifac_netname }} - - -
+
+ {{ $t("interface.bitrate") }} {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }} + {{ $t("interface.tx") }} {{ formatBytes(iface._stats?.txb ?? 0) }} + {{ $t("interface.rx") }} {{ formatBytes(iface._stats?.rxb ?? 0) }} + {{ $t("interface.noise") }} {{ iface._stats?.noise_floor }} dBm + {{ $t("interface.clients") }} {{ iface._stats?.clients }} +
+
+ {{ iface._stats.ifac_size * 8 }}-bit IFAC + • {{ iface._stats.ifac_netname }} + + +
-
+
", - items: "
Item 1
", + button: '', + items: '', ...slots, }, global: { @@ -38,7 +38,7 @@ describe("DropDownMenu UI", () => { }); it("renders items slot when open", async () => { - const wrapper = mountDropDown({ items: "
Custom
" }); + const wrapper = mountDropDown({ items: '
Custom
' }); await wrapper.find("button").trigger("click"); expect(wrapper.find(".custom-item").exists()).toBe(true); expect(wrapper.text()).toContain("Custom"); diff --git a/tests/frontend/IconButton.test.js b/tests/frontend/IconButton.test.js index 88a0de4..b899058 100644 --- a/tests/frontend/IconButton.test.js +++ b/tests/frontend/IconButton.test.js @@ -4,13 +4,13 @@ import IconButton from "../../meshchatx/src/frontend/components/IconButton.vue"; function mountIconButton(slots = {}) { return mount(IconButton, { - slots: { default: slots.default ?? "X" }, + slots: { default: slots.default ?? 'X' }, }); } describe("IconButton UI", () => { it("renders button with slot content", () => { - const wrapper = mountIconButton({ default: "+" }); + const wrapper = mountIconButton({ default: '+' }); expect(wrapper.find("button").exists()).toBe(true); expect(wrapper.find(".icon").exists()).toBe(true); expect(wrapper.text()).toContain("+"); diff --git a/tests/frontend/LoadTimePerformance.test.js b/tests/frontend/LoadTimePerformance.test.js index c84645b..a13fc3d 100644 --- a/tests/frontend/LoadTimePerformance.test.js +++ b/tests/frontend/LoadTimePerformance.test.js @@ -34,8 +34,8 @@ vi.mock("../../meshchatx/src/frontend/js/GlobalEmitter", () => ({ default: { on: vi.fn(), off: vi.fn(), emit: vi.fn() }, })); -const MaterialDesignIcon = { template: "
", props: ["iconName"] }; -const LxmfUserIcon = { template: "
" }; +const MaterialDesignIcon = { template: '
', props: ["iconName"] }; +const LxmfUserIcon = { template: '
' }; function makePropagationNode(i) { return { @@ -107,7 +107,9 @@ describe("Load time with prefilled data", () => { expect(wrapper.vm.paginatedNodes.length).toBe(20); expect(loadMs).toBeLessThan(MAX_PROP_NODES_MS); if (process.env.CI !== "true") { - console.log(`Propagation nodes: ${count} nodes loaded in ${loadMs.toFixed(0)}ms (max ${MAX_PROP_NODES_MS}ms)`); + console.log( + `Propagation nodes: ${count} nodes loaded in ${loadMs.toFixed(0)}ms (max ${MAX_PROP_NODES_MS}ms)` + ); } }); }); @@ -148,7 +150,9 @@ describe("Load time with prefilled data", () => { expect(wrapper.vm.displayedConversations.length).toBe(count); expect(end - start).toBeLessThan(MAX_MESSAGES_ANNOUNCES_MS); if (process.env.CI !== "true") { - console.log(`Messages: ${count} conversations in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)`); + console.log( + `Messages: ${count} conversations in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)` + ); } }); }); @@ -190,7 +194,9 @@ describe("Load time with prefilled data", () => { expect(wrapper.vm.searchedPeers.length).toBe(count); expect(end - start).toBeLessThan(MAX_MESSAGES_ANNOUNCES_MS); if (process.env.CI !== "true") { - console.log(`Announces (messages): ${count} peers in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)`); + console.log( + `Announces (messages): ${count} peers in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)` + ); } }); }); @@ -220,7 +226,7 @@ describe("Load time with prefilled data", () => { components: { MaterialDesignIcon, IconButton: { template: "" }, - DropDownMenu: { template: "
" }, + DropDownMenu: { template: '
' }, DropDownMenuItem: { template: "
" }, }, mocks: { $t: (key) => key }, @@ -233,7 +239,9 @@ describe("Load time with prefilled data", () => { expect(wrapper.vm.searchedNodes.length).toBe(count); expect(end - start).toBeLessThan(MAX_NOMADNET_NODES_MS); if (process.env.CI !== "true") { - console.log(`NomadNet nodes: ${count} nodes in ${(end - start).toFixed(0)}ms (max ${MAX_NOMADNET_NODES_MS}ms)`); + console.log( + `NomadNet nodes: ${count} nodes in ${(end - start).toFixed(0)}ms (max ${MAX_NOMADNET_NODES_MS}ms)` + ); } }); }); diff --git a/tests/frontend/MessagesSidebar.test.js b/tests/frontend/MessagesSidebar.test.js index 0f6b387..cc42f20 100644 --- a/tests/frontend/MessagesSidebar.test.js +++ b/tests/frontend/MessagesSidebar.test.js @@ -20,8 +20,8 @@ vi.mock("../../meshchatx/src/frontend/js/Utils", () => ({ }, })); -const MaterialDesignIcon = { template: "
", props: ["iconName"] }; -const LxmfUserIcon = { template: "
" }; +const MaterialDesignIcon = { template: '
', props: ["iconName"] }; +const LxmfUserIcon = { template: '
' }; function defaultProps(overrides = {}) { return { @@ -89,7 +89,9 @@ describe("MessagesSidebar UI", () => { await announcesTab.trigger("click"); await wrapper.vm.$nextTick(); expect(wrapper.vm.tab).toBe("announces"); - expect(wrapper.text()).toMatch(/messages\.search_placeholder_announces|messages\.no_peers_discovered|messages\.waiting_for_announce/); + expect(wrapper.text()).toMatch( + /messages\.search_placeholder_announces|messages\.no_peers_discovered|messages\.waiting_for_announce/ + ); }); it("emits folder-click when All Messages is clicked", async () => { @@ -154,7 +156,7 @@ describe("MessagesSidebar UI", () => { ], }); await wrapper.vm.$nextTick(); - const selectionBtn = wrapper.find("button[title=\"Selection Mode\"]"); + const selectionBtn = wrapper.find('button[title="Selection Mode"]'); expect(selectionBtn.exists()).toBe(true); await selectionBtn.trigger("click"); await wrapper.vm.$nextTick(); @@ -182,6 +184,9 @@ describe("MessagesSidebar UI", () => { const row = wrapper.find(".conversation-item"); await row.trigger("click"); expect(wrapper.emitted("conversation-click")).toBeTruthy(); - expect(wrapper.emitted("conversation-click")[0][0]).toMatchObject({ destination_hash: "dest1", display_name: "Bob" }); + expect(wrapper.emitted("conversation-click")[0][0]).toMatchObject({ + destination_hash: "dest1", + display_name: "Bob", + }); }); }); diff --git a/tests/frontend/NotificationBell.test.js b/tests/frontend/NotificationBell.test.js index dbeb02f..b789cd3 100644 --- a/tests/frontend/NotificationBell.test.js +++ b/tests/frontend/NotificationBell.test.js @@ -10,7 +10,7 @@ vi.mock("../../meshchatx/src/frontend/js/Utils", () => ({ default: { formatTimeAgo: (d) => "1h ago" }, })); -const MaterialDesignIcon = { template: "
", props: ["iconName"] }; +const MaterialDesignIcon = { template: '
', props: ["iconName"] }; function mountBell(options = {}) { return mount(NotificationBell, { diff --git a/tests/frontend/SidebarLink.test.js b/tests/frontend/SidebarLink.test.js index 00b21ad..8402e62 100644 --- a/tests/frontend/SidebarLink.test.js +++ b/tests/frontend/SidebarLink.test.js @@ -5,8 +5,7 @@ import SidebarLink from "../../meshchatx/src/frontend/components/SidebarLink.vue const RouterLinkStub = { name: "RouterLinkStub", props: ["to"], - template: - '', + template: '', methods: { navigate(e) { if (e) e.preventDefault(); @@ -18,7 +17,7 @@ function mountSidebarLink(props = {}, slots = {}) { return mount(SidebarLink, { props: { to: { name: "messages" }, ...props }, slots: { - icon: "I", + icon: 'I', text: "Messages", ...slots, }, diff --git a/tests/frontend/VisualizerOptimization.test.js b/tests/frontend/VisualizerOptimization.test.js index a85a65a..8d3cee0 100644 --- a/tests/frontend/VisualizerOptimization.test.js +++ b/tests/frontend/VisualizerOptimization.test.js @@ -243,51 +243,51 @@ describe("NetworkVisualiser Optimization and Abort", () => { expect(end - start).toBeLessThan(100); // Should be very fast }); - it("performance: icon cache hit vs miss for 500 nodes", async () => { - vi.spyOn(NetworkVisualiser.methods, "init").mockImplementation(() => {}); - const wrapper = mountVisualiser(); + it("performance: icon cache hit vs miss for 500 nodes", async () => { + vi.spyOn(NetworkVisualiser.methods, "init").mockImplementation(() => {}); + const wrapper = mountVisualiser(); - // Setup 500 nodes with the same icon - const iconInfo = { icon_name: "test", foreground_colour: "#000", background_colour: "#fff" }; - wrapper.vm.pathTable = Array.from({ length: 500 }, (_, i) => ({ hash: `h${i}`, interface: "eth0", hops: 1 })); - wrapper.vm.announces = wrapper.vm.pathTable.reduce((acc, cur) => { - acc[cur.hash] = { - destination_hash: cur.hash, - aspect: "lxmf.delivery", - display_name: "node", - lxmf_user_icon: iconInfo, - }; - return acc; - }, {}); - wrapper.vm.conversations = wrapper.vm.pathTable.reduce((acc, cur) => { - acc[cur.hash] = { lxmf_user_icon: iconInfo }; - return acc; - }, {}); + // Setup 500 nodes with the same icon + const iconInfo = { icon_name: "test", foreground_colour: "#000", background_colour: "#fff" }; + wrapper.vm.pathTable = Array.from({ length: 500 }, (_, i) => ({ hash: `h${i}`, interface: "eth0", hops: 1 })); + wrapper.vm.announces = wrapper.vm.pathTable.reduce((acc, cur) => { + acc[cur.hash] = { + destination_hash: cur.hash, + aspect: "lxmf.delivery", + display_name: "node", + lxmf_user_icon: iconInfo, + }; + return acc; + }, {}); + wrapper.vm.conversations = wrapper.vm.pathTable.reduce((acc, cur) => { + acc[cur.hash] = { lxmf_user_icon: iconInfo }; + return acc; + }, {}); - // Mock createIconImage to have some delay for the "miss" case - wrapper.vm.createIconImage = vi.fn().mockImplementation(async () => { - // Add a tiny delay to ensure "miss" is always measurable - await new Promise((r) => setTimeout(r, 0)); - return "blob:mock-icon"; - }); - - const startMiss = performance.now(); - await wrapper.vm.processVisualization(); - const endMiss = performance.now(); - const missTime = endMiss - startMiss; - - // Second run will hit the cache check in processVisualization - // so it won't even call createIconImage. - const startHit = performance.now(); - await wrapper.vm.processVisualization(); - const endHit = performance.now(); - const hitTime = endHit - startHit; - - console.log(`Icon cache MISS for 500 nodes: ${missTime.toFixed(2)}ms`); - console.log(`Icon cache HIT for 500 nodes: ${hitTime.toFixed(2)}ms`); - - // Cache hit should be significantly faster, but we allow for some - // environmental noise in CI environments. - expect(hitTime).toBeLessThan(missTime + 200); + // Mock createIconImage to have some delay for the "miss" case + wrapper.vm.createIconImage = vi.fn().mockImplementation(async () => { + // Add a tiny delay to ensure "miss" is always measurable + await new Promise((r) => setTimeout(r, 0)); + return "blob:mock-icon"; }); + + const startMiss = performance.now(); + await wrapper.vm.processVisualization(); + const endMiss = performance.now(); + const missTime = endMiss - startMiss; + + // Second run will hit the cache check in processVisualization + // so it won't even call createIconImage. + const startHit = performance.now(); + await wrapper.vm.processVisualization(); + const endHit = performance.now(); + const hitTime = endHit - startHit; + + console.log(`Icon cache MISS for 500 nodes: ${missTime.toFixed(2)}ms`); + console.log(`Icon cache HIT for 500 nodes: ${hitTime.toFixed(2)}ms`); + + // Cache hit should be significantly faster, but we allow for some + // environmental noise in CI environments. + expect(hitTime).toBeLessThan(missTime + 200); + }); });