# Scoring Engine Вычисляет score (0..100) для каждой ноды. Ноды со score = -100 исключаются из роутинга. ## Lethal Conditions (score → -100) Если хотя бы одно условие истинно — нода исключается: If at least one is true — node is excluded from routing. | Condition | Threshold | |-----------|-----------| | `status != "ok"` | Any status other than `ok` | | Stale | > `stale_threshold_sec` (default 20s) | | `active_calls >= max_calls` | 100% capacity used | | `idle_cpu < idle_cpu_min` | < 5% | | `call_failure_rate > call_failure_rate_lethal` | > 15% | | `disabled == true` | Manual admin disable | ## Weighted Score (0..100) ``` call_score = clamp(100 - (active_calls / max_calls * 100), 0, 100) load_score = clamp(100 - (load_avg * 50), 0, 100) idle_score = clamp(idle_cpu, 0, 100) fail_score = clamp(100 - call_failure_rate, 0, 100) score = call_score × 0.40 + load_score × 0.30 + idle_score × 0.20 + fail_score × 0.10 ``` Weight defaults (configurable in `config.json`): call 40%, load 30%, idle 20%, fail 10%. ## Example Node `pbx-01`: `active_calls=42`, `max_calls=250`, `load_avg=0.85`, `idle_cpu=35`, `fail_rate=0.5` ``` call_score = clamp(100 - (42/250)*100, 0, 100) = 83.2 load_score = clamp(100 - 0.85*50, 0, 100) = 57.5 idle_score = clamp(35, 0, 100) = 35.0 fail_score = clamp(100 - 0.5, 0, 100) = 99.5 score = 83.2×0.40 + 57.5×0.30 + 35.0×0.20 + 99.5×0.10 = 70.1 ``` ## Caching `best_node_id`, `best_score`, and `fallback_active` are recalculated on every metric update. `/api/route` reads them under RLock — O(1) constant time. ## Configuration ```json { "stale_threshold_sec": 20, "scoring": { "idle_cpu_min": 5, "call_failure_rate_lethal": 15.0, "weights": { "call_score": 0.40, "load_score": 0.30, "idle_score": 0.20, "fail_score": 0.10 } } } ``` Weights must sum to 1.0. All thresholds configurable in `config.json`.