Sync cezen-portal/ from live working copy (V1.0 GA polish + search-icon ordering fix)

Brings the ISO-packaged portal copy current with the live cezen-portal/
working directory, which had drifted across the entire V1.0 GA polish
initiative (Phases 1-10) plus this session's topnav icon-order bugfix.

- 52 existing files updated, 2 new pages added (about.html, health.html)
- cgit/cezen-portal/ is now byte-identical to cezen-portal/
- Backend files (rag_ingest.py, cezen_license.py) already in sync;
  main.py intentionally left as-is (cgit is ahead there with
  workstation-category commercial-fit logic not yet backported to the
  live working copy)

Next: rebuild the ISO (bash autoinstall/build-iso.sh) to bake this in.
This commit is contained in:
Jino Jose committed 2026-07-09 11:11:38 +05:30
1 parent 7540da04a8
commit b3eec34158
57 files changed
+2832 -1354

No files matched your search

+62 -53
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Model Router — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=4">
<link rel="stylesheet" href="style.css?v=12">
<style>
.mr-content { max-width:1060px; margin:0 auto; padding:32px 40px 60px; }
@media(max-width:768px){ .mr-content { padding:20px 16px 48px; } }
@@ -80,7 +80,8 @@
/* ── Fallback / cloud ── */
.mr-fallback-row { display:flex; align-items:center; gap:14px; padding:14px 18px; border:1px solid var(--bdr); border-radius:10px; background:var(--bg); margin-bottom:10px; }
.mr-fallback-icon { font-size:20px; }
.mr-fallback-icon { width:20px; height:20px; color:var(--text-secondary); }
.mr-fallback-icon svg { width:100%; height:100%; }
.mr-fallback-info { flex:1; }
.mr-fallback-label { font-size:13px; font-weight:600; color:var(--ink); }
.mr-fallback-sub { font-size:12px; color:var(--lt); margin-top:2px; }
@@ -117,7 +118,8 @@
.mr-flow { display:flex; align-items:center; gap:0; overflow-x:auto; padding:8px 0 16px; }
.mr-flow-node { background:var(--navy2); border:1px solid var(--bdr); border-radius:10px; padding:10px 16px; text-align:center; flex-shrink:0; min-width:90px; }
.mr-flow-node.active { border-color:var(--purple); background:rgba(124,58,237,.06); }
.mr-flow-node-icon { font-size:20px; margin-bottom:4px; }
.mr-flow-node-icon { width:20px; height:20px; margin:0 auto 4px; color:var(--text-secondary); }
.mr-flow-node-icon svg { width:100%; height:100%; }
.mr-flow-node-label { font-size:11px; font-weight:700; color:var(--ink); }
.mr-flow-node-sub { font-size:10px; color:var(--lt); margin-top:2px; }
.mr-flow-arrow { color:var(--xlt); font-size:18px; padding:0 6px; flex-shrink:0; }
@@ -157,6 +159,7 @@
<a href="dashboard.html">Dashboard</a>
<a href="analytics.html">Usage Analytics</a>
<a href="audit.html">Governance & Audit</a>
<a href="health.html">Health Center</a>
<a href="feedback.html">Feedback &amp; Ratings</a>
<span class="nav-drop-cat">MANAGE /</span>
<a href="users.html">Users</a>
@@ -196,7 +199,9 @@
</div>
</div>
</nav>
<a href="notifications.html" style="position:relative">🔔</a>
<a href="notifications.html" class="nav-icon-btn" style="position:relative" aria-label="Notifications">
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg></span>
</a>
<span class="badge" data-brand="tier">Basic Tier</span>
<div id="nav-org-logo" class="nav-org-logo"></div>
</header>
@@ -211,61 +216,61 @@
<!-- Stats -->
<div class="mr-stats" id="stats-row">
<div class="mr-stat"><div class="mr-stat-label">📡 Routes Active</div><div class="mr-stat-val" id="s-active">—</div><div class="mr-stat-sub">of <span id="s-total">—</span> configured</div></div>
<div class="mr-stat"><div class="mr-stat-label">🚀 Requests Routed</div><div class="mr-stat-val" id="s-routed">—</div><div class="mr-stat-sub">last 24 hours</div></div>
<div class="mr-stat"><div class="mr-stat-label">⚡ Avg Latency Saved</div><div class="mr-stat-val" id="s-latency">—</div><div class="mr-stat-sub">vs. always using large model</div></div>
<div class="mr-stat"><div class="mr-stat-label">☁️ Cloud Fallback</div><div class="mr-stat-val" id="s-cloud">—</div><div class="mr-stat-sub" id="s-cloud-sub">—</div></div>
<div class="mr-stat"><div class="mr-stat-label">Routes Active</div><div class="mr-stat-val" id="s-active">—</div><div class="mr-stat-sub">of <span id="s-total">—</span> configured</div></div>
<div class="mr-stat"><div class="mr-stat-label">Requests Routed</div><div class="mr-stat-val" id="s-routed">—</div><div class="mr-stat-sub">last 24 hours</div></div>
<div class="mr-stat"><div class="mr-stat-label">Avg Latency Saved</div><div class="mr-stat-val" id="s-latency">—</div><div class="mr-stat-sub">vs. always using large model</div></div>
<div class="mr-stat"><div class="mr-stat-label">Cloud Fallback</div><div class="mr-stat-val" id="s-cloud">—</div><div class="mr-stat-sub" id="s-cloud-sub">—</div></div>
</div>
<!-- Request flow diagram -->
<div class="mr-card" style="margin-bottom:20px">
<div class="mr-card-header">
<span style="font-size:18px">🔀</span>
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="M15 15 21 21"/><path d="M4 4h5l6 6"/></svg></span>
<div class="mr-card-title">How Routing Works</div>
</div>
<div class="mr-card-body" style="padding:16px 22px">
<div class="mr-flow">
<div class="mr-flow-node active"><div class="mr-flow-node-icon">💬</div><div class="mr-flow-node-label">Request</div><div class="mr-flow-node-sub">User query</div></div>
<div class="mr-flow-arrow">→</div>
<div class="mr-flow-node active"><div class="mr-flow-node-icon">🔀</div><div class="mr-flow-node-label">Router</div><div class="mr-flow-node-sub">Match rules</div></div>
<div class="mr-flow-arrow">→</div>
<div class="mr-flow-node"><div class="mr-flow-node-icon">🖼️</div><div class="mr-flow-node-label">Vision</div><div class="mr-flow-node-sub">Images/docs</div></div>
<div class="mr-flow-node active"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div><div class="mr-flow-node-label">Request</div><div class="mr-flow-node-sub">User query</div></div>
<div class="mr-flow-arrow"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg></span></div>
<div class="mr-flow-node active"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="M15 15 21 21"/><path d="M4 4h5l6 6"/></svg></div><div class="mr-flow-node-label">Router</div><div class="mr-flow-node-sub">Match rules</div></div>
<div class="mr-flow-arrow"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg></span></div>
<div class="mr-flow-node"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="10.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg></div><div class="mr-flow-node-label">Vision</div><div class="mr-flow-node-sub">Images/docs</div></div>
<div class="mr-flow-arrow" style="color:var(--bdr)">·</div>
<div class="mr-flow-node"><div class="mr-flow-node-icon">🧠</div><div class="mr-flow-node-label">Reasoning</div><div class="mr-flow-node-sub">Complex tasks</div></div>
<div class="mr-flow-node"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M9.5 2a3.5 3.5 0 0 0-3.4 2.7A3.5 3.5 0 0 0 3 8.2a3.5 3.5 0 0 0 1.2 2.6A3.5 3.5 0 0 0 4 12a3.5 3.5 0 0 0 2.1 3.2A3.5 3.5 0 0 0 9.5 18H10v4"/><path d="M14.5 2a3.5 3.5 0 0 1 3.4 2.7A3.5 3.5 0 0 1 21 8.2a3.5 3.5 0 0 1-1.2 2.6A3.5 3.5 0 0 1 20 12a3.5 3.5 0 0 1-2.1 3.2A3.5 3.5 0 0 1 14.5 18H14v4"/><path d="M10 10h4"/><path d="M9 14h6"/></svg></div><div class="mr-flow-node-label">Reasoning</div><div class="mr-flow-node-sub">Complex tasks</div></div>
<div class="mr-flow-arrow" style="color:var(--bdr)">·</div>
<div class="mr-flow-node"><div class="mr-flow-node-icon">⚡</div><div class="mr-flow-node-label">Fast</div><div class="mr-flow-node-sub">Simple Q&amp;A</div></div>
<div class="mr-flow-node"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M13 2 3 14h7l-1 8 10-12h-7z"/></svg></div><div class="mr-flow-node-label">Fast</div><div class="mr-flow-node-sub">Simple Q&amp;A</div></div>
<div class="mr-flow-arrow" style="color:var(--bdr)">·</div>
<div class="mr-flow-node"><div class="mr-flow-node-icon">📚</div><div class="mr-flow-node-label">Embedding</div><div class="mr-flow-node-sub">RAG / search</div></div>
<div class="mr-flow-node"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg></div><div class="mr-flow-node-label">Embedding</div><div class="mr-flow-node-sub">RAG / search</div></div>
<div class="mr-flow-arrow" style="color:var(--bdr)">·</div>
<div class="mr-flow-node" style="border-style:dashed;opacity:.6"><div class="mr-flow-node-icon">☁️</div><div class="mr-flow-node-label">Cloud</div><div class="mr-flow-node-sub">Fallback only</div></div>
<div class="mr-flow-node" style="border-style:dashed;opacity:.6"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M7 18a4 4 0 1 1 .8-7.9A5 5 0 0 1 17 9a4 4 0 1 1 0 9Z"/></svg></div><div class="mr-flow-node-label">Cloud</div><div class="mr-flow-node-sub">Fallback only</div></div>
</div>
<p style="font-size:12px;color:var(--lt);margin:0">Rules are evaluated in priority order (top → bottom). The first matching rule wins. Requests that match no rule go to the Default model.</p>
<p style="font-size:12px;color:var(--lt);margin:0">Rules are evaluated in priority order from top to bottom. The first matching rule wins. Requests that match no rule go to the Default model.</p>
</div>
</div>
<!-- Routing rules -->
<div class="mr-card" style="margin-bottom:20px">
<div class="mr-card-header">
<span style="font-size:18px">📋</span>
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 3h6"/><path d="M10 7h4"/><rect x="5" y="5" width="14" height="16" rx="2"/></svg></span>
<div class="mr-card-title">Routing Rules</div>
<span style="font-size:12px;color:var(--lt)">Drag to reorder priority</span>
<button class="mr-btn primary" style="margin-left:8px" onclick="saveRoutes()">💾 Save Rules</button>
<button class="mr-btn primary" style="margin-left:8px" onclick="saveRoutes()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 21h14"/><path d="M19 21V7l-3-3H5v17"/><path d="M9 10h6"/><path d="M9 14h6"/></svg></span><span>Save Rules</span></span></button>
</div>
<div class="mr-card-body">
<div id="routes-list"><!-- injected --></div>
<button class="mr-add-route" onclick="addRoute()">+ Add Routing Rule</button>
<button class="mr-add-route" onclick="addRoute()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span><span>Add Routing Rule</span></span></button>
</div>
</div>
<!-- Fallback & cloud -->
<div class="mr-card" style="margin-bottom:20px">
<div class="mr-card-header">
<span style="font-size:18px">🔩</span>
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01A1.65 1.65 0 0 0 10.5 3.1V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
<div class="mr-card-title">Fallback &amp; Cloud Settings</div>
</div>
<div class="mr-card-body">
<div class="mr-fallback-row">
<div class="mr-fallback-icon">🏠</div>
<div class="mr-fallback-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 11.5 12 4l9 7.5"/><path d="M5 10v10h14V10"/></svg></div>
<div class="mr-fallback-info">
<div class="mr-fallback-label">Default (Local) Model</div>
<div class="mr-fallback-sub">Used when no routing rule matches. Always stays on-premises.</div>
@@ -276,7 +281,7 @@
</div>
<div class="mr-fallback-row">
<div class="mr-fallback-icon">☁️</div>
<div class="mr-fallback-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 18a4 4 0 1 1 .8-7.9A5 5 0 0 1 17 9a4 4 0 1 1 0 9Z"/></svg></div>
<div class="mr-fallback-info">
<div class="mr-fallback-label">Cloud Fallback</div>
<div class="mr-fallback-sub">Route to a cloud provider when local models are overloaded or unavailable. <strong>Disabled by default</strong> — enable only if data privacy policy permits.</div>
@@ -288,7 +293,7 @@
</div>
<div id="cloud-settings" style="display:none;margin-top:12px;padding:16px;background:rgba(217,119,6,.04);border:1px solid rgba(217,119,6,.2);border-radius:10px">
<div style="font-size:12px;font-weight:700;color:#D97706;margin-bottom:12px">⚠️ Cloud fallback will send queries outside your on-premises environment. Ensure this complies with your data privacy policy.</div>
<div style="font-size:12px;font-weight:700;color:#D97706;margin-bottom:12px">Cloud fallback will send queries outside your on-premises environment. Ensure this complies with your data privacy policy.</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">
<div class="mr-field">
<label>Provider</label>
@@ -319,7 +324,7 @@
</div>
<div class="mr-fallback-row" style="margin-top:10px">
<div class="mr-fallback-icon">🔒</div>
<div class="mr-fallback-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></div>
<div class="mr-fallback-info">
<div class="mr-fallback-label">PII Shield</div>
<div class="mr-fallback-sub">Detect and redact personally identifiable information before routing to any cloud provider.</div>
@@ -331,7 +336,7 @@
</div>
<div style="margin-top:14px;display:flex;gap:10px">
<button class="mr-btn primary" onclick="saveFallback()">💾 Save Fallback Settings</button>
<button class="mr-btn primary" onclick="saveFallback()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 21h14"/><path d="M19 21V7l-3-3H5v17"/><path d="M9 10h6"/><path d="M9 14h6"/></svg></span><span>Save Fallback Settings</span></span></button>
</div>
</div>
</div>
@@ -339,7 +344,7 @@
<!-- Live test panel -->
<div class="mr-card">
<div class="mr-card-header">
<span style="font-size:18px">🧪</span>
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 3v5l-5 9a2 2 0 0 0 1.75 3h12.5A2 2 0 0 0 20 17l-5-9V3"/><path d="M9 8h6"/></svg></span>
<div class="mr-card-title">Live Routing Test</div>
<span style="font-size:12px;color:var(--lt)">See which route would be selected for a given input</span>
</div>
@@ -362,7 +367,7 @@
<option value="admin">Admin</option>
</select>
</div>
<button class="mr-btn primary" style="align-self:flex-start" onclick="testRoute()">▶ Test Routing</button>
<button class="mr-btn primary" style="align-self:flex-start" onclick="testRoute()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m8 5 11 7-11 7z"/></svg></span><span>Test Routing</span></span></button>
</div>
<div class="mr-test-result" id="test-result">
@@ -380,6 +385,18 @@ const _API = '/api';
let routes = [];
let models = [];
let routeCounter = 0;
function routerIconSvg(name, sizeClass = 'icon-md') {
const icons = {
route:'<svg viewBox="0 0 24 24"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="M15 15 21 21"/><path d="M4 4h5l6 6"/></svg>',
plus:'<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>',
up:'<svg viewBox="0 0 24 24"><path d="m18 15-6-6-6 6"/></svg>',
down:'<svg viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>',
trash:'<svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg>',
chevronRight:'<svg viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>',
drag:'<svg viewBox="0 0 24 24"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg>'
};
return `<span class="icon-svg ${sizeClass}" aria-hidden="true">${icons[name] || icons.route}</span>`;
}
const CONDITION_TYPES = {
keyword: { label:'Contains keyword', placeholder:'e.g. invoice, contract' },
@@ -437,7 +454,7 @@ function renderRoutes() {
document.getElementById('routes-list').innerHTML = routes.map((r,i) => `
<div class="mr-route ${r.enabled ? 'active-route' : ''}" id="route-${r._id}">
<div class="mr-route-header" onclick="toggleRoute(${r._id})">
<span class="mr-route-drag" title="Drag to reorder">⋮⋮</span>
<span class="mr-route-drag" title="Drag to reorder">${routerIconSvg('drag', 'icon-sm')}</span>
<div class="mr-route-priority">${i+1}</div>
<div class="mr-route-name">${esc(r.name)}</div>
<div class="mr-route-model">${esc(r.model)}</div>
@@ -445,7 +462,7 @@ function renderRoutes() {
<input type="checkbox" ${r.enabled?'checked':''} onchange="toggleEnabled(${r._id},this.checked)">
<div class="mr-route-toggle-track"></div>
</label>
<span class="mr-route-chevron">▶</span>
<span class="mr-route-chevron">${routerIconSvg('chevronRight', 'icon-sm')}</span>
</div>
<div class="mr-route-body">
<div class="mr-field-grid">
@@ -460,17 +477,17 @@ function renderRoutes() {
</select>
</div>
<div class="mr-field">
<label>Max Tokens</label>
<label>Max Tokens <span class="cezen-help" tabindex="0" data-tip="The maximum length of the model's response for queries routed here, in tokens (roughly 3/4 of a word each). Higher allows longer answers but costs more time/VRAM.">?</span></label>
<input type="number" value="${r.max_tokens||2048}" min="128" max="32768" onchange="updateRoute(${r._id},'max_tokens',parseInt(this.value))">
</div>
<div class="mr-field">
<label>Temperature</label>
<label>Temperature <span class="cezen-help" tabindex="0" data-tip="Controls randomness: near 0 gives focused, deterministic answers (good for facts/code); near 1 gives more varied, creative answers (good for brainstorming/writing).">?</span></label>
<input type="number" value="${r.temperature??0.7}" min="0" max="1" step="0.1" onchange="updateRoute(${r._id},'temperature',parseFloat(this.value))">
</div>
</div>
<div class="mr-field" style="margin-bottom:10px">
<label>Match Conditions <span style="color:var(--lt);font-weight:400">— ALL conditions must match (AND logic)</span></label>
<label>Match Conditions <span style="color:var(--lt);font-weight:400">— ALL conditions must match (AND logic)</span> <span class="cezen-help" tabindex="0" data-tip="Routes are checked top to bottom; the first route whose conditions all match wins. Put your most specific routes near the top and a no-conditions catch-all route at the bottom.">?</span></label>
<div class="mr-conditions" id="conds-${r._id}">
${(r.conditions||[]).map((c,ci) => renderCondChip(r._id, c, ci)).join('')}
</div>
@@ -479,7 +496,7 @@ function renderRoutes() {
${Object.entries(CONDITION_TYPES).map(([k,v])=>`<option value="${k}">${v.label}</option>`).join('')}
</select>
<input type="text" id="cond-val-${r._id}" placeholder="value…" style="padding:6px 10px;border:1px solid var(--bdr);border-radius:8px;font-family:inherit;font-size:12px;color:var(--ink);flex:1;min-width:120px;background:var(--navy2)">
<button class="mr-add-cond" onclick="addCondition(${r._id})">+ Add condition</button>
<button class="mr-add-cond" onclick="addCondition(${r._id})"><span class="icon-label">${routerIconSvg('plus', 'icon-sm')}<span>Add condition</span></span></button>
</div>
<div class="mr-field-hint">No conditions = always match (use as catch-all at the bottom of the list)</div>
</div>
@@ -490,9 +507,9 @@ function renderRoutes() {
</div>
<div class="mr-route-actions">
<button class="mr-btn ghost" onclick="moveRoute(${r._id},-1)">↑ Move Up</button>
<button class="mr-btn ghost" onclick="moveRoute(${r._id},1)">↓ Move Down</button>
<button class="mr-btn danger" onclick="deleteRoute(${r._id})">🗑 Delete</button>
<button class="mr-btn ghost" onclick="moveRoute(${r._id},-1)"><span class="icon-label">${routerIconSvg('up', 'icon-sm')}<span>Move Up</span></span></button>
<button class="mr-btn ghost" onclick="moveRoute(${r._id},1)"><span class="icon-label">${routerIconSvg('down', 'icon-sm')}<span>Move Down</span></span></button>
<button class="mr-btn danger" onclick="deleteRoute(${r._id})"><span class="icon-label">${routerIconSvg('trash', 'icon-sm')}<span>Delete</span></span></button>
</div>
</div>
</div>`).join('');
@@ -540,7 +557,7 @@ function toggleEnabled(id, val) {
function addCondition(routeId) {
const type = document.getElementById(`cond-type-${routeId}`)?.value;
const val = document.getElementById(`cond-val-${routeId}`)?.value.trim();
if (!val) { alert('Enter a condition value'); return; }
if (!val) { showToast('Enter a condition value before saving this rule.', 'warn'); return; }
const r = routes.find(r => r._id === routeId);
if (!r) return;
r.conditions.push({ type, value: val });
@@ -615,7 +632,7 @@ function testRoute() {
const contentType = document.getElementById('test-content-type').value;
const userRole = document.getElementById('test-user-role').value;
if (!prompt) { alert('Enter a prompt to test'); return; }
if (!prompt) { showToast('Enter a prompt to test.', 'warn'); return; }
const tokenEst = Math.round(prompt.split(/\s+/).length * 1.3);
let matched = null;
@@ -648,7 +665,7 @@ function testRoute() {
<div class="mr-result-route">
<div class="mr-result-row">
<div class="mr-result-key">Status</div>
<div class="mr-result-val"><span class="mr-result-badge matched">✓ Rule matched</span></div>
<div class="mr-result-val"><span class="mr-result-badge matched">Rule matched</span></div>
</div>
<div class="mr-result-row">
<div class="mr-result-key">Route</div>
@@ -683,7 +700,7 @@ function testRoute() {
<div class="mr-result-route">
<div class="mr-result-row">
<div class="mr-result-key">Status</div>
<div class="mr-result-val"><span class="mr-result-badge default">→ Default model</span></div>
<div class="mr-result-val"><span class="mr-result-badge default">Default model</span></div>
</div>
<div class="mr-result-row">
<div class="mr-result-key">Model</div>
@@ -693,7 +710,7 @@ function testRoute() {
<div class="mr-result-key">Reason</div>
<div class="mr-result-val" style="color:var(--lt)">No active rule matched</div>
</div>
${cloudEnabled ? `<div class="mr-result-row"><div class="mr-result-key">Cloud</div><div class="mr-result-val"><span class="mr-result-badge fallback">☁ Fallback enabled</span></div></div>` : ''}
${cloudEnabled ? `<div class="mr-result-row"><div class="mr-result-key">Cloud</div><div class="mr-result-val"><span class="mr-result-badge fallback">Fallback enabled</span></div></div>` : ''}
<div style="margin-top:10px;font-size:12px;color:var(--lt)">
Add a rule or adjust conditions to route this type of query to a specific model.
</div>
@@ -747,14 +764,6 @@ async function loadModels() {
} catch(_) { models = ['llama3','llama3:70b','llama3:8b','llava','codellama','nomic-embed-text']; }
}
function showToast(msg) {
const t = document.createElement('div');
t.textContent = msg;
t.style.cssText = 'position:fixed;bottom:24px;right:24px;background:#F0FDFA;color:#6D28D9;border:1px solid #99F6E4;border-radius:10px;padding:11px 18px;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,.1)';
document.body.appendChild(t);
setTimeout(()=>t.remove(), 2500);
}
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
@@ -772,6 +781,6 @@ function esc(s) {
</script>
<script src="auth.js"></script>
<script src="branding.js"></script>
<script src="branding.js?v=3"></script>
</body>
</html>