diff --git a/docs/routes.md b/docs/routes.md index 6c794ef..a252505 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -17,10 +17,10 @@ Each route carries these knobs: | Field | Default | Description | | --- | --- | --- | | `match_width` | `1` | Path-hash prefix width in bytes (1/2/3). Higher widths disambiguate nodes that share a short public-key prefix. | -| `window_hours` | `24` | Rolling lookback window for the live status card. | -| `packet_count_threshold` | `3` | Distinct matching packets at/above which the route is `healthy`. "Distinct" is per underlying event, not per transmission — see [How health is evaluated](#how-health-is-evaluated) above. | -| `clear_threshold` | _(2× threshold)_ | Comfort bar for the `clear`/`marginal` split. Omit/null to use twice the threshold. | -| `max_hop_span` | _(unlimited)_ | Caps the position gap between the first and last matched node, to reject matches that wander too far. | +| `window_hours` | `48` | Rolling lookback window for the live status card. | +| `packet_count_threshold` | `5` | Distinct matching packets at/above which the route is `healthy`. "Distinct" is per underlying event, not per transmission — see [How health is evaluated](#how-health-is-evaluated) above. | +| `clear_threshold` | _(3× threshold)_ | Comfort bar for the `clear`/`marginal` split. Omit/null to use three times the threshold. | +| `max_hop_span` | `8` | Caps the position gap between the first and last matched node, to reject matches that wander too far. | | `reversible` | `true` | Also match the path in reverse direction. | | `enabled` | `true` | When `false`, the route is skipped by the evaluator and reports `unknown`/`no_coverage`. | diff --git a/docs/seeding.md b/docs/seeding.md index 2127bc7..72bbf46 100644 --- a/docs/seeding.md +++ b/docs/seeding.md @@ -126,9 +126,9 @@ routes: description: A140 corridor route visibility: community match_width: 1 - window_hours: 24 - packet_count_threshold: 3 - # clear_threshold: 10 # optional; omit/null = 2x threshold + window_hours: 48 + packet_count_threshold: 5 + # clear_threshold: 15 # optional; omit/null = 3x threshold # max_hop_span: 8 # optional; omit/null = unlimited enabled: true reversible: true # match both directions (A->B and B->A) diff --git a/src/meshcore_hub/api/metrics.py b/src/meshcore_hub/api/metrics.py index aa04912..927558b 100644 --- a/src/meshcore_hub/api/metrics.py +++ b/src/meshcore_hub/api/metrics.py @@ -348,7 +348,7 @@ def collect_metrics(session: Any) -> bytes: ) route_clear = Gauge( "meshcore_route_clear_threshold", - "Effective clear threshold (2x threshold when unset)", + "Effective clear threshold (3x threshold when unset)", ["route"], registry=registry, ) diff --git a/src/meshcore_hub/collector/cli.py b/src/meshcore_hub/collector/cli.py index 8e78cc0..08b2474 100644 --- a/src/meshcore_hub/collector/cli.py +++ b/src/meshcore_hub/collector/cli.py @@ -806,7 +806,7 @@ def _import_routes( route.match_width = match_width route.window_hours = value.get("window_hours", 48) route.packet_count_threshold = value.get( - "packet_count_threshold", 3 + "packet_count_threshold", 5 ) route.clear_threshold = value.get("clear_threshold") route.max_hop_span = value.get("max_hop_span", 8) @@ -827,7 +827,7 @@ def _import_routes( visibility=visibility, match_width=match_width, window_hours=value.get("window_hours", 48), - packet_count_threshold=value.get("packet_count_threshold", 3), + packet_count_threshold=value.get("packet_count_threshold", 5), clear_threshold=value.get("clear_threshold"), max_hop_span=value.get("max_hop_span", 8), enabled=value.get("enabled", True), diff --git a/src/meshcore_hub/collector/routes.py b/src/meshcore_hub/collector/routes.py index cd286b2..6874d22 100644 --- a/src/meshcore_hub/collector/routes.py +++ b/src/meshcore_hub/collector/routes.py @@ -27,8 +27,8 @@ from meshcore_hub.common.models.route_result_history import RouteResultHistory logger = logging.getLogger(__name__) #: Multiplier for the relative default comfort bar (``clear_threshold = None`` -#: means ``effective_clear = 2 × packet_count_threshold``). -CLEAR_DEFAULT_MULTIPLIER = 2 +#: means ``effective_clear = 3 × packet_count_threshold``). +CLEAR_DEFAULT_MULTIPLIER = 3 #: Cap on candidate receptions for preview to bound work per call. PREVIEW_CANDIDATE_CAP = 5000 @@ -54,7 +54,7 @@ def derive_expected_hash(public_key: str, match_width: int) -> str: def effective_clear_threshold(route: Route) -> int: - """The effective comfort bar: explicit value or ``2 × threshold``.""" + """The effective comfort bar: explicit value or ``3 × threshold``.""" return route.clear_threshold or ( route.packet_count_threshold * CLEAR_DEFAULT_MULTIPLIER ) @@ -1239,7 +1239,7 @@ def preview_route( match_width: int = config.get("match_width") or 1 observer_ids: Optional[list[str]] = config.get("observer_ids") or None max_hop_span: Optional[int] = config.get("max_hop_span") - threshold: int = config.get("packet_count_threshold") or 3 + threshold: int = config.get("packet_count_threshold") or 5 clear_bar: Optional[int] = config.get("clear_threshold") reversible: bool = config.get("reversible", True) diff --git a/src/meshcore_hub/common/models/route.py b/src/meshcore_hub/common/models/route.py index cf5c0a0..e44468a 100644 --- a/src/meshcore_hub/common/models/route.py +++ b/src/meshcore_hub/common/models/route.py @@ -41,7 +41,7 @@ class Route(Base, UUIDMixin, TimestampMixin): match_width: Path-hash prefix width in bytes (1/2/3) window_hours: Evaluation lookback window in hours packet_count_threshold: Minimum distinct packets for healthy - clear_threshold: Comfort bar for the clear/marginal split (null = 2x threshold) + clear_threshold: Comfort bar for the clear/marginal split (null = 3x threshold) max_hop_span: Max hops between first and last configured node (null = unlimited) enabled: Whether this route is actively evaluated """ @@ -74,7 +74,7 @@ class Route(Base, UUIDMixin, TimestampMixin): ) packet_count_threshold: Mapped[int] = mapped_column( Integer, - default=3, + default=5, nullable=False, ) clear_threshold: Mapped[Optional[int]] = mapped_column( diff --git a/src/meshcore_hub/common/schemas/routes.py b/src/meshcore_hub/common/schemas/routes.py index d2df380..074a0c9 100644 --- a/src/meshcore_hub/common/schemas/routes.py +++ b/src/meshcore_hub/common/schemas/routes.py @@ -67,10 +67,10 @@ class RouteCreate(BaseModel): default=48, ge=1, le=720, description="Evaluation window in hours" ) packet_count_threshold: int = Field( - default=3, ge=1, le=10000, description="Minimum distinct packets for healthy" + default=5, ge=1, le=10000, description="Minimum distinct packets for healthy" ) clear_threshold: Optional[int] = Field( - default=None, description="Comfort bar (null = 2x threshold)" + default=None, description="Comfort bar (null = 3x threshold)" ) max_hop_span: Optional[int] = Field( default=8, description="Max hop distance between first and last node" @@ -233,7 +233,7 @@ class RoutePreviewRequest(BaseModel): ) match_width: int = Field(default=1, ge=1, le=3) window_hours: int = Field(default=48, ge=1, le=720) - packet_count_threshold: int = Field(default=3, ge=1, le=10000) + packet_count_threshold: int = Field(default=5, ge=1, le=10000) clear_threshold: Optional[int] = None max_hop_span: Optional[int] = Field(default=8) observer_public_keys: Optional[list[str]] = None diff --git a/src/meshcore_hub/web/static/js/spa/pages/routes.js b/src/meshcore_hub/web/static/js/spa/pages/routes.js index f86a128..8ba9c89 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/routes.js +++ b/src/meshcore_hub/web/static/js/spa/pages/routes.js @@ -405,13 +405,13 @@ function renderRouteModal({ modalState, onSave, onCancel, saving }) {