mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-13 04:46:05 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 480798e117 | |||
| 704a3d8a87 | |||
| 96e108037c | |||
| 97aade3632 | |||
| e43584912b | |||
| fccde36ecb | |||
| e631f9b0cc | |||
| b52431616e | |||
| 8446d99df1 | |||
| 8e1e913fcd | |||
| b74137dc72 | |||
| c83f9b0005 | |||
| 9f4737d350 | |||
| 29e9a5f701 | |||
| f0f06671cc | |||
| b1595e479c | |||
| 25df69bfbc |
@@ -29,3 +29,4 @@ references/
|
|||||||
# local Docker compose files
|
# local Docker compose files
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
|
.docker-certs/
|
||||||
|
|||||||
+353
-334
@@ -1,172 +1,191 @@
|
|||||||
|
## [3.6.7] - 2026-03-31
|
||||||
|
|
||||||
|
* Misc: Remove armv7 (for now)
|
||||||
|
|
||||||
|
## [3.6.6] - 2026-03-31
|
||||||
|
|
||||||
|
* Misc: Please I'm begging for the build scripts to be working now
|
||||||
|
|
||||||
|
## [3.6.5] - 2026-03-31
|
||||||
|
|
||||||
|
* Bugfix: Maybe fix problem with publish script
|
||||||
|
|
||||||
|
## [3.6.4] - 2026-03-31
|
||||||
|
|
||||||
|
* Feature: Clarify New Channel/Contact button
|
||||||
|
* Bugfix: Rename "Best RSSI" to "Strongest Neighbor"
|
||||||
|
* Bugfix: Improve layout of Trace pane
|
||||||
|
* Misc: Docker setup improvements
|
||||||
|
|
||||||
## [3.6.3] - 2026-03-30
|
## [3.6.3] - 2026-03-30
|
||||||
|
|
||||||
Feature: Add multi-byte trace
|
* Feature: Add multi-byte trace
|
||||||
Feature: Show node name on discovered node if we know it
|
* Feature: Show node name on discovered node if we know it
|
||||||
Feature: Add docker installation script
|
* Feature: Add docker installation script
|
||||||
Feature: Add historical noise floor to stats
|
* Feature: Add historical noise floor to stats
|
||||||
Feature: Add trace tool
|
* Feature: Add trace tool
|
||||||
Bugfix: 100x performance on statistics endpoint with indices and better queries
|
* Bugfix: 100x performance on statistics endpoint with indices and better queries
|
||||||
Misc: Performance and correctness improvements for backend-of-the-frontend
|
* Misc: Performance and correctness improvements for backend-of-the-frontend
|
||||||
Misc: Reorganize scripts
|
* Misc: Reorganize scripts
|
||||||
|
|
||||||
## [3.6.2] - 2026-03-29
|
## [3.6.2] - 2026-03-29
|
||||||
|
|
||||||
Feature: Be more flexible about timing and volume of full contact offload
|
* Feature: Be more flexible about timing and volume of full contact offload
|
||||||
Feature: Improve room server and repeater ops to be much more clearer about auth status
|
* Feature: Improve room server and repeater ops to be much more clearer about auth status
|
||||||
Feature: Show last error status on integrations
|
* Feature: Show last error status on integrations
|
||||||
Feature: Push multi-platform docker builds
|
* Feature: Push multi-platform docker builds
|
||||||
Bugfix: Fix advert interval time unit display
|
* Bugfix: Fix advert interval time unit display
|
||||||
Bugfix: Don't cast RSSI/SNR to string for community MQTT
|
* Bugfix: Don't cast RSSI/SNR to string for community MQTT
|
||||||
Bugfix: Map uploader follows redirect
|
* Bugfix: Map uploader follows redirect
|
||||||
Misc: Thin out unnecessary cruft in unreads endpoint
|
* Misc: Thin out unnecessary cruft in unreads endpoint
|
||||||
Misc: Fall back gracefully if linked to an unknown contact
|
* Misc: Fall back gracefully if linked to an unknown contact
|
||||||
|
|
||||||
## [3.6.1] - 2026-03-26
|
## [3.6.1] - 2026-03-26
|
||||||
|
|
||||||
Feature: MeshCore Map integration
|
* Feature: MeshCore Map integration
|
||||||
Feature: Add warning screen about bots
|
* Feature: Add warning screen about bots
|
||||||
Feature: Favicon reflects unread message state
|
* Feature: Favicon reflects unread message state
|
||||||
Feature: Show hop map in larger modal
|
* Feature: Show hop map in larger modal
|
||||||
Feature: Add prebuilt frontend install script
|
* Feature: Add prebuilt frontend install script
|
||||||
Feature: Add clean service installer script
|
* Feature: Add clean service installer script
|
||||||
Feature: Swipe in to show menu
|
* Feature: Swipe in to show menu
|
||||||
Bugfix: Invalid backend API path serves error, not fallback index
|
* Bugfix: Invalid backend API path serves error, not fallback index
|
||||||
Bugfix: Fix some spacing/page height issues
|
* Bugfix: Fix some spacing/page height issues
|
||||||
Misc: Misc. bugfixes and performance and test improvements
|
* Misc: Misc. bugfixes and performance and test improvements
|
||||||
|
|
||||||
## [3.6.0] - 2026-03-22
|
## [3.6.0] - 2026-03-22
|
||||||
|
|
||||||
Feature: Add incoming-packet analytics
|
* Feature: Add incoming-packet analytics
|
||||||
Feature: BYOPacket for analysis
|
* Feature: BYOPacket for analysis
|
||||||
Feature: Add room activity to stats view
|
* Feature: Add room activity to stats view
|
||||||
Bugfix: Handle Heltec v3 serial noise
|
* Bugfix: Handle Heltec v3 serial noise
|
||||||
Misc: Swap repeaters and room servers for better ordering
|
* Misc: Swap repeaters and room servers for better ordering
|
||||||
|
|
||||||
## [3.5.0] - 2026-03-19
|
## [3.5.0] - 2026-03-19
|
||||||
|
|
||||||
Feature: Add room server alpha support
|
* Feature: Add room server alpha support
|
||||||
Feature: Add option to force-reset node clock when it's too far ahead
|
* Feature: Add option to force-reset node clock when it's too far ahead
|
||||||
Feature: DMs auto-retry before resorting to flood
|
* Feature: DMs auto-retry before resorting to flood
|
||||||
Feature: Add impulse zero-hop advert
|
* Feature: Add impulse zero-hop advert
|
||||||
Feature: Utilize PATH packets to correctly source a contact's route
|
* Feature: Utilize PATH packets to correctly source a contact's route
|
||||||
Feature: Metrics view on raw packet pane
|
* Feature: Metrics view on raw packet pane
|
||||||
Feature: Metric, Imperial, and Smoots are now selectable for distance display
|
* Feature: Metric, Imperial, and Smoots are now selectable for distance display
|
||||||
Feature: Allow favorites to be sorted
|
* Feature: Allow favorites to be sorted
|
||||||
Feature: Add multi-ack support
|
* Feature: Add multi-ack support
|
||||||
Feature: Password-remember checkbox on repeaters + room servers
|
* Feature: Password-remember checkbox on repeaters + room servers
|
||||||
Bugfix: Serialize radio disconnect in a lock
|
* Bugfix: Serialize radio disconnect in a lock
|
||||||
Bugfix: Fix contact bar layout issues
|
* Bugfix: Fix contact bar layout issues
|
||||||
Bugfix: Fix sidebar ordering for contacts by advert recency
|
* Bugfix: Fix sidebar ordering for contacts by advert recency
|
||||||
Bugfix: Fix version reporting in community MQTT
|
* Bugfix: Fix version reporting in community MQTT
|
||||||
Bugfix: Fix Apprise duplicate names
|
* Bugfix: Fix Apprise duplicate names
|
||||||
Bugfix: Be better about identity resolution in the stats pane
|
* Bugfix: Be better about identity resolution in the stats pane
|
||||||
Misc: Docs, test, and performance enhancements
|
* Misc: Docs, test, and performance enhancements
|
||||||
Misc: Don't prompt "Are you sure" when leaving an unedited interation
|
* Misc: Don't prompt "Are you sure" when leaving an unedited interation
|
||||||
Misc: Log node time on startup
|
* Misc: Log node time on startup
|
||||||
Misc: Improve community MQTT error bubble-up
|
* Misc: Improve community MQTT error bubble-up
|
||||||
Misc: Unread DMs always have a red unread counter
|
* Misc: Unread DMs always have a red unread counter
|
||||||
Misc: Improve information in the debug view to show DB status
|
* Misc: Improve information in the debug view to show DB status
|
||||||
|
|
||||||
## [3.4.1] - 2026-03-16
|
## [3.4.1] - 2026-03-16
|
||||||
|
|
||||||
Bugfix: Improve handling of version information on prebuilt bundles
|
* Bugfix: Improve handling of version information on prebuilt bundles
|
||||||
Bugfix: Improve frontend usability on disconnected radio
|
* Bugfix: Improve frontend usability on disconnected radio
|
||||||
Misc: Docs and readme updates
|
* Misc: Docs and readme updates
|
||||||
Misc: Overhaul DM ingest and frontend state handling
|
* Misc: Overhaul DM ingest and frontend state handling
|
||||||
|
|
||||||
## [3.4.0] - 2026-03-16
|
## [3.4.0] - 2026-03-16
|
||||||
|
|
||||||
Feature: Add radio model and stats display
|
* Feature: Add radio model and stats display
|
||||||
Feature: Add prebuilt frontends, then deleted that and moved to prebuilt release artifacts
|
* Feature: Add prebuilt frontends, then deleted that and moved to prebuilt release artifacts
|
||||||
Bugfix: Misc. frontend performance and correctness fixes
|
* Bugfix: Misc. frontend performance and correctness fixes
|
||||||
Bugfix: Fix same-second same-content DM send collition
|
* Bugfix: Fix same-second same-content DM send collition
|
||||||
Bugfix: Discard clearly-wrong GPS data
|
* Bugfix: Discard clearly-wrong GPS data
|
||||||
Bugfix: Prevent repeater clock skew drift on page nav
|
* Bugfix: Prevent repeater clock skew drift on page nav
|
||||||
Misc: Use repeater's advertised location if we haven't loaded one from repeater admin
|
* Misc: Use repeater's advertised location if we haven't loaded one from repeater admin
|
||||||
Misc: Don't permit invalid fanout configs to be saved ever`
|
* Misc: Don't permit invalid fanout configs to be saved ever`
|
||||||
|
|
||||||
## [3.3.0] - 2026-03-13
|
## [3.3.0] - 2026-03-13
|
||||||
|
|
||||||
Feature: Use dashed lines to show collapsed ambiguous router results
|
* Feature: Use dashed lines to show collapsed ambiguous router results
|
||||||
Feature: Jump to unred
|
* Feature: Jump to unred
|
||||||
Feature: Local channel management to prevent need to reload channel every time
|
* Feature: Local channel management to prevent need to reload channel every time
|
||||||
Feature: Debug endpoint
|
* Feature: Debug endpoint
|
||||||
Feature: Force-singleton channel management
|
* Feature: Force-singleton channel management
|
||||||
Feature: Local node discovery
|
* Feature: Local node discovery
|
||||||
Feature: Node routing discovery
|
* Feature: Node routing discovery
|
||||||
Bugfix: Don't tell users to us npm ci
|
* Bugfix: Don't tell users to us npm ci
|
||||||
Bugfix: Fallback polling dm message persistence
|
* Bugfix: Fallback polling dm message persistence
|
||||||
Bugfix: All native-JS inputs are now modals
|
* Bugfix: All native-JS inputs are now modals
|
||||||
Bugfix: Same-second send collision resolution
|
* Bugfix: Same-second send collision resolution
|
||||||
Bugfix: Proper browser updates on resend
|
* Bugfix: Proper browser updates on resend
|
||||||
Bugfix: Don't use last-heard when we actually want last-advert for path discovery for nodes
|
* Bugfix: Don't use last-heard when we actually want last-advert for path discovery for nodes
|
||||||
Bugfix: Don't treat prefix-matching DM echoes as acks like we do for channel messages
|
* Bugfix: Don't treat prefix-matching DM echoes as acks like we do for channel messages
|
||||||
Misc: Visualizer data layer overhaul for future map work
|
* Misc: Visualizer data layer overhaul for future map work
|
||||||
Misc: Parallelize docker tests
|
* Misc: Parallelize docker tests
|
||||||
|
|
||||||
## [3.2.0] - 2026-03-12
|
## [3.2.0] - 2026-03-12
|
||||||
|
|
||||||
Feature: Improve ambiguous-sender DM handling and visibility
|
* Feature: Improve ambiguous-sender DM handling and visibility
|
||||||
Feature: Allow for toggling of node GPS broadcast
|
* Feature: Allow for toggling of node GPS broadcast
|
||||||
Feature: Add path width to bot and move example to full kwargs
|
* Feature: Add path width to bot and move example to full kwargs
|
||||||
Feature: Improve node map color contrast
|
* Feature: Improve node map color contrast
|
||||||
Bugfix: More accurate tracking of contact data
|
* Bugfix: More accurate tracking of contact data
|
||||||
Bugfix: Misc. frontend performance and bugfixes
|
* Bugfix: Misc. frontend performance and bugfixes
|
||||||
Misc: Clearer warnings on user-key linkage
|
* Misc: Clearer warnings on user-key linkage
|
||||||
Misc: Documentation improvements
|
* Misc: Documentation improvements
|
||||||
|
|
||||||
## [3.1.1] - 2026-03-11
|
## [3.1.1] - 2026-03-11
|
||||||
|
|
||||||
Feature: Add basic auth
|
* Feature: Add basic auth
|
||||||
Feature: SQS fanout
|
* Feature: SQS fanout
|
||||||
Feature: Enrich contact info pane
|
* Feature: Enrich contact info pane
|
||||||
Feature: Search operators for node and channel
|
* Feature: Search operators for node and channel
|
||||||
Feature: Pause radio connection attempts from Radio settings
|
* Feature: Pause radio connection attempts from Radio settings
|
||||||
Feature: New themes! What a great use of time!
|
* Feature: New themes! What a great use of time!
|
||||||
Feature: Github workflows runs for validation
|
* Feature: Github workflows runs for validation
|
||||||
Bugfix: More consistent log format with times
|
* Bugfix: More consistent log format with times
|
||||||
Bugfix: Patch meshcore_py bluetooth eager reconnection out during pauses
|
* Bugfix: Patch meshcore_py bluetooth eager reconnection out during pauses
|
||||||
|
|
||||||
## [3.1.0] - 2026-03-11
|
## [3.1.0] - 2026-03-11
|
||||||
|
|
||||||
Feature: Add basic auth
|
* Feature: Add basic auth
|
||||||
Feature: SQS fanout
|
* Feature: SQS fanout
|
||||||
Feature: Enrich contact info pane
|
* Feature: Enrich contact info pane
|
||||||
Feature: Search operators for node and channel
|
* Feature: Search operators for node and channel
|
||||||
Feature: Pause radio connection attempts from Radio settings
|
* Feature: Pause radio connection attempts from Radio settings
|
||||||
Feature: New themes! What a great use of time!
|
* Feature: New themes! What a great use of time!
|
||||||
Feature: Github workflows runs for validation
|
* Feature: Github workflows runs for validation
|
||||||
Bugfix: More consistent log format with times
|
* Bugfix: More consistent log format with times
|
||||||
Bugfix: Patch meshcore_py bluetooth eager reconnection out during pauses
|
* Bugfix: Patch meshcore_py bluetooth eager reconnection out during pauses
|
||||||
|
|
||||||
## [3.0.0] - 2026-03-10
|
## [3.0.0] - 2026-03-10
|
||||||
|
|
||||||
Feature: Custom regions per-channel
|
* Feature: Custom regions per-channel
|
||||||
Feature: Add custom contact pathing
|
* Feature: Add custom contact pathing
|
||||||
Feature: Corrupt packets are more clear that they're corrupt
|
* Feature: Corrupt packets are more clear that they're corrupt
|
||||||
Feature: Better, faster patterns around background fetching with explicit opt-in for recurring sync if the app detects you need it
|
* Feature: Better, faster patterns around background fetching with explicit opt-in for recurring sync if the app detects you need it
|
||||||
Feature: More consistent icons
|
* Feature: More consistent icons
|
||||||
Feature: Add per-channel local notifications
|
* Feature: Add per-channel local notifications
|
||||||
Feature: New themes
|
* Feature: New themes
|
||||||
Feature: Massive codebase refactor and overhaul
|
* Feature: Massive codebase refactor and overhaul
|
||||||
Bugfix: Fix packet parsing for trace packets
|
* Bugfix: Fix packet parsing for trace packets
|
||||||
Bugfix: Refetch channels on reconnect
|
* Bugfix: Refetch channels on reconnect
|
||||||
Bugfix: Load All on repeater pane on mobile doesn't etend into lower text
|
* Bugfix: Load All on repeater pane on mobile doesn't etend into lower text
|
||||||
Bugfix: Timestamps in logs
|
* Bugfix: Timestamps in logs
|
||||||
Bugfix: Correct wrong clock sync command
|
* Bugfix: Correct wrong clock sync command
|
||||||
Misc: Improve bot error bubble up
|
* Misc: Improve bot error bubble up
|
||||||
Misc: Update to non-lib-included meshcore-decoder version
|
* Misc: Update to non-lib-included meshcore-decoder version
|
||||||
Misc: Revise refactors to be more LLM friendly
|
* Misc: Revise refactors to be more LLM friendly
|
||||||
Misc: Fix script executability
|
* Misc: Fix script executability
|
||||||
Misc: Better logging format with timestamp
|
* Misc: Better logging format with timestamp
|
||||||
Misc: Repeater advert buttons separate flood and one-hop
|
* Misc: Repeater advert buttons separate flood and one-hop
|
||||||
Misc: Preserve repeater pane on navigation away
|
* Misc: Preserve repeater pane on navigation away
|
||||||
Misc: Clearer iconography and coloring for status bar buttons
|
* Misc: Clearer iconography and coloring for status bar buttons
|
||||||
Misc: Search bar to top bar
|
* Misc: Search bar to top bar
|
||||||
|
|
||||||
## [2.7.9] - 2026-03-08
|
## [2.7.9] - 2026-03-08
|
||||||
|
|
||||||
Bugfix: Don't obscure new integration dropdown on session boundary
|
* Bugfix: Don't obscure new integration dropdown on session boundary
|
||||||
|
|
||||||
## [2.7.8] - 2026-03-08
|
## [2.7.8] - 2026-03-08
|
||||||
|
|
||||||
@@ -174,287 +193,287 @@ Bugfix: Don't obscure new integration dropdown on session boundary
|
|||||||
|
|
||||||
## [2.7.8] - 2026-03-08
|
## [2.7.8] - 2026-03-08
|
||||||
|
|
||||||
Bugfix: Improve frontend asset resolution and fixup the build/push script
|
* Bugfix: Improve frontend asset resolution and fixup the build/push script
|
||||||
|
|
||||||
## [2.7.1] - 2026-03-08
|
## [2.7.1] - 2026-03-08
|
||||||
|
|
||||||
Bugfix: Fix historical DM packet length passing
|
* Bugfix: Fix historical DM packet length passing
|
||||||
Misc: Follow better inclusion patterns for the patched meshcore-decoder and just publish the dang package
|
* Misc: Follow better inclusion patterns for the patched meshcore-decoder and just publish the dang package
|
||||||
Misc: Patch a bewildering browser quirk that cause large raw packet lists to extend past the bottom of the page
|
* Misc: Patch a bewildering browser quirk that cause large raw packet lists to extend past the bottom of the page
|
||||||
|
|
||||||
## [2.7.0] - 2026-03-08
|
## [2.7.0] - 2026-03-08
|
||||||
|
|
||||||
Feature: Multibyte path support
|
* Feature: Multibyte path support
|
||||||
Feature: Add multibyte statistics to statistics pane
|
* Feature: Add multibyte statistics to statistics pane
|
||||||
Feature: Add path bittage to contact info pane
|
* Feature: Add path bittage to contact info pane
|
||||||
Feature: Put tools in a collapsible
|
* Feature: Put tools in a collapsible
|
||||||
|
|
||||||
## [2.6.1] - 2026-03-08
|
## [2.6.1] - 2026-03-08
|
||||||
|
|
||||||
Misc: Fix busted docker builds; we don't have a 2.6.0 build sorry
|
* Misc: Fix busted docker builds; we don't have a 2.6.0 build sorry
|
||||||
|
|
||||||
## [2.6.0] - 2026-03-08
|
## [2.6.0] - 2026-03-08
|
||||||
|
|
||||||
Feature: A11y improvements
|
* Feature: A11y improvements
|
||||||
Feature: New themes
|
* Feature: New themes
|
||||||
Feature: Backfill channel sender identity when available
|
* Feature: Backfill channel sender identity when available
|
||||||
Feature: Modular fanout bus, including Webhooks, more customizable community MQTT, and Apprise
|
* Feature: Modular fanout bus, including Webhooks, more customizable community MQTT, and Apprise
|
||||||
Bugfix: Unreads now respect blocklist
|
* Bugfix: Unreads now respect blocklist
|
||||||
Bugfix: Unreads can't accumulate on an open thread
|
* Bugfix: Unreads can't accumulate on an open thread
|
||||||
Bugfix: Channel name in broadcasts
|
* Bugfix: Channel name in broadcasts
|
||||||
Bugfix: Add missing httpx dependency
|
* Bugfix: Add missing httpx dependency
|
||||||
Bugfix: Improvements to radio startup frontend-blocking time and radio status reporting
|
* Bugfix: Improvements to radio startup frontend-blocking time and radio status reporting
|
||||||
Misc: Improved button signage for app movement
|
* Misc: Improved button signage for app movement
|
||||||
Misc: Test, performance, and documentation improvements
|
* Misc: Test, performance, and documentation improvements
|
||||||
|
|
||||||
## [2.5.0] - 2026-03-05
|
## [2.5.0] - 2026-03-05
|
||||||
|
|
||||||
Feature: Far better accessibility across the app (with far to go)
|
* Feature: Far better accessibility across the app (with far to go)
|
||||||
Feature: Add community MQTT stats reporting, and improve over a few commits
|
* Feature: Add community MQTT stats reporting, and improve over a few commits
|
||||||
Feature: Color schemes and misc. settings reorg
|
* Feature: Color schemes and misc. settings reorg
|
||||||
Feature: Add why-active to filtered nodes
|
* Feature: Add why-active to filtered nodes
|
||||||
Feature: Add channel and contact info box
|
* Feature: Add channel and contact info box
|
||||||
Feature: Add contact blocking
|
* Feature: Add contact blocking
|
||||||
Feature: Add potential repeater path map display
|
* Feature: Add potential repeater path map display
|
||||||
Feature: Add flood scoping/regions
|
* Feature: Add flood scoping/regions
|
||||||
Feature: Global message search
|
* Feature: Global message search
|
||||||
Feature: Fully safe bot disable
|
* Feature: Fully safe bot disable
|
||||||
Feature: Add default #remoteterm channel (lol sorry I had to)
|
* Feature: Add default #remoteterm channel (lol sorry I had to)
|
||||||
Feature: Custom recency pruning in visualizer
|
* Feature: Custom recency pruning in visualizer
|
||||||
Bugfix: Be more cautious around null byte stripping
|
* Bugfix: Be more cautious around null byte stripping
|
||||||
Bugfix: Clear channel-add interface on not-add-another
|
* Bugfix: Clear channel-add interface on not-add-another
|
||||||
Bugfix: Add status/name/MQTT LWT
|
* Bugfix: Add status/name/MQTT LWT
|
||||||
Bugfix: Channel deletion propagates over WS
|
* Bugfix: Channel deletion propagates over WS
|
||||||
Bugfix: Show map location for all nodes on link, not 7-day-limited
|
* Bugfix: Show map location for all nodes on link, not 7-day-limited
|
||||||
Bugfix: Hide private key channel keys by default
|
* Bugfix: Hide private key channel keys by default
|
||||||
Misc: Logline to show if cleanup loop on non-sync'd meshcore radio links fixes anything
|
* Misc: Logline to show if cleanup loop on non-sync'd meshcore radio links fixes anything
|
||||||
Misc: Doc, changelog, and test improvements
|
* Misc: Doc, changelog, and test improvements
|
||||||
Misc: Add, and remove, package lock (sorry Windows users)
|
* Misc: Add, and remove, package lock (sorry Windows users)
|
||||||
Misc: Don't show mark all as read if not necessary
|
* Misc: Don't show mark all as read if not necessary
|
||||||
Misc: Fix stale closures and misc. frontend perf/correctness improvements
|
* Misc: Fix stale closures and misc. frontend perf/correctness improvements
|
||||||
Misc: Add Windows startup notes
|
* Misc: Add Windows startup notes
|
||||||
Misc: E2E expansion + improvement
|
* Misc: E2E expansion + improvement
|
||||||
Misc: Move around visualizer settings
|
* Misc: Move around visualizer settings
|
||||||
|
|
||||||
## [2.4.0] - 2026-03-02
|
## [2.4.0] - 2026-03-02
|
||||||
|
|
||||||
Feature: Add community MQTT reporting (e.g. LetsMesh.net)
|
* Feature: Add community MQTT reporting (e.g. LetsMesh.net)
|
||||||
Misc: Build scripts and library attribution
|
* Misc: Build scripts and library attribution
|
||||||
Misc: Add sign of life to E2E tests
|
* Misc: Add sign of life to E2E tests
|
||||||
|
|
||||||
## [2.3.0] - 2026-03-01
|
## [2.3.0] - 2026-03-01
|
||||||
|
|
||||||
Feature: Click path description to reset to flood
|
* Feature: Click path description to reset to flood
|
||||||
Feature: Add MQTT publishing
|
* Feature: Add MQTT publishing
|
||||||
Feature: Visualizer remembers settings
|
* Feature: Visualizer remembers settings
|
||||||
Bugfix: Fix prefetch usage
|
* Bugfix: Fix prefetch usage
|
||||||
Bugfix: Fixed an issue where busy channels can result in double-display of incoming messages
|
* Bugfix: Fixed an issue where busy channels can result in double-display of incoming messages
|
||||||
Misc: Drop py3.12 requirement
|
* Misc: Drop py3.12 requirement
|
||||||
Misc: Performance, documentation, test, and file structure optimizations
|
* Misc: Performance, documentation, test, and file structure optimizations
|
||||||
Misc: Add arrows between route nodes on contact info
|
* Misc: Add arrows between route nodes on contact info
|
||||||
Misc: Show repeater path/type in title bar
|
* Misc: Show repeater path/type in title bar
|
||||||
|
|
||||||
## [2.2.0] - 2026-02-28
|
## [2.2.0] - 2026-02-28
|
||||||
|
|
||||||
Feature: Track advert paths and use to disambiguate repeater identity in visualizer
|
* Feature: Track advert paths and use to disambiguate repeater identity in visualizer
|
||||||
Feature: Contact info pane
|
* Feature: Contact info pane
|
||||||
Feature: Overhaul repeater interface
|
* Feature: Overhaul repeater interface
|
||||||
Bugfix: Misc. frontend rendering + perf improvements
|
* Bugfix: Misc. frontend rendering + perf improvements
|
||||||
Bugfix: Better behavior around radio locking and autofetch/polling
|
* Bugfix: Better behavior around radio locking and autofetch/polling
|
||||||
Bugfix: Clear channel name field on new-channel modal tab change
|
* Bugfix: Clear channel name field on new-channel modal tab change
|
||||||
Bugfix: Repeater inforbox can scroll
|
* Bugfix: Repeater inforbox can scroll
|
||||||
Bugfix: Better handling of historical DM encrypts
|
* Bugfix: Better handling of historical DM encrypts
|
||||||
Bugfix: Handle errors if returned in prefetch phase
|
* Bugfix: Handle errors if returned in prefetch phase
|
||||||
Misc: Radio event response failure is logged/surfaced better
|
* Misc: Radio event response failure is logged/surfaced better
|
||||||
Misc: Improve test coverage and remove dead code
|
* Misc: Improve test coverage and remove dead code
|
||||||
Misc: Documentation and errata improvements
|
* Misc: Documentation and errata improvements
|
||||||
Misc: Database storage optimization
|
* Misc: Database storage optimization
|
||||||
|
|
||||||
## [2.1.0] - 2026-02-23
|
## [2.1.0] - 2026-02-23
|
||||||
|
|
||||||
Feature: Add ability to remember last-used channel on load
|
* Feature: Add ability to remember last-used channel on load
|
||||||
Feature: Add `docker compose` support (thanks @suymur !)
|
* Feature: Add `docker compose` support (thanks @suymur !)
|
||||||
Feature: Better-aligned favicon (lol)
|
* Feature: Better-aligned favicon (lol)
|
||||||
Bugfix: Disable autocomplete on message field
|
* Bugfix: Disable autocomplete on message field
|
||||||
Bugfix: Legacy hash restoration on page load
|
* Bugfix: Legacy hash restoration on page load
|
||||||
Bugfix: Align resend buttons in pathing modal
|
* Bugfix: Align resend buttons in pathing modal
|
||||||
Bugfix: Update README.md (briefly), then docker-compose.yaml, to reflect correct docker image host
|
* Bugfix: Update README.md (briefly), then docker-compose.yaml, to reflect correct docker image host
|
||||||
Bugfix: Correct settings pane scroll lock on zoom (thanks @yellowcooln !)
|
* Bugfix: Correct settings pane scroll lock on zoom (thanks @yellowcooln !)
|
||||||
Bugfix: Improved repeater comms on busy meshes
|
* Bugfix: Improved repeater comms on busy meshes
|
||||||
Bugfix: Drain before autofetch from radio
|
* Bugfix: Drain before autofetch from radio
|
||||||
Bugfix: Fix, or document exceptions to, sub-second resolution message failure
|
* Bugfix: Fix, or document exceptions to, sub-second resolution message failure
|
||||||
Bugfix: Improved handling of radio connection, disconnection, and connection-aliveness-status
|
* Bugfix: Improved handling of radio connection, disconnection, and connection-aliveness-status
|
||||||
Bugfix: Force server-side keystore update when radio key changes
|
* Bugfix: Force server-side keystore update when radio key changes
|
||||||
Bugfix: Reduce WS churn for incoming message handling
|
* Bugfix: Reduce WS churn for incoming message handling
|
||||||
Bugfix: Fix content type signalling for irrelevant endpoints
|
* Bugfix: Fix content type signalling for irrelevant endpoints
|
||||||
Bugfix: Handle stuck post-connect failure state
|
* Bugfix: Handle stuck post-connect failure state
|
||||||
Misc: Documentation & version parsing improvements
|
* Misc: Documentation & version parsing improvements
|
||||||
Misc: Hide char counter on mobile for short messages
|
* Misc: Hide char counter on mobile for short messages
|
||||||
Misc: Typo fixes in docs and settings
|
* Misc: Typo fixes in docs and settings
|
||||||
Misc: Add dynamic webmanifest for hosts that can support it
|
* Misc: Add dynamic webmanifest for hosts that can support it
|
||||||
Misc: Improve DB size via dropping unnecessary uniqs, indices, vacuum, and offering ability to drop historical matches packets
|
* Misc: Improve DB size via dropping unnecessary uniqs, indices, vacuum, and offering ability to drop historical matches packets
|
||||||
Misc: Drop weird rounded bounding box for settings
|
* Misc: Drop weird rounded bounding box for settings
|
||||||
Misc: Move resend buttons to pathing modal
|
* Misc: Move resend buttons to pathing modal
|
||||||
Misc: Improved comments around database ownership on *nix systems
|
* Misc: Improved comments around database ownership on *nix systems
|
||||||
Misc: Move to SSoT for message dedupe on frontend
|
* Misc: Move to SSoT for message dedupe on frontend
|
||||||
Misc: Move DM ack clearing to standard poll, and increase hold time between polling
|
* Misc: Move DM ack clearing to standard poll, and increase hold time between polling
|
||||||
Misc: Holistic testing overhaul
|
* Misc: Holistic testing overhaul
|
||||||
|
|
||||||
## [2.0.1] - 2026-02-16
|
## [2.0.1] - 2026-02-16
|
||||||
|
|
||||||
Bugfix: Fix missing trigger condition on statistics pane expansion on mobile
|
* Bugfix: Fix missing trigger condition on statistics pane expansion on mobile
|
||||||
|
|
||||||
## [2.0.0] - 2026-02-16
|
## [2.0.0] - 2026-02-16
|
||||||
|
|
||||||
Feature: Frontend UX + log overhaul
|
* Feature: Frontend UX + log overhaul
|
||||||
Bugfix: Use contact object from DB for broadcast rather than handrolling
|
* Bugfix: Use contact object from DB for broadcast rather than handrolling
|
||||||
Bugfix: Fix out of order path WS messages overwriting each other
|
* Bugfix: Fix out of order path WS messages overwriting each other
|
||||||
Bugfix: Make broadcast timestamp match fallback logic used in storage code
|
* Bugfix: Make broadcast timestamp match fallback logic used in storage code
|
||||||
Bugfix: Fix repeater command timestamp selection logic
|
* Bugfix: Fix repeater command timestamp selection logic
|
||||||
Bugfix: Use actual pubkey matching for path update, and don't action serial path update events (use RX packet)
|
* Bugfix: Use actual pubkey matching for path update, and don't action serial path update events (use RX packet)
|
||||||
Bugfix: Add missing radio operation locks in a few spots
|
* Bugfix: Add missing radio operation locks in a few spots
|
||||||
Bugfix: Fix dedupe for frontend raw packet delivery (mesh visualizer much more active now!)
|
* Bugfix: Fix dedupe for frontend raw packet delivery (mesh visualizer much more active now!)
|
||||||
Bugfix: Less aggressive dedupe for advert packets (we don't care about the payload, we care about the path, duh)
|
* Bugfix: Less aggressive dedupe for advert packets (we don't care about the payload, we care about the path, duh)
|
||||||
Misc: Visualizer layout refinement & option labels
|
* Misc: Visualizer layout refinement & option labels
|
||||||
|
|
||||||
## [1.10.0] - 2026-02-16
|
## [1.10.0] - 2026-02-16
|
||||||
|
|
||||||
Feature: Collapsible sidebar sections with per-section unread badge (thanks @rgregg !)
|
* Feature: Collapsible sidebar sections with per-section unread badge (thanks @rgregg !)
|
||||||
Feature: 3D mesh visualizer
|
* Feature: 3D mesh visualizer
|
||||||
Feature: Statistics pane
|
* Feature: Statistics pane
|
||||||
Feature: Support incoming/outgoing indication for bot invocations
|
* Feature: Support incoming/outgoing indication for bot invocations
|
||||||
Feature: Quick byte-perfect message resend if you got unlucky with repeats (thanks @rgregg -- we had a parallel implementation but I appreciate your work!)
|
* Feature: Quick byte-perfect message resend if you got unlucky with repeats (thanks @rgregg -- we had a parallel implementation but I appreciate your work!)
|
||||||
Bugfix: Fix top padding out outgoing message
|
* Bugfix: Fix top padding out outgoing message
|
||||||
Bugfix: Frontend performance, appearance, and Lighthouse improvements (prefetches, form labelling, contrast, channel/roomlist changes)
|
* Bugfix: Frontend performance, appearance, and Lighthouse improvements (prefetches, form labelling, contrast, channel/roomlist changes)
|
||||||
Bugfix: Multiple-sent messages had path appearing delays until rerender
|
* Bugfix: Multiple-sent messages had path appearing delays until rerender
|
||||||
Bugfix: Fix ack/message race condition that caused dropped ack displays until rerender
|
* Bugfix: Fix ack/message race condition that caused dropped ack displays until rerender
|
||||||
Misc: Dedupe contacts/rooms by key and not name to prevent name collisions creating unreachable conversations
|
* Misc: Dedupe contacts/rooms by key and not name to prevent name collisions creating unreachable conversations
|
||||||
Misc: s/stopped/idle/ for room finder
|
* Misc: s/stopped/idle/ for room finder
|
||||||
|
|
||||||
## [1.9.3] - 2026-02-12
|
## [1.9.3] - 2026-02-12
|
||||||
|
|
||||||
Feature: Upgrade the room finder to support two-word rooms
|
* Feature: Upgrade the room finder to support two-word rooms
|
||||||
|
|
||||||
## [1.9.2] - 2026-02-12
|
## [1.9.2] - 2026-02-12
|
||||||
|
|
||||||
Feature: Options dialog sucks less
|
* Feature: Options dialog sucks less
|
||||||
Bugfix: Move tests to isolated memory DB
|
* Bugfix: Move tests to isolated memory DB
|
||||||
Bugfix: Mention case sensitivity
|
* Bugfix: Mention case sensitivity
|
||||||
Bugfix: Stale header retention on settings page view
|
* Bugfix: Stale header retention on settings page view
|
||||||
Bugfix: Non-isolated path writing
|
* Bugfix: Non-isolated path writing
|
||||||
Bugfix: Nullable contact fields are now passed as real nulls
|
* Bugfix: Nullable contact fields are now passed as real nulls
|
||||||
Bugfix: Look at all fields on message reconcile, not just text
|
* Bugfix: Look at all fields on message reconcile, not just text
|
||||||
Bugfix: Make mark-all-as-read atomic
|
* Bugfix: Make mark-all-as-read atomic
|
||||||
Misc: Purge unused WS handlers from back when we did chans and contacts over WS, not API
|
* Misc: Purge unused WS handlers from back when we did chans and contacts over WS, not API
|
||||||
Misc: Massive test and AGENTS.md overhauls and additions
|
* Misc: Massive test and AGENTS.md overhauls and additions
|
||||||
|
|
||||||
## [1.9.1] - 2026-02-10
|
## [1.9.1] - 2026-02-10
|
||||||
|
|
||||||
Feature: Contacts and channels use keys, not names
|
* Feature: Contacts and channels use keys, not names
|
||||||
Bugfix: Fix falsy casting of 0 in lat lon and timing data
|
* Bugfix: Fix falsy casting of 0 in lat lon and timing data
|
||||||
Bugfix: Show message length in bytes, not chars
|
* Bugfix: Show message length in bytes, not chars
|
||||||
Bugfix: Fix phantom unread badges on focused convos
|
* Bugfix: Fix phantom unread badges on focused convos
|
||||||
Misc: Bot invocation to async
|
* Misc: Bot invocation to async
|
||||||
Misc: Use full key, not prefix, where we can
|
* Misc: Use full key, not prefix, where we can
|
||||||
|
|
||||||
## [1.9.0] - 2026-02-10
|
## [1.9.0] - 2026-02-10
|
||||||
|
|
||||||
Feature: Favorited contacts are preferentially loaded onto the radio
|
* Feature: Favorited contacts are preferentially loaded onto the radio
|
||||||
Feature: Add recent-message caching for fast switching
|
* Feature: Add recent-message caching for fast switching
|
||||||
Feature: Add echo paths modal when echo-heard checkbox is clicked
|
* Feature: Add echo paths modal when echo-heard checkbox is clicked
|
||||||
Feature: Add experimental byte-perfect double-send for bad RF environments to try to punch the message out
|
* Feature: Add experimental byte-perfect double-send for bad RF environments to try to punch the message out
|
||||||
Frontend: Better styling on echo + message path display
|
Frontend: Better styling on echo + message path display
|
||||||
Bugfix: Prevent frontend static file serving path traversal vuln
|
* Bugfix: Prevent frontend static file serving path traversal vuln
|
||||||
Bugfix: Safer prefix-claiming for DMs we don't have the key for
|
* Bugfix: Safer prefix-claiming for DMs we don't have the key for
|
||||||
Bugfix: Prevent injection from mentions with special characters
|
* Bugfix: Prevent injection from mentions with special characters
|
||||||
Bugfix: Fix repeaters comms showing in wrong channel when repeater operations are in flight and the channel is changed quickly
|
* Bugfix: Fix repeaters comms showing in wrong channel when repeater operations are in flight and the channel is changed quickly
|
||||||
Bugfix: App can boot and test without a frontend dir
|
* Bugfix: App can boot and test without a frontend dir
|
||||||
Misc: Improve and consistent-ify (?) backend radio operation lock management
|
* Misc: Improve and consistent-ify (?) backend radio operation lock management
|
||||||
Misc: Frontend performance and safety enhancements
|
* Misc: Frontend performance and safety enhancements
|
||||||
Misc: Move builds to non-bundled; usage requires building the Frontend
|
* Misc: Move builds to non-bundled; usage requires building the Frontend
|
||||||
Misc: Update tests and agent docs
|
* Misc: Update tests and agent docs
|
||||||
|
|
||||||
## [1.8.0] - 2026-02-07
|
## [1.8.0] - 2026-02-07
|
||||||
|
|
||||||
Feature: Single hop ping
|
* Feature: Single hop ping
|
||||||
Feature: PWA viewport fixes(thanks @rgregg)
|
* Feature: PWA viewport fixes(thanks @rgregg)
|
||||||
Feature (?): No frontend distribution; build it yourself ;P
|
Feature (?): No frontend distribution; build it yourself ;P
|
||||||
Bugfix: Fix channel message send race condition (concurrent sends could corrupt shared radio slot)
|
* Bugfix: Fix channel message send race condition (concurrent sends could corrupt shared radio slot)
|
||||||
Bugfix: Fix TOCTOU race in radio reconnect (duplicate connections under contention)
|
* Bugfix: Fix TOCTOU race in radio reconnect (duplicate connections under contention)
|
||||||
Bugfix: Better guarding around reconnection
|
* Bugfix: Better guarding around reconnection
|
||||||
Bugfix: Duplicate websocket connection fixes
|
* Bugfix: Duplicate websocket connection fixes
|
||||||
Bugfix: Settings tab error cleanliness on tab swap
|
* Bugfix: Settings tab error cleanliness on tab swap
|
||||||
Bugfix: Fix path traversal vuln
|
* Bugfix: Fix path traversal vuln
|
||||||
UI: Swap visualizer legend ordering (yay prettier)
|
UI: Swap visualizer legend ordering (yay prettier)
|
||||||
Misc: Perf and locking improvements
|
* Misc: Perf and locking improvements
|
||||||
Misc: Always flood advertisements
|
* Misc: Always flood advertisements
|
||||||
Misc: Better packet dupe handling
|
* Misc: Better packet dupe handling
|
||||||
Misc: Dead code cleanup, test improvements
|
* Misc: Dead code cleanup, test improvements
|
||||||
|
|
||||||
## [1.7.1] - 2026-02-03
|
## [1.7.1] - 2026-02-03
|
||||||
|
|
||||||
Feature: Clickable hyperlinks
|
* Feature: Clickable hyperlinks
|
||||||
Bugfix: More consistent public key normalization
|
* Bugfix: More consistent public key normalization
|
||||||
Bugfix: Use more reliable cursor paging
|
* Bugfix: Use more reliable cursor paging
|
||||||
Bugfix: Fix null timestamp dedupe failure
|
* Bugfix: Fix null timestamp dedupe failure
|
||||||
Bugfix: More consistent prefix-based message claiming on key receipt
|
* Bugfix: More consistent prefix-based message claiming on key receipt
|
||||||
Misc: Bot can respond to its own messages
|
* Misc: Bot can respond to its own messages
|
||||||
Misc: Additional tests
|
* Misc: Additional tests
|
||||||
Misc: Remove unneeded message dedupe logic
|
* Misc: Remove unneeded message dedupe logic
|
||||||
Misc: Resync settings after radio settings mutation
|
* Misc: Resync settings after radio settings mutation
|
||||||
|
|
||||||
## [1.7.0] - 2026-01-27
|
## [1.7.0] - 2026-01-27
|
||||||
|
|
||||||
Feature: Multi-bot functionality
|
* Feature: Multi-bot functionality
|
||||||
Bugfix: Adjust bot code editor display and add line numbers
|
* Bugfix: Adjust bot code editor display and add line numbers
|
||||||
Bugfix: Fix clock filtering and contact lookup behavior bugs
|
* Bugfix: Fix clock filtering and contact lookup behavior bugs
|
||||||
Bugfix: Fix repeater message duplication issue
|
* Bugfix: Fix repeater message duplication issue
|
||||||
Bugfix: Correct outbound message timestamp assignment (affecting outgoing messages seen as incoming)
|
* Bugfix: Correct outbound message timestamp assignment (affecting outgoing messages seen as incoming)
|
||||||
UI: Move advertise button to identity tab
|
UI: Move advertise button to identity tab
|
||||||
Misc: Clarify fallback functionality for missing private key export in logs
|
* Misc: Clarify fallback functionality for missing private key export in logs
|
||||||
|
|
||||||
## [1.6.0] - 2026-01-26
|
## [1.6.0] - 2026-01-26
|
||||||
|
|
||||||
Feature: Visualizer: extract public key from AnonReq, add heuristic repeater disambiguation, add reset button, draggable nodes
|
* Feature: Visualizer: extract public key from AnonReq, add heuristic repeater disambiguation, add reset button, draggable nodes
|
||||||
Feature: Customizable advertising interval
|
* Feature: Customizable advertising interval
|
||||||
Feature: In-app bot setup
|
* Feature: In-app bot setup
|
||||||
Bugfix: Force contact onto radio before DM send
|
* Bugfix: Force contact onto radio before DM send
|
||||||
Misc: Remove unused code
|
* Misc: Remove unused code
|
||||||
|
|
||||||
## [1.5.0] - 2026-01-19
|
## [1.5.0] - 2026-01-19
|
||||||
|
|
||||||
Feature: Network visualizer
|
* Feature: Network visualizer
|
||||||
|
|
||||||
## [1.4.1] - 2026-01-19
|
## [1.4.1] - 2026-01-19
|
||||||
|
|
||||||
Feature: Add option to attempt historical DM decrypt on new-contact advertisement (disabled by default)
|
* Feature: Add option to attempt historical DM decrypt on new-contact advertisement (disabled by default)
|
||||||
Feature: Server-side preference management for favorites, read status, etc.
|
* Feature: Server-side preference management for favorites, read status, etc.
|
||||||
UI: More compact hop labelling
|
UI: More compact hop labelling
|
||||||
Bugfix: Misc. race conditions and websocket handling
|
* Bugfix: Misc. race conditions and websocket handling
|
||||||
Bugfix: Reduce fetching cadence by loading all contact data at start to prevent fetches on advertise-driven update
|
* Bugfix: Reduce fetching cadence by loading all contact data at start to prevent fetches on advertise-driven update
|
||||||
|
|
||||||
## [1.4.0] - 2026-01-18
|
## [1.4.0] - 2026-01-18
|
||||||
|
|
||||||
UI: Improve button layout for room searcher
|
UI: Improve button layout for room searcher
|
||||||
UI: Improve favicon coloring
|
UI: Improve favicon coloring
|
||||||
UI: Improve status bar button layout on small screen
|
UI: Improve status bar button layout on small screen
|
||||||
Feature: Show multi-path hop display with distance estimates
|
* Feature: Show multi-path hop display with distance estimates
|
||||||
Feature: Search rooms and contacts by key, not just name
|
* Feature: Search rooms and contacts by key, not just name
|
||||||
Bugfix: Historical DM decryption now works as expected
|
* Bugfix: Historical DM decryption now works as expected
|
||||||
Bugfix: Don't double-set active conversation after addition; wait for backend room name normalization
|
* Bugfix: Don't double-set active conversation after addition; wait for backend room name normalization
|
||||||
|
|
||||||
## [1.3.1] - 2026-01-17
|
## [1.3.1] - 2026-01-17
|
||||||
|
|
||||||
UI: Rework restart handling
|
UI: Rework restart handling
|
||||||
Feature: Add `dutycyle_start` command to logged-in repeater session to start five min duty cycle tracking
|
* Feature: Add `dutycyle_start` command to logged-in repeater session to start five min duty cycle tracking
|
||||||
Bug: Improve error message rendering from server-side errors
|
Bug: Improve error message rendering from server-side errors
|
||||||
UI: Remove octothorpe from channel listing
|
UI: Remove octothorpe from channel listing
|
||||||
|
|
||||||
## [1.3.0] - 2026-01-17
|
## [1.3.0] - 2026-01-17
|
||||||
|
|
||||||
Feature: Rework database schema to drop unnecessary columns and dedupe payloads at the DB level
|
* Feature: Rework database schema to drop unnecessary columns and dedupe payloads at the DB level
|
||||||
Feature: Massive frontend settings overhaul. It ain't gorgeous but it's easier to navigate.
|
* Feature: Massive frontend settings overhaul. It ain't gorgeous but it's easier to navigate.
|
||||||
Feature: Drop repeater login wait time; vestigial from debugging a different issue
|
* Feature: Drop repeater login wait time; vestigial from debugging a different issue
|
||||||
|
|
||||||
## [1.2.1] - 2026-01-17
|
## [1.2.1] - 2026-01-17
|
||||||
|
|
||||||
@@ -462,27 +481,27 @@ Update: Update meshcore-hashtag-cracker to include sender-identification correct
|
|||||||
|
|
||||||
## [1.2.0] - 2026-01-16
|
## [1.2.0] - 2026-01-16
|
||||||
|
|
||||||
Feature: Add favorites
|
* Feature: Add favorites
|
||||||
|
|
||||||
## [1.1.0] - 2026-01-14
|
## [1.1.0] - 2026-01-14
|
||||||
|
|
||||||
Bugfix: Use actual pathing data from advertisements, not just always flood (oops)
|
* Bugfix: Use actual pathing data from advertisements, not just always flood (oops)
|
||||||
Bugfix: Autosync radio clock periodically to prevent drift (would show up most commonly as issues with repeater comms)
|
* Bugfix: Autosync radio clock periodically to prevent drift (would show up most commonly as issues with repeater comms)
|
||||||
|
|
||||||
## [1.0.3] - 2026-01-13
|
## [1.0.3] - 2026-01-13
|
||||||
|
|
||||||
Bugfix: Add missing test management packages
|
* Bugfix: Add missing test management packages
|
||||||
Improvement: Drop unnecessary repeater timeouts, and retain timeout for login only -- repeater ops are faster AND more reliable!
|
* Improvement: Drop unnecessary repeater timeouts, and retain timeout for login only -- repeater ops are faster AND more reliable!
|
||||||
|
|
||||||
## [1.0.2] - 2026-01-13
|
## [1.0.2] - 2026-01-13
|
||||||
|
|
||||||
Improvement: Add delays between router ops to prevent traffic collisions
|
* Improvement: Add delays between router ops to prevent traffic collisions
|
||||||
|
|
||||||
## [1.0.1] - 2026-01-13
|
## [1.0.1] - 2026-01-13
|
||||||
|
|
||||||
Bugixes: Cleaner DB shutdown, radio reconnect contention, packet dedupe garbage removal
|
* Bugixes: Cleaner DB shutdown, radio reconnect contention, packet dedupe garbage removal
|
||||||
|
|
||||||
## [1.0.0] - 2026-01-13
|
## [1.0.0] - 2026-01-13
|
||||||
|
|
||||||
Initial full release!
|
* Initial full release!
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ Source checkouts expect a normal frontend build in `frontend/dist`.
|
|||||||
|
|
||||||
Local Docker builds are architecture-native by default. On Apple Silicon Macs and ARM64 Linux hosts such as Raspberry Pi, `docker compose build` / `docker compose up --build` will produce an ARM64 image unless you override the platform.
|
Local Docker builds are architecture-native by default. On Apple Silicon Macs and ARM64 Linux hosts such as Raspberry Pi, `docker compose build` / `docker compose up --build` will produce an ARM64 image unless you override the platform.
|
||||||
|
|
||||||
|
For serial-device passthrough, use rootful Docker. In practice that usually means starting the stack with `sudo docker compose ...` unless your Docker daemon is already configured for rootful access via your user/group. Rootless Docker has been observed to fail on serial-device mappings even when the compose file itself is correct.
|
||||||
|
|
||||||
Create a local `docker-compose.yml` in one of two ways:
|
Create a local `docker-compose.yml` in one of two ways:
|
||||||
|
|
||||||
1. Copy the example file and edit it by hand:
|
1. Copy the example file and edit it by hand:
|
||||||
@@ -128,7 +130,7 @@ The guided Docker flow can collect BLE settings, but BLE access from Docker stil
|
|||||||
Then customize the local compose file for your transport and launch:
|
Then customize the local compose file for your transport and launch:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up # -d for background once you validate it's working
|
sudo docker compose up # add -d for background once you validate it's working
|
||||||
```
|
```
|
||||||
|
|
||||||
The database is stored in `./data/` (bind-mounted), so the container shares the same database as the native app.
|
The database is stored in `./data/` (bind-mounted), so the container shares the same database as the native app.
|
||||||
@@ -136,14 +138,14 @@ The database is stored in `./data/` (bind-mounted), so the container shares the
|
|||||||
To rebuild after pulling updates:
|
To rebuild after pulling updates:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose pull
|
sudo docker compose pull
|
||||||
docker compose up -d
|
sudo docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
The example file and setup script default to the published Docker Hub image. To build locally from your checkout instead, replace:
|
The example file and setup script default to the published Docker Hub image. To build locally from your checkout instead, replace:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
image: jkingsman/remoteterm-meshcore:latest
|
image: docker.io/jkingsman/remoteterm-meshcore:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
with:
|
with:
|
||||||
@@ -155,7 +157,7 @@ build: .
|
|||||||
Then run:
|
Then run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
sudo docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
The container runs as root by default for maximum serial passthrough compatibility across host setups. On Linux, if you switch between native and Docker runs, `./data` can end up root-owned. If you do not need that serial compatibility behavior, you can enable the optional `user: "${UID:-1000}:${GID:-1000}"` line in `docker-compose.yml` to keep ownership aligned with your host user.
|
The container runs as root by default for maximum serial passthrough compatibility across host setups. On Linux, if you switch between native and Docker runs, `./data` can end up root-owned. If you do not need that serial compatibility behavior, you can enable the optional `user: "${UID:-1000}:${GID:-1000}"` line in `docker-compose.yml` to keep ownership aligned with your host user.
|
||||||
@@ -163,7 +165,7 @@ The container runs as root by default for maximum serial passthrough compatibili
|
|||||||
To stop:
|
To stop:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose down
|
sudo docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
## Standard Environment Variables
|
## Standard Environment Variables
|
||||||
|
|||||||
@@ -60,8 +60,17 @@ async def sample_noise_floor_once(*, blocking: bool = False) -> None:
|
|||||||
|
|
||||||
async def _noise_floor_sampling_loop() -> None:
|
async def _noise_floor_sampling_loop() -> None:
|
||||||
while True:
|
while True:
|
||||||
await sample_noise_floor_once()
|
try:
|
||||||
await asyncio.sleep(NOISE_FLOOR_SAMPLE_INTERVAL_SECONDS)
|
await sample_noise_floor_once()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Noise floor sampling loop crashed during sample")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(NOISE_FLOOR_SAMPLE_INTERVAL_SECONDS)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def start_noise_floor_sampling() -> None:
|
async def start_noise_floor_sampling() -> None:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
remoteterm:
|
remoteterm:
|
||||||
# build: .
|
# build: .
|
||||||
image: jkingsman/remoteterm-meshcore:latest
|
image: docker.io/jkingsman/remoteterm-meshcore:latest
|
||||||
|
|
||||||
# Optional on Linux: run container as your host user to avoid root-owned files in ./data
|
# Optional on Linux: run container as your host user to avoid root-owned files in ./data
|
||||||
# This is less reliable for serial-device access than running as root and may require
|
# This is less reliable for serial-device access than running as root and may require
|
||||||
@@ -12,9 +12,12 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
|
||||||
################################################
|
#####################################################################
|
||||||
# Map your radio by stable device ID if available. #
|
# Map your radio by stable device ID if available. #
|
||||||
################################################
|
# If your by-id path contains ':' characters, Docker Compose cannot #
|
||||||
|
# represent it here directly; use a colon-free host alias instead. #
|
||||||
|
# (e.g. /dev/ttyUSB0) #
|
||||||
|
#####################################################################
|
||||||
devices:
|
devices:
|
||||||
- /dev/serial/by-id/your-meshcore-radio:/dev/meshcore-radio
|
- /dev/serial/by-id/your-meshcore-radio:/dev/meshcore-radio
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "remoteterm-meshcore-frontend",
|
"name": "remoteterm-meshcore-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "3.6.3",
|
"version": "3.6.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+40
-2
@@ -31,6 +31,12 @@ interface ChannelUnreadMarker {
|
|||||||
lastReadAt: number | null;
|
lastReadAt: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NewMessagePrefillRequest {
|
||||||
|
tab: 'hashtag';
|
||||||
|
hashtagName: string;
|
||||||
|
nonce: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface UnreadBoundaryBackfillParams {
|
interface UnreadBoundaryBackfillParams {
|
||||||
activeConversation: Conversation | null;
|
activeConversation: Conversation | null;
|
||||||
unreadMarker: ChannelUnreadMarker | null;
|
unreadMarker: ChannelUnreadMarker | null;
|
||||||
@@ -77,6 +83,8 @@ export function App() {
|
|||||||
const messageInputRef = useRef<MessageInputHandle>(null);
|
const messageInputRef = useRef<MessageInputHandle>(null);
|
||||||
const [rawPackets, setRawPackets] = useState<RawPacket[]>([]);
|
const [rawPackets, setRawPackets] = useState<RawPacket[]>([]);
|
||||||
const [channelUnreadMarker, setChannelUnreadMarker] = useState<ChannelUnreadMarker | null>(null);
|
const [channelUnreadMarker, setChannelUnreadMarker] = useState<ChannelUnreadMarker | null>(null);
|
||||||
|
const [newMessagePrefillRequest, setNewMessagePrefillRequest] =
|
||||||
|
useState<NewMessagePrefillRequest | null>(null);
|
||||||
const [visibilityVersion, setVisibilityVersion] = useState(0);
|
const [visibilityVersion, setVisibilityVersion] = useState(0);
|
||||||
const lastUnreadBackfillAttemptRef = useRef<string | null>(null);
|
const lastUnreadBackfillAttemptRef = useRef<string | null>(null);
|
||||||
const {
|
const {
|
||||||
@@ -103,8 +111,8 @@ export function App() {
|
|||||||
setDistanceUnit,
|
setDistanceUnit,
|
||||||
handleCloseSettingsView,
|
handleCloseSettingsView,
|
||||||
handleToggleSettingsView,
|
handleToggleSettingsView,
|
||||||
handleOpenNewMessage,
|
handleOpenNewMessage: openNewMessageModal,
|
||||||
handleCloseNewMessage,
|
handleCloseNewMessage: closeNewMessageModal,
|
||||||
handleToggleCracker,
|
handleToggleCracker,
|
||||||
} = useAppShell();
|
} = useAppShell();
|
||||||
|
|
||||||
@@ -413,6 +421,34 @@ export function App() {
|
|||||||
[fetchUndecryptedCount, setChannels]
|
[fetchUndecryptedCount, setChannels]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleOpenNewMessage = useCallback(() => {
|
||||||
|
setNewMessagePrefillRequest(null);
|
||||||
|
openNewMessageModal();
|
||||||
|
}, [openNewMessageModal]);
|
||||||
|
|
||||||
|
const handleCloseNewMessage = useCallback(() => {
|
||||||
|
setNewMessagePrefillRequest(null);
|
||||||
|
closeNewMessageModal();
|
||||||
|
}, [closeNewMessageModal]);
|
||||||
|
|
||||||
|
const handleChannelReferenceClick = useCallback(
|
||||||
|
(channelName: string) => {
|
||||||
|
const existingChannel = channels.find((channel) => channel.name === channelName);
|
||||||
|
if (existingChannel) {
|
||||||
|
handleNavigateToChannel(existingChannel.key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewMessagePrefillRequest((previous) => ({
|
||||||
|
tab: 'hashtag',
|
||||||
|
hashtagName: channelName.slice(1),
|
||||||
|
nonce: (previous?.nonce ?? 0) + 1,
|
||||||
|
}));
|
||||||
|
openNewMessageModal();
|
||||||
|
},
|
||||||
|
[channels, handleNavigateToChannel, openNewMessageModal]
|
||||||
|
);
|
||||||
|
|
||||||
const statusProps = {
|
const statusProps = {
|
||||||
health,
|
health,
|
||||||
config,
|
config,
|
||||||
@@ -468,6 +504,7 @@ export function App() {
|
|||||||
onOpenContactInfo: handleOpenContactInfo,
|
onOpenContactInfo: handleOpenContactInfo,
|
||||||
onOpenChannelInfo: handleOpenChannelInfo,
|
onOpenChannelInfo: handleOpenChannelInfo,
|
||||||
onSenderClick: handleSenderClick,
|
onSenderClick: handleSenderClick,
|
||||||
|
onChannelReferenceClick: handleChannelReferenceClick,
|
||||||
onLoadOlder: fetchOlderMessages,
|
onLoadOlder: fetchOlderMessages,
|
||||||
onResendChannelMessage: handleResendChannelMessage,
|
onResendChannelMessage: handleResendChannelMessage,
|
||||||
onTargetReached: () => setTargetMessageId(null),
|
onTargetReached: () => setTargetMessageId(null),
|
||||||
@@ -526,6 +563,7 @@ export function App() {
|
|||||||
};
|
};
|
||||||
const newMessageModalProps = {
|
const newMessageModalProps = {
|
||||||
undecryptedCount,
|
undecryptedCount,
|
||||||
|
prefillRequest: newMessagePrefillRequest,
|
||||||
onCreateContact: handleCreateContact,
|
onCreateContact: handleCreateContact,
|
||||||
onCreateChannel: handleCreateChannel,
|
onCreateChannel: handleCreateChannel,
|
||||||
onCreateHashtagChannel: handleCreateHashtagChannel,
|
onCreateHashtagChannel: handleCreateHashtagChannel,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ interface ConversationPaneProps {
|
|||||||
onOpenContactInfo: (publicKey: string, fromChannel?: boolean) => void;
|
onOpenContactInfo: (publicKey: string, fromChannel?: boolean) => void;
|
||||||
onOpenChannelInfo: (channelKey: string) => void;
|
onOpenChannelInfo: (channelKey: string) => void;
|
||||||
onSenderClick: (sender: string) => void;
|
onSenderClick: (sender: string) => void;
|
||||||
|
onChannelReferenceClick?: (channelName: string) => void;
|
||||||
onLoadOlder: () => Promise<void>;
|
onLoadOlder: () => Promise<void>;
|
||||||
onResendChannelMessage: (messageId: number, newTimestamp?: boolean) => Promise<void>;
|
onResendChannelMessage: (messageId: number, newTimestamp?: boolean) => Promise<void>;
|
||||||
onTargetReached: () => void;
|
onTargetReached: () => void;
|
||||||
@@ -131,6 +132,7 @@ export function ConversationPane({
|
|||||||
onOpenContactInfo,
|
onOpenContactInfo,
|
||||||
onOpenChannelInfo,
|
onOpenChannelInfo,
|
||||||
onSenderClick,
|
onSenderClick,
|
||||||
|
onChannelReferenceClick,
|
||||||
onLoadOlder,
|
onLoadOlder,
|
||||||
onResendChannelMessage,
|
onResendChannelMessage,
|
||||||
onTargetReached,
|
onTargetReached,
|
||||||
@@ -284,6 +286,7 @@ export function ConversationPane({
|
|||||||
activeConversation.type === 'channel' ? onDismissUnreadMarker : undefined
|
activeConversation.type === 'channel' ? onDismissUnreadMarker : undefined
|
||||||
}
|
}
|
||||||
onSenderClick={activeConversation.type === 'channel' ? onSenderClick : undefined}
|
onSenderClick={activeConversation.type === 'channel' ? onSenderClick : undefined}
|
||||||
|
onChannelReferenceClick={onChannelReferenceClick}
|
||||||
onLoadOlder={onLoadOlder}
|
onLoadOlder={onLoadOlder}
|
||||||
onResendChannelMessage={
|
onResendChannelMessage={
|
||||||
activeConversation.type === 'channel' ? onResendChannelMessage : undefined
|
activeConversation.type === 'channel' ? onResendChannelMessage : undefined
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import {
|
|||||||
import type { Channel, Contact, Message, MessagePath, RadioConfig, RawPacket } from '../types';
|
import type { Channel, Contact, Message, MessagePath, RadioConfig, RawPacket } from '../types';
|
||||||
import { CONTACT_TYPE_REPEATER, CONTACT_TYPE_ROOM } from '../types';
|
import { CONTACT_TYPE_REPEATER, CONTACT_TYPE_ROOM } from '../types';
|
||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { formatTime, parseSenderFromText } from '../utils/messageParser';
|
import {
|
||||||
|
findLinkedChannelReferences,
|
||||||
|
formatTime,
|
||||||
|
parseSenderFromText,
|
||||||
|
} from '../utils/messageParser';
|
||||||
import { formatHopCounts, type SenderInfo } from '../utils/pathUtils';
|
import { formatHopCounts, type SenderInfo } from '../utils/pathUtils';
|
||||||
import { getDirectContactRoute } from '../utils/pathUtils';
|
import { getDirectContactRoute } from '../utils/pathUtils';
|
||||||
import { ContactAvatar } from './ContactAvatar';
|
import { ContactAvatar } from './ContactAvatar';
|
||||||
@@ -33,6 +37,7 @@ interface MessageListProps {
|
|||||||
onSenderClick?: (sender: string) => void;
|
onSenderClick?: (sender: string) => void;
|
||||||
onLoadOlder?: () => void;
|
onLoadOlder?: () => void;
|
||||||
onResendChannelMessage?: (messageId: number, newTimestamp?: boolean) => void;
|
onResendChannelMessage?: (messageId: number, newTimestamp?: boolean) => void;
|
||||||
|
onChannelReferenceClick?: (channelName: string) => void;
|
||||||
radioName?: string;
|
radioName?: string;
|
||||||
config?: RadioConfig | null;
|
config?: RadioConfig | null;
|
||||||
onOpenContactInfo?: (publicKey: string, fromChannel?: boolean) => void;
|
onOpenContactInfo?: (publicKey: string, fromChannel?: boolean) => void;
|
||||||
@@ -48,8 +53,64 @@ interface MessageListProps {
|
|||||||
const URL_PATTERN =
|
const URL_PATTERN =
|
||||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
|
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
|
||||||
|
|
||||||
// Helper to convert URLs in a plain text string into clickable links
|
function renderChannelReferences(
|
||||||
function linkifyText(text: string, keyPrefix: string): ReactNode[] {
|
text: string,
|
||||||
|
keyPrefix: string,
|
||||||
|
onChannelReferenceClick?: (channelName: string) => void
|
||||||
|
): ReactNode[] {
|
||||||
|
const references = findLinkedChannelReferences(text);
|
||||||
|
if (references.length === 0) {
|
||||||
|
return [text];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: ReactNode[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
references.forEach((reference, index) => {
|
||||||
|
if (reference.start > lastIndex) {
|
||||||
|
parts.push(text.slice(lastIndex, reference.start));
|
||||||
|
}
|
||||||
|
|
||||||
|
const className =
|
||||||
|
'rounded px-0.5 font-medium text-primary underline underline-offset-2 transition-colors';
|
||||||
|
if (onChannelReferenceClick) {
|
||||||
|
parts.push(
|
||||||
|
<button
|
||||||
|
key={`${keyPrefix}-channel-${index}`}
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
className,
|
||||||
|
'inline border-0 bg-transparent p-0 align-baseline hover:text-primary/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
||||||
|
)}
|
||||||
|
onClick={() => onChannelReferenceClick(reference.label)}
|
||||||
|
>
|
||||||
|
{reference.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
parts.push(
|
||||||
|
<span key={`${keyPrefix}-channel-${index}`} className={className}>
|
||||||
|
{reference.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = reference.end;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
parts.push(text.slice(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to convert URLs and channel references in a plain text string into rich content
|
||||||
|
function linkifyText(
|
||||||
|
text: string,
|
||||||
|
keyPrefix: string,
|
||||||
|
onChannelReferenceClick?: (channelName: string) => void
|
||||||
|
): ReactNode[] {
|
||||||
const parts: ReactNode[] = [];
|
const parts: ReactNode[] = [];
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
@@ -58,7 +119,13 @@ function linkifyText(text: string, keyPrefix: string): ReactNode[] {
|
|||||||
URL_PATTERN.lastIndex = 0;
|
URL_PATTERN.lastIndex = 0;
|
||||||
while ((match = URL_PATTERN.exec(text)) !== null) {
|
while ((match = URL_PATTERN.exec(text)) !== null) {
|
||||||
if (match.index > lastIndex) {
|
if (match.index > lastIndex) {
|
||||||
parts.push(text.slice(lastIndex, match.index));
|
parts.push(
|
||||||
|
...renderChannelReferences(
|
||||||
|
text.slice(lastIndex, match.index),
|
||||||
|
`${keyPrefix}-text-${keyIndex}`,
|
||||||
|
onChannelReferenceClick
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
parts.push(
|
parts.push(
|
||||||
<a
|
<a
|
||||||
@@ -74,15 +141,27 @@ function linkifyText(text: string, keyPrefix: string): ReactNode[] {
|
|||||||
lastIndex = match.index + match[0].length;
|
lastIndex = match.index + match[0].length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastIndex === 0) return [text];
|
if (lastIndex === 0) {
|
||||||
|
return renderChannelReferences(text, keyPrefix, onChannelReferenceClick);
|
||||||
|
}
|
||||||
if (lastIndex < text.length) {
|
if (lastIndex < text.length) {
|
||||||
parts.push(text.slice(lastIndex));
|
parts.push(
|
||||||
|
...renderChannelReferences(
|
||||||
|
text.slice(lastIndex),
|
||||||
|
`${keyPrefix}-tail`,
|
||||||
|
onChannelReferenceClick
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to render text with highlighted @[Name] mentions and clickable URLs
|
// Helper to render text with highlighted @[Name] mentions and clickable URLs
|
||||||
function renderTextWithMentions(text: string, radioName?: string): ReactNode {
|
function renderTextWithMentions(
|
||||||
|
text: string,
|
||||||
|
radioName?: string,
|
||||||
|
onChannelReferenceClick?: (channelName: string) => void
|
||||||
|
): ReactNode {
|
||||||
const mentionPattern = /@\[([^\]]+)\]/g;
|
const mentionPattern = /@\[([^\]]+)\]/g;
|
||||||
const parts: ReactNode[] = [];
|
const parts: ReactNode[] = [];
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
@@ -92,7 +171,13 @@ function renderTextWithMentions(text: string, radioName?: string): ReactNode {
|
|||||||
while ((match = mentionPattern.exec(text)) !== null) {
|
while ((match = mentionPattern.exec(text)) !== null) {
|
||||||
// Add text before the match (with linkification)
|
// Add text before the match (with linkification)
|
||||||
if (match.index > lastIndex) {
|
if (match.index > lastIndex) {
|
||||||
parts.push(...linkifyText(text.slice(lastIndex, match.index), `pre-${keyIndex}`));
|
parts.push(
|
||||||
|
...linkifyText(
|
||||||
|
text.slice(lastIndex, match.index),
|
||||||
|
`pre-${keyIndex}`,
|
||||||
|
onChannelReferenceClick
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mentionedName = match[1];
|
const mentionedName = match[1];
|
||||||
@@ -115,7 +200,7 @@ function renderTextWithMentions(text: string, radioName?: string): ReactNode {
|
|||||||
|
|
||||||
// Add remaining text after last match (with linkification)
|
// Add remaining text after last match (with linkification)
|
||||||
if (lastIndex < text.length) {
|
if (lastIndex < text.length) {
|
||||||
parts.push(...linkifyText(text.slice(lastIndex), `post-${keyIndex}`));
|
parts.push(...linkifyText(text.slice(lastIndex), `post-${keyIndex}`, onChannelReferenceClick));
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.length > 0 ? parts : text;
|
return parts.length > 0 ? parts : text;
|
||||||
@@ -188,6 +273,7 @@ export function MessageList({
|
|||||||
onSenderClick,
|
onSenderClick,
|
||||||
onLoadOlder,
|
onLoadOlder,
|
||||||
onResendChannelMessage,
|
onResendChannelMessage,
|
||||||
|
onChannelReferenceClick,
|
||||||
radioName,
|
radioName,
|
||||||
config,
|
config,
|
||||||
onOpenContactInfo,
|
onOpenContactInfo,
|
||||||
@@ -911,7 +997,7 @@ export function MessageList({
|
|||||||
<div className="break-words whitespace-pre-wrap">
|
<div className="break-words whitespace-pre-wrap">
|
||||||
{content.split('\n').map((line, i, arr) => (
|
{content.split('\n').map((line, i, arr) => (
|
||||||
<span key={i}>
|
<span key={i}>
|
||||||
{renderTextWithMentions(line, radioName)}
|
{renderTextWithMentions(line, radioName, onChannelReferenceClick)}
|
||||||
{i < arr.length - 1 && <br />}
|
{i < arr.length - 1 && <br />}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Dice5 } from 'lucide-react';
|
import { Dice5 } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -20,6 +20,11 @@ type Tab = 'new-contact' | 'new-channel' | 'hashtag';
|
|||||||
interface NewMessageModalProps {
|
interface NewMessageModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
undecryptedCount: number;
|
undecryptedCount: number;
|
||||||
|
prefillRequest?: {
|
||||||
|
tab: 'hashtag';
|
||||||
|
hashtagName: string;
|
||||||
|
nonce: number;
|
||||||
|
} | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onCreateContact: (name: string, publicKey: string, tryHistorical: boolean) => Promise<void>;
|
onCreateContact: (name: string, publicKey: string, tryHistorical: boolean) => Promise<void>;
|
||||||
onCreateChannel: (name: string, key: string, tryHistorical: boolean) => Promise<void>;
|
onCreateChannel: (name: string, key: string, tryHistorical: boolean) => Promise<void>;
|
||||||
@@ -29,6 +34,7 @@ interface NewMessageModalProps {
|
|||||||
export function NewMessageModal({
|
export function NewMessageModal({
|
||||||
open,
|
open,
|
||||||
undecryptedCount,
|
undecryptedCount,
|
||||||
|
prefillRequest = null,
|
||||||
onClose,
|
onClose,
|
||||||
onCreateContact,
|
onCreateContact,
|
||||||
onCreateChannel,
|
onCreateChannel,
|
||||||
@@ -53,6 +59,24 @@ export function NewMessageModal({
|
|||||||
setError('');
|
setError('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !prefillRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTab(prefillRequest.tab);
|
||||||
|
setName(prefillRequest.hashtagName);
|
||||||
|
setContactKey('');
|
||||||
|
setChannelKey('');
|
||||||
|
setTryHistorical(false);
|
||||||
|
setPermitCapitals(false);
|
||||||
|
setError('');
|
||||||
|
setLoading(false);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
hashtagInputRef.current?.focus();
|
||||||
|
});
|
||||||
|
}, [open, prefillRequest]);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
setError('');
|
setError('');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -171,24 +171,17 @@ function isNeighborIdentityResolvable(item: NeighborStat, contacts: Contact[]):
|
|||||||
return resolveContact(item.key, contacts) !== null;
|
return resolveContact(item.key, contacts) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatStrongestPacketDetail(
|
function formatStrongestNeighborDetail(
|
||||||
stats: ReturnType<typeof buildRawPacketStatsSnapshot>,
|
stats: ReturnType<typeof buildRawPacketStatsSnapshot>,
|
||||||
contacts: Contact[]
|
contacts: Contact[]
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (!stats.strongestPacketPayloadType) {
|
const strongestNeighbor = stats.strongestNeighbors[0];
|
||||||
|
if (!strongestNeighbor || strongestNeighbor.bestRssi === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedLabel =
|
const resolvedNeighbor = resolveNeighbor(strongestNeighbor, contacts);
|
||||||
resolveContactLabel(stats.strongestPacketSourceKey, contacts) ??
|
return `${formatRssi(resolvedNeighbor.bestRssi)} best heard`;
|
||||||
stats.strongestPacketSourceLabel;
|
|
||||||
if (resolvedLabel) {
|
|
||||||
return `${resolvedLabel} · ${stats.strongestPacketPayloadType}`;
|
|
||||||
}
|
|
||||||
if (stats.strongestPacketPayloadType === 'GroupText') {
|
|
||||||
return '<unknown sender> · GroupText';
|
|
||||||
}
|
|
||||||
return stats.strongestPacketPayloadType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCoverageMessage(
|
function getCoverageMessage(
|
||||||
@@ -450,8 +443,13 @@ export function RawPacketFeedView({
|
|||||||
[nowSec, rawPacketStatsSession, selectedWindow]
|
[nowSec, rawPacketStatsSession, selectedWindow]
|
||||||
);
|
);
|
||||||
const coverageMessage = getCoverageMessage(stats, rawPacketStatsSession);
|
const coverageMessage = getCoverageMessage(stats, rawPacketStatsSession);
|
||||||
const strongestPacketDetail = useMemo(
|
const strongestNeighbor = useMemo(() => {
|
||||||
() => formatStrongestPacketDetail(stats, contacts),
|
const topNeighbor = stats.strongestNeighbors[0];
|
||||||
|
return topNeighbor ? resolveNeighbor(topNeighbor, contacts) : null;
|
||||||
|
}, [contacts, stats]);
|
||||||
|
|
||||||
|
const strongestNeighborDetail = useMemo(
|
||||||
|
() => formatStrongestNeighborDetail(stats, contacts),
|
||||||
[contacts, stats]
|
[contacts, stats]
|
||||||
);
|
);
|
||||||
const strongestNeighbors = useMemo(
|
const strongestNeighbors = useMemo(
|
||||||
@@ -578,9 +576,9 @@ export function RawPacketFeedView({
|
|||||||
detail={`${formatPercent(stats.pathBearingRate)} path-bearing packets`}
|
detail={`${formatPercent(stats.pathBearingRate)} path-bearing packets`}
|
||||||
/>
|
/>
|
||||||
<StatTile
|
<StatTile
|
||||||
label="Best RSSI"
|
label="Strongest Neighbor"
|
||||||
value={formatRssi(stats.bestRssi)}
|
value={strongestNeighbor?.label ?? '-'}
|
||||||
detail={strongestPacketDetail ?? 'No signal sample in window'}
|
detail={strongestNeighborDetail ?? 'No neighbor RSSI sample in window'}
|
||||||
/>
|
/>
|
||||||
<StatTile
|
<StatTile
|
||||||
label="Median RSSI"
|
label="Median RSSI"
|
||||||
|
|||||||
@@ -853,41 +853,45 @@ export function Sidebar({
|
|||||||
aria-label="Conversations"
|
aria-label="Conversations"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-border">
|
<div className="px-3 py-2 border-b border-border">
|
||||||
<div className="relative min-w-0 flex-1">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search channels/contacts..."
|
|
||||||
aria-label="Search conversations"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className={cn('h-7 text-[13px] bg-background/50', searchQuery ? 'pr-8' : 'pr-3')}
|
|
||||||
/>
|
|
||||||
{searchQuery && (
|
|
||||||
<button
|
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded"
|
|
||||||
onClick={() => setSearchQuery('')}
|
|
||||||
title="Clear search"
|
|
||||||
aria-label="Clear search"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onNewMessage}
|
onClick={onNewMessage}
|
||||||
title="New Message"
|
title="Add channel or contact"
|
||||||
aria-label="New message"
|
aria-label="Add channel or contact"
|
||||||
className="h-7 w-7 shrink-0 p-0 text-muted-foreground hover:text-foreground transition-colors"
|
className="h-8 w-full justify-start gap-2 border-primary/20 bg-primary/5 px-3 text-[13px] text-primary hover:bg-primary/10 hover:text-primary"
|
||||||
>
|
>
|
||||||
<SquarePen className="h-4 w-4" />
|
<SquarePen className="h-4 w-4" />
|
||||||
|
<span>Add Channel/Contact</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* List */}
|
{/* List */}
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto [contain:layout_paint]">
|
<div className="flex-1 min-h-0 overflow-y-auto [contain:layout_paint]">
|
||||||
|
<div className="px-3 py-2 border-b border-border/60">
|
||||||
|
<div className="relative min-w-0">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search channels/contacts..."
|
||||||
|
aria-label="Search conversations"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className={cn('h-7 text-[13px] bg-background/50', searchQuery ? 'pr-8' : 'pr-3')}
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded"
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
title="Clear search"
|
||||||
|
aria-label="Clear search"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Tools */}
|
{/* Tools */}
|
||||||
{toolRows.length > 0 && (
|
{toolRows.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -318,8 +318,8 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col overflow-y-auto">
|
<div className="flex h-full min-h-0 flex-col overflow-y-auto lg:overflow-hidden">
|
||||||
<div className="border-b border-border px-4 py-3">
|
<div className="shrink-0 border-b border-border px-4 py-3">
|
||||||
<h2 className="text-base font-semibold">Trace</h2>
|
<h2 className="text-base font-semibold">Trace</h2>
|
||||||
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
|
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
|
||||||
Build a repeater loop and trace it back to the local radio. The selectable hop list only
|
Build a repeater loop and trace it back to the local radio. The selectable hop list only
|
||||||
@@ -329,7 +329,7 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
|
|||||||
|
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4 lg:min-h-0 lg:flex-row lg:overflow-hidden">
|
<div className="flex flex-1 flex-col gap-4 p-4 lg:min-h-0 lg:flex-row lg:overflow-hidden">
|
||||||
<section className="flex w-full flex-col rounded-lg border border-border bg-card lg:min-h-0 lg:max-w-[24rem]">
|
<section className="flex w-full flex-col rounded-lg border border-border bg-card lg:min-h-0 lg:max-w-[24rem]">
|
||||||
<div className="border-b border-border p-4">
|
<div className="shrink-0 border-b border-border p-4">
|
||||||
<h3 className="text-sm font-semibold">Repeater Hops</h3>
|
<h3 className="text-sm font-semibold">Repeater Hops</h3>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
Search by name or key, then add repeaters in the order you want to traverse them.
|
Search by name or key, then add repeaters in the order you want to traverse them.
|
||||||
@@ -446,14 +446,30 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="flex flex-1 flex-col gap-4 lg:min-h-0 lg:overflow-hidden">
|
<section className="flex flex-1 flex-col gap-4 lg:min-h-0 lg:overflow-hidden">
|
||||||
<div className="rounded-lg border border-border bg-card">
|
<div className="flex flex-col rounded-lg border border-border bg-card lg:min-h-0 lg:max-h-[50%]">
|
||||||
<div className="border-b border-border px-4 py-3">
|
<div className="shrink-0 flex items-start justify-between gap-3 border-b border-border px-4 py-3">
|
||||||
<h3 className="text-sm font-semibold">Trace Path</h3>
|
<div>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<h3 className="text-sm font-semibold">Trace Path</h3>
|
||||||
The first node is display-only. The terminal node is the local radio.
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
</p>
|
The first node is display-only. The terminal node is the local radio.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{draftHops.length > 0 ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="shrink-0 text-muted-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
setDraftHops([]);
|
||||||
|
clearPendingResult();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[42vh] space-y-2 overflow-y-auto p-4 lg:max-h-none lg:overflow-y-visible">
|
<div className="space-y-2 p-4 lg:min-h-0 lg:flex-1 lg:overflow-y-auto">
|
||||||
<TraceNodeRow
|
<TraceNodeRow
|
||||||
title={localRadioName}
|
title={localRadioName}
|
||||||
subtitle={getShortKey(localRadioKey)}
|
subtitle={getShortKey(localRadioKey)}
|
||||||
@@ -542,7 +558,7 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
|
|||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-border px-4 py-3">
|
<div className="shrink-0 flex flex-wrap items-center justify-between gap-3 border-t border-border px-4 py-3">
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{draftHops.length === 0
|
{draftHops.length === 0
|
||||||
? 'No hops selected'
|
? 'No hops selected'
|
||||||
@@ -555,12 +571,26 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col rounded-lg border border-border bg-card lg:min-h-0 lg:flex-1">
|
<div className="flex flex-col rounded-lg border border-border bg-card lg:min-h-0 lg:flex-1">
|
||||||
<div className="border-b border-border px-4 py-3">
|
<div className="shrink-0 flex items-center justify-between gap-3 border-b border-border px-4 py-3">
|
||||||
<h3 className="text-sm font-semibold">
|
<h3 className="text-sm font-semibold">
|
||||||
Results{result ? ` (${result.timeout_seconds.toFixed(1)}s)` : ''}
|
Results{result ? ` (${result.timeout_seconds.toFixed(1)}s)` : ''}
|
||||||
</h3>
|
</h3>
|
||||||
|
{result || error ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="shrink-0 text-muted-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
setResult(null);
|
||||||
|
setError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[42vh] min-h-0 flex-1 space-y-3 overflow-y-auto p-4 lg:max-h-none">
|
<div className="min-h-0 flex-1 space-y-3 p-4 lg:overflow-y-auto">
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
@@ -140,6 +140,59 @@ describe('MessageList channel sender rendering', () => {
|
|||||||
expect(screen.getByRole('button', { name: 'View info for Alice' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'View info for Alice' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders valid channel references as clickable links and ignores invalid ones', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onChannelReferenceClick = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MessageList
|
||||||
|
messages={[
|
||||||
|
createMessage({
|
||||||
|
text: 'Alice: Join #mesh-room now skip #bad--room and visit https://example.com/#also-skip',
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
contacts={[]}
|
||||||
|
loading={false}
|
||||||
|
onChannelReferenceClick={onChannelReferenceClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const linkedChannel = screen.getByRole('button', { name: '#mesh-room' });
|
||||||
|
expect(linkedChannel).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: '#bad--room' })).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('link', { name: 'https://example.com/#also-skip' })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(linkedChannel);
|
||||||
|
|
||||||
|
expect(onChannelReferenceClick).toHaveBeenCalledWith('#mesh-room');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('links valid channel references in direct messages too', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onChannelReferenceClick = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MessageList
|
||||||
|
messages={[
|
||||||
|
createMessage({
|
||||||
|
type: 'PRIV',
|
||||||
|
text: 'check #ops-room',
|
||||||
|
conversation_key: 'ab'.repeat(32),
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
contacts={[]}
|
||||||
|
loading={false}
|
||||||
|
onChannelReferenceClick={onChannelReferenceClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: '#ops-room' }));
|
||||||
|
|
||||||
|
expect(onChannelReferenceClick).toHaveBeenCalledWith('#ops-room');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders and dismisses an unread marker at the first unread message boundary', async () => {
|
it('renders and dismisses an unread marker at the first unread message boundary', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const messages = [
|
const messages = [
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { parseSenderFromText, formatTime } from '../utils/messageParser';
|
import {
|
||||||
|
findLinkedChannelReferences,
|
||||||
|
formatTime,
|
||||||
|
isValidLinkedChannelName,
|
||||||
|
parseSenderFromText,
|
||||||
|
} from '../utils/messageParser';
|
||||||
|
|
||||||
describe('parseSenderFromText', () => {
|
describe('parseSenderFromText', () => {
|
||||||
it('extracts sender and content from "sender: message" format', () => {
|
it('extracts sender and content from "sender: message" format', () => {
|
||||||
@@ -95,3 +100,33 @@ describe('formatTime', () => {
|
|||||||
expect(result).toMatch(/\d{1,2}:\d{2}/); // time portion
|
expect(result).toMatch(/\d{1,2}:\d{2}/); // time portion
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('linked channel references', () => {
|
||||||
|
it('accepts lowercase alphanumeric names with single dashes', () => {
|
||||||
|
expect(isValidLinkedChannelName('ops')).toBe(true);
|
||||||
|
expect(isValidLinkedChannelName('ops-1')).toBe(true);
|
||||||
|
expect(isValidLinkedChannelName('1-2-3')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects uppercase, leading or trailing dashes, and repeated dashes', () => {
|
||||||
|
expect(isValidLinkedChannelName('Ops')).toBe(false);
|
||||||
|
expect(isValidLinkedChannelName('-ops')).toBe(false);
|
||||||
|
expect(isValidLinkedChannelName('ops-')).toBe(false);
|
||||||
|
expect(isValidLinkedChannelName('ops--room')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds standalone linked channel references in message text', () => {
|
||||||
|
expect(findLinkedChannelReferences('Join #mesh-room then say hi in #ops2')).toEqual([
|
||||||
|
{ label: '#mesh-room', start: 5, end: 15 },
|
||||||
|
{ label: '#ops2', start: 31, end: 36 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores invalid or embedded channel-like text', () => {
|
||||||
|
expect(
|
||||||
|
findLinkedChannelReferences(
|
||||||
|
'skip #Bad #bad--name abc#ops #ops- #opsRoom #ops_room #good-room,'
|
||||||
|
)
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ describe('NewMessageModal form reset', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderModal(open = true) {
|
function renderModal(
|
||||||
|
open = true,
|
||||||
|
overrides: Partial<Parameters<typeof NewMessageModal>[0]> = {}
|
||||||
|
) {
|
||||||
return render(
|
return render(
|
||||||
<NewMessageModal
|
<NewMessageModal
|
||||||
open={open}
|
open={open}
|
||||||
@@ -41,6 +44,7 @@ describe('NewMessageModal form reset', () => {
|
|||||||
onCreateContact={onCreateContact}
|
onCreateContact={onCreateContact}
|
||||||
onCreateChannel={onCreateChannel}
|
onCreateChannel={onCreateChannel}
|
||||||
onCreateHashtagChannel={onCreateHashtagChannel}
|
onCreateHashtagChannel={onCreateHashtagChannel}
|
||||||
|
{...overrides}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,6 +54,26 @@ describe('NewMessageModal form reset', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('hashtag tab', () => {
|
describe('hashtag tab', () => {
|
||||||
|
it('prefills the hashtag tab from a linked channel request', async () => {
|
||||||
|
renderModal(true, {
|
||||||
|
prefillRequest: {
|
||||||
|
tab: 'hashtag',
|
||||||
|
hashtagName: 'mesh-room',
|
||||||
|
nonce: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('tab', { name: 'Hashtag Channel' })).toHaveAttribute(
|
||||||
|
'data-state',
|
||||||
|
'active'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect((screen.getByPlaceholderText('channel-name') as HTMLInputElement).value).toBe(
|
||||||
|
'mesh-room'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('clears name after successful Create', async () => {
|
it('clears name after successful Create', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const { unmount } = renderModal();
|
const { unmount } = renderModal();
|
||||||
|
|||||||
@@ -283,6 +283,8 @@ describe('RawPacketFeedView', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: /show stats/i }));
|
fireEvent.click(screen.getByRole('button', { name: /show stats/i }));
|
||||||
fireEvent.change(screen.getByLabelText('Stats window'), { target: { value: 'session' } });
|
fireEvent.change(screen.getByLabelText('Stats window'), { target: { value: 'session' } });
|
||||||
expect(screen.getAllByText('Alpha').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('Alpha').length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText('Strongest Neighbor')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('-70 dBm best heard')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('marks unresolved neighbor identities explicitly', () => {
|
it('marks unresolved neighbor identities explicitly', () => {
|
||||||
|
|||||||
@@ -122,6 +122,46 @@ describe('Sidebar section summaries', () => {
|
|||||||
expect(within(getSectionHeaderContainer('Repeaters')).getByText('4')).toBeInTheDocument();
|
expect(within(getSectionHeaderContainer('Repeaters')).getByText('4')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders a full add channel/contact button above search and calls onNewMessage', () => {
|
||||||
|
const onNewMessage = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Sidebar
|
||||||
|
contacts={[]}
|
||||||
|
channels={[makeChannel(PUBLIC_CHANNEL_KEY, 'Public')]}
|
||||||
|
activeConversation={null}
|
||||||
|
onSelectConversation={vi.fn()}
|
||||||
|
onNewMessage={onNewMessage}
|
||||||
|
lastMessageTimes={{}}
|
||||||
|
unreadCounts={{}}
|
||||||
|
mentions={{}}
|
||||||
|
showCracker={false}
|
||||||
|
crackerRunning={false}
|
||||||
|
onToggleCracker={vi.fn()}
|
||||||
|
onMarkAllRead={vi.fn()}
|
||||||
|
favorites={[]}
|
||||||
|
legacySortOrder="recent"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const addButton = screen.getByRole('button', { name: 'Add channel or contact' });
|
||||||
|
const search = screen.getByLabelText('Search conversations');
|
||||||
|
const nav = screen.getByRole('navigation', { name: 'Conversations' });
|
||||||
|
const toolsButton = screen.getByRole('button', { name: 'Tools' });
|
||||||
|
|
||||||
|
expect(addButton).toHaveTextContent('Add Channel/Contact');
|
||||||
|
expect(
|
||||||
|
addButton.compareDocumentPosition(search) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(nav.compareDocumentPosition(search) & Node.DOCUMENT_POSITION_CONTAINED_BY).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
search.compareDocumentPosition(toolsButton) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
expect(onNewMessage).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('turns favorites and channels rollups red when they contain a mention', () => {
|
it('turns favorites and channels rollups red when they contain a mention', () => {
|
||||||
renderSidebar({
|
renderSidebar({
|
||||||
mentions: {
|
mentions: {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
* Parse sender from channel message text.
|
* Parse sender from channel message text.
|
||||||
* Channel messages have format "sender: message".
|
* Channel messages have format "sender: message".
|
||||||
*/
|
*/
|
||||||
|
const HASHTAG_CHANNEL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||||
|
const HASHTAG_CHANNEL_REFERENCE_PATTERN = /(^|\s)(#[a-z0-9]+(?:-[a-z0-9]+)*)(?=$|\s)/g;
|
||||||
|
|
||||||
export function parseSenderFromText(text: string): { sender: string | null; content: string } {
|
export function parseSenderFromText(text: string): { sender: string | null; content: string } {
|
||||||
const colonIndex = text.indexOf(': ');
|
const colonIndex = text.indexOf(': ');
|
||||||
if (colonIndex > 0 && colonIndex < 50) {
|
if (colonIndex > 0 && colonIndex < 50) {
|
||||||
@@ -17,6 +20,35 @@ export function parseSenderFromText(text: string): { sender: string | null; cont
|
|||||||
return { sender: null, content: text };
|
return { sender: null, content: text };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HashtagChannelReference {
|
||||||
|
label: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidLinkedChannelName(name: string): boolean {
|
||||||
|
return HASHTAG_CHANNEL_NAME_PATTERN.test(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findLinkedChannelReferences(text: string): HashtagChannelReference[] {
|
||||||
|
const references: HashtagChannelReference[] = [];
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
HASHTAG_CHANNEL_REFERENCE_PATTERN.lastIndex = 0;
|
||||||
|
while ((match = HASHTAG_CHANNEL_REFERENCE_PATTERN.exec(text)) !== null) {
|
||||||
|
const prefix = match[1];
|
||||||
|
const label = match[2];
|
||||||
|
const start = match.index + prefix.length;
|
||||||
|
references.push({
|
||||||
|
label,
|
||||||
|
start,
|
||||||
|
end: start + label.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return references;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Unix timestamp to a time string.
|
* Format a Unix timestamp to a time string.
|
||||||
* Shows date for messages not from today.
|
* Shows date for messages not from today.
|
||||||
|
|||||||
@@ -106,9 +106,6 @@ export interface RawPacketStatsSnapshot {
|
|||||||
medianRssi: number | null;
|
medianRssi: number | null;
|
||||||
bestRssi: number | null;
|
bestRssi: number | null;
|
||||||
rssiBuckets: RankedPacketStat[];
|
rssiBuckets: RankedPacketStat[];
|
||||||
strongestPacketSourceKey: string | null;
|
|
||||||
strongestPacketSourceLabel: string | null;
|
|
||||||
strongestPacketPayloadType: string | null;
|
|
||||||
coverageSeconds: number;
|
coverageSeconds: number;
|
||||||
windowFullyCovered: boolean;
|
windowFullyCovered: boolean;
|
||||||
oldestStoredTimestamp: number | null;
|
oldestStoredTimestamp: number | null;
|
||||||
@@ -377,8 +374,6 @@ export function buildRawPacketStatsSnapshot(
|
|||||||
['Weak (<-85 dBm)', 0],
|
['Weak (<-85 dBm)', 0],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let strongestPacket: RawPacketStatsObservation | null = null;
|
|
||||||
|
|
||||||
for (const packet of packets) {
|
for (const packet of packets) {
|
||||||
payloadCounts.set(packet.payloadType, (payloadCounts.get(packet.payloadType) ?? 0) + 1);
|
payloadCounts.set(packet.payloadType, (payloadCounts.get(packet.payloadType) ?? 0) + 1);
|
||||||
routeCounts.set(packet.routeType, (routeCounts.get(packet.routeType) ?? 0) + 1);
|
routeCounts.set(packet.routeType, (routeCounts.get(packet.routeType) ?? 0) + 1);
|
||||||
@@ -436,10 +431,6 @@ export function buildRawPacketStatsSnapshot(
|
|||||||
} else {
|
} else {
|
||||||
rssiBucketCounts.set('Weak (<-85 dBm)', (rssiBucketCounts.get('Weak (<-85 dBm)') ?? 0) + 1);
|
rssiBucketCounts.set('Weak (<-85 dBm)', (rssiBucketCounts.get('Weak (<-85 dBm)') ?? 0) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!strongestPacket || strongestPacket.rssi === null || packet.rssi > strongestPacket.rssi) {
|
|
||||||
strongestPacket = packet;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,9 +518,6 @@ export function buildRawPacketStatsSnapshot(
|
|||||||
medianRssi,
|
medianRssi,
|
||||||
bestRssi,
|
bestRssi,
|
||||||
rssiBuckets: rankedBreakdown(rssiBucketCounts, rssiValues.length),
|
rssiBuckets: rankedBreakdown(rssiBucketCounts, rssiValues.length),
|
||||||
strongestPacketSourceKey: strongestPacket?.sourceKey ?? null,
|
|
||||||
strongestPacketSourceLabel: strongestPacket?.sourceLabel ?? null,
|
|
||||||
strongestPacketPayloadType: strongestPacket?.payloadType ?? null,
|
|
||||||
coverageSeconds,
|
coverageSeconds,
|
||||||
windowFullyCovered,
|
windowFullyCovered,
|
||||||
oldestStoredTimestamp,
|
oldestStoredTimestamp,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "remoteterm-meshcore"
|
name = "remoteterm-meshcore"
|
||||||
version = "3.6.3"
|
version = "3.6.7"
|
||||||
description = "RemoteTerm - Web interface for MeshCore radio mesh networks"
|
description = "RemoteTerm - Web interface for MeshCore radio mesh networks"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
Regular → Executable
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck source=scripts/build/release_common.sh
|
||||||
|
source "$SCRIPT_DIR/release_common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: scripts/build/create_github_release.sh --version X.Y.Z --asset PATH [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version VERSION Release version / tag (required)
|
||||||
|
--asset PATH Asset to attach; may be specified multiple times
|
||||||
|
--notes-file PATH Markdown release notes file; defaults to CHANGELOG section
|
||||||
|
--full-git-hash HASH Commit to tag if the tag does not already exist locally
|
||||||
|
--title TITLE Release title (default: version)
|
||||||
|
--help Show this message
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
VERSION=""
|
||||||
|
TITLE=""
|
||||||
|
NOTES_FILE=""
|
||||||
|
FULL_GIT_HASH=""
|
||||||
|
ASSETS=()
|
||||||
|
TEMP_NOTES_FILE=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$TEMP_NOTES_FILE" ] && [ -f "$TEMP_NOTES_FILE" ]; then
|
||||||
|
rm -f "$TEMP_NOTES_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--version)
|
||||||
|
VERSION="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--asset)
|
||||||
|
ASSETS+=("${2:-}")
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--notes-file)
|
||||||
|
NOTES_FILE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--full-git-hash)
|
||||||
|
FULL_GIT_HASH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--title)
|
||||||
|
TITLE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
release_die "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$VERSION" ] || release_die "--version is required"
|
||||||
|
[ "${#ASSETS[@]}" -gt 0 ] || release_die "At least one --asset is required"
|
||||||
|
release_validate_version "$VERSION"
|
||||||
|
|
||||||
|
REPO_ROOT="$(release_repo_root)"
|
||||||
|
TITLE="${TITLE:-$VERSION}"
|
||||||
|
FULL_GIT_HASH="${FULL_GIT_HASH:-$(release_resolve_full_hash "$REPO_ROOT")}"
|
||||||
|
|
||||||
|
for asset in "${ASSETS[@]}"; do
|
||||||
|
[ -f "$asset" ] || release_die "Asset not found: $asset"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$NOTES_FILE" ]; then
|
||||||
|
TEMP_NOTES_FILE="$(mktemp)"
|
||||||
|
release_extract_changelog_section "$REPO_ROOT" "$VERSION" "$TEMP_NOTES_FILE"
|
||||||
|
NOTES_FILE="$TEMP_NOTES_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$NOTES_FILE" ] || release_die "Notes file not found: $NOTES_FILE"
|
||||||
|
|
||||||
|
if ! git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
|
||||||
|
echo "[create_github_release] Creating local tag $VERSION at $FULL_GIT_HASH..." >&2
|
||||||
|
git -C "$REPO_ROOT" tag -a "$VERSION" "$FULL_GIT_HASH" -F "$NOTES_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git -C "$REPO_ROOT" ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
|
||||||
|
echo "[create_github_release] Pushing tag $VERSION to origin..." >&2
|
||||||
|
git -C "$REPO_ROOT" push origin "$VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if gh release view "$VERSION" >/dev/null 2>&1; then
|
||||||
|
echo "[create_github_release] Updating existing GitHub release $VERSION..." >&2
|
||||||
|
gh release upload "$VERSION" "${ASSETS[@]}" --clobber
|
||||||
|
gh release edit "$VERSION" --title "$TITLE" --notes-file "$NOTES_FILE"
|
||||||
|
else
|
||||||
|
echo "[create_github_release] Creating GitHub release $VERSION..." >&2
|
||||||
|
gh release create "$VERSION" "${ASSETS[@]}" --title "$TITLE" --notes-file "$NOTES_FILE" --verify-tag
|
||||||
|
fi
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck source=scripts/build/release_common.sh
|
||||||
|
source "$SCRIPT_DIR/release_common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: scripts/build/extract_release_notes.sh --version X.Y.Z --output PATH
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version VERSION Release version to extract from CHANGELOG.md
|
||||||
|
--output PATH Output markdown file path
|
||||||
|
--changelog PATH Override changelog path
|
||||||
|
--help Show this message
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
VERSION=""
|
||||||
|
OUTPUT_FILE=""
|
||||||
|
CHANGELOG_PATH=""
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--version)
|
||||||
|
VERSION="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
OUTPUT_FILE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--changelog)
|
||||||
|
CHANGELOG_PATH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
release_die "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$VERSION" ] || release_die "--version is required"
|
||||||
|
[ -n "$OUTPUT_FILE" ] || release_die "--output is required"
|
||||||
|
release_validate_version "$VERSION"
|
||||||
|
|
||||||
|
REPO_ROOT="$(release_repo_root)"
|
||||||
|
release_extract_changelog_section "$REPO_ROOT" "$VERSION" "$OUTPUT_FILE" "${CHANGELOG_PATH:-$REPO_ROOT/CHANGELOG.md}"
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck source=scripts/build/release_common.sh
|
||||||
|
source "$SCRIPT_DIR/release_common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: scripts/build/package_release_artifact.sh --version X.Y.Z [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version VERSION Release version (required)
|
||||||
|
--git-hash HASH Short git hash to embed in artifact naming
|
||||||
|
--full-git-hash HASH Full git hash to archive
|
||||||
|
--output PATH Output zip path
|
||||||
|
--bundle-name NAME Bundle folder name inside the zip
|
||||||
|
--skip-prebuilt-build Reuse existing frontend/prebuilt instead of rebuilding it
|
||||||
|
--help Show this message
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
VERSION=""
|
||||||
|
GIT_HASH=""
|
||||||
|
FULL_GIT_HASH=""
|
||||||
|
OUTPUT_PATH=""
|
||||||
|
BUNDLE_NAME="Remote-Terminal-for-MeshCore"
|
||||||
|
SKIP_PREBUILT_BUILD=0
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--version)
|
||||||
|
VERSION="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--git-hash)
|
||||||
|
GIT_HASH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--full-git-hash)
|
||||||
|
FULL_GIT_HASH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
OUTPUT_PATH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--bundle-name)
|
||||||
|
BUNDLE_NAME="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--skip-prebuilt-build)
|
||||||
|
SKIP_PREBUILT_BUILD=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
release_die "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$VERSION" ] || release_die "--version is required"
|
||||||
|
release_validate_version "$VERSION"
|
||||||
|
|
||||||
|
REPO_ROOT="$(release_repo_root)"
|
||||||
|
FULL_GIT_HASH="${FULL_GIT_HASH:-$(release_resolve_full_hash "$REPO_ROOT")}"
|
||||||
|
GIT_HASH="${GIT_HASH:-$(release_resolve_short_hash "$REPO_ROOT" "$FULL_GIT_HASH")}"
|
||||||
|
OUTPUT_PATH="${OUTPUT_PATH:-$REPO_ROOT/remoteterm-prebuilt-frontend-v${VERSION}-${GIT_HASH}.zip}"
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
BUNDLE_DIR="$WORK_DIR/$BUNDLE_NAME"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if [ "$SKIP_PREBUILT_BUILD" -eq 0 ]; then
|
||||||
|
echo "[package_release_artifact] Building frontend prebuilt bundle..." >&2
|
||||||
|
(
|
||||||
|
cd "$REPO_ROOT/frontend"
|
||||||
|
npm run packaged-build
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -d "$REPO_ROOT/frontend/prebuilt" ] || release_die "frontend/prebuilt is missing; run with frontend built or omit --skip-prebuilt-build"
|
||||||
|
|
||||||
|
mkdir -p "$BUNDLE_DIR/frontend"
|
||||||
|
git -C "$REPO_ROOT" archive "$FULL_GIT_HASH" | tar -x -C "$BUNDLE_DIR"
|
||||||
|
cp -R "$REPO_ROOT/frontend/prebuilt" "$BUNDLE_DIR/frontend/prebuilt"
|
||||||
|
|
||||||
|
cat > "$BUNDLE_DIR/build_info.json" <<EOF
|
||||||
|
{
|
||||||
|
"version": "$VERSION",
|
||||||
|
"commit_hash": "$GIT_HASH",
|
||||||
|
"build_source": "prebuilt-release"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
rm -f "$OUTPUT_PATH"
|
||||||
|
(
|
||||||
|
cd "$WORK_DIR"
|
||||||
|
zip -qr "$OUTPUT_PATH" "$BUNDLE_NAME"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "$OUTPUT_PATH"
|
||||||
Regular → Executable
Regular → Executable
+93
-155
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -euo pipefail
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -10,95 +10,63 @@ NC='\033[0m' # No Color
|
|||||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
RELEASE_WORK_DIR=""
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
RELEASE_BUNDLE_DIR_NAME="Remote-Terminal-for-MeshCore"
|
# shellcheck source=scripts/build/release_common.sh
|
||||||
RELEASE_ASSET=""
|
source "$SCRIPT_DIR/release_common.sh"
|
||||||
DOCKER_IMAGE="jkingsman/remoteterm-meshcore"
|
|
||||||
|
DOCKER_IMAGE="docker.io/jkingsman/remoteterm-meshcore"
|
||||||
DOCKER_PLATFORMS="linux/amd64,linux/arm64"
|
DOCKER_PLATFORMS="linux/amd64,linux/arm64"
|
||||||
|
VERSION=""
|
||||||
|
NOTES_FILE=""
|
||||||
|
SKIP_QUALITY=0
|
||||||
|
RELEASE_ASSET_PATH=""
|
||||||
|
|
||||||
cleanup_release_build_artifacts() {
|
usage() {
|
||||||
if [ -d "$REPO_ROOT/frontend/prebuilt" ]; then
|
cat <<'EOF'
|
||||||
rm -rf "$REPO_ROOT/frontend/prebuilt"
|
Usage: scripts/build/publish.sh [options]
|
||||||
fi
|
|
||||||
if [ -n "$RELEASE_WORK_DIR" ] && [ -d "$RELEASE_WORK_DIR" ]; then
|
Options:
|
||||||
rm -rf "$RELEASE_WORK_DIR"
|
--version VERSION Release version; prompts if omitted
|
||||||
fi
|
--notes-file PATH File containing changelog entry lines; prompts if omitted
|
||||||
if [ -n "$RELEASE_ASSET" ] && [ -f "$REPO_ROOT/$RELEASE_ASSET" ]; then
|
--skip-quality Skip ./scripts/quality/all_quality.sh
|
||||||
rm -f "$REPO_ROOT/$RELEASE_ASSET"
|
--help Show this message
|
||||||
fi
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
trap cleanup_release_build_artifacts EXIT
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
ensure_buildx_builder() {
|
--version)
|
||||||
if ! docker buildx version >/dev/null 2>&1; then
|
VERSION="${2:-}"
|
||||||
echo -e "${RED}Error: docker buildx is required for multi-arch Docker builds.${NC}"
|
shift 2
|
||||||
exit 1
|
;;
|
||||||
fi
|
--notes-file)
|
||||||
|
NOTES_FILE="${2:-}"
|
||||||
local current_builder
|
shift 2
|
||||||
current_builder="$(docker buildx inspect --format '{{ .Name }}' 2>/dev/null || true)"
|
;;
|
||||||
|
--skip-quality)
|
||||||
if [ -n "$current_builder" ]; then
|
SKIP_QUALITY=1
|
||||||
docker buildx inspect --bootstrap >/dev/null
|
shift
|
||||||
return
|
;;
|
||||||
fi
|
--help)
|
||||||
|
usage
|
||||||
if docker buildx inspect remoteterm-multiarch >/dev/null 2>&1; then
|
exit 0
|
||||||
docker buildx use remoteterm-multiarch >/dev/null
|
;;
|
||||||
else
|
*)
|
||||||
docker buildx create --name remoteterm-multiarch --use >/dev/null
|
usage >&2
|
||||||
fi
|
release_die "Unknown argument: $1"
|
||||||
docker buildx inspect --bootstrap >/dev/null
|
;;
|
||||||
}
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
echo -e "${YELLOW}=== RemoteTerm for MeshCore Publish Script ===${NC}"
|
echo -e "${YELLOW}=== RemoteTerm for MeshCore Publish Script ===${NC}"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# Run backend linting and type checking
|
if [ "$SKIP_QUALITY" -eq 0 ]; then
|
||||||
echo -e "${YELLOW}Running backend lint (Ruff)...${NC}"
|
echo -e "${YELLOW}Running repo quality gate...${NC}"
|
||||||
uv run ruff check app/ tests/ --fix
|
./scripts/quality/all_quality.sh
|
||||||
uv run ruff format app/ tests/
|
echo -e "${GREEN}Quality gate passed!${NC}"
|
||||||
# validate
|
echo
|
||||||
uv run ruff check app/ tests/
|
fi
|
||||||
uv run ruff format --check app/ tests/
|
|
||||||
echo -e "${GREEN}Backend lint passed!${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo -e "${YELLOW}Running backend type check (Pyright)...${NC}"
|
|
||||||
uv run pyright app/
|
|
||||||
echo -e "${GREEN}Backend type check passed!${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Run backend tests
|
|
||||||
echo -e "${YELLOW}Running backend tests...${NC}"
|
|
||||||
PYTHONPATH=. uv run pytest tests/ -v
|
|
||||||
echo -e "${GREEN}Backend tests passed!${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Run frontend linting and formatting check
|
|
||||||
echo -e "${YELLOW}Running frontend lint (ESLint)...${NC}"
|
|
||||||
cd "$REPO_ROOT/frontend"
|
|
||||||
npm run lint
|
|
||||||
echo -e "${GREEN}Frontend lint passed!${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo -e "${YELLOW}Checking frontend formatting (Prettier)...${NC}"
|
|
||||||
npm run format:check
|
|
||||||
echo -e "${GREEN}Frontend formatting OK!${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Run frontend tests and build
|
|
||||||
echo -e "${YELLOW}Running frontend tests...${NC}"
|
|
||||||
npm run test:run
|
|
||||||
echo -e "${GREEN}Frontend tests passed!${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo -e "${YELLOW}Building frontend...${NC}"
|
|
||||||
npm run build
|
|
||||||
echo -e "${GREEN}Frontend build complete!${NC}"
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo -e "${YELLOW}Regenerating LICENSES.md...${NC}"
|
echo -e "${YELLOW}Regenerating LICENSES.md...${NC}"
|
||||||
bash scripts/build/collect_licenses.sh LICENSES.md
|
bash scripts/build/collect_licenses.sh LICENSES.md
|
||||||
@@ -113,13 +81,11 @@ echo -n " package.json: "
|
|||||||
grep '"version"' frontend/package.json | head -1 | sed 's/.*"version": "\(.*\)".*/\1/'
|
grep '"version"' frontend/package.json | head -1 | sed 's/.*"version": "\(.*\)".*/\1/'
|
||||||
echo
|
echo
|
||||||
|
|
||||||
read -r -p "Enter new version (e.g., 1.2.3): " VERSION
|
if [ -z "$VERSION" ]; then
|
||||||
VERSION="$(printf '%s' "$VERSION" | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
read -r -p "Enter new version (e.g., 1.2.3): " VERSION
|
||||||
|
|
||||||
if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
echo -e "${RED}Error: Version must be in format X.Y.Z${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
VERSION="$(release_trim "$VERSION")"
|
||||||
|
release_validate_version "$VERSION"
|
||||||
|
|
||||||
# Update pyproject.toml
|
# Update pyproject.toml
|
||||||
echo -e "${YELLOW}Updating pyproject.toml...${NC}"
|
echo -e "${YELLOW}Updating pyproject.toml...${NC}"
|
||||||
@@ -137,11 +103,28 @@ echo -e "${GREEN}Version updated to $VERSION${NC}"
|
|||||||
echo
|
echo
|
||||||
|
|
||||||
# Prompt for changelog entry
|
# Prompt for changelog entry
|
||||||
echo -e "${YELLOW}Enter changelog entry for version $VERSION${NC}"
|
RAW_CHANGELOG_INPUT_FILE="$(mktemp)"
|
||||||
echo -e "${YELLOW}(Enter your changes, then press Ctrl+D when done):${NC}"
|
FORMATTED_CHANGELOG_INPUT_FILE="$(mktemp)"
|
||||||
echo
|
cleanup() {
|
||||||
|
rm -f "$RAW_CHANGELOG_INPUT_FILE" "$FORMATTED_CHANGELOG_INPUT_FILE"
|
||||||
|
rm -rf "${REPO_ROOT:?}/frontend/prebuilt"
|
||||||
|
if [ -n "$RELEASE_ASSET_PATH" ] && [ -f "$RELEASE_ASSET_PATH" ]; then
|
||||||
|
rm -f "$RELEASE_ASSET_PATH"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
CHANGELOG_ENTRY=$(cat)
|
if [ -n "$NOTES_FILE" ]; then
|
||||||
|
cp "$NOTES_FILE" "$RAW_CHANGELOG_INPUT_FILE"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Enter changelog entry for version $VERSION${NC}"
|
||||||
|
echo -e "${YELLOW}(Enter your changes, then press Ctrl+D when done):${NC}"
|
||||||
|
echo
|
||||||
|
cat > "$RAW_CHANGELOG_INPUT_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
release_format_markdown_list "$RAW_CHANGELOG_INPUT_FILE" "$FORMATTED_CHANGELOG_INPUT_FILE"
|
||||||
|
[ -s "$FORMATTED_CHANGELOG_INPUT_FILE" ] || release_die "Changelog entry cannot be empty"
|
||||||
|
|
||||||
# Create changelog entry with date
|
# Create changelog entry with date
|
||||||
DATE=$(date +%Y-%m-%d)
|
DATE=$(date +%Y-%m-%d)
|
||||||
@@ -157,7 +140,7 @@ if [ -f CHANGELOG.md ]; then
|
|||||||
echo
|
echo
|
||||||
echo "$CHANGELOG_HEADER"
|
echo "$CHANGELOG_HEADER"
|
||||||
echo
|
echo
|
||||||
echo "$CHANGELOG_ENTRY"
|
cat "$FORMATTED_CHANGELOG_INPUT_FILE"
|
||||||
echo
|
echo
|
||||||
tail -n +2 CHANGELOG.md
|
tail -n +2 CHANGELOG.md
|
||||||
} > CHANGELOG.md.tmp
|
} > CHANGELOG.md.tmp
|
||||||
@@ -167,7 +150,7 @@ if [ -f CHANGELOG.md ]; then
|
|||||||
{
|
{
|
||||||
echo "$CHANGELOG_HEADER"
|
echo "$CHANGELOG_HEADER"
|
||||||
echo
|
echo
|
||||||
echo "$CHANGELOG_ENTRY"
|
cat "$FORMATTED_CHANGELOG_INPUT_FILE"
|
||||||
echo
|
echo
|
||||||
cat CHANGELOG.md
|
cat CHANGELOG.md
|
||||||
} > CHANGELOG.md.tmp
|
} > CHANGELOG.md.tmp
|
||||||
@@ -180,7 +163,7 @@ else
|
|||||||
echo
|
echo
|
||||||
echo "$CHANGELOG_HEADER"
|
echo "$CHANGELOG_HEADER"
|
||||||
echo
|
echo
|
||||||
echo "$CHANGELOG_ENTRY"
|
cat "$FORMATTED_CHANGELOG_INPUT_FILE"
|
||||||
} > CHANGELOG.md
|
} > CHANGELOG.md
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -200,78 +183,33 @@ echo
|
|||||||
GIT_HASH=$(git rev-parse --short HEAD)
|
GIT_HASH=$(git rev-parse --short HEAD)
|
||||||
FULL_GIT_HASH=$(git rev-parse HEAD)
|
FULL_GIT_HASH=$(git rev-parse HEAD)
|
||||||
RELEASE_ASSET="remoteterm-prebuilt-frontend-v${VERSION}-${GIT_HASH}.zip"
|
RELEASE_ASSET="remoteterm-prebuilt-frontend-v${VERSION}-${GIT_HASH}.zip"
|
||||||
|
RELEASE_ASSET_PATH="$REPO_ROOT/$RELEASE_ASSET"
|
||||||
|
|
||||||
echo -e "${YELLOW}Building packaged frontend artifact...${NC}"
|
echo -e "${YELLOW}Building packaged frontend artifact...${NC}"
|
||||||
cd "$REPO_ROOT/frontend"
|
scripts/build/package_release_artifact.sh \
|
||||||
npm run packaged-build
|
--version "$VERSION" \
|
||||||
cd "$REPO_ROOT"
|
--git-hash "$GIT_HASH" \
|
||||||
|
--full-git-hash "$FULL_GIT_HASH" \
|
||||||
RELEASE_WORK_DIR=$(mktemp -d)
|
--output "$RELEASE_ASSET_PATH"
|
||||||
RELEASE_BUNDLE_DIR="$RELEASE_WORK_DIR/$RELEASE_BUNDLE_DIR_NAME"
|
|
||||||
mkdir -p "$RELEASE_BUNDLE_DIR"
|
|
||||||
git archive "$FULL_GIT_HASH" | tar -x -C "$RELEASE_BUNDLE_DIR"
|
|
||||||
mkdir -p "$RELEASE_BUNDLE_DIR/frontend"
|
|
||||||
cp -R "$REPO_ROOT/frontend/prebuilt" "$RELEASE_BUNDLE_DIR/frontend/prebuilt"
|
|
||||||
cat > "$RELEASE_BUNDLE_DIR/build_info.json" <<EOF
|
|
||||||
{
|
|
||||||
"version": "$VERSION",
|
|
||||||
"commit_hash": "$GIT_HASH",
|
|
||||||
"build_source": "prebuilt-release"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
rm -f "$REPO_ROOT/$RELEASE_ASSET"
|
|
||||||
(
|
|
||||||
cd "$RELEASE_WORK_DIR"
|
|
||||||
zip -qr "$REPO_ROOT/$RELEASE_ASSET" "$(basename "$RELEASE_BUNDLE_DIR")"
|
|
||||||
)
|
|
||||||
echo -e "${GREEN}Packaged release artifact created: $RELEASE_ASSET${NC}"
|
echo -e "${GREEN}Packaged release artifact created: $RELEASE_ASSET${NC}"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# Build and push multi-arch docker image
|
# Build and push multi-arch docker image
|
||||||
echo -e "${YELLOW}Building and pushing multi-arch Docker image...${NC}"
|
echo -e "${YELLOW}Building and pushing multi-arch Docker image...${NC}"
|
||||||
ensure_buildx_builder
|
scripts/build/push_docker_multiarch.sh \
|
||||||
docker buildx build \
|
--version "$VERSION" \
|
||||||
--platform "$DOCKER_PLATFORMS" \
|
--git-hash "$GIT_HASH" \
|
||||||
--build-arg COMMIT_HASH="$GIT_HASH" \
|
--image "$DOCKER_IMAGE" \
|
||||||
-t "$DOCKER_IMAGE:latest" \
|
--platforms "$DOCKER_PLATFORMS"
|
||||||
-t "$DOCKER_IMAGE:$VERSION" \
|
|
||||||
-t "$DOCKER_IMAGE:$GIT_HASH" \
|
|
||||||
--push \
|
|
||||||
.
|
|
||||||
echo -e "${GREEN}Multi-arch Docker build + push complete!${NC}"
|
echo -e "${GREEN}Multi-arch Docker build + push complete!${NC}"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# Create GitHub release using the changelog notes for this version.
|
# Create GitHub release using the changelog notes for this version.
|
||||||
echo -e "${YELLOW}Creating GitHub release...${NC}"
|
echo -e "${YELLOW}Creating GitHub release...${NC}"
|
||||||
RELEASE_NOTES_FILE=$(mktemp)
|
scripts/build/create_github_release.sh \
|
||||||
{
|
--version "$VERSION" \
|
||||||
echo "$CHANGELOG_HEADER"
|
--full-git-hash "$FULL_GIT_HASH" \
|
||||||
echo
|
--asset "$RELEASE_ASSET_PATH"
|
||||||
echo "$CHANGELOG_ENTRY"
|
|
||||||
} > "$RELEASE_NOTES_FILE"
|
|
||||||
|
|
||||||
# Create and push the release tag first so GitHub release creation does not
|
|
||||||
# depend on resolving a symbolic ref like HEAD on the remote side. Use the same
|
|
||||||
# changelog-derived notes for the annotated tag message.
|
|
||||||
if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
|
|
||||||
echo -e "${YELLOW}Tag $VERSION already exists locally; reusing it.${NC}"
|
|
||||||
else
|
|
||||||
git tag -a "$VERSION" "$FULL_GIT_HASH" -F "$RELEASE_NOTES_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
|
|
||||||
echo -e "${YELLOW}Tag $VERSION already exists on origin; not pushing it again.${NC}"
|
|
||||||
else
|
|
||||||
git push origin "$VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
gh release create "$VERSION" \
|
|
||||||
"$RELEASE_ASSET" \
|
|
||||||
--title "$VERSION" \
|
|
||||||
--notes-file "$RELEASE_NOTES_FILE" \
|
|
||||||
--verify-tag
|
|
||||||
|
|
||||||
rm -f "$RELEASE_NOTES_FILE"
|
|
||||||
echo -e "${GREEN}GitHub release created!${NC}"
|
echo -e "${GREEN}GitHub release created!${NC}"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck source=scripts/build/release_common.sh
|
||||||
|
source "$SCRIPT_DIR/release_common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: scripts/build/push_docker_multiarch.sh --version X.Y.Z [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version VERSION Release version (required)
|
||||||
|
--git-hash HASH Short git hash to tag alongside the version
|
||||||
|
--image IMAGE Docker image name (default: docker.io/jkingsman/remoteterm-meshcore)
|
||||||
|
--platforms CSV Buildx platforms CSV (default: linux/amd64,linux/arm64)
|
||||||
|
--help Show this message
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
VERSION=""
|
||||||
|
GIT_HASH=""
|
||||||
|
IMAGE="docker.io/jkingsman/remoteterm-meshcore"
|
||||||
|
PLATFORMS="linux/amd64,linux/arm64"
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--version)
|
||||||
|
VERSION="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--git-hash)
|
||||||
|
GIT_HASH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--image)
|
||||||
|
IMAGE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--platforms)
|
||||||
|
PLATFORMS="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
release_die "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$VERSION" ] || release_die "--version is required"
|
||||||
|
release_validate_version "$VERSION"
|
||||||
|
|
||||||
|
REPO_ROOT="$(release_repo_root)"
|
||||||
|
GIT_HASH="${GIT_HASH:-$(release_resolve_short_hash "$REPO_ROOT")}"
|
||||||
|
|
||||||
|
echo "[push_docker_multiarch] Ensuring docker buildx builder..." >&2
|
||||||
|
release_ensure_buildx_builder
|
||||||
|
|
||||||
|
docker_buildx_args=(
|
||||||
|
build
|
||||||
|
--platform "$PLATFORMS"
|
||||||
|
--build-arg "COMMIT_HASH=$GIT_HASH"
|
||||||
|
-t "$IMAGE:latest"
|
||||||
|
-t "$IMAGE:$VERSION"
|
||||||
|
-t "$IMAGE:$GIT_HASH"
|
||||||
|
--push
|
||||||
|
.
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "[push_docker_multiarch] Building and pushing $IMAGE for $PLATFORMS..." >&2
|
||||||
|
(
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
docker buildx "${docker_buildx_args[@]}"
|
||||||
|
)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
release_repo_root() {
|
||||||
|
(
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
release_die() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
release_trim() {
|
||||||
|
printf '%s' "$1" | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
release_validate_version() {
|
||||||
|
local version="$1"
|
||||||
|
[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || release_die "Version must be in format X.Y.Z"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_resolve_full_hash() {
|
||||||
|
local repo_root="$1"
|
||||||
|
local ref="${2:-HEAD}"
|
||||||
|
git -C "$repo_root" rev-parse "$ref"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_resolve_short_hash() {
|
||||||
|
local repo_root="$1"
|
||||||
|
local ref="${2:-HEAD}"
|
||||||
|
git -C "$repo_root" rev-parse --short "$ref"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_format_markdown_list() {
|
||||||
|
local input_file="$1"
|
||||||
|
local output_file="$2"
|
||||||
|
awk '
|
||||||
|
/^[[:space:]]*$/ { next }
|
||||||
|
{
|
||||||
|
sub(/^[[:space:]]+/, "", $0)
|
||||||
|
if ($0 ~ /^\* /) {
|
||||||
|
print
|
||||||
|
} else if ($0 ~ /^- /) {
|
||||||
|
sub(/^- /, "* ", $0)
|
||||||
|
print
|
||||||
|
} else {
|
||||||
|
print "* " $0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$input_file" > "$output_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_extract_changelog_section() {
|
||||||
|
local repo_root="$1"
|
||||||
|
local version="$2"
|
||||||
|
local output_file="$3"
|
||||||
|
local changelog_path="${4:-$repo_root/CHANGELOG.md}"
|
||||||
|
|
||||||
|
# Use index() for literal matching so dots in version strings are not
|
||||||
|
# treated as regex wildcards (e.g. 3.6.5 won't match 31615).
|
||||||
|
awk -v ver="$version" '
|
||||||
|
BEGIN { header = "## [" ver "]" }
|
||||||
|
index($0, header) == 1 { capture = 1; print; next }
|
||||||
|
capture && /^## \[/ { exit }
|
||||||
|
capture { print }
|
||||||
|
' "$changelog_path" > "$output_file"
|
||||||
|
|
||||||
|
[ -s "$output_file" ] || release_die "Could not find CHANGELOG entry for version $version"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_ensure_buildx_builder() {
|
||||||
|
if ! docker buildx version >/dev/null 2>&1; then
|
||||||
|
release_die "docker buildx is required for multi-arch Docker builds"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Multi-platform builds require the docker-container driver. The default
|
||||||
|
# builder uses the "docker" driver which only supports the host platform.
|
||||||
|
# Check the current builder's driver first; only create a new one if needed.
|
||||||
|
local current_driver
|
||||||
|
current_driver="$(docker buildx inspect --format '{{ .Driver }}' 2>/dev/null || true)"
|
||||||
|
if [ "$current_driver" = "docker-container" ]; then
|
||||||
|
docker buildx inspect --bootstrap >/dev/null
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker buildx inspect remoteterm-multiarch >/dev/null 2>&1; then
|
||||||
|
docker buildx use remoteterm-multiarch >/dev/null
|
||||||
|
else
|
||||||
|
docker buildx create --name remoteterm-multiarch --use >/dev/null
|
||||||
|
fi
|
||||||
|
docker buildx inspect --bootstrap >/dev/null
|
||||||
|
}
|
||||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+184
-12
@@ -21,10 +21,18 @@ NC='\033[0m'
|
|||||||
REPO_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
REPO_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
COMPOSE_FILE="$REPO_DIR/docker-compose.yml"
|
COMPOSE_FILE="$REPO_DIR/docker-compose.yml"
|
||||||
EXAMPLE_FILE="$REPO_DIR/docker-compose.example.yml"
|
EXAMPLE_FILE="$REPO_DIR/docker-compose.example.yml"
|
||||||
|
SNAKEOIL_CERT_DIR="$REPO_DIR/.docker-certs"
|
||||||
|
SNAKEOIL_CERT_BASENAME="remoteterm-snakeoil.crt"
|
||||||
|
SNAKEOIL_KEY_BASENAME="remoteterm-snakeoil.key"
|
||||||
|
SNAKEOIL_CERT_HOST_PATH="$SNAKEOIL_CERT_DIR/$SNAKEOIL_CERT_BASENAME"
|
||||||
|
SNAKEOIL_KEY_HOST_PATH="$SNAKEOIL_CERT_DIR/$SNAKEOIL_KEY_BASENAME"
|
||||||
|
SNAKEOIL_CERT_CONTAINER_PATH="/app/certs/$SNAKEOIL_CERT_BASENAME"
|
||||||
|
SNAKEOIL_KEY_CONTAINER_PATH="/app/certs/$SNAKEOIL_KEY_BASENAME"
|
||||||
|
|
||||||
IMAGE_MODE="image"
|
IMAGE_MODE="image"
|
||||||
TRANSPORT_MODE="serial"
|
TRANSPORT_MODE="serial"
|
||||||
SERIAL_HOST_PATH="/dev/ttyACM0"
|
SERIAL_HOST_PATH="/dev/ttyACM0"
|
||||||
|
SERIAL_COMPOSE_HOST_PATH="/dev/ttyACM0"
|
||||||
SERIAL_CONTAINER_PATH="/dev/meshcore-radio"
|
SERIAL_CONTAINER_PATH="/dev/meshcore-radio"
|
||||||
TCP_HOST=""
|
TCP_HOST=""
|
||||||
TCP_PORT="4000"
|
TCP_PORT="4000"
|
||||||
@@ -35,7 +43,9 @@ ENABLE_AUTH="N"
|
|||||||
AUTH_USERNAME=""
|
AUTH_USERNAME=""
|
||||||
AUTH_PASSWORD=""
|
AUTH_PASSWORD=""
|
||||||
RUN_AS_HOST_USER="N"
|
RUN_AS_HOST_USER="N"
|
||||||
|
ENABLE_SNAKEOIL_TLS="Y"
|
||||||
BLE_MANUAL_WARNING=false
|
BLE_MANUAL_WARNING=false
|
||||||
|
LOCAL_ACCESS_IP=""
|
||||||
SERIAL_FOUND_HOST_PATHS=()
|
SERIAL_FOUND_HOST_PATHS=()
|
||||||
SERIAL_FOUND_LABELS=()
|
SERIAL_FOUND_LABELS=()
|
||||||
SERIAL_FOUND_DISPLAYS=()
|
SERIAL_FOUND_DISPLAYS=()
|
||||||
@@ -89,6 +99,118 @@ yaml_quote() {
|
|||||||
printf "'%s'" "$value"
|
printf "'%s'" "$value"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalize_serial_host_path_for_compose() {
|
||||||
|
local selected_path="$1"
|
||||||
|
local resolved_path=""
|
||||||
|
|
||||||
|
if [[ "$selected_path" != *:* ]]; then
|
||||||
|
SERIAL_COMPOSE_HOST_PATH="$selected_path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolved_path="$(readlink -f "$selected_path" 2>/dev/null || true)"
|
||||||
|
if [ -z "$resolved_path" ]; then
|
||||||
|
echo -e "${RED}Error:${NC} the selected serial path contains ':' and could not be resolved to a raw /dev/tty-style device path."
|
||||||
|
echo "Selected path: $selected_path"
|
||||||
|
echo "Please enter the raw serial device path instead (for example /dev/ttyACM0)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$resolved_path" == *:* ]]; then
|
||||||
|
echo -e "${RED}Error:${NC} the selected serial path still resolves to a path containing ':', which Docker Compose cannot use here."
|
||||||
|
echo "Selected path: $selected_path"
|
||||||
|
echo "Resolved path: $resolved_path"
|
||||||
|
echo "Please enter the raw serial device path instead (for example /dev/ttyACM0)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Note:${NC} the selected serial path contains ':', so Docker Compose will use the resolved raw device path instead: ${resolved_path}"
|
||||||
|
SERIAL_COMPOSE_HOST_PATH="$resolved_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_primary_local_ip() {
|
||||||
|
local ip=""
|
||||||
|
local iface=""
|
||||||
|
|
||||||
|
if command -v hostname &>/dev/null; then
|
||||||
|
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ip" ] && command -v ip &>/dev/null; then
|
||||||
|
ip="$(ip route get 1.1.1.1 2>/dev/null | awk '/src/ {for (i = 1; i <= NF; i++) if ($i == "src") {print $(i + 1); exit}}')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ip" ] && command -v route &>/dev/null && command -v ipconfig &>/dev/null; then
|
||||||
|
iface="$(route -n get default 2>/dev/null | awk '/interface:/{print $2; exit}')"
|
||||||
|
if [ -n "$iface" ]; then
|
||||||
|
ip="$(ipconfig getifaddr "$iface" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ip" ]; then
|
||||||
|
ip="127.0.0.1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_snakeoil_requirements() {
|
||||||
|
local dep
|
||||||
|
|
||||||
|
for dep in openssl mktemp; do
|
||||||
|
if ! command -v "$dep" &>/dev/null; then
|
||||||
|
echo -e "${RED}Error: ${dep} is required to generate the snakeoil TLS certificate.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_snakeoil_certificate() {
|
||||||
|
local san_ip="$1"
|
||||||
|
local tmp_config=""
|
||||||
|
|
||||||
|
mkdir -p "$SNAKEOIL_CERT_DIR"
|
||||||
|
tmp_config="$(mktemp)"
|
||||||
|
|
||||||
|
cat >"$tmp_config" <<EOF
|
||||||
|
[req]
|
||||||
|
default_bits = 2048
|
||||||
|
distinguished_name = req_distinguished_name
|
||||||
|
x509_extensions = v3_req
|
||||||
|
prompt = no
|
||||||
|
|
||||||
|
[req_distinguished_name]
|
||||||
|
CN = RemoteTerm Snakeoil
|
||||||
|
O = RemoteTerm for MeshCore
|
||||||
|
|
||||||
|
[v3_req]
|
||||||
|
subjectAltName = @alt_names
|
||||||
|
|
||||||
|
[alt_names]
|
||||||
|
DNS.1 = localhost
|
||||||
|
IP.1 = 127.0.0.1
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ -n "$san_ip" ] && [ "$san_ip" != "127.0.0.1" ]; then
|
||||||
|
printf 'IP.2 = %s\n' "$san_ip" >>"$tmp_config"
|
||||||
|
fi
|
||||||
|
|
||||||
|
openssl req \
|
||||||
|
-x509 \
|
||||||
|
-nodes \
|
||||||
|
-newkey rsa:2048 \
|
||||||
|
-days 3650 \
|
||||||
|
-keyout "$SNAKEOIL_KEY_HOST_PATH" \
|
||||||
|
-out "$SNAKEOIL_CERT_HOST_PATH" \
|
||||||
|
-config "$tmp_config" \
|
||||||
|
-extensions v3_req >/dev/null 2>&1
|
||||||
|
|
||||||
|
rm -f "$tmp_config"
|
||||||
|
|
||||||
|
chmod 600 "$SNAKEOIL_KEY_HOST_PATH"
|
||||||
|
chmod 644 "$SNAKEOIL_CERT_HOST_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
echo -e "${BOLD}=== RemoteTerm for MeshCore — Docker Setup ===${NC}"
|
echo -e "${BOLD}=== RemoteTerm for MeshCore — Docker Setup ===${NC}"
|
||||||
echo
|
echo
|
||||||
echo -e " Repo directory : ${CYAN}${REPO_DIR}${NC}"
|
echo -e " Repo directory : ${CYAN}${REPO_DIR}${NC}"
|
||||||
@@ -183,7 +305,8 @@ case "$TRANSPORT_CHOICE" in
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}Serial passthrough: ${SERIAL_HOST_PATH} -> ${SERIAL_CONTAINER_PATH}${NC}"
|
normalize_serial_host_path_for_compose "$SERIAL_HOST_PATH"
|
||||||
|
echo -e "${GREEN}Serial passthrough: ${SERIAL_COMPOSE_HOST_PATH} -> ${SERIAL_CONTAINER_PATH}${NC}"
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
TRANSPORT_MODE="tcp"
|
TRANSPORT_MODE="tcp"
|
||||||
@@ -266,6 +389,24 @@ else
|
|||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
|
|
||||||
|
echo -e "${BOLD}─── HTTPS / Snakeoil TLS ────────────────────────────────────────────${NC}"
|
||||||
|
echo "Generating a local self-signed certificate enables HTTPS-only browser features"
|
||||||
|
echo "such as the channel key finder and, in some browsers, notifications."
|
||||||
|
echo "Browsers will still warn that the certificate is untrusted."
|
||||||
|
echo
|
||||||
|
read -r -p "Generate and enable a snakeoil TLS certificate? [Y/n]: " ENABLE_SNAKEOIL_TLS
|
||||||
|
ENABLE_SNAKEOIL_TLS="${ENABLE_SNAKEOIL_TLS:-Y}"
|
||||||
|
LOCAL_ACCESS_IP="$(detect_primary_local_ip)"
|
||||||
|
if [[ "$ENABLE_SNAKEOIL_TLS" =~ ^[Yy]$ ]]; then
|
||||||
|
ensure_snakeoil_requirements
|
||||||
|
generate_snakeoil_certificate "$LOCAL_ACCESS_IP"
|
||||||
|
echo -e "${GREEN}Generated snakeoil TLS certificate in ${SNAKEOIL_CERT_DIR}.${NC}"
|
||||||
|
echo -e "${YELLOW}Browsers will show an untrusted/self-signed certificate warning.${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}Skipping snakeoil TLS generation. The container will serve plain HTTP.${NC}"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
if [ "$(uname -s)" = "Linux" ]; then
|
if [ "$(uname -s)" = "Linux" ]; then
|
||||||
echo -e "${BOLD}─── Container User ──────────────────────────────────────────────────${NC}"
|
echo -e "${BOLD}─── Container User ──────────────────────────────────────────────────${NC}"
|
||||||
echo "The container runs as root by default for maximum serial compatibility."
|
echo "The container runs as root by default for maximum serial compatibility."
|
||||||
@@ -295,7 +436,7 @@ mkdir -p "$REPO_DIR/data"
|
|||||||
if [ "$IMAGE_MODE" = "build" ]; then
|
if [ "$IMAGE_MODE" = "build" ]; then
|
||||||
echo " build: ."
|
echo " build: ."
|
||||||
else
|
else
|
||||||
echo " image: jkingsman/remoteterm-meshcore:latest"
|
echo " image: docker.io/jkingsman/remoteterm-meshcore:latest"
|
||||||
fi
|
fi
|
||||||
if [[ "$RUN_AS_HOST_USER" =~ ^[Yy]$ ]]; then
|
if [[ "$RUN_AS_HOST_USER" =~ ^[Yy]$ ]]; then
|
||||||
echo " user: \"$(id -u):$(id -g)\""
|
echo " user: \"$(id -u):$(id -g)\""
|
||||||
@@ -304,9 +445,27 @@ mkdir -p "$REPO_DIR/data"
|
|||||||
echo " - \"8000:8000\""
|
echo " - \"8000:8000\""
|
||||||
echo " volumes:"
|
echo " volumes:"
|
||||||
echo " - ./data:/app/data"
|
echo " - ./data:/app/data"
|
||||||
|
if [[ "$ENABLE_SNAKEOIL_TLS" =~ ^[Yy]$ ]]; then
|
||||||
|
echo " - ./.docker-certs:/app/certs:ro"
|
||||||
|
fi
|
||||||
if [ "$TRANSPORT_MODE" = "serial" ]; then
|
if [ "$TRANSPORT_MODE" = "serial" ]; then
|
||||||
echo " devices:"
|
echo " devices:"
|
||||||
echo " - ${SERIAL_HOST_PATH}:${SERIAL_CONTAINER_PATH}"
|
echo " - ${SERIAL_COMPOSE_HOST_PATH}:${SERIAL_CONTAINER_PATH}"
|
||||||
|
fi
|
||||||
|
if [[ "$ENABLE_SNAKEOIL_TLS" =~ ^[Yy]$ ]]; then
|
||||||
|
echo " command:"
|
||||||
|
echo " - uv"
|
||||||
|
echo " - run"
|
||||||
|
echo " - uvicorn"
|
||||||
|
echo " - app.main:app"
|
||||||
|
echo " - --host"
|
||||||
|
echo " - 0.0.0.0"
|
||||||
|
echo " - --port"
|
||||||
|
echo " - \"8000\""
|
||||||
|
echo " - --ssl-keyfile"
|
||||||
|
echo " - $SNAKEOIL_KEY_CONTAINER_PATH"
|
||||||
|
echo " - --ssl-certfile"
|
||||||
|
echo " - $SNAKEOIL_CERT_CONTAINER_PATH"
|
||||||
fi
|
fi
|
||||||
echo " environment:"
|
echo " environment:"
|
||||||
echo " MESHCORE_DATABASE_PATH: $(yaml_quote "data/meshcore.db")"
|
echo " MESHCORE_DATABASE_PATH: $(yaml_quote "data/meshcore.db")"
|
||||||
@@ -333,15 +492,18 @@ echo -e "${GREEN}Generated ${COMPOSE_FILE}.${NC}"
|
|||||||
echo
|
echo
|
||||||
echo -e "${BOLD}Docker commands${NC}"
|
echo -e "${BOLD}Docker commands${NC}"
|
||||||
if [ "$IMAGE_MODE" = "build" ]; then
|
if [ "$IMAGE_MODE" = "build" ]; then
|
||||||
echo " docker compose up -d --build # build the local image and start RemoteTerm in the background"
|
echo " sudo docker compose up -d --build # build the local image and start RemoteTerm in the background"
|
||||||
else
|
else
|
||||||
echo " docker compose up -d # start RemoteTerm in the background"
|
echo " sudo docker compose up -d # start RemoteTerm in the background"
|
||||||
fi
|
fi
|
||||||
echo " docker compose logs -f # follow the container logs live"
|
echo " sudo docker compose logs -f # follow the container logs live"
|
||||||
echo
|
echo
|
||||||
echo " docker compose down # stop and remove the running container"
|
echo " sudo docker compose down # stop and remove the running container"
|
||||||
echo " docker compose restart # restart the container without changing the image"
|
echo " sudo docker compose restart # restart the container without changing the image"
|
||||||
echo " docker compose pull && docker compose up -d # upgrade to the latest published image and restart"
|
echo " sudo docker compose pull && sudo docker compose up -d # upgrade to the latest published image and restart"
|
||||||
|
echo
|
||||||
|
echo -e "${YELLOW}Note:${NC} serial passthrough generally needs ${BOLD}rootful Docker${NC}."
|
||||||
|
echo "If Docker is running rootless on this host, serial-device mappings may fail even with a valid compose file."
|
||||||
if [ "$TRANSPORT_MODE" = "ble" ] || [ "$BLE_MANUAL_WARNING" = true ]; then
|
if [ "$TRANSPORT_MODE" = "ble" ] || [ "$BLE_MANUAL_WARNING" = true ]; then
|
||||||
echo
|
echo
|
||||||
echo -e "${RED}BLE requires more than the generated env vars.${NC}"
|
echo -e "${RED}BLE requires more than the generated env vars.${NC}"
|
||||||
@@ -351,6 +513,16 @@ echo
|
|||||||
echo -e "${GREEN}Your new docker file is ready at ${COMPOSE_FILE}.${NC}"
|
echo -e "${GREEN}Your new docker file is ready at ${COMPOSE_FILE}.${NC}"
|
||||||
echo -e "${GREEN}Feel free to edit it by hand as desired, or:${NC}"
|
echo -e "${GREEN}Feel free to edit it by hand as desired, or:${NC}"
|
||||||
echo
|
echo
|
||||||
echo -e "${PURPLE}┌──────────────────────────────────────────────┐${NC}"
|
echo -e "${PURPLE}┌───────────────────────────────────────────────┐${NC}"
|
||||||
echo -e "${PURPLE}│ Run ${GREEN}${BOLD}docker compose up -d${NC}${PURPLE} to get started. │${NC}"
|
echo -e "${PURPLE}│ Run ${GREEN}${BOLD}sudo docker compose up -d${NC}${PURPLE} to get started. │${NC}"
|
||||||
echo -e "${PURPLE}└──────────────────────────────────────────────┘${NC}"
|
echo -e "${PURPLE}└───────────────────────────────────────────────┘${NC}"
|
||||||
|
if [[ "$ENABLE_SNAKEOIL_TLS" =~ ^[Yy]$ ]]; then
|
||||||
|
echo
|
||||||
|
echo -e "After the container starts, open ${CYAN}https://${LOCAL_ACCESS_IP}:8000${NC}. Note that this address may change if you use DHCP/have not configured a static IP for your host via your router."
|
||||||
|
echo -e "${YELLOW}Expect an untrusted/self-signed certificate warning the first time you connect.${NC}"
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo -e "After the container starts, open ${CYAN}http://${LOCAL_ACCESS_IP}:8000${NC}. Note that this address may change if you use DHCP/have not configured a static IP for your host via your router."
|
||||||
|
fi
|
||||||
|
echo "If the interface does not appear, follow the logs to view errors with:"
|
||||||
|
echo " sudo docker compose logs -f"
|
||||||
|
|||||||
Regular → Executable
@@ -19,7 +19,7 @@ test.describe('Create contact flow', () => {
|
|||||||
await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible();
|
await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible();
|
||||||
|
|
||||||
// Open new message modal
|
// Open new message modal
|
||||||
await page.getByTitle('New Message').click();
|
await page.getByRole('button', { name: /add channel or contact/i }).click();
|
||||||
const dialog = page.getByRole('dialog');
|
const dialog = page.getByRole('dialog');
|
||||||
await expect(dialog).toBeVisible();
|
await expect(dialog).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ test.describe('Create hashtag channel flow', () => {
|
|||||||
await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible();
|
await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible();
|
||||||
|
|
||||||
// Open new message modal
|
// Open new message modal
|
||||||
await page.getByTitle('New Message').click();
|
await page.getByRole('button', { name: /add channel or contact/i }).click();
|
||||||
const dialog = page.getByRole('dialog');
|
const dialog = page.getByRole('dialog');
|
||||||
await expect(dialog).toBeVisible();
|
await expect(dialog).toBeVisible();
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ test.describe('Create hashtag channel flow', () => {
|
|||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible();
|
await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible();
|
||||||
|
|
||||||
await page.getByTitle('New Message').click();
|
await page.getByRole('button', { name: /add channel or contact/i }).click();
|
||||||
const dialog = page.getByRole('dialog');
|
const dialog = page.getByRole('dialog');
|
||||||
await expect(dialog).toBeVisible();
|
await expect(dialog).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ test.describe('Historical packet decryption', () => {
|
|||||||
await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible();
|
await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible();
|
||||||
|
|
||||||
// Open new message modal → Hashtag tab
|
// Open new message modal → Hashtag tab
|
||||||
await page.getByTitle('New Message').click();
|
await page.getByRole('button', { name: /add channel or contact/i }).click();
|
||||||
const dialog = page.getByRole('dialog');
|
const dialog = page.getByRole('dialog');
|
||||||
await expect(dialog).toBeVisible();
|
await expect(dialog).toBeVisible();
|
||||||
await dialog.getByRole('tab', { name: /Hashtag/i }).click();
|
await dialog.getByRole('tab', { name: /Hashtag/i }).click();
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import asyncio
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.services import radio_noise_floor
|
||||||
|
|
||||||
|
|
||||||
|
class TestNoiseFloorSamplingLoop:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_logs_and_continues_after_unexpected_sample_exception(self):
|
||||||
|
sample_calls = 0
|
||||||
|
sleep_calls = 0
|
||||||
|
|
||||||
|
async def fake_sample() -> None:
|
||||||
|
nonlocal sample_calls
|
||||||
|
sample_calls += 1
|
||||||
|
if sample_calls == 1:
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
async def fake_sleep(_seconds: int) -> None:
|
||||||
|
nonlocal sleep_calls
|
||||||
|
sleep_calls += 1
|
||||||
|
if sleep_calls >= 2:
|
||||||
|
raise asyncio.CancelledError()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(radio_noise_floor, "sample_noise_floor_once", side_effect=fake_sample),
|
||||||
|
patch.object(radio_noise_floor.asyncio, "sleep", side_effect=fake_sleep),
|
||||||
|
patch.object(radio_noise_floor.logger, "exception") as mock_exception,
|
||||||
|
):
|
||||||
|
with pytest.raises(asyncio.CancelledError):
|
||||||
|
await radio_noise_floor._noise_floor_sampling_loop()
|
||||||
|
|
||||||
|
assert sample_calls == 2
|
||||||
|
assert sleep_calls == 2
|
||||||
|
mock_exception.assert_called_once()
|
||||||
@@ -1098,7 +1098,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remoteterm-meshcore"
|
name = "remoteterm-meshcore"
|
||||||
version = "3.6.3"
|
version = "3.6.7"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiomqtt" },
|
{ name = "aiomqtt" },
|
||||||
|
|||||||
Reference in New Issue
Block a user