+
{{ t('admin.channelMonitor.form.noActiveKey') }}
-
-
+
+
+
+
+ | {{ t('common.name') }} |
+ {{ t('keys.apiKey') }} |
+ {{ t('keys.group') }} |
+
+
+
+
+ | {{ k.name }} |
+ {{ maskApiKey(k.key) }} |
+
+
+ {{ k.group.name }}
+
+ —
+ |
+
+
+
@@ -39,14 +68,18 @@
diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue
index 23d0f4e9..8b9fbdea 100644
--- a/frontend/src/components/layout/AppSidebar.vue
+++ b/frontend/src/components/layout/AppSidebar.vue
@@ -611,7 +611,9 @@ const userNavItems = computed((): NavItem[] => {
{ path: '/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
- { path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon },
+ ...(appStore.cachedPublicSettings?.channel_monitor_enabled
+ ? [{ path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon }]
+ : []),
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
...(appStore.cachedPublicSettings?.payment_enabled
? [
@@ -650,7 +652,9 @@ const personalNavItems = computed((): NavItem[] => {
const items: NavItem[] = [
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
- { path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon },
+ ...(appStore.cachedPublicSettings?.channel_monitor_enabled
+ ? [{ path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon }]
+ : []),
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
...(appStore.cachedPublicSettings?.payment_enabled
? [
@@ -715,7 +719,9 @@ const adminNavItems = computed((): NavItem[] => {
expandOnly: true,
children: [
{ path: '/admin/channels/pricing', label: t('nav.channelPricing'), icon: PriceTagIcon },
- { path: '/admin/channels/monitor', label: t('nav.channelMonitor'), icon: SignalIcon },
+ ...(appStore.cachedPublicSettings?.channel_monitor_enabled
+ ? [{ path: '/admin/channels/monitor', label: t('nav.channelMonitor'), icon: SignalIcon }]
+ : []),
],
},
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
diff --git a/frontend/src/composables/useChannelMonitorFormat.ts b/frontend/src/composables/useChannelMonitorFormat.ts
index 7ffdaa42..a9253622 100644
--- a/frontend/src/composables/useChannelMonitorFormat.ts
+++ b/frontend/src/composables/useChannelMonitorFormat.ts
@@ -76,6 +76,32 @@ export function useChannelMonitorFormat() {
}
}
+ /**
+ * Tailwind class for a provider radio-button-style picker (active/inactive state).
+ * Reuses the same emerald/orange/sky palette as providerBadgeClass to keep
+ * visual semantics consistent across badges and pickers.
+ */
+ function providerPickerClass(p: Provider | string, active: boolean): string {
+ switch (p) {
+ case PROVIDER_OPENAI:
+ return active
+ ? 'border-emerald-500 bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-400'
+ : 'border-gray-200 bg-white text-gray-600 hover:border-emerald-300 hover:text-emerald-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-emerald-500/50'
+ case PROVIDER_ANTHROPIC:
+ return active
+ ? 'border-orange-500 bg-orange-50 text-orange-700 dark:bg-orange-500/15 dark:text-orange-300 dark:border-orange-400'
+ : 'border-gray-200 bg-white text-gray-600 hover:border-orange-300 hover:text-orange-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-orange-500/50'
+ case PROVIDER_GEMINI:
+ return active
+ ? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300 dark:border-sky-400'
+ : 'border-gray-200 bg-white text-gray-600 hover:border-sky-300 hover:text-sky-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-sky-500/50'
+ default:
+ return active
+ ? 'border-gray-400 bg-gray-50 text-gray-700 dark:border-dark-500 dark:bg-dark-700 dark:text-gray-200'
+ : 'border-gray-200 bg-white text-gray-600 hover:border-gray-300 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400'
+ }
+ }
+
function formatLatency(ms: number | null | undefined): string {
if (ms == null) return t('monitorCommon.latencyEmpty')
return String(Math.round(ms))
@@ -110,6 +136,7 @@ export function useChannelMonitorFormat() {
statusBadgeClass,
providerLabel,
providerBadgeClass,
+ providerPickerClass,
formatLatency,
formatPercent,
formatAvailability,
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index 51aa9920..ef3a4057 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -2137,7 +2137,7 @@ export default {
form: {
name: 'Name',
namePlaceholder: 'Enter monitor name',
- provider: 'Provider',
+ provider: 'Platform',
endpoint: 'Endpoint',
endpointPlaceholder: 'https://api.example.com',
useCurrentDomain: 'Use current service',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index 021b8992..25bce657 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -2216,7 +2216,7 @@ export default {
form: {
name: '名称',
namePlaceholder: '输入监控名称',
- provider: '供应商',
+ provider: '平台',
endpoint: '上游地址',
endpointPlaceholder: 'https://api.example.com',
useCurrentDomain: '使用当前服务',
diff --git a/frontend/src/utils/maskApiKey.ts b/frontend/src/utils/maskApiKey.ts
new file mode 100644
index 00000000..ab54980b
--- /dev/null
+++ b/frontend/src/utils/maskApiKey.ts
@@ -0,0 +1,6 @@
+// Mask an API key for display: reveals first 6 + last 4; short keys (≤12) show `first 4 + ***`.
+export function maskApiKey(key: string): string {
+ if (!key) return ''
+ if (key.length <= 12) return `${key.slice(0, 4)}***`
+ return `${key.slice(0, 6)}...${key.slice(-4)}`
+}
diff --git a/frontend/src/views/user/ChannelStatusView.vue b/frontend/src/views/user/ChannelStatusView.vue
index af427cca..d9100890 100644
--- a/frontend/src/views/user/ChannelStatusView.vue
+++ b/frontend/src/views/user/ChannelStatusView.vue
@@ -160,14 +160,34 @@ watch(items, () => {
void ensureDetailsForWindow()
})
+function startTimer() {
+ if (countdownTimer !== undefined) return
+ countdownTimer = setInterval(tick, 1000) as unknown as number
+}
+
+function stopTimer() {
+ if (countdownTimer !== undefined) {
+ clearInterval(countdownTimer)
+ countdownTimer = undefined
+ }
+}
+
+watch(
+ () => appStore.cachedPublicSettings?.channel_monitor_enabled,
+ (enabled) => {
+ if (enabled === false) stopTimer()
+ else startTimer()
+ },
+)
+
// ── Lifecycle ──
onMounted(() => {
void reload(false)
- countdownTimer = setInterval(tick, 1000) as unknown as number
+ if (appStore.cachedPublicSettings?.channel_monitor_enabled !== false) startTimer()
})
onBeforeUnmount(() => {
- if (countdownTimer !== undefined) clearInterval(countdownTimer)
+ stopTimer()
if (abortController) abortController.abort()
})
diff --git a/frontend/src/views/user/KeysView.vue b/frontend/src/views/user/KeysView.vue
index 34cccf9c..cf29e4bd 100644
--- a/frontend/src/views/user/KeysView.vue
+++ b/frontend/src/views/user/KeysView.vue
@@ -61,7 +61,7 @@
- {{ maskKey(value) }}
+ {{ maskApiKey(value) }}