From 2b2bf0fb908ba510cedc21002eeeb95af532daae Mon Sep 17 00:00:00 2001
From: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
Date: Tue, 22 Sep 2026 00:36:47 +0100
Subject: [PATCH] fix(ui): refine provider endpoint controls
---
static/js/admin.js | 82 ++++++++---
static/style.css | 68 ++++++++-
tests/test_endpoint_card_ui_polish.py | 193 ++++++++++++++++++++++++++
3 files changed, 323 insertions(+), 20 deletions(-)
create mode 100644 tests/test_endpoint_card_ui_polish.py
diff --git a/static/js/admin.js b/static/js/admin.js
index e302acfd6..dd00c59d3 100644
--- a/static/js/admin.js
+++ b/static/js/admin.js
@@ -599,6 +599,44 @@ function _ensureEndpointBulkControls() {
_updateEndpointBulkControls();
}
+function shouldDisplayEndpointBaseUrl(ep) {
+ if (!ep || !ep.base_url) return false;
+ if (isChatgptSubscriptionEndpoint(ep)) return false;
+ if (isFeatherlessEndpoint(ep)) return false;
+ return true;
+}
+
+function isFeatherlessEndpoint(ep) {
+ if (!ep) return false;
+ if (ep.provider === 'featherless') return true;
+ const url = String(ep.base_url || '').toLowerCase();
+ try {
+ const host = new URL(url).hostname;
+ return host === 'api.featherless.ai' || host.endsWith('.featherless.ai');
+ } catch (_) {
+ return url.includes('api.featherless.ai');
+ }
+}
+
+function endpointDetailHtml(ep, category) {
+ const showUrl = shouldDisplayEndpointBaseUrl(ep);
+ const copyBtn = (showUrl && category === 'local')
+ ? ``
+ : '';
+ const keyLabel = ep.has_key
+ ? (ep.api_key_fingerprint ? ` (key ${esc(ep.api_key_fingerprint)})` : ' (key set)')
+ : '';
+ const parts = [];
+ if (showUrl) {
+ parts.push(esc(ep.base_url) + copyBtn);
+ if (keyLabel) parts.push(keyLabel);
+ } else if (keyLabel) {
+ parts.push(keyLabel.trim());
+ }
+ if (!parts.length) return '';
+ return `
${parts.join('')}
`;
+}
+
// ChatGPT per-endpoint usage panel expanded state persistence.
// Preserves only endpoint/auth identifiers, never tokens, secrets, or labels.
const CHATGPT_USAGE_EXPANDED_KEY = 'odysseus-chatgpt-usage-expanded';
@@ -690,20 +728,24 @@ async function loadEndpoints() {
// `ep.models` is the *visible* set — when every model is hidden it's
// empty, but we still need to render the expand panel so the user can
// un-hide them. Gate on the total instead.
- const hasModels = ep.online && totalCount > 0;
+ const isChatgptAccount = isChatgptSubscriptionEndpoint(ep);
+ const isFeatherless = isFeatherlessEndpoint(ep);
+ const hasModels = ep.online && (totalCount > 0 || isFeatherless);
+ const countText = (isChatgptAccount || isFeatherless)
+ ? `${visibleCount} models enabled`
+ : `${visibleCount}/${totalCount} models enabled`;
const statusBadge = ep.status === 'empty'
? 'no models'
: ep.online
- ? `${visibleCount}/${totalCount} models enabled`
+ ? `${countText}`
: 'offline';
const justAddedClass = (_recentlyAddedEpId && String(ep.id) === _recentlyAddedEpId) ? ' adm-ep-just-added' : '';
const category = ep.category || (_isLocalEndpoint(ep.base_url) ? 'local' : 'api');
const kindLabel = ep.endpoint_kind && ep.endpoint_kind !== 'auto' ? ep.endpoint_kind.toUpperCase() : '';
- const keyLabel = ep.has_key
- ? (ep.api_key_fingerprint ? ` (key ${esc(ep.api_key_fingerprint)})` : ' (key set)')
- : '';
- const isChatgptAccount = isChatgptSubscriptionEndpoint(ep);
const isUsageExpanded = isChatgptAccount && _isChatgptUsageExpanded(ep.id, ep.provider_auth_id);
+ const epTitle = isChatgptAccount
+ ? chatgptAccountTitle(ep)
+ : (isFeatherless && (!ep.name || ep.name === 'api.featherless.ai') ? 'Featherless.ai' : ep.name);
return `
@@ -713,7 +755,7 @@ async function loadEndpoints() {
` : ''}
${providerLogoFromUrl(ep.base_url) || ''}
- ${esc(isChatgptAccount ? chatgptAccountTitle(ep) : ep.name)}
+ ${esc(epTitle)}
${ep.model_type === 'image' ? 'Image' : ''}
${kindLabel ? `${esc(kindLabel)}` : ''}
${statusBadge}
@@ -735,7 +777,7 @@ async function loadEndpoints() {
` : ''}
-
${esc(ep.base_url)}${category === 'local' ? `
` : ''}${keyLabel}
+ ${endpointDetailHtml(ep, category)}
${hasModels ? `
` : ''}
`;
});
@@ -994,19 +1036,21 @@ async function loadEndpoints() {
const mode = ['none', 'compact', 'full'].includes(String(m.tool_mode || '').toLowerCase())
? String(m.tool_mode).toLowerCase()
: '';
- return `
-
';
@@ -3765,5 +3809,7 @@ export function close() {
settingsModule.close();
}
+export { shouldDisplayEndpointBaseUrl, isFeatherlessEndpoint, endpointDetailHtml };
+
const adminModule = { open, close, _initData, get _initialized() { return initialized; } };
export default adminModule;
diff --git a/static/style.css b/static/style.css
index 14f77835f..7c0ff6016 100644
--- a/static/style.css
+++ b/static/style.css
@@ -17954,6 +17954,69 @@ body:has(.doc-version-panel:not(.hidden)) .hamburger-btn {
transform-origin: center;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
+.adm-model-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 28px;
+ padding: 2px 4px;
+ box-sizing: border-box;
+ width: 100%;
+}
+.adm-model-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1 1 auto;
+ min-width: 0;
+ cursor: pointer;
+}
+.adm-model-name {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+.adm-model-tools-col {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ flex: 0 0 auto;
+ margin-left: auto;
+}
+.adm-model-tools-label {
+ font-size: 10px;
+ opacity: 0.45;
+ flex-shrink: 0;
+ user-select: none;
+}
+.adm-model-tool-mode,
+.admin-tools-select {
+ height: 24px;
+ min-height: 24px;
+ line-height: 22px;
+ font-size: 11px;
+ padding: 0 18px 0 6px;
+ width: 124px;
+ max-width: 130px;
+ flex-shrink: 0;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ background-color: var(--select-bg);
+ color: var(--select-fg);
+ box-sizing: border-box;
+ vertical-align: middle;
+}
+@media (max-width: 480px) {
+ .adm-model-tool-mode,
+ .admin-tools-select {
+ width: 105px;
+ max-width: 110px;
+ font-size: 10px;
+ padding: 0 14px 0 4px;
+ }
+}
.adm-model-row:hover .adm-check-dot {
border-color: var(--accent, var(--red));
transform: scale(1.15);
@@ -49631,9 +49694,10 @@ body.theme-frosted .modal {
/* ChatGPT Subscription per-account usage card (Settings → Added Models) */
.adm-chatgpt-controls {
display: flex;
+ justify-content: flex-end;
gap: 6px;
align-items: center;
- margin: 4px 0 4px 22px;
+ margin: 4px 0 2px auto;
}
.adm-chatgpt-usage-chevron {
font-size: 10px;
@@ -49642,7 +49706,7 @@ body.theme-frosted .modal {
margin-left: 2px;
}
.adm-chatgpt-usage-host {
- margin: 4px 0 6px 22px;
+ margin: 4px 0 6px 0;
}
.adm-chatgpt-usage {
display: flex;
diff --git a/tests/test_endpoint_card_ui_polish.py b/tests/test_endpoint_card_ui_polish.py
new file mode 100644
index 000000000..afd160b25
--- /dev/null
+++ b/tests/test_endpoint_card_ui_polish.py
@@ -0,0 +1,193 @@
+"""Tests for Part A — Endpoint Card UI Polish and Model Tools Selector Layout."""
+
+import json
+import shutil
+import subprocess
+from pathlib import Path
+import pytest
+
+_REPO = Path(__file__).resolve().parent.parent
+_ADMIN_JS = _REPO / "static" / "js" / "admin.js"
+_ADMIN = _ADMIN_JS.read_text(encoding="utf-8")
+_STYLE = (_REPO / "static" / "style.css").read_text(encoding="utf-8")
+pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node not on PATH")
+
+
+def _run_node(script: str):
+ proc = subprocess.run(
+ ["node", "--input-type=module"],
+ input=script,
+ capture_output=True,
+ text=True,
+ cwd=str(_REPO),
+ timeout=30,
+ )
+ assert proc.returncode == 0, proc.stderr
+ return json.loads(proc.stdout.strip())
+
+
+class TestEndpointCardUrlPresentation:
+ def test_should_display_endpoint_base_url_policy(self):
+ js = f"""
+ import fs from 'node:fs';
+ import {{ isChatgptSubscriptionEndpoint }} from '{(_REPO / 'static' / 'js' / 'chatgptSubscriptionUsage.js').as_posix()}';
+ const source = fs.readFileSync('{_ADMIN_JS.as_posix()}', 'utf8');
+ const fnStart = source.indexOf('function shouldDisplayEndpointBaseUrl');
+ const fnEnd = source.indexOf('// ChatGPT per-endpoint usage panel', fnStart);
+ const fnCode = source.slice(fnStart, fnEnd);
+ const fns = new Function('isChatgptSubscriptionEndpoint', 'esc',
+ fnCode + '; return {{ shouldDisplayEndpointBaseUrl, isFeatherlessEndpoint, endpointDetailHtml }};'
+ )(isChatgptSubscriptionEndpoint, x => String(x));
+
+ const results = {{
+ chatgpt: fns.shouldDisplayEndpointBaseUrl({{
+ base_url: 'https://chatgpt.com/backend-api/codex',
+ provider: 'chatgpt-subscription',
+ provider_auth_id: 'auth-123'
+ }}),
+ featherless: fns.shouldDisplayEndpointBaseUrl({{
+ base_url: 'https://api.featherless.ai/v1',
+ name: 'Featherless.ai'
+ }}),
+ featherless_subdomain: fns.shouldDisplayEndpointBaseUrl({{
+ base_url: 'https://eu.featherless.ai/v1'
+ }}),
+ generic: fns.shouldDisplayEndpointBaseUrl({{
+ base_url: 'https://api.example.com/v1',
+ name: 'Custom AI'
+ }}),
+ local: fns.shouldDisplayEndpointBaseUrl({{
+ base_url: 'http://127.0.0.1:11434',
+ name: 'Ollama'
+ }}),
+ null_ep: fns.shouldDisplayEndpointBaseUrl(null)
+ }};
+ console.log(JSON.stringify(results));
+ """
+ out = _run_node(js)
+ assert out["chatgpt"] is False, "ChatGPT raw transport URL must be hidden"
+ assert out["featherless"] is False, "Featherless raw transport URL must be hidden"
+ assert out["featherless_subdomain"] is False, "Featherless domain variants must be hidden"
+ assert out["generic"] is True, "Generic custom endpoint URL must remain visible"
+ assert out["local"] is True, "Local endpoint URL must remain visible"
+ assert out["null_ep"] is False
+
+ def test_endpoint_detail_html_suppresses_first_class_urls_and_keeps_others(self):
+ js = f"""
+ import fs from 'node:fs';
+ import {{ isChatgptSubscriptionEndpoint }} from '{(_REPO / 'static' / 'js' / 'chatgptSubscriptionUsage.js').as_posix()}';
+ const source = fs.readFileSync('{_ADMIN_JS.as_posix()}', 'utf8');
+ const fnStart = source.indexOf('function shouldDisplayEndpointBaseUrl');
+ const fnEnd = source.indexOf('// ChatGPT per-endpoint usage panel', fnStart);
+ const fnCode = source.slice(fnStart, fnEnd);
+ const fns = new Function('isChatgptSubscriptionEndpoint', 'esc',
+ fnCode + '; return {{ shouldDisplayEndpointBaseUrl, isFeatherlessEndpoint, endpointDetailHtml }};'
+ )(isChatgptSubscriptionEndpoint, x => String(x));
+
+ const chatgptEp = {{
+ base_url: 'https://chatgpt.com/backend-api/codex',
+ provider: 'chatgpt-subscription',
+ provider_auth_id: 'auth-1',
+ has_key: false
+ }};
+ const featherlessEp = {{
+ base_url: 'https://api.featherless.ai/v1',
+ name: 'Featherless.ai',
+ has_key: true,
+ api_key_fingerprint: 'abcd1234'
+ }};
+ const genericEp = {{
+ base_url: 'https://api.custom.com/v1',
+ name: 'Custom',
+ has_key: true,
+ api_key_fingerprint: 'ef5678'
+ }};
+ const localEp = {{
+ base_url: 'http://127.0.0.1:11434',
+ name: 'Local',
+ has_key: false
+ }};
+
+ const results = {{
+ chatgptHtml: fns.endpointDetailHtml(chatgptEp, 'api'),
+ featherlessHtml: fns.endpointDetailHtml(featherlessEp, 'api'),
+ genericHtml: fns.endpointDetailHtml(genericEp, 'api'),
+ localHtml: fns.endpointDetailHtml(localEp, 'local')
+ }};
+ console.log(JSON.stringify(results));
+ """
+ out = _run_node(js)
+ # ChatGPT card suppresses raw base URL and has no key, so no detail line
+ assert "chatgpt.com" not in out["chatgptHtml"]
+ assert out["chatgptHtml"] == ""
+
+ # Featherless suppresses raw base URL but can display key fingerprint if present
+ assert "api.featherless.ai" not in out["featherlessHtml"]
+ assert "abcd1234" in out["featherlessHtml"]
+
+ # Generic custom endpoint retains URL and key
+ assert "https://api.custom.com/v1" in out["genericHtml"]
+ assert "ef5678" in out["genericHtml"]
+
+ # Local endpoint retains URL and copy affordance
+ assert "http://127.0.0.1:11434" in out["localHtml"]
+ assert 'data-adm-copy-url="http://127.0.0.1:11434"' in out["localHtml"]
+ assert "admin-ep-copy-btn" in out["localHtml"]
+
+
+class TestChatGPTCardLayoutAndControls:
+ def test_chatgpt_controls_secondary_action_layout(self):
+ # Verify right-alignment in style.css
+ assert ".adm-chatgpt-controls {" in _STYLE
+ controls_block = _STYLE.split(".adm-chatgpt-controls {")[1].split("}")[0]
+ assert "justify-content: flex-end" in controls_block
+ assert "display: flex" in controls_block
+
+ def test_chatgpt_usage_attributes_and_lazy_fetch_retained(self):
+ assert 'aria-controls="adm-chatgpt-usage-${esc(ep.id)}"' in _ADMIN
+ assert 'aria-expanded="${isUsageExpanded ? \'true\' : \'false\'}"' in _ADMIN
+ assert "data-adm-chatgpt-usage-toggle" in _ADMIN
+ assert "data-adm-chatgpt-reconnect" in _ADMIN
+ assert "adm-chatgpt-usage-host" in _ADMIN
+ # Lazy loading on toggle click
+ assert "_loadChatgptUsage(host, authId, epId)" in _ADMIN
+ assert "_isChatgptUsageExpanded(epId, authId)" in _ADMIN
+
+
+class TestModelRowToolsSelectLayout:
+ def test_css_classes_prevent_clipping_and_guarantee_layout(self):
+ # Audit classes in style.css
+ assert ".adm-model-row {" in _STYLE
+ assert ".adm-model-label {" in _STYLE
+ assert ".adm-model-name {" in _STYLE
+ assert ".adm-model-tools-col {" in _STYLE
+ assert ".adm-model-tool-mode," in _STYLE or ".adm-model-tool-mode {" in _STYLE
+ assert ".admin-tools-select" in _STYLE
+
+ # Verify tool-mode select styling
+ tool_mode_css = _STYLE.split(".adm-model-tool-mode,")[1].split("}")[0]
+ assert "height: 24px" in tool_mode_css
+ assert "line-height: 22px" in tool_mode_css
+ assert "padding: 0 18px 0 6px" in tool_mode_css
+ assert "box-sizing: border-box" in tool_mode_css
+ assert "width: 124px" in tool_mode_css
+ assert "vertical-align: middle" in tool_mode_css
+
+ # Verify right column is fixed / shrink-safe
+ col_css = _STYLE.split(".adm-model-tools-col {")[1].split("}")[0]
+ assert "flex: 0 0 auto" in col_css
+ assert "margin-left: auto" in col_css
+
+ # Verify model name is flexible with ellipsis
+ name_css = _STYLE.split(".adm-model-name {")[1].split("}")[0]
+ assert "overflow: hidden" in name_css
+ assert "text-overflow: ellipsis" in name_css
+ assert "white-space: nowrap" in name_css
+ assert "flex: 1" in name_css
+
+ def test_admin_js_uses_classes_instead_of_inline_clipping_styles(self):
+ assert 'class="adm-model-row"' in _ADMIN
+ assert 'class="adm-model-label"' in _ADMIN
+ assert 'class="adm-model-name"' in _ADMIN
+ assert 'class="adm-model-tools-col"' in _ADMIN
+ assert 'class="adm-model-tool-mode admin-tools-select"' in _ADMIN