Skip to content

feat(meshcore): saved-regions catalog + region pickers for scopes (#3770)#3783

Merged
Yeraze merged 1 commit into
mainfrom
feat/3770-region-scope-mgmt
Jun 26, 2026
Merged

feat(meshcore): saved-regions catalog + region pickers for scopes (#3770)#3783
Yeraze merged 1 commit into
mainfrom
feat/3770-region-scope-mgmt

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Closes #3770

Summary

MeshMonitor can request regions/scopes from nearby MeshCore repeaters, but until now scopes had to be typed by hand everywhere. This adds a user-maintained, global catalog of MeshCore region names and wires it into every place a scope is chosen, so operators save a region once and pick it from a list thereafter.

A MeshCore "scope" is a transport code derived purely from a region name (sha256("#region")[:16]), so the catalog is not source-scoped — it is global-by-design, mirroring the existing channel_database / automations tables.

Data model

New table meshcore_saved_regions (migration 108, all three backends, idempotent):

column notes
id PK autoincrement
name region name, normalized (lowercase, no leading #, [a-z0-9-]), UNIQUE
note optional
created_at / updated_at timestamps
  • Drizzle schema (src/db/schema/savedRegions.ts) + activeSchema wiring.
  • SavedRegionsRepository (src/db/repositories/savedRegions.ts): getAllAsync, getByNameAsync, addAsync (normalizing + idempotent — returns existing on duplicate, updates note), updateNoteAsync, deleteAsync. Exposed as databaseService.savedRegions.
  • Added to the migrate-db TABLE_ORDER (global, no FK).

API

Mounted under the existing source-scoped meshcore router (reusing its auth), operating on the global table:

  • GET /api/sources/:id/meshcore/saved-regions — list
  • POST /api/sources/:id/meshcore/saved-regions{ name, note? }, idempotent add
  • DELETE /api/sources/:id/meshcore/saved-regions/:regionId — delete

configuration read/write permission, consistent with the default-scope routes.

The 4 UI touchpoints

  1. Save a repeater-reported region — each discovered-region chip in MeshCoreSettingsView gets a save button (shows when already saved).
  2. Manage the list (add/delete) — a new "Saved regions" section in MeshCoreSettingsView with an add field and per-item delete.
  3. Channel settings dropdown — the channel scope field in MeshCoreChannelsConfigSection gains a <datalist> populated from the saved catalog (free typing still allowed).
  4. Per-message scope override dropdown — the override <datalist> in MeshCoreChannelsView now offers the union of saved + discovered regions, de-duplicated.

useMeshCore gains fetchSavedRegions / addSavedRegion / deleteSavedRegion.

Testing

  • New savedRegions repository unit tests: normalization, idempotent add, note update on re-add, invalid-name rejection, case-insensitive lookup, ordering, delete.
  • New meshcore route tests: list / add / delete happy paths plus auth (401), missing-name (400), invalid-name (400), invalid-id (400).
  • migrations.test.ts count 107→108 + last-name assertion; migrationTables schema-coverage updated.
  • Full Vitest suite green (7612 passed, 0 failed) — excluding 4 spiderfier suites that fail to resolve ts-overlapping-marker-spiderfier-leaflet on the base tree too (pre-existing worktree dependency issue, unrelated to this change).
  • tsc clean on all changed files; no new settings keys; no device-comms code touched (no system-test label needed).

🤖 Generated with Claude Code

)

Add a user-maintained, GLOBAL catalog of MeshCore region names so operators
can save/manage regions and pick them when setting scopes, instead of typing
the scope every time. A MeshCore "scope" is a transport code derived purely
from a region name (sha256("#region")[:16]), so the catalog is not source-
scoped — it mirrors the global-by-design channel_database / automations tables.

Data model:
- New table meshcore_saved_regions (id, name UNIQUE+normalized, note,
  created_at, updated_at) via migration 108 (SQLite/Postgres/MySQL, idempotent).
- Schema + activeSchema wiring; SavedRegionsRepository with normalize/add
  (idempotent)/list/getByName/delete; exposed as databaseService.savedRegions.
- Added to migrate-db TABLE_ORDER (global, no FK).

API (under the source-scoped meshcore router, operating on the global table):
- GET    /saved-regions      list
- POST   /saved-regions      add (idempotent; { name, note? })
- DELETE /saved-regions/:id  delete

UI (4 touchpoints from the issue):
1. Save a repeater-reported region: + button on each discovered-region chip
   in MeshCoreSettingsView (✓ when already saved).
2. Manage the list: add/delete section in MeshCoreSettingsView.
3. Channel settings: scope field gains a datalist of saved regions
   (MeshCoreChannelsConfigSection).
4. Per-message override: the scope-override datalist now unions saved +
   discovered regions (MeshCoreChannelsView).
useMeshCore gains fetchSavedRegions/addSavedRegion/deleteSavedRegion.

Tests:
- savedRegions repository unit tests (normalize, idempotent add, note update,
  invalid-name reject, case-insensitive lookup, ordering, delete).
- meshcore routes tests for list/add/delete (auth, validation, 400 paths).
- migrations.test count 107→108 + last-name; migrationTables coverage updated.
Full vitest suite green (excludes 4 pre-existing env-broken spiderfier suites).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(meshcore): saved-regions catalog + region pickers for scopes

Tasks

  • Gather context and read changed files
  • Review data model and migration
  • Review API routes and repository
  • Review UI components and hooks
  • Review tests
  • Post final review

Overall Assessment

This is a well-structured feature with clear design rationale. The global-by-design decision is correct, the layering is clean, and the test coverage is solid. A few items worth addressing:


Issues

1. normalizeRegionName strips spaces but allows multi-hyphen sequences

src/db/repositories/savedRegions.ts:37

.replace(/[^a-z0-9-]/g, '');

Spaces get dropped, so "foo bar" normalizes to "foobar" rather than "foo-bar". This is non-obvious to users — someone typing "foo bar" in the UI might expect "foo-bar". Worth documenting this explicitly or replacing spaces with hyphens before stripping other characters. Also, strings like "a---b" or "---" (all hyphens) pass validation. The trailing-hyphen edge case ("foo-") would become a valid name.

2. MeshCoreSettingsView: pre-strip of # duplicates normalization already done in the repo

src/components/MeshCore/MeshCoreSettingsView.tsx:72

const trimmed = name.trim().replace(/^#/, '');

The repository's addAsync → normalizeRegionName already strips leading #. This double-stripping is harmless but could hide divergence if the server-side normalization changes. Consider passing the raw string and letting the server normalize.

3. Race condition in addAsync when note update fails

src/db/repositories/savedRegions.ts:92-95

if (existing) {
  if (trimmedNote !== null && trimmedNote !== existing.note) {
    await this.updateNoteAsync(existing.id, trimmedNote);
    return { ...existing, note: trimmedNote };
  }
  return existing;
}

The returned object is a synthesized { ...existing, note: trimmedNote } without re-reading from DB. If updateNoteAsync throws, the error propagates correctly — but the pattern of constructing the return value rather than re-fetching means the caller could get a stale object if a future refactor adds more fields to updateNoteAsync. Low risk currently, but worth noting.

4. MeshCoreChannelsConfigSection fetches saved regions directly via csrfFetch instead of actions.fetchSavedRegions

src/components/MeshCore/MeshCoreChannelsConfigSection.tsx:131-136

This component fetches the saved-regions catalog independently with a raw csrfFetch call, while MeshCoreChannelsView (correctly) uses actions.fetchSavedRegions. This inconsistency means the catalog is fetched via two different code paths, making it harder to change or cache in the future. If MeshCoreChannelsConfigSection doesn't receive actions, this is understandable, but if it does receive the necessary hook, centralizing this would be cleaner.

5. Delete endpoint does not verify the region exists

src/server/routes/meshcoreRoutes.ts:740

await databaseService.savedRegions.deleteAsync(id);
res.json({ success: true });

A DELETE for a non-existent ID still returns { success: true }. This is common REST practice (idempotent delete), but if the client relies on success: true to infer the row was actually removed, it may silently swallow typos. Consider documenting this as intentional.


Minor Observations

  • MySQL VARCHAR(255) for note (src/db/schema/savedRegions.ts:44): The other two backends use TEXT (unlimited). For a short note field this is plenty, but if you ever want longer notes on SQLite/Postgres, the MySQL limit would lag. Consistent schema type across backends is preferable (e.g., TEXT on MySQL too, or VARCHAR(255) on all three).

  • datalist IDs are per-channel-index in MeshCoreChannelsConfigSection (mc-ch-scope-regions-${idx}): Multiple channels editing at once (which the current UI prevents via editingIdx !== null guard) would create duplicate IDs. The guard makes this safe, but the guard is enforced at the call-site, not the component. Fine as-is.

  • PLAN_3770.md committed to the repo: Planning documents generally don't belong in a production branch unless the project convention supports them. Consider removing before merge.

  • src/db/repositories/savedRegions.ts:45: The private map(row: any) method uses any. Given that the rest of the repo treats any in repository row mapping as an accepted pattern due to Drizzle's dynamic types, this is consistent — just noting it.

  • Test: POST /saved-regions rejects missing name — the test posts {} (no name key). It would also be useful to test { name: '' } (empty string), which is a different code path (the !name.trim() branch).


What's done well

  • The idempotent addAsync (returns existing on duplicate, updates note) is exactly the right behavior for a UX that lets users hit the "+" on a chip without error.
  • The regionsDiscoveredRef guard in MeshCoreChannelsView preventing re-discovery on reconnects is a good defensive pattern.
  • All three DB backends are properly covered with idempotent DDL.
  • The normalizeRegionName function is exported and directly unit-tested — good.
  • Auth is consistently gated on configuration read/write, matching the default-scope routes.
  • Correct encodeURIComponent usage on the delete URL in useMeshCore.ts:874.

@Yeraze Yeraze merged commit f9efc64 into main Jun 26, 2026
19 checks passed
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.

[FEAT]MeshCore: Improve Region/Scope management

1 participant