Skip to content

feat(node): msh.to device hardware links ("I want one" section + Settings directory)#5714

Merged
jamesarich merged 4 commits into
release/2.8.0from
claude/stoic-maxwell-53ea9e
Jun 4, 2026
Merged

feat(node): msh.to device hardware links ("I want one" section + Settings directory)#5714
jamesarich merged 4 commits into
release/2.8.0from
claude/stoic-maxwell-53ea9e

Conversation

@jamesarich

Copy link
Copy Markdown
Collaborator

What

Ports the Meshtastic-Apple device msh.to links feature (Meshtastic-Apple#1898, spec 010-device-mshto-links) to Android. When viewing a node's hardware, a collapsible "I want one" section surfaces:

  • the vendor page and product-variant links (prominent), and
  • marketplace listings (AliExpress, Amazon, Rokland, Hexaspot, Tindie, Muzi), filtered to the user's region.

Every link opens via the https://msh.to/{shortCode} redirect. A Settings → Device Links directory lists all imported short codes.

Note: this is hardware purchase/vendor links — not channel/contact deep-link sharing.

Why

Connects users to relevant resources (vendor pages, regional retailers) for the hardware they own or are viewing, supporting the Meshtastic backer/partner ecosystem — parity with the iOS app.

How

Mirrors the existing DeviceHardware pipeline (bundled-JSON seed + Room + reconcile-on-refresh):

  • Assets — bundled androidApp/src/main/assets/urls.json (copied as-is from the meshtastic/msh.to repo) + app-maintained marketplaces.json (shipping regions + prefix/suffix match patterns).
  • Persistence — new Room device_link table (DB v38→39 auto-migration; 39.json schema committed), DeviceLinkEntity/DeviceLinkDao/DeviceLinkLocalDataSource; added DeviceHardwareDao.getAllTargets().
  • DomainDeviceLinkRepository(Impl) + a pure, unit-tested DeviceLinkMatcher (exact-vendor → variant → marketplace prefix/suffix matching, rak-prefix strip, locale region filter). isVendor/orphan reconciliation rides DeviceHardwareRepositoryImpl's network refresh; self-seeds on first read.
  • Locale — new currentRegionCode() KMP expect/actual (android/jvm = Locale.country, ios noop) for marketplace region filtering.
  • UIDeviceLinksSection on the node detail screen (fed via MetricsState.deviceLinks from CommonGetNodeDetailsUseCase); DeviceLinkDirectoryScreen + SettingsRoute.DeviceLinks. Desktop Koin graph gets a stub binding for the Android-only JSON data source.

Reviewer notes

  • The architecture-as-enum decode pitfall flagged in the Apple spec is already safe on Android (architecture is a String, parsed with ignoreUnknownKeys).
  • Bundled-only for v1 (no dedicated network fetch); link reconciliation piggybacks the existing device-hardware refresh, matching Apple's v1.
  • New tests: DeviceLinkMatcherTest (11, covers the spec's acceptance scenarios) and DeviceLinkRepositoryImplTest (3, in-memory DB).
  • Local verification green: spotlessCheck, detekt, assembleDebug, test, allTests (1213), kmpSmokeCompile.
  • The commit also includes the routine .agent_memory session-handover + strings-index.txt regeneration.

🤖 Generated with Claude Code

Port the Meshtastic-Apple device-links feature (spec 010-device-mshto-links):
surface vendor, product-variant, and region-filtered marketplace purchase links
for a node's hardware in a collapsible "I want one" section, plus a Settings
"Device Links" directory. Every link resolves via https://msh.to/{shortCode}.

- Bundle urls.json (from meshtastic/msh.to) + app-maintained marketplaces.json
- New Room `device_link` table (DB v38->39 auto-migration) + DAO/local data source
- DeviceLinkRepository(Impl) + pure DeviceLinkMatcher (exact-vendor / variant /
  marketplace prefix-suffix matching, rak-prefix strip, locale region filter);
  isVendor + orphan reconciliation rides the existing device-hardware refresh,
  self-seeds on first read
- currentRegionCode() KMP expect/actual for marketplace region filtering
- UI: DeviceLinksSection (node detail, via MetricsState.deviceLinks) and
  DeviceLinkDirectoryScreen + SettingsRoute.DeviceLinks; desktop Koin stub
- Tests: DeviceLinkMatcherTest (11) + DeviceLinkRepositoryImplTest (3)

Verified: spotlessCheck, detekt, assembleDebug, test, allTests, kmpSmokeCompile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the enhancement New feature or request label Jun 2, 2026
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

📸 Screenshot reference staleness — advisory

This PR modifies preview composables but does not update screenshot reference images.

Reference images in screenshot-tests/src/screenshotTestDebug/reference/ must be regenerated when previews change, or validateDebugScreenshotTest will fail.

Changed preview files:

feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt

How to update:

./gradlew :screenshot-tests:updateDebugScreenshotTest

Then commit the updated reference PNGs.

If this change is intentionally preview-only (e.g., adding a preview that doesn't need a test yet), add the skip-preview-check label.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

✅ Docs staleness check passed

This PR includes updates to docs/en/ alongside the source changes. Thank you!

jamesarich and others added 2 commits June 2, 2026 10:50
- Unify marketplace classification into a single delimiter-aware
  DeviceLinkMatcher.marketplaceKeyFor used by both import-time region tagging
  and the matcher/UI. Fixes 36 aliexpress-prefix codes that were tagged
  null-region (rendered bold like vendor links yet sorted last), and stops
  bare names like `muziworks` inheriting `muzi` regions.
- Run DeviceLinkRepository.reconcile() outside the device-hardware network
  timeout so a deadline can't cancel it mid-write (partial orphan prune).
- Memoize bundled urls.json/marketplaces.json parsing (immutable assets) to
  drop repeated disk I/O + JSON parse on the node-detail hot path.
- Add the Device Links entry to DesktopSettingsScreen for cross-platform parity
  (route/screen/VM were commonMain but unreachable on desktop).
- Reuse core/ui ListItem in the directory instead of a hand-rolled row.
- Drop the dead `rak-` variant branch; expose expand/collapse state to
  accessibility via stateDescription.
- Tests: +marketplaceKeyForUsesDelimiterBounds, +aliexpress-prefix and
  +bare-name import tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Only add the "I want one" DeviceLinksSection as a LazyColumn item when there
  are links to show. The previous unconditional `item {}` left an empty slot
  that `Arrangement.spacedBy(24.dp)` still padded, shifting NodeDetailContent
  layout and breaking the screenshot reference comparison. Guarding the item
  restores byte-identical rendering (validateDebugScreenshotTest: 234/234) with
  no reference-image changes needed.
- Document the "I want one" section and the Settings → Device Links directory in
  docs/en/user/nodes.md (addresses the docs staleness advisory).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jamesarich

Copy link
Copy Markdown
Collaborator Author

Addressed both advisories in b39879a3:

  • 📸 Screenshot check — the real failure wasn't the new (un-wrapped) preview; it was that NodeDetailContent unconditionally added an empty item {} for the links section, which Arrangement.spacedBy(24.dp) still padded, shifting the ScreenshotNodeDetailContent* references. Fixed by only adding the item when there are links to show — rendering is now byte-identical (:screenshot-tests:validateDebugScreenshotTest passes 234/234) with no reference-image changes.
  • 📄 Docs check — documented the "I want one" section and the Settings → Device Links directory under docs/en/user/nodes.md (Node Detail), and bumped last_updated.

@jamesarich jamesarich marked this pull request as ready for review June 2, 2026 16:12
@jamesarich jamesarich added this to the 2.8.0 milestone Jun 2, 2026
@jamesarich jamesarich mentioned this pull request Jun 3, 2026
@jamesarich jamesarich changed the base branch from main to release/2.8.0 June 4, 2026 03:00
Resolved conflicts:
- Room schema renumbered v39 -> v41 (FTS5=v39, air-quality=v40): version=41,
  AutoMigration(40->41), DeviceLinkEntity table, regenerated 41.json, kept release's 39.json
- Routes / SettingsNavigation / SettingsScreen: union DeviceLinks + AppFunctionsSettings entries
- proto pinned to 6b1ded4 (master HEAD)
- session_context.md: take release/2.8.0
@jamesarich jamesarich merged commit adb5bf4 into release/2.8.0 Jun 4, 2026
15 checks passed
@jamesarich jamesarich deleted the claude/stoic-maxwell-53ea9e branch June 4, 2026 09:33
jamesarich added a commit that referenced this pull request Jun 10, 2026
…ings directory) (#5714)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jamesarich added a commit that referenced this pull request Jun 10, 2026
…cher

Replace the bundled urls.json + marketplaces.json and the client-side DeviceLinkMatcher heuristic with a fetch of the now-live /resource/deviceLinks endpoint, which returns fully-resolved links (type, targets, regions). The client seeds from a bundled snapshot, refreshes from the network, and filters the cache - mirroring the DeviceHardware repository pattern.

- core/model: NetworkDeviceLink DTOs; DeviceLink drops originalUrl, adds targets
- core/network: ApiService.getDeviceLinks() + DeviceLinksRemoteDataSource
- core/database: device_link drops original_url, adds targets; DB v42 -> v43
- core/data: rewrite DeviceLinkRepositoryImpl (seed -> single-flight refresh); delete DeviceLinkMatcher + the MshTo datasource/model
- assets: replace urls.json + marketplaces.json with a device_links.json snapshot

Internal links are excluded from the directory per the API contract ("clients exclude 'internal' from purchase UI"). F-Droid and desktop fall back to the bundled snapshot when the API is unavailable, as they do for device hardware.

Follow-up to #5714; depends on meshtastic/api#94 and meshtastic/msh.to#3 (both merged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jamesarich added a commit that referenced this pull request Jun 10, 2026
…ings directory) (#5714)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jamesarich added a commit that referenced this pull request Jun 10, 2026
…ings directory) (#5714)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jamesarich added a commit that referenced this pull request Jun 12, 2026
…ings directory) (#5714)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jamesarich added a commit that referenced this pull request Jun 13, 2026
…ings directory) (#5714)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jamesarich added a commit that referenced this pull request Jun 16, 2026
…ings directory) (#5714)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jamesarich added a commit that referenced this pull request Jun 16, 2026
…ings directory) (#5714)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jamesarich added a commit that referenced this pull request Jun 16, 2026
…ings directory) (#5714)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant