feat(routing): linked complexity-boundary sliders — your own low/medium/high
The tier boundaries lived on the router and were global for every customer of a hosted router; per-gateway tuning meant hand-editing score ranges. The Routing page now has one track with two draggable handles (the low|medium and medium|high boundaries) over three colored zones: handles push each other (low can't cross medium — it shoves it along, and vice versa), the numbers ride on the handles and the legend updates live, so nobody types ranges by hand. Unchecked = the router's own bands, untouched behavior. Under the hood the gateway re-maps the router's CONTINUOUS complexity score with the user's boundaries ([routing.bands] low/medium in gateway.toml, validated 0 < low < medium < 1) before tier chains, escalation and stats — verified live: with bands at 0.70/0.72 a score-0.27 request that used to be medium routed low, and back after restoring the defaults. 2 new strings in all 7 languages.
This commit is contained in:
@@ -931,6 +931,7 @@ async fn get_routing(State(state): State<SharedState>) -> ApiResult {
|
|||||||
.collect();
|
.collect();
|
||||||
ok(json!({
|
ok(json!({
|
||||||
"tiers": tiers_to_map(&live.cfg.routing),
|
"tiers": tiers_to_map(&live.cfg.routing),
|
||||||
|
"bands": live.cfg.routing.bands,
|
||||||
"tasks": tasks,
|
"tasks": tasks,
|
||||||
"default": live.cfg.routing.default,
|
"default": live.cfg.routing.default,
|
||||||
"policy": live.cfg.routing.policy,
|
"policy": live.cfg.routing.policy,
|
||||||
@@ -978,6 +979,8 @@ struct RoutingBody {
|
|||||||
tiers: HashMap<String, Vec<String>>,
|
tiers: HashMap<String, Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
tasks: HashMap<String, TierTargets>,
|
tasks: HashMap<String, TierTargets>,
|
||||||
|
#[serde(default)]
|
||||||
|
bands: Option<crate::config::TierBands>,
|
||||||
default: String,
|
default: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
policy: RoutingPolicy,
|
policy: RoutingPolicy,
|
||||||
@@ -997,6 +1000,7 @@ async fn put_routing(State(state): State<SharedState>, Json(body): Json<RoutingB
|
|||||||
// empty rule (no models AND no forced tier) = "по сложности"
|
// empty rule (no models AND no forced tier) = "по сложности"
|
||||||
.filter(|(_, v)| !v.models().is_empty())
|
.filter(|(_, v)| !v.models().is_empty())
|
||||||
.collect(),
|
.collect(),
|
||||||
|
bands: body.bands,
|
||||||
default: body.default,
|
default: body.default,
|
||||||
policy: body.policy,
|
policy: body.policy,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -404,6 +404,31 @@ pub struct RoutingCfg {
|
|||||||
/// called "tasks" — serde extracts it before the flattened tier map.
|
/// called "tasks" — serde extracts it before the flattened tier map.
|
||||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
pub tasks: HashMap<String, TierTargets>,
|
pub tasks: HashMap<String, TierTargets>,
|
||||||
|
/// Local complexity bands: re-map the router's continuous score into
|
||||||
|
/// low/medium/high with YOUR OWN boundaries. The hosted router's bands
|
||||||
|
/// are global for all customers; these are per-gateway. `None` = keep
|
||||||
|
/// the router's tier verdict as delivered.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bands: Option<TierBands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Score boundaries: `score ≤ low` → low, `≤ medium` → medium, else high.
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TierBands {
|
||||||
|
pub low: f32,
|
||||||
|
pub medium: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TierBands {
|
||||||
|
pub fn tier_of(&self, score: f32) -> &'static str {
|
||||||
|
if score <= self.low {
|
||||||
|
"low"
|
||||||
|
} else if score <= self.medium {
|
||||||
|
"medium"
|
||||||
|
} else {
|
||||||
|
"high"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A routing tier is either a list of model_ids or the string `default`.
|
/// A routing tier is either a list of model_ids or the string `default`.
|
||||||
@@ -861,6 +886,7 @@ impl Default for Config {
|
|||||||
default: String::new(),
|
default: String::new(),
|
||||||
policy: RoutingPolicy::default(),
|
policy: RoutingPolicy::default(),
|
||||||
tasks: HashMap::new(),
|
tasks: HashMap::new(),
|
||||||
|
bands: None,
|
||||||
},
|
},
|
||||||
breaker: BreakerCfg::default(),
|
breaker: BreakerCfg::default(),
|
||||||
protocols: ProtocolsCfg {
|
protocols: ProtocolsCfg {
|
||||||
@@ -988,6 +1014,15 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(b) = &self.routing.bands {
|
||||||
|
if !(b.low > 0.0 && b.low < b.medium && b.medium < 1.0) {
|
||||||
|
anyhow::bail!(
|
||||||
|
"config: routing.bands must satisfy 0 < low < medium < 1 (got {} / {})",
|
||||||
|
b.low,
|
||||||
|
b.medium
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (task, targets) in &self.routing.tasks {
|
for (task, targets) in &self.routing.tasks {
|
||||||
for id in &targets.models() {
|
for id in &targets.models() {
|
||||||
if !ids.contains(id.as_str()) {
|
if !ids.contains(id.as_str()) {
|
||||||
|
|||||||
@@ -231,6 +231,19 @@ impl Pipeline {
|
|||||||
let prof = profile.as_deref().unwrap_or(&live.cfg.route.profile);
|
let prof = profile.as_deref().unwrap_or(&live.cfg.route.profile);
|
||||||
match live.router.route(&text, prof).await {
|
match live.router.route(&text, prof).await {
|
||||||
Ok(Some(dec)) => {
|
Ok(Some(dec)) => {
|
||||||
|
let mut dec = dec;
|
||||||
|
// per-gateway complexity bands: re-map the
|
||||||
|
// router's continuous score with the user's
|
||||||
|
// own boundaries (sliders on the Routing page)
|
||||||
|
if let Some(b) = &live.cfg.routing.bands {
|
||||||
|
if matches!(
|
||||||
|
dec.complexity_tier.as_str(),
|
||||||
|
"low" | "medium" | "high"
|
||||||
|
) {
|
||||||
|
dec.complexity_tier =
|
||||||
|
b.tier_of(dec.complexity_score).to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
let src = dec.source.clone();
|
let src = dec.source.clone();
|
||||||
let (cands, task_rule) = live
|
let (cands, task_rule) = live
|
||||||
.routing
|
.routing
|
||||||
|
|||||||
Vendored
+17
@@ -597,6 +597,23 @@ body.modal-open { overflow: hidden; }
|
|||||||
.pair-model { min-width: 220px; max-width: 340px; padding: 4px 8px; font-size: 12.5px; }
|
.pair-model { min-width: 220px; max-width: 340px; padding: 4px 8px; font-size: 12.5px; }
|
||||||
.pair-tier { max-width: 150px; padding: 4px 8px; font-size: 12.5px; }
|
.pair-tier { max-width: 150px; padding: 4px 8px; font-size: 12.5px; }
|
||||||
|
|
||||||
|
/* Complexity bands slider */
|
||||||
|
.bands-wrap { margin: 4px 0 16px; }
|
||||||
|
.bands-track { position: relative; height: 14px; border-radius: 8px; background: var(--panel-2, #23252c); margin: 26px 8px 10px; }
|
||||||
|
.bands-track.disabled { opacity: .45; pointer-events: none; }
|
||||||
|
.bands-zone { position: absolute; top: 0; bottom: 0; }
|
||||||
|
.bands-zone.z-low { left: 0; background: color-mix(in srgb, var(--good) 35%, transparent); border-radius: 8px 0 0 8px; }
|
||||||
|
.bands-zone.z-med { background: color-mix(in srgb, var(--warn, #d9a514) 35%, transparent); }
|
||||||
|
.bands-zone.z-high { background: color-mix(in srgb, var(--bad, #e5484d) 35%, transparent); border-radius: 0 8px 8px 0; }
|
||||||
|
.bands-handle { position: absolute; top: -5px; width: 24px; height: 24px; margin-left: -12px; border-radius: 50%;
|
||||||
|
background: var(--fg, #fff); border: 3px solid var(--accent, #6c6cf5); cursor: grab; touch-action: none; z-index: 2; }
|
||||||
|
.bands-handle:active { cursor: grabbing; }
|
||||||
|
.bands-val { position: absolute; top: -24px; left: 50%; transform: translateX(-50%); font-size: 11.5px;
|
||||||
|
font-family: var(--mono, monospace); color: var(--fg); background: var(--panel, #1b1d23); padding: 1px 6px;
|
||||||
|
border-radius: 6px; border: 1px solid var(--border, #333); white-space: nowrap; }
|
||||||
|
.bands-val.raised { top: -46px; }
|
||||||
|
.bands-legend { display: flex; gap: 8px; margin: 6px 8px 0; flex-wrap: wrap; }
|
||||||
|
|
||||||
/* Benchmark view */
|
/* Benchmark view */
|
||||||
@keyframes bm-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
|
@keyframes bm-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
|
||||||
tbody tr:last-child { animation: bm-in .25s ease; }
|
tbody tr:last-child { animation: bm-in .25s ease; }
|
||||||
|
|||||||
Vendored
+2
@@ -629,4 +629,6 @@ export default {
|
|||||||
"routing.tasks.tierHint": "Welches Modell diese Aufgabe bei welcher Komplexität bedient; \"jede\" führt auf allen Stufen. Reihenfolge = Fallback-Reihenfolge. High eskaliert weiterhin über \"jede\"-Einträge.",
|
"routing.tasks.tierHint": "Welches Modell diese Aufgabe bei welcher Komplexität bedient; \"jede\" führt auf allen Stufen. Reihenfolge = Fallback-Reihenfolge. High eskaliert weiterhin über \"jede\"-Einträge.",
|
||||||
"routing.tasks.tierAny": "jede Komplexität",
|
"routing.tasks.tierAny": "jede Komplexität",
|
||||||
"routing.tasks.addModel": "Modell",
|
"routing.tasks.addModel": "Modell",
|
||||||
|
"routing.bands.enable": "Eigene Komplexitätsgrenzen",
|
||||||
|
"routing.bands.hint": "Ziehen Sie die Griffe: wo low endet und high beginnt — die Griffe schieben einander, die Zahlen aktualisieren sich live. Der kontinuierliche Router-Score wird mit IHREN Grenzen neu eingeteilt; ohne Häkchen gelten die Grenzen des Routers.",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+2
@@ -622,4 +622,6 @@ export default {
|
|||||||
"routing.tasks.tierHint": "Which model serves this task at which complexity; \"any\" leads at every tier. Order = fallback order. High verdicts still escalate over any-tier pins.",
|
"routing.tasks.tierHint": "Which model serves this task at which complexity; \"any\" leads at every tier. Order = fallback order. High verdicts still escalate over any-tier pins.",
|
||||||
"routing.tasks.tierAny": "any complexity",
|
"routing.tasks.tierAny": "any complexity",
|
||||||
"routing.tasks.addModel": "model",
|
"routing.tasks.addModel": "model",
|
||||||
|
"routing.bands.enable": "My complexity boundaries",
|
||||||
|
"routing.bands.hint": "Drag the handles to set where low ends and where high begins — the handles push each other, the numbers update live. The router's continuous score is re-mapped with YOUR boundaries; unchecked = the router's own bands.",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+2
@@ -629,4 +629,6 @@ export default {
|
|||||||
"routing.tasks.tierHint": "Qué modelo atiende esta tarea en qué complejidad; «cualquiera» lidera en todos los niveles. El orden = orden de respaldo. Un veredicto high sigue escalando sobre las entradas «cualquiera».",
|
"routing.tasks.tierHint": "Qué modelo atiende esta tarea en qué complejidad; «cualquiera» lidera en todos los niveles. El orden = orden de respaldo. Un veredicto high sigue escalando sobre las entradas «cualquiera».",
|
||||||
"routing.tasks.tierAny": "cualquier complejidad",
|
"routing.tasks.tierAny": "cualquier complejidad",
|
||||||
"routing.tasks.addModel": "modelo",
|
"routing.tasks.addModel": "modelo",
|
||||||
|
"routing.bands.enable": "Mis límites de complejidad",
|
||||||
|
"routing.bands.hint": "Arrastra los tiradores: dónde termina low y dónde empieza high — se empujan entre sí y las cifras se actualizan en vivo. La puntuación continua del router se re-mapea con TUS límites; sin marcar = las bandas del propio router.",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+2
@@ -629,4 +629,6 @@ export default {
|
|||||||
"routing.tasks.tierHint": "Quel modèle sert cette tâche à quelle complexité ; « toute » mène à tous les niveaux. L'ordre = ordre de repli. Un verdict high escalade toujours au-dessus des entrées « toute ».",
|
"routing.tasks.tierHint": "Quel modèle sert cette tâche à quelle complexité ; « toute » mène à tous les niveaux. L'ordre = ordre de repli. Un verdict high escalade toujours au-dessus des entrées « toute ».",
|
||||||
"routing.tasks.tierAny": "toute complexité",
|
"routing.tasks.tierAny": "toute complexité",
|
||||||
"routing.tasks.addModel": "modèle",
|
"routing.tasks.addModel": "modèle",
|
||||||
|
"routing.bands.enable": "Mes limites de complexité",
|
||||||
|
"routing.bands.hint": "Faites glisser les poignées : où finit low et où commence high — elles se poussent l'une l'autre, les chiffres se mettent à jour en direct. Le score continu du routeur est re-découpé avec VOS limites ; décoché = les bandes du routeur.",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+2
@@ -621,4 +621,6 @@ export default {
|
|||||||
"routing.tasks.tierHint": "Какая модель обслуживает задачу на какой сложности; «любая» ведёт на всех уровнях. Порядок = порядок фолбэка. Вердикт high по-прежнему эскалирует поверх «любых».",
|
"routing.tasks.tierHint": "Какая модель обслуживает задачу на какой сложности; «любая» ведёт на всех уровнях. Порядок = порядок фолбэка. Вердикт high по-прежнему эскалирует поверх «любых».",
|
||||||
"routing.tasks.tierAny": "любая сложность",
|
"routing.tasks.tierAny": "любая сложность",
|
||||||
"routing.tasks.addModel": "модель",
|
"routing.tasks.addModel": "модель",
|
||||||
|
"routing.bands.enable": "Мои границы сложности",
|
||||||
|
"routing.bands.hint": "Тяните ручки: где кончается low и где начинается high — ручки толкают друг друга, цифры обновляются на лету. Непрерывный score роутера пересчитывается ВАШИМИ границами; галочка снята — действуют пороги самого роутера.",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+2
@@ -629,4 +629,6 @@ export default {
|
|||||||
"routing.tasks.tierHint": "Bu görevi hangi karmaşıklıkta hangi model karşılar; \"her\" tüm katmanlarda önde gider. Sıra = yedekleme sırası. High kararı \"her\" girdilerinin üzerine yine tırmanır.",
|
"routing.tasks.tierHint": "Bu görevi hangi karmaşıklıkta hangi model karşılar; \"her\" tüm katmanlarda önde gider. Sıra = yedekleme sırası. High kararı \"her\" girdilerinin üzerine yine tırmanır.",
|
||||||
"routing.tasks.tierAny": "her karmaşıklık",
|
"routing.tasks.tierAny": "her karmaşıklık",
|
||||||
"routing.tasks.addModel": "model",
|
"routing.tasks.addModel": "model",
|
||||||
|
"routing.bands.enable": "Kendi karmaşıklık sınırlarım",
|
||||||
|
"routing.bands.hint": "Tutamaçları sürükleyin: low nerede biter, high nerede başlar — tutamaçlar birbirini iter, sayılar anlık güncellenir. Yönlendiricinin sürekli puanı SİZİN sınırlarınızla yeniden bölümlenir; işaret kaldırılırsa yönlendiricinin kendi bantları geçerli.",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+2
@@ -629,4 +629,6 @@ export default {
|
|||||||
"routing.tasks.tierHint": "哪个模型在哪个复杂度上处理该任务;「任意」在所有层级领先。顺序即回退顺序。high 判定仍会越过「任意」条目升级。",
|
"routing.tasks.tierHint": "哪个模型在哪个复杂度上处理该任务;「任意」在所有层级领先。顺序即回退顺序。high 判定仍会越过「任意」条目升级。",
|
||||||
"routing.tasks.tierAny": "任意复杂度",
|
"routing.tasks.tierAny": "任意复杂度",
|
||||||
"routing.tasks.addModel": "模型",
|
"routing.tasks.addModel": "模型",
|
||||||
|
"routing.bands.enable": "自定义复杂度边界",
|
||||||
|
"routing.bands.hint": "拖动手柄设定 low 到哪里结束、high 从哪里开始 — 手柄会相互推动,数字实时更新。路由器的连续得分将按你的边界重新划分;取消勾选则使用路由器自身的边界。",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+89
-1
@@ -71,6 +71,7 @@ export async function renderRouting() {
|
|||||||
const st = {
|
const st = {
|
||||||
tiers,
|
tiers,
|
||||||
taskPairs,
|
taskPairs,
|
||||||
|
bands: routing.bands || null, // null = пороги роутера как есть
|
||||||
default: routing.default || ids[0] || "",
|
default: routing.default || ids[0] || "",
|
||||||
policy: routing.policy || { mode: "fixed_table", min_class: {} },
|
policy: routing.policy || { mode: "fixed_table", min_class: {} },
|
||||||
};
|
};
|
||||||
@@ -120,6 +121,92 @@ export async function renderRouting() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Linked boundary sliders: one track, two handles (low|medium boundaries),
|
||||||
|
// three colored zones. Handles push each other, numbers update live —
|
||||||
|
// nobody types score ranges by hand.
|
||||||
|
function bandsCard() {
|
||||||
|
const DEF = { low: 0.33, medium: 0.48 };
|
||||||
|
const enabled = !!st.bands;
|
||||||
|
const b = st.bands || { ...DEF };
|
||||||
|
const MIN_GAP = 0.02;
|
||||||
|
|
||||||
|
const track = h("div", { class: "bands-track" });
|
||||||
|
const zoneLow = h("div", { class: "bands-zone z-low" });
|
||||||
|
const zoneMed = h("div", { class: "bands-zone z-med" });
|
||||||
|
const zoneHigh = h("div", { class: "bands-zone z-high" });
|
||||||
|
const hLow = h("div", { class: "bands-handle", tabindex: "0" }, h("span", { class: "bands-val" }));
|
||||||
|
const hMed = h("div", { class: "bands-handle", tabindex: "0" }, h("span", { class: "bands-val" }));
|
||||||
|
const legend = h("div", { class: "bands-legend" });
|
||||||
|
track.append(zoneLow, zoneMed, zoneHigh, hLow, hMed);
|
||||||
|
|
||||||
|
function paint() {
|
||||||
|
const lo = b.low, me = b.medium;
|
||||||
|
zoneLow.style.width = lo * 100 + "%";
|
||||||
|
zoneMed.style.left = lo * 100 + "%";
|
||||||
|
zoneMed.style.width = (me - lo) * 100 + "%";
|
||||||
|
zoneHigh.style.left = me * 100 + "%";
|
||||||
|
zoneHigh.style.width = (1 - me) * 100 + "%";
|
||||||
|
hLow.style.left = lo * 100 + "%";
|
||||||
|
hMed.style.left = me * 100 + "%";
|
||||||
|
hLow.querySelector(".bands-val").textContent = lo.toFixed(2);
|
||||||
|
hMed.querySelector(".bands-val").textContent = me.toFixed(2);
|
||||||
|
// подписи не наезжают, когда ручки рядом
|
||||||
|
hMed.querySelector(".bands-val").classList.toggle("raised", me - lo < 0.09);
|
||||||
|
mount(legend,
|
||||||
|
h("span", { class: "badge tier-low" }, `low ≤ ${lo.toFixed(2)}`),
|
||||||
|
h("span", { class: "badge tier-medium" }, `medium ≤ ${me.toFixed(2)}`),
|
||||||
|
h("span", { class: "badge tier-high" }, `high > ${me.toFixed(2)}`));
|
||||||
|
track.classList.toggle("disabled", !st.bands);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(handle, which) {
|
||||||
|
handle.addEventListener("pointerdown", (e) => {
|
||||||
|
if (!st.bands) return;
|
||||||
|
handle.setPointerCapture(e.pointerId);
|
||||||
|
const rect = track.getBoundingClientRect();
|
||||||
|
const move = (ev) => {
|
||||||
|
let v = (ev.clientX - rect.left) / rect.width;
|
||||||
|
v = Math.min(0.98, Math.max(0.02, Math.round(v * 100) / 100));
|
||||||
|
if (which === "low") {
|
||||||
|
b.low = v;
|
||||||
|
// связанность: low толкает medium перед собой
|
||||||
|
if (b.medium < b.low + MIN_GAP) b.medium = Math.min(0.99, b.low + MIN_GAP);
|
||||||
|
} else {
|
||||||
|
b.medium = v;
|
||||||
|
// и medium толкает low назад
|
||||||
|
if (b.low > b.medium - MIN_GAP) b.low = Math.max(0.01, b.medium - MIN_GAP);
|
||||||
|
}
|
||||||
|
st.bands = { low: b.low, medium: b.medium };
|
||||||
|
paint();
|
||||||
|
};
|
||||||
|
const up = () => {
|
||||||
|
handle.removeEventListener("pointermove", move);
|
||||||
|
handle.removeEventListener("pointerup", up);
|
||||||
|
};
|
||||||
|
handle.addEventListener("pointermove", move);
|
||||||
|
handle.addEventListener("pointerup", up);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
drag(hLow, "low");
|
||||||
|
drag(hMed, "medium");
|
||||||
|
|
||||||
|
const toggle = h("input", { type: "checkbox", checked: enabled ? "" : null });
|
||||||
|
toggle.checked = enabled;
|
||||||
|
toggle.addEventListener("change", () => {
|
||||||
|
st.bands = toggle.checked ? { low: b.low, medium: b.medium } : null;
|
||||||
|
paint();
|
||||||
|
});
|
||||||
|
paint();
|
||||||
|
return h(
|
||||||
|
"div",
|
||||||
|
{ class: "bands-wrap" },
|
||||||
|
h("label", { class: "check", style: "margin-bottom:8px" }, toggle, t("routing.bands.enable")),
|
||||||
|
h("div", { class: "muted small", style: "margin-bottom:10px" }, t("routing.bands.hint")),
|
||||||
|
track,
|
||||||
|
legend
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function tierRow(tier) {
|
function tierRow(tier) {
|
||||||
return h(
|
return h(
|
||||||
"div",
|
"div",
|
||||||
@@ -257,7 +344,7 @@ export async function renderRouting() {
|
|||||||
}
|
}
|
||||||
if (Object.keys(rule).length) tasks[k] = rule;
|
if (Object.keys(rule).length) tasks[k] = rule;
|
||||||
}
|
}
|
||||||
await api.putRouting({ tiers: st.tiers, tasks, default: st.default, policy: st.policy });
|
await api.putRouting({ tiers: st.tiers, tasks, bands: st.bands, default: st.default, policy: st.policy });
|
||||||
toast(t("toast.saved"), "good");
|
toast(t("toast.saved"), "good");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(String(e.message || e), "bad");
|
toast(String(e.message || e), "bad");
|
||||||
@@ -282,6 +369,7 @@ export async function renderRouting() {
|
|||||||
"div",
|
"div",
|
||||||
{ class: "card" },
|
{ class: "card" },
|
||||||
h("div", { class: "card-head" }, h("h3", {}, t("routing.tiers")), h("span", { class: "card-head sub" }, t("routing.tierHint"))),
|
h("div", { class: "card-head" }, h("h3", {}, t("routing.tiers")), h("span", { class: "card-head sub" }, t("routing.tierHint"))),
|
||||||
|
bandsCard(),
|
||||||
...Object.keys(st.tiers).map(tierRow)
|
...Object.keys(st.tiers).map(tierRow)
|
||||||
),
|
),
|
||||||
tasksCard(),
|
tasksCard(),
|
||||||
|
|||||||
Reference in New Issue
Block a user