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>
342 lines
9.7 KiB
JavaScript
342 lines
9.7 KiB
JavaScript
'use strict';
|
|
|
|
const path = require('path');
|
|
const scan = require('./scan');
|
|
const parse = require('./parse');
|
|
const utils = require('./utils');
|
|
const constants = require('./constants');
|
|
const isObject = val => val && typeof val === 'object' && !Array.isArray(val);
|
|
|
|
/**
|
|
* Creates a matcher function from one or more glob patterns. The
|
|
* returned function takes a string to match as its first argument,
|
|
* and returns true if the string is a match. The returned matcher
|
|
* function also takes a boolean as the second argument that, when true,
|
|
* returns an object with additional information.
|
|
*
|
|
* ```js
|
|
* const picomatch = require('picomatch');
|
|
* // picomatch(glob[, options]);
|
|
*
|
|
* const isMatch = picomatch('*.!(*a)');
|
|
* console.log(isMatch('a.a')); //=> false
|
|
* console.log(isMatch('a.b')); //=> true
|
|
* ```
|
|
* @name picomatch
|
|
* @param {String|Array} `globs` One or more glob patterns.
|
|
* @param {Object=} `options`
|
|
* @return {Function=} Returns a matcher function.
|
|
* @api public
|
|
*/
|
|
|
|
const picomatch = (glob, options, returnState = false) => {
|
|
if (Array.isArray(glob)) {
|
|
const fns = glob.map(input => picomatch(input, options, returnState));
|
|
const arrayMatcher = str => {
|
|
for (const isMatch of fns) {
|
|
const state = isMatch(str);
|
|
if (state) return state;
|
|
}
|
|
return false;
|
|
};
|
|
return arrayMatcher;
|
|
}
|
|
|
|
const isState = isObject(glob) && glob.tokens && glob.input;
|
|
|
|
if (glob === '' || (typeof glob !== 'string' && !isState)) {
|
|
throw new TypeError('Expected pattern to be a non-empty string');
|
|
}
|
|
|
|
const opts = options || {};
|
|
const posix = utils.isWindows(options);
|
|
const regex = isState
|
|
? picomatch.compileRe(glob, options)
|
|
: picomatch.makeRe(glob, options, false, true);
|
|
|
|
const state = regex.state;
|
|
delete regex.state;
|
|
|
|
let isIgnored = () => false;
|
|
if (opts.ignore) {
|
|
const ignoreOpts = { ...options, ignore: null, onMatch: null, onResult: null };
|
|
isIgnored = picomatch(opts.ignore, ignoreOpts, returnState);
|
|
}
|
|
|
|
const matcher = (input, returnObject = false) => {
|
|
const { isMatch, match, output } = picomatch.test(input, regex, options, { glob, posix });
|
|
const result = { glob, state, regex, posix, input, output, match, isMatch };
|
|
|
|
if (typeof opts.onResult === 'function') {
|
|
opts.onResult(result);
|
|
}
|
|
|
|
if (isMatch === false) {
|
|
result.isMatch = false;
|
|
return returnObject ? result : false;
|
|
}
|
|
|
|
if (isIgnored(input)) {
|
|
if (typeof opts.onIgnore === 'function') {
|
|
opts.onIgnore(result);
|
|
}
|
|
result.isMatch = false;
|
|
return returnObject ? result : false;
|
|
}
|
|
|
|
if (typeof opts.onMatch === 'function') {
|
|
opts.onMatch(result);
|
|
}
|
|
return returnObject ? result : true;
|
|
};
|
|
|
|
if (returnState) {
|
|
matcher.state = state;
|
|
}
|
|
|
|
return matcher;
|
|
};
|
|
|
|
/**
|
|
* Test `input` with the given `regex`. This is used by the main
|
|
* `picomatch()` function to test the input string.
|
|
*
|
|
* ```js
|
|
* const picomatch = require('picomatch');
|
|
* // picomatch.test(input, regex[, options]);
|
|
*
|
|
* console.log(picomatch.test('foo/bar', /^(?:([^/]*?)\/([^/]*?))$/));
|
|
* // { isMatch: true, match: [ 'foo/', 'foo', 'bar' ], output: 'foo/bar' }
|
|
* ```
|
|
* @param {String} `input` String to test.
|
|
* @param {RegExp} `regex`
|
|
* @return {Object} Returns an object with matching info.
|
|
* @api public
|
|
*/
|
|
|
|
picomatch.test = (input, regex, options, { glob, posix } = {}) => {
|
|
if (typeof input !== 'string') {
|
|
throw new TypeError('Expected input to be a string');
|
|
}
|
|
|
|
if (input === '') {
|
|
return { isMatch: false, output: '' };
|
|
}
|
|
|
|
const opts = options || {};
|
|
const format = opts.format || (posix ? utils.toPosixSlashes : null);
|
|
let match = input === glob;
|
|
let output = (match && format) ? format(input) : input;
|
|
|
|
if (match === false) {
|
|
output = format ? format(input) : input;
|
|
match = output === glob;
|
|
}
|
|
|
|
if (match === false || opts.capture === true) {
|
|
if (opts.matchBase === true || opts.basename === true) {
|
|
match = picomatch.matchBase(input, regex, options, posix);
|
|
} else {
|
|
match = regex.exec(output);
|
|
}
|
|
}
|
|
|
|
return { isMatch: Boolean(match), match, output };
|
|
};
|
|
|
|
/**
|
|
* Match the basename of a filepath.
|
|
*
|
|
* ```js
|
|
* const picomatch = require('picomatch');
|
|
* // picomatch.matchBase(input, glob[, options]);
|
|
* console.log(picomatch.matchBase('foo/bar.js', '*.js'); // true
|
|
* ```
|
|
* @param {String} `input` String to test.
|
|
* @param {RegExp|String} `glob` Glob pattern or regex created by [.makeRe](#makeRe).
|
|
* @return {Boolean}
|
|
* @api public
|
|
*/
|
|
|
|
picomatch.matchBase = (input, glob, options, posix = utils.isWindows(options)) => {
|
|
const regex = glob instanceof RegExp ? glob : picomatch.makeRe(glob, options);
|
|
return regex.test(path.basename(input));
|
|
};
|
|
|
|
/**
|
|
* Returns true if **any** of the given glob `patterns` match the specified `string`.
|
|
*
|
|
* ```js
|
|
* const picomatch = require('picomatch');
|
|
* // picomatch.isMatch(string, patterns[, options]);
|
|
*
|
|
* console.log(picomatch.isMatch('a.a', ['b.*', '*.a'])); //=> true
|
|
* console.log(picomatch.isMatch('a.a', 'b.*')); //=> false
|
|
* ```
|
|
* @param {String|Array} str The string to test.
|
|
* @param {String|Array} patterns One or more glob patterns to use for matching.
|
|
* @param {Object} [options] See available [options](#options).
|
|
* @return {Boolean} Returns true if any patterns match `str`
|
|
* @api public
|
|
*/
|
|
|
|
picomatch.isMatch = (str, patterns, options) => picomatch(patterns, options)(str);
|
|
|
|
/**
|
|
* Parse a glob pattern to create the source string for a regular
|
|
* expression.
|
|
*
|
|
* ```js
|
|
* const picomatch = require('picomatch');
|
|
* const result = picomatch.parse(pattern[, options]);
|
|
* ```
|
|
* @param {String} `pattern`
|
|
* @param {Object} `options`
|
|
* @return {Object} Returns an object with useful properties and output to be used as a regex source string.
|
|
* @api public
|
|
*/
|
|
|
|
picomatch.parse = (pattern, options) => {
|
|
if (Array.isArray(pattern)) return pattern.map(p => picomatch.parse(p, options));
|
|
return parse(pattern, { ...options, fastpaths: false });
|
|
};
|
|
|
|
/**
|
|
* Scan a glob pattern to separate the pattern into segments.
|
|
*
|
|
* ```js
|
|
* const picomatch = require('picomatch');
|
|
* // picomatch.scan(input[, options]);
|
|
*
|
|
* const result = picomatch.scan('!./foo/*.js');
|
|
* console.log(result);
|
|
* { prefix: '!./',
|
|
* input: '!./foo/*.js',
|
|
* start: 3,
|
|
* base: 'foo',
|
|
* glob: '*.js',
|
|
* isBrace: false,
|
|
* isBracket: false,
|
|
* isGlob: true,
|
|
* isExtglob: false,
|
|
* isGlobstar: false,
|
|
* negated: true }
|
|
* ```
|
|
* @param {String} `input` Glob pattern to scan.
|
|
* @param {Object} `options`
|
|
* @return {Object} Returns an object with
|
|
* @api public
|
|
*/
|
|
|
|
picomatch.scan = (input, options) => scan(input, options);
|
|
|
|
/**
|
|
* Compile a regular expression from the `state` object returned by the
|
|
* [parse()](#parse) method.
|
|
*
|
|
* @param {Object} `state`
|
|
* @param {Object} `options`
|
|
* @param {Boolean} `returnOutput` Intended for implementors, this argument allows you to return the raw output from the parser.
|
|
* @param {Boolean} `returnState` Adds the state to a `state` property on the returned regex. Useful for implementors and debugging.
|
|
* @return {RegExp}
|
|
* @api public
|
|
*/
|
|
|
|
picomatch.compileRe = (state, options, returnOutput = false, returnState = false) => {
|
|
if (returnOutput === true) {
|
|
return state.output;
|
|
}
|
|
|
|
const opts = options || {};
|
|
const prepend = opts.contains ? '' : '^';
|
|
const append = opts.contains ? '' : '$';
|
|
|
|
let source = `${prepend}(?:${state.output})${append}`;
|
|
if (state && state.negated === true) {
|
|
source = `^(?!${source}).*$`;
|
|
}
|
|
|
|
const regex = picomatch.toRegex(source, options);
|
|
if (returnState === true) {
|
|
regex.state = state;
|
|
}
|
|
|
|
return regex;
|
|
};
|
|
|
|
/**
|
|
* Create a regular expression from a parsed glob pattern.
|
|
*
|
|
* ```js
|
|
* const picomatch = require('picomatch');
|
|
* const state = picomatch.parse('*.js');
|
|
* // picomatch.compileRe(state[, options]);
|
|
*
|
|
* console.log(picomatch.compileRe(state));
|
|
* //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/
|
|
* ```
|
|
* @param {String} `state` The object returned from the `.parse` method.
|
|
* @param {Object} `options`
|
|
* @param {Boolean} `returnOutput` Implementors may use this argument to return the compiled output, instead of a regular expression. This is not exposed on the options to prevent end-users from mutating the result.
|
|
* @param {Boolean} `returnState` Implementors may use this argument to return the state from the parsed glob with the returned regular expression.
|
|
* @return {RegExp} Returns a regex created from the given pattern.
|
|
* @api public
|
|
*/
|
|
|
|
picomatch.makeRe = (input, options = {}, returnOutput = false, returnState = false) => {
|
|
if (!input || typeof input !== 'string') {
|
|
throw new TypeError('Expected a non-empty string');
|
|
}
|
|
|
|
let parsed = { negated: false, fastpaths: true };
|
|
|
|
if (options.fastpaths !== false && (input[0] === '.' || input[0] === '*')) {
|
|
parsed.output = parse.fastpaths(input, options);
|
|
}
|
|
|
|
if (!parsed.output) {
|
|
parsed = parse(input, options);
|
|
}
|
|
|
|
return picomatch.compileRe(parsed, options, returnOutput, returnState);
|
|
};
|
|
|
|
/**
|
|
* Create a regular expression from the given regex source string.
|
|
*
|
|
* ```js
|
|
* const picomatch = require('picomatch');
|
|
* // picomatch.toRegex(source[, options]);
|
|
*
|
|
* const { output } = picomatch.parse('*.js');
|
|
* console.log(picomatch.toRegex(output));
|
|
* //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/
|
|
* ```
|
|
* @param {String} `source` Regular expression source string.
|
|
* @param {Object} `options`
|
|
* @return {RegExp}
|
|
* @api public
|
|
*/
|
|
|
|
picomatch.toRegex = (source, options) => {
|
|
try {
|
|
const opts = options || {};
|
|
return new RegExp(source, opts.flags || (opts.nocase ? 'i' : ''));
|
|
} catch (err) {
|
|
if (options && options.debug === true) throw err;
|
|
return /$^/;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Picomatch constants.
|
|
* @return {Object}
|
|
*/
|
|
|
|
picomatch.constants = constants;
|
|
|
|
/**
|
|
* Expose "picomatch"
|
|
*/
|
|
|
|
module.exports = picomatch;
|