From 083de6418fea287bb96941fcb320adc3789403bc Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:36:43 +0200 Subject: [PATCH] web: fix federation for multi protocol (#722) * web: fix federation for multi protocol * web: fix short name emojis * web: address review comments * ci: fix the codeql gap * ci: fix the codeql gap * ci: fix the codeql gap * ci: remove swift --- .github/workflows/codeql.yml | 2 +- web/lib/potato_mesh/application/federation.rb | 5 ++- .../js/app/__tests__/main-protocol.test.js | 43 +++++++++++++++++++ web/public/assets/js/app/main.js | 22 +++++----- web/public/assets/styles/base.css | 9 ++++ web/spec/app_spec.rb | 15 +++++++ web/views/layouts/app.erb | 8 ++-- 7 files changed, 88 insertions(+), 16 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 45d54e7..16f383c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,7 +23,7 @@ on: jobs: analyze: name: Analyze (${{ matrix.language }}) - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + runs-on: ubuntu-latest permissions: security-events: write packages: read diff --git a/web/lib/potato_mesh/application/federation.rb b/web/lib/potato_mesh/application/federation.rb index 90f06c1..db76972 100644 --- a/web/lib/potato_mesh/application/federation.rb +++ b/web/lib/potato_mesh/application/federation.rb @@ -297,9 +297,12 @@ module PotatoMesh def shutdown_federation_background_work!(timeout: nil) request_federation_shutdown! timeout_value = timeout || PotatoMesh::Config.federation_shutdown_timeout_seconds + # Drain the worker pool first so federation threads blocked in + # wait_for_federation_tasks unblock promptly instead of waiting + # for each task's individual timeout to expire. + shutdown_federation_worker_pool! stop_federation_thread!(:initial_federation_thread, timeout: timeout_value) stop_federation_thread!(:federation_thread, timeout: timeout_value) - shutdown_federation_worker_pool! clear_federation_crawl_state! end diff --git a/web/public/assets/js/app/__tests__/main-protocol.test.js b/web/public/assets/js/app/__tests__/main-protocol.test.js index d0c3c8a..0b2fb4c 100644 --- a/web/public/assets/js/app/__tests__/main-protocol.test.js +++ b/web/public/assets/js/app/__tests__/main-protocol.test.js @@ -346,3 +346,46 @@ test('createMessageChatEntry: meshtastic message with @[Name] is NOT resolved as assert.ok(shortNameCount <= 1, 'only the sender badge should be present, no mention badge'); }); }); + +// --- renderShortHtml badge padding --- + +test('renderShortHtml leaves 4-char ASCII names unpadded', () => { + withApp(() => { + const html = globalThis.PotatoMesh.renderShortHtml('0ac7', 'CLIENT'); + assert.ok(!html.includes(' 0ac7'), 'should not add leading space'); + assert.ok(!html.includes('0ac7 '), 'should not add trailing space'); + }); +}); + +test('renderShortHtml adds single space padding for short emoji names', () => { + withApp(() => { + const html = globalThis.PotatoMesh.renderShortHtml('\u26A1', 'CLIENT'); + // Should produce " ⚡ " — one leading, one trailing space (as  ) + assert.ok(html.includes(' \u26A1 '), 'emoji should have one space on each side'); + // Should NOT have double leading spaces + assert.ok(!html.includes('  \u26A1'), 'should not double-pad emoji'); + }); +}); + +test('renderShortHtml adds single space padding for surrogate pair emoji', () => { + withApp(() => { + const html = globalThis.PotatoMesh.renderShortHtml('\uD83D\uDE43', 'CLIENT'); + // 🙃 is a surrogate pair (length 2 in JS) but 1 grapheme + assert.ok(html.includes(' \uD83D\uDE43 '), 'surrogate emoji should have one space on each side'); + }); +}); + +test('renderShortHtml adds single space padding for ZWJ emoji sequence', () => { + withApp(() => { + const zwj = '\u{1F3C3}\u{200D}\u{2642}\u{FE0F}'; // 🏃‍♂️ — length 5, 1 grapheme + const html = globalThis.PotatoMesh.renderShortHtml(zwj, 'CLIENT'); + assert.ok(html.includes(` ${zwj} `), 'ZWJ emoji should have one space on each side'); + }); +}); + +test('renderShortHtml adds single space padding for plain 2-char name', () => { + withApp(() => { + const html = globalThis.PotatoMesh.renderShortHtml('ab', 'CLIENT'); + assert.ok(html.includes(' ab '), '2-char name should have one space on each side'); + }); +}); diff --git a/web/public/assets/js/app/main.js b/web/public/assets/js/app/main.js index 1d6096c..f70b664 100644 --- a/web/public/assets/js/app/main.js +++ b/web/public/assets/js/app/main.js @@ -1853,22 +1853,22 @@ export function initializeApp(config) { infoAttr = attrParts.join(''); } if (!short) { - return `?   `; + return ` ? `; } - // Centre the label within a 4-column badge. padStart alone only adds - // leading spaces, producing " C" for a 1-char name with no trailing - // space. Instead distribute padding evenly: 1-char → " C ", 2-char → - // " AB ", 3-char → " ABC", 4-char → unchanged. Names already at 4+ - // chars are left as-is (meshtastic always stores exactly 4; the Ruby - // COMPANION override also produces exactly 4). + // Pad the label for the badge. For plain-ASCII names that are already + // 4 characters (meshtastic always stores exactly 4) no padding is added. + // Shorter names or names containing emoji/non-ASCII get a single space + // on each side — grapheme width varies too much for character-count + // centering to work reliably. const raw = String(short); + const graphemeCount = typeof Intl !== 'undefined' && Intl.Segmenter + ? [...new Intl.Segmenter().segment(raw)].length + : raw.length; let centred; - if (raw.length >= 4) { + if (graphemeCount >= 4) { centred = raw; } else { - const leading = Math.ceil((4 - raw.length) / 2); - const trailing = 4 - raw.length - leading; - centred = ' '.repeat(leading) + raw + ' '.repeat(trailing); + centred = ` ${raw} `; } const padded = escapeHtml(centred).replace(/ /g, ' '); const protocol = nodeData?.protocol ?? null; diff --git a/web/public/assets/styles/base.css b/web/public/assets/styles/base.css index c8a6eb3..9751a00 100644 --- a/web/public/assets/styles/base.css +++ b/web/public/assets/styles/base.css @@ -274,6 +274,15 @@ h1 { min-width: 0; } +.site-title__link { + display: inline-flex; + align-items: center; + gap: inherit; + color: inherit; + text-decoration: none; + min-width: 0; +} + .site-title-text { min-width: 0; max-width: 100%; diff --git a/web/spec/app_spec.rb b/web/spec/app_spec.rb index 90c014e..1203fbe 100644 --- a/web/spec/app_spec.rb +++ b/web/spec/app_spec.rb @@ -1311,6 +1311,13 @@ RSpec.describe "Potato Mesh Sinatra app" do expect(last_response.body).to include('class="footer-content"') end + it "renders the site title as a link to the dashboard" do + get "/" + + expect(last_response.body).to include('class="site-title__link"') + expect(last_response.body).to match(%r{}) + end + it "renders the federation instance selector when federation is enabled" do get "/" @@ -6980,6 +6987,14 @@ RSpec.describe "Potato Mesh Sinatra app" do expect(last_response.body).to include(node["node_id"]) end + it "does not render the meta row on the node detail page" do + node = nodes_fixture.first + get "/nodes/#{node["node_id"]}" + expect(last_response).to be_ok + expect(last_response.body).not_to include('id="metaRow"') + expect(last_response.body).not_to include('id="refreshBtn"') + end + it "returns 404 when the node cannot be located" do get "/nodes/!deadbeef" expect(last_response.status).to eq(404) diff --git a/web/views/layouts/app.erb b/web/views/layouts/app.erb index d714485..2afd156 100644 --- a/web/views/layouts/app.erb +++ b/web/views/layouts/app.erb @@ -78,7 +78,7 @@ show_footer = !full_screen_view || %i[charts federation].include?(view_mode) footer_slim = %i[charts federation].include?(view_mode) show_filter_input = !%i[node_detail charts federation].include?(view_mode) - show_meta_row = !%i[charts federation].include?(view_mode) + show_meta_row = !%i[node_detail charts federation].include?(view_mode) nodes_nav_href = "/nodes" nodes_nav_active = %i[nodes node_detail].include?(view_mode) federation_nav_enabled = !private_mode && federation_enabled @@ -99,8 +99,10 @@