+ x-transition:enter-start="fade-enter-from" class="w-full h-full">
+ class="w-full">
+ class="w-full">
@@ -309,6 +309,42 @@
+
+
+
diff --git a/public/js/components/account-manager.js b/public/js/components/account-manager.js
index cb564b3..fd5c043 100644
--- a/public/js/components/account-manager.js
+++ b/public/js/components/account-manager.js
@@ -5,6 +5,36 @@
window.Components = window.Components || {};
window.Components.accountManager = () => ({
+ searchQuery: '',
+ deleteTarget: '',
+
+ get filteredAccounts() {
+ const accounts = Alpine.store('data').accounts || [];
+ if (!this.searchQuery || this.searchQuery.trim() === '') {
+ return accounts;
+ }
+
+ const query = this.searchQuery.toLowerCase().trim();
+ return accounts.filter(acc => {
+ return acc.email.toLowerCase().includes(query) ||
+ (acc.projectId && acc.projectId.toLowerCase().includes(query)) ||
+ (acc.source && acc.source.toLowerCase().includes(query));
+ });
+ },
+
+ formatEmail(email) {
+ if (!email || email.length <= 40) return email;
+
+ const [user, domain] = email.split('@');
+ if (!domain) return email;
+
+ // Preserve domain integrity, truncate username if needed
+ if (user.length > 20) {
+ return `${user.substring(0, 10)}...${user.slice(-5)}@${domain}`;
+ }
+ return email;
+ },
+
async refreshAccount(email) {
const store = Alpine.store('global');
store.showToast(store.t('refreshingAccount', { email }), 'info');
@@ -88,10 +118,16 @@ window.Components.accountManager = () => ({
}
},
- async deleteAccount(email) {
+ confirmDeleteAccount(email) {
+ this.deleteTarget = email;
+ document.getElementById('delete_account_modal').showModal();
+ },
+
+ async executeDelete() {
+ const email = this.deleteTarget;
const store = Alpine.store('global');
- if (!confirm(store.t('confirmDelete'))) return;
const password = store.webuiPassword;
+
try {
const { response, newPassword } = await window.utils.request(`/api/accounts/${encodeURIComponent(email)}`, { method: 'DELETE' }, password);
if (newPassword) store.webuiPassword = newPassword;
@@ -100,6 +136,8 @@ window.Components.accountManager = () => ({
if (data.status === 'ok') {
store.showToast(store.t('deletedAccount', { email }), 'success');
Alpine.store('data').fetchData();
+ document.getElementById('delete_account_modal').close();
+ this.deleteTarget = '';
} else {
store.showToast(data.error || store.t('deleteFailed'), 'error');
}
diff --git a/public/js/components/dashboard.js b/public/js/components/dashboard.js
index 3ef00df..4a0dc74 100644
--- a/public/js/components/dashboard.js
+++ b/public/js/components/dashboard.js
@@ -464,7 +464,9 @@ window.Components.dashboard = () => ({
createDataset(label, data, color, ctx) {
const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 200);
- gradient.addColorStop(0, this.hexToRgba(color, 0.3));
+ // Reduced opacity from 0.3 to 0.12 for less visual noise
+ gradient.addColorStop(0, this.hexToRgba(color, 0.12));
+ gradient.addColorStop(0.6, this.hexToRgba(color, 0.05));
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
return {
@@ -472,12 +474,14 @@ window.Components.dashboard = () => ({
data,
borderColor: color,
backgroundColor: gradient,
- borderWidth: 2,
- tension: 0.4,
+ borderWidth: 2.5, // Slightly thicker line for better visibility
+ tension: 0.35, // Smoother curves
fill: true,
- pointRadius: 3,
- pointHoverRadius: 5,
- pointBackgroundColor: color
+ pointRadius: 2.5,
+ pointHoverRadius: 6,
+ pointBackgroundColor: color,
+ pointBorderColor: 'rgba(9, 9, 11, 0.8)',
+ pointBorderWidth: 1.5
};
},
diff --git a/public/js/components/logs-viewer.js b/public/js/components/logs-viewer.js
index 403968c..28c24a5 100644
--- a/public/js/components/logs-viewer.js
+++ b/public/js/components/logs-viewer.js
@@ -18,15 +18,28 @@ window.Components.logsViewer = () => ({
},
get filteredLogs() {
- const query = this.searchQuery.toLowerCase();
+ const query = this.searchQuery.trim();
+ if (!query) {
+ return this.logs.filter(log => this.filters[log.level]);
+ }
+
+ // Try regex first, fallback to plain text search
+ let matcher;
+ try {
+ const regex = new RegExp(query, 'i');
+ matcher = (msg) => regex.test(msg);
+ } catch (e) {
+ // Invalid regex, fallback to case-insensitive string search
+ const lowerQuery = query.toLowerCase();
+ matcher = (msg) => msg.toLowerCase().includes(lowerQuery);
+ }
+
return this.logs.filter(log => {
// Level Filter
if (!this.filters[log.level]) return false;
// Search Filter
- if (query && !log.message.toLowerCase().includes(query)) return false;
-
- return true;
+ return matcher(log.message);
});
},
diff --git a/public/js/data-store.js b/public/js/data-store.js
index 36414de..5b80947 100644
--- a/public/js/data-store.js
+++ b/public/js/data-store.js
@@ -76,17 +76,17 @@ document.addEventListener('alpine:init', () => {
const config = this.modelConfig[modelId] || {};
const family = this.getModelFamily(modelId);
- // Visibility Logic for Models Tab (quotaRows):
- // 1. If explicitly hidden via config, always hide
+ // Visibility Logic for Models Page (quotaRows):
+ // 1. If explicitly hidden via config, ALWAYS hide (clean interface)
// 2. If no config, default 'unknown' families to HIDDEN
// 3. Known families (Claude/Gemini) default to VISIBLE
- // Note: showHiddenModels toggle is for Settings page only, NOT here
+ // Note: To manage hidden models, use Settings → Models tab
let isHidden = config.hidden;
if (isHidden === undefined) {
isHidden = (family === 'other' || family === 'unknown');
}
- // Models Tab: ALWAYS hide hidden models (no toggle check)
+ // Models Page: ALWAYS hide hidden models (use Settings to restore)
if (isHidden) return;
// Filters
diff --git a/public/js/store.js b/public/js/store.js
index 24a99a1..49ff770 100644
--- a/public/js/store.js
+++ b/public/js/store.js
@@ -219,6 +219,30 @@ document.addEventListener('alpine:init', () => {
cancel: "Cancel",
passwordsNotMatch: "Passwords do not match",
passwordTooShort: "Password must be at least 6 characters",
+ // Dashboard drill-down
+ clickToViewAllAccounts: "Click to view all accounts",
+ clickToViewModels: "Click to view Models page",
+ clickToViewLimitedAccounts: "Click to view rate-limited accounts",
+ clickToFilterClaude: "Click to filter Claude models",
+ clickToFilterGemini: "Click to filter Gemini models",
+ // Accounts page
+ searchAccounts: "Search accounts...",
+ noAccountsYet: "No Accounts Yet",
+ noAccountsDesc: "Get started by adding a Google account via OAuth, or use the CLI command to import credentials.",
+ addFirstAccount: "Add Your First Account",
+ noSearchResults: "No accounts match your search",
+ clearSearch: "Clear Search",
+ disabledAccountsNote: "Disabled accounts will not be used for request routing but remain in the configuration. Dashboard statistics only include enabled accounts.",
+ dangerousOperation: "⚠️ Dangerous Operation",
+ confirmDeletePrompt: "Are you sure you want to delete account",
+ deleteWarning: "⚠️ This action cannot be undone. All configuration and historical records will be permanently deleted.",
+ // OAuth progress
+ oauthWaiting: "Waiting for OAuth authorization...",
+ oauthWaitingDesc: "Please complete the authentication in the popup window. This may take up to 2 minutes.",
+ oauthCancelled: "OAuth authorization cancelled",
+ oauthTimeout: "⏱️ OAuth authorization timed out. Please try again.",
+ oauthWindowClosed: "OAuth window was closed. Authorization may be incomplete.",
+ cancelOAuth: "Cancel",
},
zh: {
dashboard: "仪表盘",
@@ -427,12 +451,44 @@ document.addEventListener('alpine:init', () => {
cancel: "取消",
passwordsNotMatch: "密码不匹配",
passwordTooShort: "密码至少需要 6 个字符",
+ // Dashboard drill-down
+ clickToViewAllAccounts: "点击查看所有账号",
+ clickToViewModels: "点击查看模型页面",
+ clickToViewLimitedAccounts: "点击查看受限账号",
+ clickToFilterClaude: "点击筛选 Claude 模型",
+ clickToFilterGemini: "点击筛选 Gemini 模型",
+ // 账号页面
+ searchAccounts: "搜索账号...",
+ noAccountsYet: "还没有添加任何账号",
+ noAccountsDesc: "点击上方的 \"添加节点\" 按钮通过 OAuth 添加 Google 账号,或者使用 CLI 命令导入凭证。",
+ addFirstAccount: "添加第一个账号",
+ noSearchResults: "没有找到匹配的账号",
+ clearSearch: "清除搜索",
+ disabledAccountsNote: "已禁用的账号不会用于请求路由,但仍保留在配置中。仪表盘统计数据仅包含已启用的账号。",
+ dangerousOperation: "⚠️ 危险操作",
+ confirmDeletePrompt: "确定要删除账号",
+ deleteWarning: "⚠️ 此操作不可撤销,账号的所有配置和历史记录将永久删除。",
+ // OAuth 进度
+ oauthWaiting: "等待 OAuth 授权中...",
+ oauthWaitingDesc: "请在弹出窗口中完成认证。此过程最长可能需要 2 分钟。",
+ oauthCancelled: "已取消 OAuth 授权",
+ oauthTimeout: "⏱️ OAuth 授权超时,请重试。",
+ oauthWindowClosed: "OAuth 窗口已关闭,授权可能未完成。",
+ cancelOAuth: "取消",
}
},
// Toast Messages
toast: null,
+ // OAuth Progress
+ oauthProgress: {
+ active: false,
+ current: 0,
+ max: 60,
+ cancel: null
+ },
+
t(key, params = {}) {
let str = this.translations[this.lang][key] || key;
if (typeof str === 'string') {
diff --git a/public/views/accounts.html b/public/views/accounts.html
index f8ac8e1..c682aee 100644
--- a/public/views/accounts.html
+++ b/public/views/accounts.html
@@ -1,25 +1,40 @@
-
-
-
-
+
+
+
+
+
Access Credentials
-
-
+
+
Manage OAuth tokens and session states
-
+
+
+
-
-
-
+
+
-
-
-
Enabled
-
Identity (Email)
-
Source
-
Project ID
+
+
+
Enabled
+
Identity (Email)
+
Source
Health
Operations
-
+
+
+
+
+
+
+
+
+
+ No Accounts Yet
+
+
+ Get started by adding a Google account via OAuth, or use the CLI command to import credentials.
+
+
+
+
+
+
+ Add Your First Account
+
+ or
+
+ npm run accounts:add
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
No accounts match your search
+ Clear Search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/views/dashboard.html b/public/views/dashboard.html
index 6522062..d044f76 100644
--- a/public/views/dashboard.html
+++ b/public/views/dashboard.html
@@ -1,10 +1,39 @@
Real-time quota and status for
- all available models.
+
+
+
+
+
+ Models
+
+
+ Real-time quota and status for all available models.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
+
ALLCLAUDEGEMINI
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
Stat
@@ -100,6 +122,9 @@
+
+
+
\ No newline at end of file
diff --git a/public/views/settings.html b/public/views/settings.html
index b534e17..24158c4 100644
--- a/public/views/settings.html
+++ b/public/views/settings.html
@@ -73,12 +73,16 @@
English中文
@@ -91,10 +95,16 @@
-
+
+
+
+
10s300s
@@ -109,10 +119,16 @@
-
+
+
+
+
5005000
@@ -406,15 +422,14 @@
+
-
Manage visibility and
- ordering of models in the dashboard.
-
+
Configure model visibility, pinning, and request mapping.
+
Model mapping: server-side redirection. Claude Code users: see 'Claude CLI' tab for client-side setup.