feat: traceroute highlighting and UI improvements#2
Conversation
- Add clickable popups to route segments on map showing endpoints and usage count - Display recent traceroute routes in Messages tab (forward/return with SNR) - Show route age (e.g., "15m ago" or "2h ago") - Auto-fetch traceroutes when Messages tab is active - Format routes with node names and SNR values - Style route popups with Catppuccin theme 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add footer at bottom of app with version number (v0.1) - Include GitHub link to repository - Style with Catppuccin theme colors - Use flexbox layout to keep footer at bottom - Add hover effects and proper spacing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Update height calculation from 140px to 240px to account for footer - Add min-height to ensure map has reasonable minimum size - Change from fixed height to max-height for better flex behavior 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add min-width: 500px to map-container to prevent collapse - Change flex property to 1 1 auto for better flexibility - Add height: 100% to ensure proper vertical sizing - Ensures map always has visible width even when flex shrinks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Change max-height back to height for split view container - Without explicit height, container was collapsing and map wasn't rendering - Keep min-height: 600px as fallback for smaller screens 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add logo to header and settings page with proper sizing - Move map controls (Show Routes checkbox) to right side - Add DM buttons to node popups for quick direct messaging - Highlight selected node's traceroute path as red dashed line on map - Implement traceroute deduplication in database (only keep most recent per source/destination pair) - Filter traceroute messages (portnum 70) from Primary channel display - Fix full-width layout for messages and info tabs - Clean up excessive console logging 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
Claude finished @Yeraze's task —— View job Code Review Complete ✅Todo List:
📋 Final Review Summary🎯 Overall Assessment: GOODThis PR successfully implements the planned traceroute highlighting and UI improvements with solid code quality. The features work as intended and the implementation follows React/TypeScript best practices. 🚀 StrengthsFeature Implementation
Code Quality
|
This commit includes three critical fixes for the auto-upgrade system: **Bug Fix #1: Network name interpreted as image name** - Location: scripts/upgrade-watchdog.sh:126-136 - Problem: Shell variable expansion ${network:+--network "$network"} caused word-splitting where network name was interpreted as the image name - Solution: Rewrote using positional parameters (set --) with hardcoded image name to prevent word-splitting issues **Bug Fix #2: Empty environment variable causing trailing -e flag** - Location: scripts/upgrade-watchdog.sh:109-117 - Problem: Docker inspect returned empty environment variable creating a trailing -e flag with no value, consuming the hardcoded image name - Solution: Added grep -v '^$' to filter empty lines before converting to -e flags **Feature: Auto-reload frontend on version change** - Location: src/App.tsx:1650-1672 - Feature: Frontend now polls /api/health to detect backend version changes - Behavior: Automatically reloads page when backend version changes (e.g., after auto-upgrade) - Benefits: Works even when user is logged out, uses existing health endpoint Tested with upgrade from v2.16.0-1 → v2.16.1 (official release). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit fixes three critical bugs that prevented security flags from being cleared properly, causing nodes to accumulate permanently in the security list. ## Bug #1: updateNodeLowEntropyFlag preserved old details when clearing **File:** src/services/database.ts:1758 When clearing the low-entropy flag, the function incorrectly preserved the old "Known low-entropy key detected" message in keySecurityIssueDetails even though the flag was set to 0. This left nodes with confusing/stale security messages. **Fix:** Rewrote the logic to properly clean up details when clearing flags: - When setting flag: Combine low-entropy details with existing duplicate info - When clearing flag: Remove low-entropy text, preserve only duplicate info - Properly clear all details when no security issues remain ## Bug #2: Scanner never processed nodes without public keys **File:** src/server/services/duplicateKeySchedulerService.ts:103-116 The scanner only processed nodes with public keys. If a node had security flags but later lost its key (went offline, key cleared, etc.), the scanner would skip it entirely and never clear the orphaned flags. **Fix:** Added second pass to check ALL nodes for orphaned security flags: - After main scan, iterate through all nodes in database - Identify nodes without keys that still have security flags set - Clear those orphaned flags with appropriate logging ## Bug #3: Security flags ignored by upsertNode **File:** src/services/database.ts:1569-1682 When meshtasticManager detected security issues and set flags in nodeData, those flags were completely ignored by upsertNode() because INSERT/UPDATE statements didn't include the security flag columns. **Fix:** Added security flag support to upsertNode: - Added keyIsLowEntropy, duplicateKeyDetected, keySecurityIssueDetails to UPDATE - Added same columns to INSERT statement - Added proper parameter handling with boolean-to-integer conversion - Used COALESCE to preserve existing values when not explicitly provided ## Impact - Nodes no longer accumulate permanently in security list - Flags properly cleared when nodes go offline or fix their keys - Security warnings cleared within 24 hours (next scheduled scan) - Details text properly cleaned up when security issues resolved ## Testing - TypeScript compilation: ✓ Pass - Build: ✓ Pass - No database migrations required (columns already exist) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Implement infinite scrolling with @tanstack/react-virtual - Load packets in batches of 100 as user scrolls down - Add packet number column showing position in list (#1, #2, etc.) - Preserve scroll position during polling updates - Merge new packets intelligently to avoid scroll resets - Show loading indicator when fetching more packets - Only render visible rows for performance (virtual scrolling) Fixes scroll position being reset to row 100 on each poll by preserving already-loaded packets beyond the first batch and only updating when new packets arrive. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…prop Addresses review feedback on PR Yeraze#3344. Yeraze#1 Theme gallery (ThemeDocumentation) no longer calls the legacy setTheme, which forced appearanceMode='dark' and overwrote both theme slots — silently kicking system-mode users into dark mode and clobbering their light theme. Each card now offers "Apply as Dark" / "Apply as Light" that update only the corresponding slot and never change the appearance mode, matching the custom theme cards. Adds regression tests for the per-slot behavior. Yeraze#2 Remove the now-dead onThemeChange prop from SettingsTabProps and its two pass-sites (App.tsx, GlobalSettingsPage.tsx), plus the now-unused setTheme destructuring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add system appearance theme selection * fix(theme): per-slot apply in theme gallery; drop dead onThemeChange prop Addresses review feedback on PR #3344. #1 Theme gallery (ThemeDocumentation) no longer calls the legacy setTheme, which forced appearanceMode='dark' and overwrote both theme slots — silently kicking system-mode users into dark mode and clobbering their light theme. Each card now offers "Apply as Dark" / "Apply as Light" that update only the corresponding slot and never change the appearance mode, matching the custom theme cards. Adds regression tests for the per-slot behavior. #2 Remove the now-dead onThemeChange prop from SettingsTabProps and its two pass-sites (App.tsx, GlobalSettingsPage.tsx), plus the now-unused setTheme destructuring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Randall Hand <randall.hand@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- Mark messages played from the delivery-success callback, not at enqueue:
handleCommand returns { responses, playOnDelivery }; a dropped body DM now
leaves the message pending instead of losing it. (Yeraze#1)
- Wire purgeExpired into databaseMaintenanceService so expired rows are
reclaimed daily (purgeExpired returns a count). (Yeraze#2)
- Count the per-recipient cap across the recipient's identity forms via an
injected node resolver, so it can't be bypassed by addressing one node by
several name forms. (Yeraze#3)
- Mailbox bypasses the per-node cooldown (interactive flow). (Yeraze#4)
- inbox play <sender> filter matches !hex/node-num forms too. (Yeraze#5)
- Non-DM commands return [] (no unsolicited DM). (Yeraze#6)
- inbox delete returns the same response for not-yours vs non-existent ids
(no id enumeration). (Yeraze#7)
- Wrap the mailbox dispatch in try/catch like the script branch. (Yeraze#8)
- Remove the command-prefix tolerance: canonical msg/inbox only. (Yeraze#9)
- Use shared nodeIdHex (unsigned coerce) for the mailbox log target. (Yeraze#10)
Docs + tests updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gaps (#3578) Addresses Claude Code Review findings on PR #3578: - Sanitize device-controlled message before logging/forwarding: strip control chars (log-injection defense) and bound length to 500 (#9, #10). - Widen protected-cap regex to {1,8} hex digits so a short node id still reconciles (#4). - Exclude clientNotification from the unknown-FromRadio debug JSON.stringify dump (#3). - Drop the redundant `&& this.sourceId` guard (always set) and comment why the toast still fires when the DB revert fails (#2, #5). - Frontend: name the level magic numbers and note they mirror the backend NOTIFICATION_LEVEL (#1). - Tests: sanitizer (control chars/whitespace/truncation/empty), short-hex-id parse, and the DB-revert-failure path (#6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
…8 favorite/ignore cap (#3548) (#3578) * feat(notifications): surface device ClientNotifications + handle firmware 2.8 favorite/ignore cap (#3548) MeshMonitor decoded FromRadio.ClientNotification (mesh.proto field 16) and dropped it. Surface these device-originated warnings/errors as toasts, and handle the firmware 2.8 protected-node-cap refusal for Set Favorite / Ignore. Backend: - clientNotificationPolicy.ts (pure/testable): suppression patterns (recurring power-save "sleeping for N interval" INFO + key-verification variants + empty messages), the 2.8 protected-node-cap refusal parser, level->severity mapping, and a per-source ToastThrottle that dedupes identical messages within a window. - meshtasticProtobufService.ts: add the clientNotification dispatch branch (was falling through to the generic catch-all). - dataEventEmitter.ts: client-notification event type + emitClientNotification(). - meshtasticManager.ts: handleClientNotification() reverts the optimistic favorite/ignore flag and re-broadcasts the node when the device refuses at the protected-node cap, then applies the suppression/throttle policy and emits the toast event. Source-scoped throughout. Frontend: - DeviceNotificationToaster.tsx: listens for client-notification inside ToastProvider, maps level->severity, shows the toast. Wired into App.tsx. - WS forwarding + per-source room filtering needed no changes. Scope: the 2.8 NodeDB warm-tier restructure and the on-disk snr_q4 field do NOT affect the over-the-air wire MeshMonitor reads. OTA NodeInfo.snr stays a float in dB; no protobuf/decode change. A regression test guards this. The cap-refusal warning is only emitted by firmware for the locally-connected node (from == 0). Tests (20 new, full suite green): policy unit tests, manager handler tests (reconciliation/suppression/throttle, per-source), and protobuf dispatch + SNR float guard. Docs: FAQ (node count + blocked-node 2.8 behavior; device-notification toasts), IgnoredNodesSection inline help, CHANGELOG, and the support plan dev-note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 * address review: sanitize device notification text, robustness + test gaps (#3578) Addresses Claude Code Review findings on PR #3578: - Sanitize device-controlled message before logging/forwarding: strip control chars (log-injection defense) and bound length to 500 (#9, #10). - Widen protected-cap regex to {1,8} hex digits so a short node id still reconciles (#4). - Exclude clientNotification from the unknown-FromRadio debug JSON.stringify dump (#3). - Drop the redundant `&& this.sourceId` guard (always set) and comment why the toast still fires when the DB revert fails (#2, #5). - Frontend: name the level magic numbers and note they mirror the backend NOTIFICATION_LEVEL (#1). - Tests: sanitizer (control chars/whitespace/truncation/empty), short-hex-id parse, and the DB-revert-failure path (#6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Mark messages played from the delivery-success callback, not at enqueue:
handleCommand returns { responses, playOnDelivery }; a dropped body DM now
leaves the message pending instead of losing it. (Yeraze#1)
- Wire purgeExpired into databaseMaintenanceService so expired rows are
reclaimed daily (purgeExpired returns a count). (Yeraze#2)
- Count the per-recipient cap across the recipient's identity forms via an
injected node resolver, so it can't be bypassed by addressing one node by
several name forms. (Yeraze#3)
- Mailbox bypasses the per-node cooldown (interactive flow). (Yeraze#4)
- inbox play <sender> filter matches !hex/node-num forms too. (Yeraze#5)
- Non-DM commands return [] (no unsolicited DM). (Yeraze#6)
- inbox delete returns the same response for not-yours vs non-existent ids
(no id enumeration). (Yeraze#7)
- Wrap the mailbox dispatch in try/catch like the script branch. (Yeraze#8)
- Remove the command-prefix tolerance: canonical msg/inbox only. (Yeraze#9)
- Use shared nodeIdHex (unsigned coerce) for the mailbox log target. (Yeraze#10)
Docs + tests updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ore) (#3538) * feat: add Dead Drop / Mailbox auto-responder (async message store) A per-source 'mesh voicemail': a node DMs the radio `msg <name> <text>` and the message is held until the named recipient retrieves it via `inbox` / `inbox play`. Implemented as a new auto-responder responseType ('mailbox'), reusing the existing DM-gating, per-node cooldown, param extraction, and per-source scoping. - DB: dead_drop_messages table (SQLite/PostgreSQL/MySQL) + migration 092 - Repository (Drizzle-only) + DatabaseService.deadDrop accessor - DeadDropService: command brain (store/inbox/play[/sender]/delete/clear, 180-byte cap, per-recipient & per-sender caps, 7-day expiry, batch play) - meshtasticManager: 'mailbox' responseType dispatch branch - UI: 'Mailbox' response type option + guidance in the auto-responder editor - Tests: 34 (migration registry, repository perSource, service) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(dead-drop): add Mailbox option to the Add-Trigger form select The Mailbox response type was only added to the per-trigger edit view (TriggerItem); the separate Add-Trigger form in AutoResponderSection had its own type <select> (Text/HTTP/Script) that was missed, so new mailbox triggers couldn't be created from the UI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(dead-drop): don't require response text to add a Mailbox trigger The Add button's disabled gate required a non-empty response field for all types; Mailbox has no response, so the button stayed greyed out. Exempt mailbox from the response-required check (matches validateResponse). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(dead-drop): accept mailbox responseType in settings save validation The settings-save validator required a non-empty response for every trigger and only allowed text/http/script responseTypes, so saving a Mailbox trigger failed with a generic 400. Exempt mailbox from the response-required check and add it to the responseType allowlist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(dead-drop): settings-save accepts mailbox triggers without response text Regression coverage for the mailbox responseType in the autoResponderTriggers validator: a mailbox trigger with empty response now saves (200), while non-mailbox empty responses and unknown responseTypes still 400. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(dead-drop): tolerate optional command keyword prefix The mailbox service parsed hardcoded msg/inbox, but a trigger configured with prefixed keywords (e.g. betamsg/betainbox, to coexist with another responder already using msg/inbox) would fire the handler yet fall through to help. Strip an optional non-space prefix from the leading verb so prefixed keywords parse correctly; no-op for plain msg/inbox. Caught by live over-the-air testing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(dead-drop): add Mailbox feature docs + live testing brief - automation.md: Mailbox response type + 'Mailbox (Dead Drop)' section (commands, recipient matching, delivery format, limits, command-prefix coexistence, configuration). - dev-notes/DEAD_DROP_TESTING.md: architecture, automated coverage, and the over-the-air validation results (ALTO MF / ALTO LF / ZN Office). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(dead-drop): register dead_drop_messages in migrate-db table lists The cross-database migrate-db CLI tracks every schema table in TABLE_ORDER / SKIP_TABLES; migrationTables.test.ts fails if a new schema table isn't listed. Add dead_drop_messages to TABLE_ORDER and SOURCE_SCOPED_TABLES (it carries a sourceId, like auto_favorite_targets). Caught by the full Vitest suite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(dead-drop): address PR review feedback - Mark messages played from the delivery-success callback, not at enqueue: handleCommand returns { responses, playOnDelivery }; a dropped body DM now leaves the message pending instead of losing it. (#1) - Wire purgeExpired into databaseMaintenanceService so expired rows are reclaimed daily (purgeExpired returns a count). (#2) - Count the per-recipient cap across the recipient's identity forms via an injected node resolver, so it can't be bypassed by addressing one node by several name forms. (#3) - Mailbox bypasses the per-node cooldown (interactive flow). (#4) - inbox play <sender> filter matches !hex/node-num forms too. (#5) - Non-DM commands return [] (no unsolicited DM). (#6) - inbox delete returns the same response for not-yours vs non-existent ids (no id enumeration). (#7) - Wrap the mailbox dispatch in try/catch like the script branch. (#8) - Remove the command-prefix tolerance: canonical msg/inbox only. (#9) - Use shared nodeIdHex (unsigned coerce) for the mailbox log target. (#10) Docs + tests updated to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(dead-drop): recommend enabling Verify response on the mailbox trigger Played-state is committed from the delivery-success callback; with Verify response off (maxAttempts=1) a single unacked send could mark a voicemail played on transmit. Document enabling Verify response so undelivered bodies resurface. (PR #3538 review follow-up) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: chrisn <chrisn@DebDev1.corp.tlclocal.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Randall Hand <randall.hand@gmail.com>
Addresses Claude review finding #2: when a socket drop is handled with auto-reconnect disabled, handleUnexpectedDisconnect() left this.nativeBackend pointing at the closed connection, so sendBridgeCommand()'s `!nativeBackend` guard never tripped and callers got a write-to-closed error instead of a clean "disconnected" until /connect ran. Now tears down and nulls the dead backend (connectionState is set to 'disconnected' first so any re-emitted event short-circuits). Added a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
…ter-disconnect (#3705) (#3794) * fix(meshcore): detect socket drops + add user-configurable heartbeat (#3705) Two gaps left a MeshCore source unable to recover from a dropped real-node link without a manual reconnect or container restart: 1. The manager ignored the native backend's own 'disconnected' event (it only logged it), so a socket/serial-level drop left `connected = true`. isConnected() kept returning true, so the Virtual Node server answered AppStart with a stale SelfInfo while real sends silently failed, and nothing recovered. handleUnexpectedDisconnect() now reflects reality (drops to disconnected + stops the VN server) or, when auto-reconnect is enabled, hands off to the existing backoff machinery. An intentionalTeardown guard plus a stale-backend-instance check keep it from firing on our own teardowns. 2. Heartbeat/auto-reconnect was unreachable: neither the source config type nor meshcoreConfigFromSource() carried heartbeatIntervalSeconds, and there was no UI for it — so startHeartbeat() always early-returned and the reconnect path (incl. PR #3706's fix) never ran. Added a heartbeat field to the MeshCore source form (mirroring Meshtastic), plumbed heartbeatIntervalSeconds through MeshCoreSourceConfig and meshcoreConfigFromSource(). The update route already restarts the manager on any config change, so editing it reconnects. Regression tests cover the drop-detection branches and the config passthrough. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 * fix(meshcore): keep manager registered on manual disconnect (#3705) The UI Disconnect (POST /api/sources/:id/disconnect) called meshcoreManagerRegistry.remove(), deleting the manager from the registry. Every /api/sources/:id/meshcore/* route is behind a guard that 404s with "No MeshCore manager for source <id>" when the manager is absent — so after a manual disconnect the page's status/read polling errored out and the source couldn't be driven again without a container restart. Disconnect now tears down the device link via manager.disconnect() (which already stops the VN server, heartbeat and schedulers) while leaving the manager registered, so /meshcore/* keeps serving a clean disconnected state and /connect re-establishes against the existing manager. Mirrors the intent of the autoConnect=false stop/start workflow without making the source unaddressable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 * docs(meshcore): document source heartbeat field + changelog (#3705) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 * fix(meshcore): release dead backend on unrecovered drop (review #3794) Addresses Claude review finding #2: when a socket drop is handled with auto-reconnect disabled, handleUnexpectedDisconnect() left this.nativeBackend pointing at the closed connection, so sendBridgeCommand()'s `!nativeBackend` guard never tripped and callers got a write-to-closed error instead of a clean "disconnected" until /connect ran. Now tears down and nulls the dead backend (connectionState is set to 'disconnected' first so any re-emitted event short-circuits). Added a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Test plan
🤖 Generated with Claude Code