From d94285eee403c8658326775a9def58f260680e21 Mon Sep 17 00:00:00 2001 From: ThreeFish Date: Thu, 30 Apr 2026 20:06:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(dashboard):=20=E4=BC=98=E5=8C=96=20Rece?= =?UTF-8?q?nt=20Active=20Sessions=20=E8=A1=A8=E6=A0=BC=20UI=EF=BC=8C?= =?UTF-8?q?=E6=B6=88=E9=99=A4=E6=BB=9A=E5=8A=A8=E6=9D=A1=E5=B9=B6=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=88=86=E9=A1=B5;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 解析 session_key JSON,分别展示 session_id(首列完整显示)、device_id 与 account_uuid(紧凑副文本) - 采用 table-layout: fixed + colgroup 固定列宽比例,消除底部水平滚动条 - 新增客户端分页(默认 30 行/页,显示总行数与页码),消除右侧垂直滚动条 - 后端 API limit 上限从 100 调整到 200,支持前端拉取更多数据 🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang --- src/coding/proxy/server/dashboard.py | 123 +++++++++++++++++++++------ 1 file changed, 99 insertions(+), 24 deletions(-) diff --git a/src/coding/proxy/server/dashboard.py b/src/coding/proxy/server/dashboard.py index 707494e..674c749 100644 --- a/src/coding/proxy/server/dashboard.py +++ b/src/coding/proxy/server/dashboard.py @@ -397,8 +397,8 @@ def _build_favicon() -> bytes: .empty-icon { font-size: 32px; margin-bottom: 8px; opacity: .5; } /* ── Sessions Panel ── */ .sessions-card { grid-column: 1 / -1; animation-delay: .1s; } - .session-table-wrap { overflow-x: auto; max-height: 480px; overflow-y: auto; } - .session-table { width: 100%; border-collapse: collapse; font-size: 13px; } + .session-table-wrap { overflow: hidden; } + .session-table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; } .session-table th { position: sticky; top: 0; z-index: 1; background: var(--bg-card); padding: 10px 12px; @@ -406,9 +406,11 @@ def _build_favicon() -> bytes: color: var(--text-secondary); text-transform: uppercase; letter-spacing: .5px; border-bottom: 1px solid var(--border); } - .session-table td { padding: 8px 12px; border-bottom: 1px solid var(--border-subtle); white-space: nowrap; } + .session-table td { padding: 8px 12px; border-bottom: 1px solid var(--border-subtle); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .session-table tr:hover td { background: var(--bg-card-hover); } - .session-key { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent-blue); cursor: default; } + .session-key { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent-blue); cursor: default; white-space: normal; overflow: visible; } + .session-id { line-height: 1.4; word-break: break-all; } + .session-meta { font-size: 10px; color: var(--text-tertiary); line-height: 1.2; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .session-tag { display: inline-block; font-size: 11px; padding: 2px 7px; border-radius: 8px; margin: 1px 2px; @@ -430,6 +432,21 @@ def _build_favicon() -> bytes: .bind-select:hover { border-color: rgba(88,166,255,.4); color: var(--text-primary); } .bind-select:focus { border-color: rgba(88,166,255,.6); box-shadow: 0 0 0 2px rgba(88,166,255,.1); } .bind-select option { background: var(--bg-card); color: var(--text-primary); } + /* ── 分页 ── */ + .session-pagination { + display: flex; align-items: center; justify-content: space-between; + padding: 10px 12px; border-top: 1px solid var(--border-subtle); + font-size: 12px; color: var(--text-secondary); + } + .page-btn { + padding: 4px 10px; border-radius: 6px; + background: rgba(48,54,61,.4); border: 1px solid rgba(255,255,255,.08); + color: var(--text-secondary); font-size: 12px; cursor: pointer; + transition: all .15s ease; + } + .page-btn:hover:not(:disabled) { background: var(--bg-card-hover); color: var(--text-primary); border-color: rgba(88,166,255,.3); } + .page-btn:disabled { opacity: .35; cursor: default; } + .page-info { font-family: 'JetBrains Mono', monospace; font-size: 12px; } /* ── 加载态 ── */ .loading { opacity: .4; pointer-events: none; } /* ── 图表标签截断 ── */ @@ -623,9 +640,21 @@ def _build_favicon() -> bytes:
+ + + + + + + + + + + + - + @@ -641,6 +670,14 @@ def _build_favicon() -> bytes:
SessionSession ID Last Active Requests Tokens
Loading...
+
+ +
+ + + +
+
@@ -1385,6 +1422,11 @@ def _build_favicon() -> bytes: if (!key || key.length <= maxLen) return escapeHtml(key) || '–'; return escapeHtml(key.slice(0, maxLen - 3)) + '…'; } +function parseSessionKey(raw) { + try { var o = JSON.parse(raw); return { device_id: o.device_id||'', account_uuid: o.account_uuid||'', session_id: o.session_id||'' }; } + catch(e) { return { device_id:'', account_uuid:'', session_id: raw || '' }; } +} +function shortId(s, n) { return s ? (s.length <= n ? s : s.slice(0, n) + '…') : ''; } function successBarHtml(pct) { if (pct == null) return '–'; var p = Math.round(pct); @@ -1415,10 +1457,17 @@ def _build_favicon() -> bytes: return '' + formatVendorLabel(v.trim()) + ''; }).join(''); } +// ── Sessions Pagination State ── +var allSessions = []; +var sessionPage = 0; +var sessionPageSize = 30; +var sessionBindMap = {}; +var sessionAvailableVendors = []; + async function updateSessions() { try { var results = await Promise.allSettled([ - fetchJSON('/api/dashboard/sessions?hours=24&limit=20'), + fetchJSON('/api/dashboard/sessions?hours=24&limit=200'), fetchJSON('/api/session-vendor'), fetchJSON('/api/status'), ]); @@ -1426,24 +1475,41 @@ def _build_favicon() -> bytes: var data = results[0].value; var bindData = results[1].status === 'fulfilled' ? results[1].value : {bindings: []}; var statusData = results[2].status === 'fulfilled' ? results[2].value : {tiers: []}; - var sessions = data.sessions || []; - var bindings = bindData.bindings || []; - var availableVendors = (statusData.tiers || []).map(function(t) { return t.name; }); - var tbody = document.getElementById('sessions-tbody'); + allSessions = data.sessions || []; + sessionBindMap = {}; + (bindData.bindings || []).forEach(function(b) { sessionBindMap[b.session_key] = b.vendors; }); + sessionAvailableVendors = (statusData.tiers || []).map(function(t) { return t.name; }); var subtitle = document.getElementById('sessions-subtitle'); if (subtitle) subtitle.textContent = 'Last ' + data.hours + 'h'; - if (!sessions.length) { - tbody.innerHTML = '
📭
No session data'; - return; - } - // Build binding lookup: session_key → vendors list - var bindMap = {}; - bindings.forEach(function(b) { bindMap[b.session_key] = b.vendors; }); - tbody.innerHTML = sessions.map(function(s) { - var boundVendors = bindMap[s.session_key]; - var selectHtml = buildBindSelect(s.session_key, boundVendors, availableVendors); + sessionPage = 0; + renderSessionPage(); + } catch (e) { + console.error('Sessions refresh error:', e); + } +} + +function renderSessionPage() { + var total = allSessions.length; + var totalPages = Math.max(1, Math.ceil(total / sessionPageSize)); + if (sessionPage >= totalPages) sessionPage = totalPages - 1; + var start = sessionPage * sessionPageSize; + var page = allSessions.slice(start, start + sessionPageSize); + var tbody = document.getElementById('sessions-tbody'); + + if (!total) { + tbody.innerHTML = '
📭
No session data'; + } else { + tbody.innerHTML = page.map(function(s) { + var parsed = parseSessionKey(s.session_key); + var boundVendors = sessionBindMap[s.session_key]; + var selectHtml = buildBindSelect(s.session_key, boundVendors, sessionAvailableVendors); return '' + - '' + truncateKey(s.session_key, 22) + '' + + '' + + '
' + escapeHtml(parsed.session_id || s.session_key) + '
' + + '
' + + 'dev:' + escapeHtml(shortId(parsed.device_id, 8)) + ' · acct:' + escapeHtml(shortId(parsed.account_uuid, 8)) + + '
' + + '' + '' + relativeTime(s.last_active_ts) + '' + '' + fmtNum(s.total_requests) + '' + '' + fmtTokens(s.total_tokens) + '' + @@ -1455,9 +1521,18 @@ def _build_favicon() -> bytes: '' + formatCategories(s.client_categories) + '' + ''; }).join(''); - } catch (e) { - console.error('Sessions refresh error:', e); } + + document.getElementById('page-info').textContent = total + ' sessions'; + document.getElementById('page-num').textContent = (sessionPage + 1) + ' / ' + totalPages; + document.getElementById('btn-prev').disabled = (sessionPage === 0); + document.getElementById('btn-next').disabled = (sessionPage >= totalPages - 1); +} + +function changePage(delta) { + var totalPages = Math.max(1, Math.ceil(allSessions.length / sessionPageSize)); + sessionPage = Math.max(0, Math.min(totalPages - 1, sessionPage + delta)); + renderSessionPage(); } function buildBindSelect(sessionKey, boundVendors, availableVendors) { @@ -1807,7 +1882,7 @@ async def dashboard_sessions( media_type="application/json", ) hours = max(1.0, min(hours, 168.0)) - limit = max(1, min(limit, 100)) + limit = max(1, min(limit, 200)) try: sessions = await token_logger.query_recent_sessions( limit=limit, hours=hours From fe8b2d0f78198faa4f837a0ca579be19b7b10c34 Mon Sep 17 00:00:00 2001 From: ThreeFish Date: Thu, 30 Apr 2026 21:03:14 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(dashboard):=20=E6=8F=90=E5=8D=87=20sess?= =?UTF-8?q?ion-key=20=E6=A0=B7=E5=BC=8F=E9=80=89=E6=8B=A9=E5=99=A8?= =?UTF-8?q?=E4=BC=98=E5=85=88=E7=BA=A7=EF=BC=8C=E7=A1=AE=E4=BF=9D=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E9=80=9A=E7=94=A8=20td=20=E8=A7=84=E5=88=99;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang --- src/coding/proxy/server/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coding/proxy/server/dashboard.py b/src/coding/proxy/server/dashboard.py index 674c749..d4647bb 100644 --- a/src/coding/proxy/server/dashboard.py +++ b/src/coding/proxy/server/dashboard.py @@ -408,7 +408,7 @@ def _build_favicon() -> bytes: } .session-table td { padding: 8px 12px; border-bottom: 1px solid var(--border-subtle); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .session-table tr:hover td { background: var(--bg-card-hover); } - .session-key { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent-blue); cursor: default; white-space: normal; overflow: visible; } + .session-table .session-key { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent-blue); cursor: default; white-space: normal; overflow: visible; } .session-id { line-height: 1.4; word-break: break-all; } .session-meta { font-size: 10px; color: var(--text-tertiary); line-height: 1.2; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .session-tag {