mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
* feat(region-routing): P1 tagging + region_routes primitive + read/write API + preview launcher - config.py: add Coverage.region_tagging (bool=False); add RegionRouteMatrix dataclass (enabled, cells) above NotificationsConfig; add region_routes field to NotificationsConfig; add explicit hydration branch for region_routes in _dict_to_dataclass mirroring destinations pattern. - coverage_area.py: add MonitoringArea.name (str|None=None, frozen); update areas_from_config to preserve name; refactor inline geom extraction from classify_event_areas into shared _event_geom_json helper; add matching_area_names(geom_json, areas)->list[str] (additive, all named matches, config-order, deduped; gate unchanged); add event_region_names convenience wrapper. - coverage_filter.py: add region_tagging ctor kwarg; stamp event.region/ regions before the gate when region_tagging=True and areas non-empty and not event.regions (never clobbers satpass preset). - pipeline/__init__.py: wire region_tagging into CoverageFilter construction. - notification_routes.py: add GET /notifications/regions (named coverage area names, config-order, deduped); GET /notifications/region-routing (matrix as JSON); POST /notifications/region-routing (explicit RMW — only region_routes changes, toggles/rules/destinations survive). - scripts/preview_dashboard.py: mesh-free launcher — dashboard API only, no mesh connector, no broadcast loop; vite runs separately. All 87 coverage tests pass; 300 total pass; 6 pre-existing failures unchanged (adapter config count mismatch + MeshCore EventType.NEW_CONTACT). * feat(region-routing): manual region x family matrix editor page Adds RegionRoutingMatrix.tsx — a plain editor over the region_routes config primitive. Rows = families (via useFamilies()), cols = regions (from GET /api/notifications/regions). Each cell exposes MT channel (ChannelPicker single + includeDisabled), MC channel name (text input), min_severity select (routine/priority/critical/immediate), and an enabled checkbox. Only cells where MT or MC is set are included in the sparse POST payload. Master enable toggle maps to top-level enabled. MT budget guard warns when more than 7 distinct MT indices are in use. Sticky family column; horizontal scroll for wide region sets. Registers route /region-routing in App.tsx and adds "Region Routing" nav entry (Map icon) under the Meshtastic section in Layout.tsx, immediately after Routing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(region-routing): regions endpoint reads saved (disk) coverage so routing columns are dynamic without a bot restart; preview reloads config after writes * feat(routing): unify MT/MC routing into per-family cards; region routing as an in-card expand; remove rules/destinations UI + standalone page * refactor(routing): move Meshtastic Routing from /notifications to /meshtastic/routing (mirror /meshcore/routing); redirect legacy path * feat(region-routing): dispatcher honors region_routes matrix (authoritative-on-match, per-region cooldown, per-channel dedup); non-matrix path unchanged Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(region-routing): matrix dedup key must match boot-restore 2-tuple form (prevents restart re-broadcast flood); regression test --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
261 lines
6.8 KiB
JavaScript
261 lines
6.8 KiB
JavaScript
const util = require('./util')
|
|
|
|
module.exports = function stringify (value, replacer, space) {
|
|
const stack = []
|
|
let indent = ''
|
|
let propertyList
|
|
let replacerFunc
|
|
let gap = ''
|
|
let quote
|
|
|
|
if (
|
|
replacer != null &&
|
|
typeof replacer === 'object' &&
|
|
!Array.isArray(replacer)
|
|
) {
|
|
space = replacer.space
|
|
quote = replacer.quote
|
|
replacer = replacer.replacer
|
|
}
|
|
|
|
if (typeof replacer === 'function') {
|
|
replacerFunc = replacer
|
|
} else if (Array.isArray(replacer)) {
|
|
propertyList = []
|
|
for (const v of replacer) {
|
|
let item
|
|
|
|
if (typeof v === 'string') {
|
|
item = v
|
|
} else if (
|
|
typeof v === 'number' ||
|
|
v instanceof String ||
|
|
v instanceof Number
|
|
) {
|
|
item = String(v)
|
|
}
|
|
|
|
if (item !== undefined && propertyList.indexOf(item) < 0) {
|
|
propertyList.push(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (space instanceof Number) {
|
|
space = Number(space)
|
|
} else if (space instanceof String) {
|
|
space = String(space)
|
|
}
|
|
|
|
if (typeof space === 'number') {
|
|
if (space > 0) {
|
|
space = Math.min(10, Math.floor(space))
|
|
gap = ' '.substr(0, space)
|
|
}
|
|
} else if (typeof space === 'string') {
|
|
gap = space.substr(0, 10)
|
|
}
|
|
|
|
return serializeProperty('', {'': value})
|
|
|
|
function serializeProperty (key, holder) {
|
|
let value = holder[key]
|
|
if (value != null) {
|
|
if (typeof value.toJSON5 === 'function') {
|
|
value = value.toJSON5(key)
|
|
} else if (typeof value.toJSON === 'function') {
|
|
value = value.toJSON(key)
|
|
}
|
|
}
|
|
|
|
if (replacerFunc) {
|
|
value = replacerFunc.call(holder, key, value)
|
|
}
|
|
|
|
if (value instanceof Number) {
|
|
value = Number(value)
|
|
} else if (value instanceof String) {
|
|
value = String(value)
|
|
} else if (value instanceof Boolean) {
|
|
value = value.valueOf()
|
|
}
|
|
|
|
switch (value) {
|
|
case null: return 'null'
|
|
case true: return 'true'
|
|
case false: return 'false'
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
return quoteString(value, false)
|
|
}
|
|
|
|
if (typeof value === 'number') {
|
|
return String(value)
|
|
}
|
|
|
|
if (typeof value === 'object') {
|
|
return Array.isArray(value) ? serializeArray(value) : serializeObject(value)
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
function quoteString (value) {
|
|
const quotes = {
|
|
"'": 0.1,
|
|
'"': 0.2,
|
|
}
|
|
|
|
const replacements = {
|
|
"'": "\\'",
|
|
'"': '\\"',
|
|
'\\': '\\\\',
|
|
'\b': '\\b',
|
|
'\f': '\\f',
|
|
'\n': '\\n',
|
|
'\r': '\\r',
|
|
'\t': '\\t',
|
|
'\v': '\\v',
|
|
'\0': '\\0',
|
|
'\u2028': '\\u2028',
|
|
'\u2029': '\\u2029',
|
|
}
|
|
|
|
let product = ''
|
|
|
|
for (let i = 0; i < value.length; i++) {
|
|
const c = value[i]
|
|
switch (c) {
|
|
case "'":
|
|
case '"':
|
|
quotes[c]++
|
|
product += c
|
|
continue
|
|
|
|
case '\0':
|
|
if (util.isDigit(value[i + 1])) {
|
|
product += '\\x00'
|
|
continue
|
|
}
|
|
}
|
|
|
|
if (replacements[c]) {
|
|
product += replacements[c]
|
|
continue
|
|
}
|
|
|
|
if (c < ' ') {
|
|
let hexString = c.charCodeAt(0).toString(16)
|
|
product += '\\x' + ('00' + hexString).substring(hexString.length)
|
|
continue
|
|
}
|
|
|
|
product += c
|
|
}
|
|
|
|
const quoteChar = quote || Object.keys(quotes).reduce((a, b) => (quotes[a] < quotes[b]) ? a : b)
|
|
|
|
product = product.replace(new RegExp(quoteChar, 'g'), replacements[quoteChar])
|
|
|
|
return quoteChar + product + quoteChar
|
|
}
|
|
|
|
function serializeObject (value) {
|
|
if (stack.indexOf(value) >= 0) {
|
|
throw TypeError('Converting circular structure to JSON5')
|
|
}
|
|
|
|
stack.push(value)
|
|
|
|
let stepback = indent
|
|
indent = indent + gap
|
|
|
|
let keys = propertyList || Object.keys(value)
|
|
let partial = []
|
|
for (const key of keys) {
|
|
const propertyString = serializeProperty(key, value)
|
|
if (propertyString !== undefined) {
|
|
let member = serializeKey(key) + ':'
|
|
if (gap !== '') {
|
|
member += ' '
|
|
}
|
|
member += propertyString
|
|
partial.push(member)
|
|
}
|
|
}
|
|
|
|
let final
|
|
if (partial.length === 0) {
|
|
final = '{}'
|
|
} else {
|
|
let properties
|
|
if (gap === '') {
|
|
properties = partial.join(',')
|
|
final = '{' + properties + '}'
|
|
} else {
|
|
let separator = ',\n' + indent
|
|
properties = partial.join(separator)
|
|
final = '{\n' + indent + properties + ',\n' + stepback + '}'
|
|
}
|
|
}
|
|
|
|
stack.pop()
|
|
indent = stepback
|
|
return final
|
|
}
|
|
|
|
function serializeKey (key) {
|
|
if (key.length === 0) {
|
|
return quoteString(key, true)
|
|
}
|
|
|
|
const firstChar = String.fromCodePoint(key.codePointAt(0))
|
|
if (!util.isIdStartChar(firstChar)) {
|
|
return quoteString(key, true)
|
|
}
|
|
|
|
for (let i = firstChar.length; i < key.length; i++) {
|
|
if (!util.isIdContinueChar(String.fromCodePoint(key.codePointAt(i)))) {
|
|
return quoteString(key, true)
|
|
}
|
|
}
|
|
|
|
return key
|
|
}
|
|
|
|
function serializeArray (value) {
|
|
if (stack.indexOf(value) >= 0) {
|
|
throw TypeError('Converting circular structure to JSON5')
|
|
}
|
|
|
|
stack.push(value)
|
|
|
|
let stepback = indent
|
|
indent = indent + gap
|
|
|
|
let partial = []
|
|
for (let i = 0; i < value.length; i++) {
|
|
const propertyString = serializeProperty(String(i), value)
|
|
partial.push((propertyString !== undefined) ? propertyString : 'null')
|
|
}
|
|
|
|
let final
|
|
if (partial.length === 0) {
|
|
final = '[]'
|
|
} else {
|
|
if (gap === '') {
|
|
let properties = partial.join(',')
|
|
final = '[' + properties + ']'
|
|
} else {
|
|
let separator = ',\n' + indent
|
|
let properties = partial.join(separator)
|
|
final = '[\n' + indent + properties + ',\n' + stepback + ']'
|
|
}
|
|
}
|
|
|
|
stack.pop()
|
|
indent = stepback
|
|
return final
|
|
}
|
|
}
|