STATUS: IMPLEMENTED. All 4 capabilities delivered. New global table
meshcore_saved_regions(migration 108) + repository + routes + 4 UI touchpoints. Full vitest suite green (excluding 4 pre-existing environmental spiderfier suites that fail on the base tree too).
A user-maintained list of saved MeshCore regions, plus four UI touchpoints to use it:
- Save a region reported by a repeater (from the discover-regions UI).
- Add / Delete entries manually in a management list.
- Channel settings: pick a saved region to set the channel scope (instead of typing).
- Per-message: pick a saved region to override the scope for one message.
Rationale: a scope is a transport code derived purely from a region name
(sha256("#region")[:16]). It is not tied to a source/node — the same physical
mesh region applies across every MeshCore source. This mirrors channel_database
and automations, which are global-by-design. The saved list is a convenience
catalog of names; uniqueness is on the (normalized) name.
Columns (all 3 backends, following automations style):
| column | type | notes |
|---|---|---|
id |
INTEGER PK autoincrement (SQLite/PG); MySQL int auto_increment |
|
name |
TEXT / varchar(64) | region name, stored WITHOUT leading #, normalized (lowercase letters/digits/hyphen) |
note |
TEXT nullable | optional user note / where it came from |
createdAt |
INTEGER / bigint | |
updatedAt |
INTEGER / bigint |
Unique index on name (case-insensitive handled in repo by normalizing before insert/lookup).
src/server/migrations/108_meshcore_saved_regions.ts:CREATE TABLE IF NOT EXISTSfor all three backends (idempotent), unique index on name.- Register in
src/db/migrations.ts(number 108, namemeshcore_saved_regions, settingsKeymigration_108_meshcore_saved_regions). - Bump
src/db/migrations.test.ts: count 107 → 108, last namemeshcore_saved_regions.
src/db/schema/savedRegions.ts—savedRegionsSqlite/Postgres/Mysql+ inferred types.- Export from
src/db/schema/index.ts. - Register in
src/db/activeSchema.ts(import, add to active select map under keymeshcoreSavedRegions, add toActiveSchematype).
src/db/repositories/savedRegions.ts—SavedRegionsRepository extends BaseRepository:getAllAsync(): Promise<DbSavedRegion[]>(ordered by name)getByNameAsync(name)addAsync(name, note?): Promise<DbSavedRegion>— normalize name, idempotent upsert (return existing if name already saved)deleteAsync(id): Promise<void>- Name normalization helper: strip leading
#, lowercase, keep[a-z0-9-], reject empty.
- Wire into
DatabaseService: import,public savedRegionsRepo,get savedRegions()getter, init ininitialize().
These are catalog-management routes; they touch the DB only (no node IO), but live
under the meshcore router for cohesion. Region management is global, so they are
NOT under /sources/:id — use a top-level meshcore regions router OR mount on the
existing meshcore router with permission requirePermission('settings','write')
(read = 'settings','read'). Confirm existing top-level mounting; if the meshcore
router is always source-scoped, add routes under /sources/:id/meshcore/saved-regions
operating on the global table (sourceId ignored) to reuse existing auth wiring.
GET .../saved-regions→ list ({ success, regions: DbSavedRegion[] })POST .../saved-regions→ body{ name, note? }→ add →{ success, region }DELETE .../saved-regions/:id→ delete →{ success }
Add to the hook: savedRegions: DbSavedRegion[], fetchSavedRegions(),
addSavedRegion(name, note?), deleteSavedRegion(id). Use csrfFetch against
${mcPrefix}/saved-regions. Fetch on mount alongside other initial loads.
The discovered-regions chips (~L272-291) already render per region. Add a small
"+ Save" affordance per chip (or a save icon) that calls addSavedRegion(region).
Disable/checkmark when already saved (cross-ref savedRegions).
New section in MeshCoreSettingsView.tsx (region/scope area): list of saved
regions, each with a delete button; an input + Add button to add manually.
Reuse the normalization/validation already used for scope input.
Where the channel scope is set. Per the investigation, channel scope is set via
updateChannelScope repo + the settings default-scope field; confirm the channel
edit UI (MeshCoreChannelsConfigSection.tsx / channel edit modal). Replace/augment
the manual scope text input with a <datalist> or <select> populated from
savedRegions (free-typing still allowed for new names). Minimal change: add
list= + <datalist> of saved regions to the existing scope input, matching the
per-message override pattern already in MeshCoreChannelsView.tsx.
MeshCoreChannelsView.tsx (~L516-563) already has a scope-override <input> with
a <datalist id="mc-scope-region-suggestions"> populated from discoveredRegions.
Extend that datalist (or add the saved regions) so saved regions appear as
suggestions — union of discoveredRegions + savedRegions, de-duplicated.
src/db/repositories/savedRegions.test.ts— add/normalize/dedupe/delete/list, name validation, global (no sourceId).src/db/migrations.test.ts— count + last name update.- Route tests in the existing meshcore routes test file (or new) — list/add/delete happy path + auth + validation (mock
DatabaseService.savedRegions.*Async). - UI: light test for the union/dedup of suggestions if non-trivial; otherwise rely on existing component tests compiling.
- Full vitest suite (0 failures),
tsc/build, lint. Nosystem-testlabel (no device-comms change).
- This feature is DB + routes + UI only; no protocol/native-backend change → low risk.
- If the channel-edit scope UI proves to not exist as a discrete field, deliver the datalist on the per-message override + the manage list + save-from-discovery, and note the channel-settings dropdown as a follow-up. All four are targeted though.