Skip to content

feat: export and import whole device configuration (#1924)#2023

Open
bruschill wants to merge 19 commits into
meshtastic:mainfrom
bruschill:feat/import-device-config
Open

feat: export and import whole device configuration (#1924)#2023
bruschill wants to merge 19 commits into
meshtastic:mainfrom
bruschill:feat/import-device-config

Conversation

@bruschill

@bruschill bruschill commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What changed?

Adds Export and Import of a node's whole configuration to Settings → Tools, using the binary DeviceProfile (.cfg) format shared with the Android app and the meshtastic CLI.

Supersedes #1940. That PR was export-only; this one carries the same export work plus the inverse import feature so the two ship together. Closes #1924.

Export

  • DeviceProfileExport.swift — entity → protobuf converters for all 8 device configs and 15 module configs (the inverse of the proto → entity mapping in UpdateSwiftData.swift), plus NodeInfoEntity.exportDeviceProfile() which assembles a DeviceProfile: long/short name, LocalConfig, LocalModuleConfig, the channel-set URL, ringtone, canned messages, and the fixed position when one is set.
  • DeviceProfileDocument.swift — a FileDocument wrapping the serialized profile bytes for .fileExporter, with an Android-style default filename and a shared .meshtasticDeviceProfile UTType.
  • Tools.swift — the export button + fileExporter, gated on a connected node, behind a confirmation dialog that warns about the sensitive material in the file.
  • UpdateSwiftData.swift — persists MQTT mapReportingShouldReportLocation in the config-update branch (it was only set on insert) so the exported value isn't a stale default.

Import

  • DeviceProfileImport.swift — pure, device-free core. parseDeviceProfile defensively decodes untrusted bytes (size cap before decode, capped read at the call site). DeviceProfileImportPlan(profile:currentUser:) is the has*-gated inverse of exportDeviceProfile() — the single source of coverage truth so export/import can't silently drift. It emits an ordered [ImportItem] plan grouped into 8 coarse toggle sections, each item carrying isSensitive/causesReboot flags.
  • DeviceProfileImporter.swift — a ProfileApplyGateway seam plus the apply engine (sequential, isConnected pre-checks, abort-on-first-failure). The production gateway forwards each item to the existing save*Config / saveUser / saveChannelSet / saveRtttlConfig / saveCannedMessageModuleMessages / setFixedPosition calls, with fromUser == toUser == the connected node (self-admin, no session passkey).
  • ImportDeviceProfileView.swift — review sheet: per-section toggles, live sensitive-material + reboot banners, a confirmation gate, per-item progress, and an applied/failed/skipped result summary.
  • Tools.swift — the Import section + .fileImporter([.meshtasticDeviceProfile]) with a capped read, parse, and the review sheet.
  • AccessoryManager+ToRadio.swift — new setFixedPosition(_:fromUser:toUser:) overload to restore explicit coordinates from a profile (the existing overload only uses the phone's live GPS fix).

Import design decisions worth a look:

  • Apply order is fixed and reboot-safe. The plan emits items in a strict order (owner → radio/device → modules → personalization → fixed position → network → bluetooth → security → Channels & LoRa last). The UI filters by section but never reorders, so the reboot-prone LoRa/channel step is always last and can't drop later sends. Network/Bluetooth are placed late because they can drop the transport the import runs over.
  • Security & Identity defaults OFF. Importing a foreign private/admin key rewrites this node's cryptographic identity and can break existing DMs / lock local admin, so it's a deliberate opt-in. Every other present section defaults ON, preserving a one-tap "restore my device" flow.
  • LoRa/channel dedupe. Export writes the same LoRaConfig into both config.lora and the channel URL's ChannelSet. The plan emits exactly one terminal item — the channel URL when it's a valid replace-mode set with a LoRa config, otherwise the standalone LoRa config (add-mode or LoRa-less URLs fall back so the region is still restored).
  • Owner merge preserves identity. The owner step starts from the live node.user.toProto() and overrides only long/short name, so id/hwModel/publicKey/role are never wiped by setOwner.
  • Partial imports are safe and escapable. An adversarial safety pass confirmed the device is left coherent and recoverable in every partial-failure case (mid-import disconnect, reboot, or a rejected send) — no bricking, no sends into a dead link, no crash. On top of that: the apply is now cancellable (a Cancel button on the applying screen, per-item Task.isCancelled/CancellationError handling, and a liveness backstop so a single stalled send can never trap the sheet); the result reports what was sent (admin sends are fire-and-forget, not device-acked), always nudges the user to reconnect and verify, and warns when a destructive channel replace may have partially applied. ImportItemKind.displayName gives human-readable labels in progress/results instead of raw case names.

Localization

  • ImportItemKind.displayName and ImportSection.title were plain Strings shown via Text(variable), which SwiftUI does not localize — routed them through .localized with casing that matches existing config-screen strings, so 25 shared labels reuse their translations. The assembled confirmation/sensitive-material messages are localized via .localized fragments + String(format:).
  • Registered all new import (and previously-unlocalized export) UI strings in Localizable.xcstrings as source-language entries (English default = key), so translators / scripts/translate-locale.sh can fill the other 17 locales.

Docs

  • Adds a Tools section to docs/user/settings.md (NFC contact tag, Export, and Import) — the Tools screen was previously undocumented — and regenerates the bundled in-app HTML.

Why did it change?

The Android app and meshtastic CLI can export and import a device's full configuration; iOS/macOS could do neither. The .cfg binary DeviceProfile format matches Android, so a profile can be backed up, restored, or cloned onto another node across the Meshtastic ecosystem. Shipping export and import together closes the loop in one change.

How is this tested?

  • DeviceProfileExportTests (Swift Testing) — 16 tests covering the converters (enum mapping, negated-flag inversion, packed IPv4 bit-pattern, admin-key slot preservation, nested map-report settings, isServer, TAK/traffic/status) and DeviceProfile assembly including a binary serialization round-trip.
  • DeviceProfileImportTests (Swift Testing) — 25 tests covering defensive parse + export↔import round-trip, coverage parity with export (all 8 device + 15 module configs; meshBeacon/remoteHardware intentionally absent), apply ordering, LoRa/channel dedupe including the add-mode and LoRa-less fallbacks, fixed-position gating, owner-identity preservation at the plan level, sensitivity/reboot flags, the engine's abort/skip/reboot semantics via a mock gateway, and cancellation handling (current + remaining items skipped, not failed).
  • Full app build succeeds for the iOS 18 simulator (Debug). All 41 tests pass.
  • The import plan was reviewed field-by-field against exportDeviceProfile() to keep the two directions symmetric, and hardened against partial-failure scenarios via an adversarial safety review.

Screenshots/Videos (when applicable)

Checklist

  • My code adheres to the project's coding and style guidelines.
  • I have conducted a self-review of my code.
  • I have commented my code, particularly in complex areas.
  • I have verified whether these changes require updates to the in-app documentation under docs/user/ or docs/developer/, and updated accordingly. Added a Tools section to docs/user/settings.md (NFC, Export & Import Configuration) and regenerated the bundled HTML.
  • I have tested the change to ensure that it works as intended.

Notes for reviewers

  • Sensitive material is exported in plaintext and written back on import. The profile includes the private key, admin keys, channel PSKs, and Wi-Fi/MQTT passwords. Export only reads it; import applies it, which is strictly more dangerous — so on import, Security & Identity defaults OFF, a live banner itemizes exactly what a selection overwrites, and a confirmation dialog gates apply. This matches Android's .cfg behavior (the format is meant to fully restore a device). No secret is logged.
  • Export ↔ import are two hand-maintained mirrors. DeviceProfileImportPlan is the inverse of exportDeviceProfile(), and each export converter is the inverse of an upsert*ConfigPacket mapping; a round-trip test locks the format, but a shared single source of truth between the directions would be the deeper fix for a future pass.
  • Partial-import safety was reviewed adversarially. The apply is intentionally non-transactional (per-item admin sends aren't atomic on the firmware side, matching the existing per-screen config saves and QR channel import), but it aborts on the first failure, never sends into a dropped link, reports exactly what was sent/skipped, and is safe to re-run (every send is idempotent). The liveness backstop guards against a send that stalls on the transport; the underlying transport-layer stall (e.g. a TCP write racing a teardown) is a pre-existing concern outside this change's scope.

Summary by CodeRabbit

  • New Features
    • Added device configuration export to .cfg files compatible with Android and the Meshtastic CLI.
    • Added configurable device profile import with section-by-section review, sensitive-data warnings, progress reporting, and failure handling.
    • Added support for restoring radio, network, module, security, channel, and fixed-position settings.
    • Added tools for creating Node Contact NFC tags.
  • Bug Fixes
    • Improved persistence of security keys, MQTT location reporting, and Store & Forward server settings.
  • Documentation
    • Updated Settings and deep-link documentation for configuration backup and restoration.

bruschill added 10 commits June 15, 2026 16:25
Adds a "Export Configuration" action to Settings → Tools that saves the
connected node's full configuration to a binary DeviceProfile (.cfg) file,
matching the format the Android app and meshtastic CLI use so it can be
backed up or imported elsewhere.

- DeviceProfileExport.swift: entity → protobuf converters for all 8 device
  configs and 15 module configs (the inverse of the proto → entity mapping
  in UpdateSwiftData.swift), plus NodeInfoEntity.exportDeviceProfile() which
  assembles a DeviceProfile (long/short name, LocalConfig, LocalModuleConfig,
  channel-set URL, ringtone, canned messages, and the fixed position when set).
- DeviceProfileDocument.swift: FileDocument wrapping the serialized profile.
- Tools.swift: export button + fileExporter; disabled with guidance when no
  node is connected.
- Tests covering the converters and DeviceProfile assembly incl. binary
  round-trip.

Fixes found in a code review of this feature:
- Export StoreForward isServer (was dropped) and the TAK, traffic-management,
  and status-message module configs (they ARE part of LocalModuleConfig).
- Preserve admin-key slot alignment instead of compacting empty slots, which
  corrupted keys on round-trip when an earlier slot was empty.
- Persist MQTT mapReportingShouldReportLocation in the upsert update branch
  (pre-existing bug that made the export read a stale default).
Add the 5 routable settings cases missing from the table
(localMeshDiscovery + its history subpath, trafficManagement,
takConfig, backupManagement, deviceLinks) and correct the tak
label to "TAK Server" to distinguish it from the new takConfig
TAK module config route.
# Conflicts:
#	Meshtastic.xcodeproj/project.pbxproj
CodeRabbit feedback:
- Clear removed positional admin-key slots on security-config sync so a node
  shrinking from 3 admin keys to 1/2 no longer keeps (and exports) retired keys.
  Still gated on a non-empty reported array so a partial/empty packet can't wipe
  stored keys.
- Refresh StoreForwardConfigEntity.isRouter on config update so server-mode
  changes are reflected in the export (the update branch never refreshed it).
- Surface .fileExporter write failures in-app (not just a log), ignoring user
  cancellation.

Also (code-review): drop the removed TrafficManagementConfig bool proto fields
from the export — they were replaced upstream by the non-zero-uint32 convention,
which was breaking the build; sanitize the export filename against path-illegal
characters; update the affected tests.

Addresses CodeRabbit + code-review feedback on meshtastic#1924.
…Type

Address the compatibility/hardening review on meshtastic#1940:

- Add a confirmation dialog before exporting that warns the .cfg contains
  sensitive security material (private/admin keys, channel PSKs, Wi-Fi/MQTT
  passwords) before the file is written.
- Match Android's filename: Meshtastic_<shortName>_<yyyyMMdd>_nodeConfig.cfg
  via a testable DeviceProfileDocument.exportFilename(...) helper and a new
  Date.exportDateStamp; the .cfg suffix comes from a stable, reused
  UTType.meshtasticDeviceProfile instead of an inline UTType per call site.
- A name made only of path-illegal characters now trims to empty and falls
  back to "Node" instead of collapsing to a string of dashes.
- Re-resolve the connected node at confirm time (not the entity captured at
  button tap) and defer the file exporter past the dialog dismissal.
- Dedup the export DateFormatter boilerplate into cached static formatters.

Tests: add DeviceProfileExportFilenameTests (filename format, fallbacks,
illegal-char stripping, UTType extension); 22 tests pass.
Address CodeRabbit review: upsertSecurityConfigPacket only rewrote admin keys
when the reported array was non-empty, so a node that drops to zero admin keys
kept stale adminKey/adminKey2/adminKey3 values in SwiftData — which the device
profile export then wrote back into the .cfg backup.

Mirror every positional slot unconditionally (an empty array now clears all
three) via a single applyAdminKeys(_:to:) helper shared by the insert and
existing-entity branches, so the two stay in lockstep and slot 0 reads as the
idiomatic `.first`. Security sub-configs arrive as full snapshots, so an empty
admin_key reflects the device's true state.
Adds an "Import Device Configuration" action to Settings → Tools, the
inverse of the Export Configuration feature (meshtastic#1940). It applies a saved
binary DeviceProfile (.cfg) file to the connected node, matching the
Android app and meshtastic CLI so a backup can be restored across the
ecosystem.

- DeviceProfileImport.swift — pure, device-free core: defensive parse
  (size-cap before decode), and DeviceProfileImportPlan, the has*-gated
  inverse of exportDeviceProfile(). Emits an ordered [ImportItem] plan
  (owner, radio/device, modules, personalization, fixed position,
  network, bluetooth, security, then Channels & LoRa last because it
  reboots) with per-item isSensitive/causesReboot flags and coarse
  ImportSection groups. Dedupes the LoRa config that export writes into
  both config.lora and the channel URL, preferring the URL when it's a
  valid replace-mode set (else the standalone LoRa config).
- DeviceProfileImporter.swift — ProfileApplyGateway seam + apply engine
  (sequential, isConnected pre-checks, abort-on-first-failure). The
  production gateway forwards each item to the existing
  save*Config/saveUser/saveChannelSet/setFixedPosition calls; the
  standalone-LoRa reboot disconnect is swallowed only when the link
  actually drops.
- ImportDeviceProfileView.swift — review sheet with per-section toggles
  (Security & Identity defaults OFF), live sensitive-material + reboot
  banners, a confirmation gate, per-item progress, and an
  applied/failed/skipped result summary.
- Tools.swift — Import section + .fileImporter([.meshtasticDeviceProfile])
  with a capped read, parse, and the review sheet.
- AccessoryManager+ToRadio.swift — new setFixedPosition(_:fromUser:toUser:)
  overload to restore explicit coordinates from a profile (the existing
  overload only uses the phone's GPS).

Tests: DeviceProfileImportTests (Swift Testing) — 22 tests covering
parse/round-trip, coverage vs. export, apply ordering, LoRa/channel
dedupe fallbacks, fixed-position and owner-identity preservation,
sensitivity/reboot flags, and the engine's abort/skip/reboot semantics
via a mock gateway. All pass; full app build succeeds for the iOS
simulator.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b278c11-3298-4587-98df-4331d07670f9

📥 Commits

Reviewing files that changed from the base of the PR and between dd6d41f and 3944c16.

📒 Files selected for processing (3)
  • Localizable.xcstrings
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift
  • Meshtastic/Resources/docs/index.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift
  • Localizable.xcstrings

📝 Walkthrough

Walkthrough

Adds connected-node device profile export/import using protobuf .cfg files, selectable import plans, ordered radio application, Settings UI, persistence fixes, tests, localization, and documentation updates.

Changes

Device Profile Export/Import

Layer / File(s) Summary
Export model and file handling
Meshtastic/Export/*, Meshtastic/Extensions/Date.swift
Converts persisted configuration entities into DeviceProfile protobuf sections, generates channel URLs and export filenames, and provides .cfg file document handling.
Import plan construction
Meshtastic/Import/DeviceProfileImport.swift
Parses bounded profile data, merges owner and security fields, creates ordered selectable items, and derives sensitivity and reboot metadata.
Import application engine
Meshtastic/Import/DeviceProfileImporter.swift, Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift
Applies selected items through an accessory gateway, handles cancellation, disconnection, failures, reboot-tolerant terminal operations, and explicit fixed positions.
Settings transfer flow
Meshtastic/Views/Settings/Tools.swift, Meshtastic/Views/Settings/ImportDeviceProfileView.swift, Localizable.xcstrings
Adds file export/import controls, review and result screens, progress reporting, warnings, alerts, and localized strings.
Configuration persistence
Meshtastic/Persistence/UpdateSwiftData.swift
Centralizes admin-key slot updates and refreshes MQTT map-report and Store Forward server fields.
Project integration
Meshtastic.xcodeproj/project.pbxproj
Registers the new source files, groups, references, and target build entries.
Export and import validation
MeshtasticTests/*DeviceProfile*Tests.swift
Tests protobuf conversion, profile assembly, filename handling, import planning, merging, ordering, cancellation, failure, reboot, and disconnection behavior.
Documentation and search metadata
docs/*, Meshtastic/Resources/docs/*
Documents configuration tools, sensitive-data warnings, import behavior, and updated Settings deep links and search metadata.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Tools
  participant ImportDeviceProfileView
  participant DeviceProfileImporter
  participant AccessoryManager
  User->>Tools: Select .cfg file
  Tools->>ImportDeviceProfileView: Present parsed import plan
  User->>ImportDeviceProfileView: Select sections and confirm
  ImportDeviceProfileView->>DeviceProfileImporter: Apply selected items
  DeviceProfileImporter->>AccessoryManager: Send configuration updates
  AccessoryManager-->>DeviceProfileImporter: Return success or failure
  DeviceProfileImporter-->>ImportDeviceProfileView: Report applied, skipped, and failed items
Loading

Possibly related PRs

Suggested reviewers: garthvh

Poem

I’m a rabbit with profiles tucked neat,
Exported in bytes, then back to the fleet.
Keys get a warning, channels reboot,
Ordered little settings hop into suit.
Tests guard the trail with a bright carrot cheer!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly summarizes the main change: whole-device configuration export and import.
Description check ✅ Passed The description matches the template and covers what changed, why, testing, screenshots, and checklist items.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

bruschill added 3 commits July 3, 2026 11:40
The committed docs/developer/deep-links.md was updated but the bundled
HTML/markdown under Meshtastic/Resources/docs/ was never regenerated, so
the in-app copy was stale. Rebuild via scripts/build-docs.sh.
Add a Tools section to docs/user/settings.md covering the NFC contact
tag, Export Configuration, and the new Import Configuration flow
(per-section review, Security defaults off, reboot/secret warnings).
Regenerate the bundled HTML via scripts/build-docs.sh.
@bruschill bruschill changed the title feat: import whole device configuration (depends on #1940) feat: export and import whole device configuration (#1924) Jul 3, 2026
@bruschill
bruschill marked this pull request as ready for review July 3, 2026 16:46
@tylerpieper

Copy link
Copy Markdown

Android will import a partial config - if a cfg doesn't have the longname populated for example, it will leave it as is on the node.

I haven't read your code yet but I want to ask that you also support a partial config import. I see a future where events and stuff can just pass an official config file that sets everything except names and keys and stuff instead of needing to do event-specific firmware and stuff.

My next PR will be for CLI to export a partial config to create these

@bruschill
bruschill force-pushed the feat/import-device-config branch from 93290f5 to 6052768 Compare July 3, 2026 16:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
docs/developer/deep-links.md (1)

58-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing hyperlink wrapper for consistency.

Every other static (parameterless) Settings route in this table is a clickable link, but this new row is plain code text.

📝 Proposed fix
-| `meshtastic:///settings/localMeshDiscovery/history` | Local Mesh Discovery (history view) |
+| [`meshtastic:///settings/localMeshDiscovery/history`](meshtastic:///settings/localMeshDiscovery/history) | Local Mesh Discovery (history view) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/developer/deep-links.md` at line 58, The new Settings deep-link entry is
listed as plain code text instead of a clickable link, unlike the other static
routes in this table. Update the deep-links markdown entry for the local mesh
discovery history route so it uses the same hyperlink wrapper style as the other
parameterless Settings routes, keeping the table formatting consistent.
Meshtastic/Views/Settings/Tools.swift (1)

128-137: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer Task { @mainactor in ... } over DispatchQueue.main.async.

This defers presenting the file exporter to the next runloop tick, but uses a GCD callback instead of Swift concurrency.

♻️ Proposed refactor
 			Button("Export Configuration") {
 				// Re-resolve the connected node at confirm time — the entity is fetched fresh rather than
 				// captured at button-tap, so it can't be a stale/faulted object if the device disconnects
 				// while the dialog is open. Defer to the next runloop so presenting the file exporter isn't
 				// swallowed by the confirmation dialog's dismissal animation.
-				DispatchQueue.main.async {
+				Task { `@MainActor` in
 					guard let node = connectedNode else { return }
 					exportConfiguration(for: node)
 				}
 			}

As per coding guidelines, "Prefer async/await over callback-based APIs for new code" and "Use Task for fire-and-forget async work."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meshtastic/Views/Settings/Tools.swift` around lines 128 - 137, The Export
Configuration action currently defers work with DispatchQueue.main.async, but
this should use Swift concurrency instead. Update the Button action in
Tools.swift to wrap the re-resolution of connectedNode and the
exportConfiguration(for:) call inside a fire-and-forget Task { `@MainActor` in ...
} so the exporter is still presented on the next runloop tick without using GCD.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/user/settings.md`:
- Line 170: The Warning callout title in the settings docs is too long and
should be shortened to match the style of nearby callout titles. Update the
warning text in the settings documentation so the title is concise, and ensure
the closing bold marker is immediately adjacent to the last word; use the
existing callout examples like the Warning and Tip titles in this file as the
style reference.

In `@Meshtastic/Export/DeviceProfileExport.swift`:
- Around line 447-448: The export logic in DeviceProfileExport’s channel
settings uses direct UInt32 casts for ChannelEntity.id and
ChannelEntity.positionPrecision, which can trap on negative persisted values.
Update the assignments in the export flow to use truncating conversion for both
fields, keeping the behavior in the DeviceProfileExport code path safe even when
malformed data is encountered.

---

Nitpick comments:
In `@docs/developer/deep-links.md`:
- Line 58: The new Settings deep-link entry is listed as plain code text instead
of a clickable link, unlike the other static routes in this table. Update the
deep-links markdown entry for the local mesh discovery history route so it uses
the same hyperlink wrapper style as the other parameterless Settings routes,
keeping the table formatting consistent.

In `@Meshtastic/Views/Settings/Tools.swift`:
- Around line 128-137: The Export Configuration action currently defers work
with DispatchQueue.main.async, but this should use Swift concurrency instead.
Update the Button action in Tools.swift to wrap the re-resolution of
connectedNode and the exportConfiguration(for:) call inside a fire-and-forget
Task { `@MainActor` in ... } so the exporter is still presented on the next
runloop tick without using GCD.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a50acc4c-65fe-4bdc-8155-145be725217b

📥 Commits

Reviewing files that changed from the base of the PR and between ea27419 and 93290f5.

📒 Files selected for processing (19)
  • Meshtastic.xcodeproj/project.pbxproj
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift
  • Meshtastic/Export/DeviceProfileDocument.swift
  • Meshtastic/Export/DeviceProfileExport.swift
  • Meshtastic/Extensions/Date.swift
  • Meshtastic/Import/DeviceProfileImport.swift
  • Meshtastic/Import/DeviceProfileImporter.swift
  • Meshtastic/Persistence/UpdateSwiftData.swift
  • Meshtastic/Resources/docs/developer/deep-links.html
  • Meshtastic/Resources/docs/index.json
  • Meshtastic/Resources/docs/markdown/developer/deep-links.md
  • Meshtastic/Resources/docs/markdown/user/settings.md
  • Meshtastic/Resources/docs/user/settings.html
  • Meshtastic/Views/Settings/ImportDeviceProfileView.swift
  • Meshtastic/Views/Settings/Tools.swift
  • MeshtasticTests/DeviceProfileExportTests.swift
  • MeshtasticTests/DeviceProfileImportTests.swift
  • docs/developer/deep-links.md
  • docs/user/settings.md

Comment thread docs/user/settings.md Outdated
Comment thread Meshtastic/Export/DeviceProfileExport.swift Outdated
Partial-import safety (from an adversarial safety review — device state was
already coherent/recoverable in every case; these address UI liveness and
honest reporting):
- Make the apply cancellable: a Cancel button on the applying screen, a
  Task.isCancelled check per item in the engine, and a CancellationError
  catch. A liveness backstop (canForceDismiss) lets the user out even if a
  single send stalls on the transport, so the modal can never trap them.
- DeviceProfileImportResult gains `wasCancelled`; isCompleteSuccess accounts
  for it. Cancellation records current + remaining items as skipped, not
  failed.
- Honest result copy: report "Sent N settings" (sends are fire-and-forget,
  not device-acked), always show a reconnect-to-verify hint, and warn that a
  failed channel replace may have partially applied. Human-readable labels
  (ImportItemKind.displayName) replace raw case names in progress/results.

Localization:
- ImportItemKind.displayName and ImportSection.title were plain Strings shown
  via Text(variable) and never localized. Route them through `.localized`
  with casing that matches existing config-screen keys so the 25 shared
  labels reuse their translations.
- Localize the assembled confirmation/sensitive-material messages via
  `.localized` fragments + String(format:).
- Register all new import (and previously-unlocalized export) UI strings in
  Localizable.xcstrings as source-language entries so translators / the
  translate-locale.sh pass can fill the other 17 locales.

Tests: add cancellation coverage (CancellationError path + pre-cancelled
task) and a displayName label check. 25 import tests pass; app builds for the
iOS 18 simulator.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
MeshtasticTests/DeviceProfileImportTests.swift (2)

453-497: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Guarded assertion asserts nothing when wasCancelled is false.

Lines 494-496 wrap the key assertion in if result.wasCancelled, so if the pre-cancelled task instead completes fully (i.e. wasCancelled is false), the test passes without verifying the invariant it's meant to check (that a coherent count of applied+skipped items exists). Consider asserting unconditionally, e.g. compute applied.count + skipped.count and compare against plan.items.count regardless of which path was taken, or split into two explicit branches with an assertion in each.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MeshtasticTests/DeviceProfileImportTests.swift` around lines 453 - 497, The
pre-cancelled task test in engineHonorsPreCancelledTask has a guarded assertion
that can be skipped when result.wasCancelled is false, so the invariant is not
always checked. Update the test to assert the applied/skipped item count against
plan.items.count unconditionally, or split the cancelled and non-cancelled paths
so each branch contains a real assertion; use the existing
DeviceProfileImporter.apply, result.wasCancelled, result.applied,
result.skipped, and plan.items symbols to keep the check tied to the importer
behavior.

280-297: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing assertion for containsSensitive after deselecting sensitive sections.

The comment claims deselecting sensitive/reboot sections "clears both aggregates," but only willReboot(in: safe) is asserted; containsSensitive(in: safe) is never checked, leaving that half of the claim unverified.

🧪 Proposed fix
 		let safe: Set<ImportSection> = [.owner, .radioAndDevice, .modules, .personalization, .fixedPosition]
 		`#expect`(!plan.willReboot(in: safe))
+		`#expect`(!plan.containsSensitive(in: safe))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MeshtasticTests/DeviceProfileImportTests.swift` around lines 280 - 297, The
DeviceProfileImportPlan flags test verifies reboot clearing but does not assert
the sensitive aggregate after deselecting sections. Update the flags() test in
DeviceProfileImportTests to also check containsSensitive(in:) against the safe
ImportSection set, alongside the existing willReboot(in:) assertion, so the
“clears both aggregates” claim is fully covered. Use the existing
plan.containsSensitive and plan.willReboot calls in the flags() test to locate
the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@MeshtasticTests/DeviceProfileImportTests.swift`:
- Around line 453-497: The pre-cancelled task test in
engineHonorsPreCancelledTask has a guarded assertion that can be skipped when
result.wasCancelled is false, so the invariant is not always checked. Update the
test to assert the applied/skipped item count against plan.items.count
unconditionally, or split the cancelled and non-cancelled paths so each branch
contains a real assertion; use the existing DeviceProfileImporter.apply,
result.wasCancelled, result.applied, result.skipped, and plan.items symbols to
keep the check tied to the importer behavior.
- Around line 280-297: The DeviceProfileImportPlan flags test verifies reboot
clearing but does not assert the sensitive aggregate after deselecting sections.
Update the flags() test in DeviceProfileImportTests to also check
containsSensitive(in:) against the safe ImportSection set, alongside the
existing willReboot(in:) assertion, so the “clears both aggregates” claim is
fully covered. Use the existing plan.containsSensitive and plan.willReboot calls
in the flags() test to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 88b0edfd-e65a-4892-af59-6e4cc60ce1a9

📥 Commits

Reviewing files that changed from the base of the PR and between 6052768 and 784fa01.

📒 Files selected for processing (5)
  • Localizable.xcstrings
  • Meshtastic/Import/DeviceProfileImport.swift
  • Meshtastic/Import/DeviceProfileImporter.swift
  • Meshtastic/Views/Settings/ImportDeviceProfileView.swift
  • MeshtasticTests/DeviceProfileImportTests.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • Meshtastic/Import/DeviceProfileImporter.swift
  • Meshtastic/Import/DeviceProfileImport.swift
  • Meshtastic/Views/Settings/ImportDeviceProfileView.swift

@bruschill

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

- DeviceProfileExport: use UInt32(truncatingIfNeeded:) for ChannelEntity.id
  and positionPrecision (both Int32) so malformed negative persisted values
  can't trap the export.
- docs/user/settings.md: shorten the import Warning callout title per the
  callout-title guideline; regenerate bundled HTML.
- DeviceProfileImportTests: assert containsSensitive is false for a
  genuinely non-sensitive selection (the old set wrongly included Modules,
  which carries the MQTT password); make the pre-cancelled-task test assert
  coherent invariants unconditionally instead of only when wasCancelled.
…file omits them

Addresses maintainer feedback on meshtastic#2023 (tylerpieper): support importing a
partial/"official" event config that sets everything except names and keys,
leaving the node's own values in place.

Block-level and owner-name partial import already worked (the plan is has*-gated
per config block, and ownerUser merges only the long/short name present). This
adds the missing "keys" half: a present SecurityConfig block previously replaced
the node's identity keypair wholesale, so an event config that only set flags or
admin keys would wipe the node's public/private key.

DeviceProfileImportPlan.securityConfig(from:base:) now merges the profile's
security block onto the node's current one, preserving the existing private/
public/admin keys whenever the profile leaves them empty. An empty key is never
a valid identity, so empty-as-absent is safe: full backups (keys present) still
restore the keypair; partial configs keep the node's identity. The review-sheet
summary reflects when identity is preserved.

Adds 6 tests covering preserve-when-absent, replace-when-present, per-field
independence, the no-base passthrough, and the emitted item's payload/summary.
All 31 DeviceProfileImportTests pass.
…config

# Conflicts:
#	Localizable.xcstrings
#	Meshtastic.xcodeproj/project.pbxproj
#	Meshtastic/Resources/docs/index.json
…config

# Conflicts:
#	Meshtastic/Resources/docs/index.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🚀 [Feature Request]: export whole configuration

2 participants