feat(web): 新增 HID OTG 自检接口与设置页环境诊断面板,并优化在线升级状态文案本地化及重启后自动刷新体验

This commit is contained in:
mofeng-git
2026-02-20 09:44:02 +08:00
parent ba1b5224ff
commit 5f03971579
6 changed files with 1318 additions and 11 deletions

View File

@@ -303,6 +303,28 @@ export const hidApi = {
screen_resolution: [number, number] | null
}>('/hid/status'),
otgSelfCheck: () =>
request<{
overall_ok: boolean
error_count: number
warning_count: number
hid_backend: string
selected_udc: string | null
bound_udc: string | null
udc_state: string | null
udc_speed: string | null
available_udcs: string[]
other_gadgets: string[]
checks: Array<{
id: string
ok: boolean
level: 'info' | 'warn' | 'error'
message: string
hint?: string
path?: string
}>
}>('/hid/otg/self-check'),
keyboard: async (type: 'down' | 'up', key: number, modifiers?: {
ctrl?: boolean
shift?: boolean

View File

@@ -440,6 +440,7 @@ export default {
hid: 'HID',
msd: 'MSD',
atx: 'ATX',
environment: 'Environment',
network: 'Network',
users: 'Users',
hardware: 'Hardware',
@@ -521,6 +522,11 @@ export default {
updatePhaseRestarting: 'Restarting',
updatePhaseSuccess: 'Success',
updatePhaseFailed: 'Failed',
updateMsgChecking: 'Checking for updates',
updateMsgDownloading: 'Downloading binary',
updateMsgVerifying: 'Verifying (SHA256)',
updateMsgInstalling: 'Replacing binary',
updateMsgRestarting: 'Restarting service',
// Auth
auth: 'Access',
authSettings: 'Access Settings',
@@ -650,6 +656,86 @@ export default {
serialNumber: 'Serial Number',
serialNumberAuto: 'Auto-generated',
descriptorWarning: 'Changing these settings will reconnect the USB device',
otgSelfCheck: {
title: 'OTG Self-Check',
desc: 'Check UDC, gadget binding, and link status',
run: 'Run Self-Check',
failed: 'Failed to run OTG self-check',
overall: 'Overall Status',
ok: 'Healthy',
hasIssues: 'Issues Found',
summary: 'Issue Summary',
counts: '{errors} errors, {warnings} warnings',
groupCounts: '{ok} passed, {warnings} warnings, {errors} errors',
notRun: 'Not run',
status: {
ok: 'Healthy',
warn: 'Warning',
error: 'Error',
skipped: 'Skipped',
},
groups: {
udc: 'UDC Basics',
gadgetConfig: 'Gadget Config',
oneKvm: 'one-kvm Gadget',
functions: 'Functions & Nodes',
link: 'Link State',
},
values: {
missing: 'Missing',
notConfigured: 'Not configured',
mounted: 'Mounted',
unmounted: 'Unmounted',
available: 'Available',
unavailable: 'Unavailable',
exists: 'Exists',
none: 'None',
unbound: 'Unbound',
noConflict: 'No conflict',
conflict: 'Conflict',
unknown: 'Unknown',
normal: 'Normal',
abnormal: 'Abnormal',
},
selectedUdc: 'Target UDC',
boundUdc: 'Bound UDC',
messages: {
udc_dir_exists: 'UDC directory check',
udc_has_entries: 'UDC check',
configfs_mounted: 'configfs check',
usb_gadget_dir_exists: 'usb_gadget check',
libcomposite_loaded: 'libcomposite check',
one_kvm_gadget_exists: 'one-kvm gadget check',
other_gadgets: 'Other gadget check',
configured_udc_valid: 'Configured UDC check',
one_kvm_bound_udc: 'Bound UDC check',
hid_functions_present: 'HID function check',
config_c1_exists: 'configs/c.1 check',
function_links_ok: 'Function link check',
hid_device_nodes: 'HID node check',
udc_conflict: 'UDC conflict check',
udc_state: 'UDC state check',
udc_speed: 'UDC speed check',
},
hints: {
udc_dir_exists: 'Ensure UDC/OTG kernel drivers are enabled',
udc_has_entries: 'Ensure OTG controller is enabled in device tree',
configfs_mounted: 'Try: mount -t configfs none /sys/kernel/config',
usb_gadget_dir_exists: 'Ensure configfs and USB gadget support are enabled',
libcomposite_loaded: 'Try: modprobe libcomposite',
one_kvm_gadget_exists: 'Enable OTG HID or MSD to let one-kvm gadget be created automatically',
other_gadgets: 'Potential UDC contention with one-kvm; check other OTG services',
configured_udc_valid: 'Please reselect UDC in HID OTG settings',
one_kvm_bound_udc: 'Ensure HID/MSD is enabled and initialized successfully',
hid_functions_present: 'Check OTG HID config and enable at least one HID function',
config_c1_exists: 'Gadget structure is incomplete; try restarting One-KVM',
function_links_ok: 'Reinitialize OTG (toggle HID backend once or restart service)',
hid_device_nodes: 'Ensure gadget is bound and check kernel logs',
udc_conflict: 'Stop other OTG services or switch one-kvm to an idle UDC',
udc_state: 'Ensure target host is connected and has recognized the USB device',
udc_speed: 'Device may not be fully enumerated; try reconnecting USB',
},
},
// WebRTC / ICE
webrtcSettings: 'WebRTC Settings',
webrtcSettingsDesc: 'Configure STUN/TURN servers for NAT traversal',

View File

@@ -440,6 +440,7 @@ export default {
hid: 'HID',
msd: 'MSD',
atx: 'ATX',
environment: '环境',
network: '网络',
users: '用户',
hardware: '硬件',
@@ -521,6 +522,11 @@ export default {
updatePhaseRestarting: '重启中',
updatePhaseSuccess: '成功',
updatePhaseFailed: '失败',
updateMsgChecking: '检查更新中',
updateMsgDownloading: '下载中',
updateMsgVerifying: '校验中SHA256',
updateMsgInstalling: '替换程序中',
updateMsgRestarting: '服务重启中',
// Auth
auth: '访问控制',
authSettings: '访问设置',
@@ -650,6 +656,86 @@ export default {
serialNumber: '序列号',
serialNumberAuto: '自动生成',
descriptorWarning: '修改这些设置将导致 USB 设备重新连接',
otgSelfCheck: {
title: 'OTG 自检',
desc: '检查 UDC、gadget 绑定和连接状态',
run: '运行自检',
failed: '执行 OTG 自检失败',
overall: '总体状态',
ok: '正常',
hasIssues: '存在问题',
summary: '问题统计',
counts: '错误 {errors},警告 {warnings}',
groupCounts: '通过 {ok},警告 {warnings},错误 {errors}',
notRun: '未执行',
status: {
ok: '正常',
warn: '告警',
error: '异常',
skipped: '跳过',
},
groups: {
udc: 'UDC 基础',
gadgetConfig: 'gadget 配置',
oneKvm: 'one-kvm gadget',
functions: '功能与设备节点',
link: '连接状态',
},
values: {
missing: '不存在',
notConfigured: '未配置',
mounted: '已挂载',
unmounted: '未挂载',
available: '可用',
unavailable: '不可用',
exists: '存在',
none: '无',
unbound: '未绑定',
noConflict: '无冲突',
conflict: '冲突',
unknown: '未知',
normal: '正常',
abnormal: '异常',
},
selectedUdc: '目标 UDC',
boundUdc: '已绑定 UDC',
messages: {
udc_dir_exists: 'UDC 目录检查',
udc_has_entries: 'UDC 检查',
configfs_mounted: 'configfs 检查',
usb_gadget_dir_exists: 'usb_gadget 检查',
libcomposite_loaded: 'libcomposite 检查',
one_kvm_gadget_exists: 'one-kvm gadget 检查',
other_gadgets: '其他 gadget 检查',
configured_udc_valid: '配置 UDC 检查',
one_kvm_bound_udc: 'gadget 绑定 UDC 检查',
hid_functions_present: 'HID 函数检查',
config_c1_exists: 'configs/c.1 检查',
function_links_ok: 'functions 链接检查',
hid_device_nodes: 'HID 设备节点检查',
udc_conflict: 'UDC 冲突检查',
udc_state: 'UDC 状态检查',
udc_speed: 'UDC 速率检查',
},
hints: {
udc_dir_exists: '请确认内核已启用 UDC/OTG 驱动',
udc_has_entries: '请确认 OTG 控制器已在设备树中启用',
configfs_mounted: '可执行: mount -t configfs none /sys/kernel/config',
usb_gadget_dir_exists: '请确认 configfs 与 USB gadget 支持已启用',
libcomposite_loaded: '可执行: modprobe libcomposite',
one_kvm_gadget_exists: '启用 OTG HID 或 MSD 后会自动创建 one-kvm gadget',
other_gadgets: '可能与 one-kvm 抢占 UDC请检查是否有其他 OTG 服务',
configured_udc_valid: '请在 HID OTG 设置中重新选择 UDC',
one_kvm_bound_udc: '请确认 HID/MSD 已启用并成功初始化',
hid_functions_present: '请检查 OTG HID 配置是否至少启用了一个 HID 功能',
config_c1_exists: 'gadget 结构不完整,请尝试重启 One-KVM',
function_links_ok: '建议重新初始化 OTG切换一次 HID 后端或重启服务)',
hid_device_nodes: '请确认 gadget 已绑定并检查内核日志',
udc_conflict: '请停用其他 OTG 服务或切换 one-kvm 到空闲 UDC',
udc_state: '请确认已连接被控机,且被控机已识别 USB 设备',
udc_speed: '设备可能未完成枚举,可尝试重插 USB',
},
},
// WebRTC / ICE
webrtcSettings: 'WebRTC 设置',
webrtcSettingsDesc: '配置 STUN/TURN 服务器以实现 NAT 穿透',

View File

@@ -7,6 +7,7 @@ import { useAuthStore } from '@/stores/auth'
import {
authApi,
configApi,
hidApi,
streamApi,
atxConfigApi,
extensionsApi,
@@ -63,6 +64,7 @@ import {
Check,
HardDrive,
Power,
Server,
Menu,
Lock,
User,
@@ -79,7 +81,7 @@ import {
Radio,
} from 'lucide-vue-next'
const { t, locale } = useI18n()
const { t, te, locale } = useI18n()
const systemStore = useSystemStore()
const configStore = useConfigStore()
const authStore = useAuthStore()
@@ -107,15 +109,16 @@ const navGroups = computed(() => [
{ id: 'hid', label: t('settings.hid'), icon: Keyboard, status: config.value.hid_backend.toUpperCase() },
...(config.value.msd_enabled ? [{ id: 'msd', label: t('settings.msd'), icon: HardDrive }] : []),
{ id: 'atx', label: t('settings.atx'), icon: Power },
{ id: 'environment', label: t('settings.environment'), icon: Server },
]
},
{
title: t('settings.extensions'),
items: [
{ id: 'ext-ttyd', label: t('extensions.ttyd.title'), icon: Terminal },
{ id: 'ext-rustdesk', label: t('extensions.rustdesk.title'), icon: ScreenShare },
{ id: 'ext-rtsp', label: t('extensions.rtsp.title'), icon: Radio },
{ id: 'ext-remote-access', label: t('extensions.remoteAccess.title'), icon: ExternalLink },
{ id: 'ext-ttyd', label: t('extensions.ttyd.title'), icon: Terminal },
]
},
{
@@ -230,6 +233,9 @@ const updateChannel = ref<UpdateChannel>('stable')
const updateOverview = ref<UpdateOverviewResponse | null>(null)
const updateStatus = ref<UpdateStatusResponse | null>(null)
const updateLoading = ref(false)
const updateSawRestarting = ref(false)
const updateSawRequestFailure = ref(false)
const updateAutoReloadTriggered = ref(false)
const updateRunning = computed(() => {
const phase = updateStatus.value?.phase
return phase === 'checking'
@@ -332,6 +338,207 @@ const showLowEndpointHint = computed(() =>
config.value.hid_backend === 'otg' && isLowEndpointUdc.value
)
type OtgSelfCheckLevel = 'info' | 'warn' | 'error'
type OtgCheckGroupStatus = 'ok' | 'warn' | 'error' | 'skipped'
interface OtgSelfCheckItem {
id: string
ok: boolean
level: OtgSelfCheckLevel
message: string
hint?: string
path?: string
}
interface OtgSelfCheckResult {
overall_ok: boolean
error_count: number
warning_count: number
hid_backend: string
selected_udc: string | null
bound_udc: string | null
udc_state: string | null
udc_speed: string | null
available_udcs: string[]
other_gadgets: string[]
checks: OtgSelfCheckItem[]
}
interface OtgCheckGroupDef {
id: string
titleKey: string
checkIds: string[]
}
interface OtgCheckGroup {
id: string
titleKey: string
status: OtgCheckGroupStatus
okCount: number
warningCount: number
errorCount: number
items: OtgSelfCheckItem[]
}
const otgSelfCheckLoading = ref(false)
const otgSelfCheckResult = ref<OtgSelfCheckResult | null>(null)
const otgSelfCheckError = ref('')
const otgRunButtonPressed = ref(false)
const otgCheckGroupDefs: OtgCheckGroupDef[] = [
{
id: 'udc',
titleKey: 'settings.otgSelfCheck.groups.udc',
checkIds: ['udc_dir_exists', 'udc_has_entries', 'configured_udc_valid'],
},
{
id: 'gadget_config',
titleKey: 'settings.otgSelfCheck.groups.gadgetConfig',
checkIds: ['configfs_mounted', 'usb_gadget_dir_exists', 'libcomposite_loaded'],
},
{
id: 'one_kvm',
titleKey: 'settings.otgSelfCheck.groups.oneKvm',
checkIds: ['one_kvm_gadget_exists', 'one_kvm_bound_udc', 'other_gadgets', 'udc_conflict'],
},
{
id: 'functions',
titleKey: 'settings.otgSelfCheck.groups.functions',
checkIds: ['hid_functions_present', 'config_c1_exists', 'function_links_ok', 'hid_device_nodes'],
},
{
id: 'link',
titleKey: 'settings.otgSelfCheck.groups.link',
checkIds: ['udc_state', 'udc_speed'],
},
]
const otgCheckGroups = computed<OtgCheckGroup[]>(() => {
const items = otgSelfCheckResult.value?.checks || []
return otgCheckGroupDefs.map((group) => {
const groupItems = items.filter(item => group.checkIds.includes(item.id))
const errorCount = groupItems.filter(item => item.level === 'error').length
const warningCount = groupItems.filter(item => item.level === 'warn').length
const okCount = Math.max(0, groupItems.length - errorCount - warningCount)
let status: OtgCheckGroupStatus = 'skipped'
if (groupItems.length > 0) {
if (errorCount > 0) status = 'error'
else if (warningCount > 0) status = 'warn'
else status = 'ok'
}
return {
id: group.id,
titleKey: group.titleKey,
status,
okCount,
warningCount,
errorCount,
items: groupItems,
}
})
})
function otgCheckLevelClass(level: OtgSelfCheckLevel): string {
if (level === 'error') return 'bg-red-500'
if (level === 'warn') return 'bg-amber-500'
return 'bg-blue-500'
}
function otgCheckStatusText(level: OtgSelfCheckLevel): string {
if (level === 'error') return t('common.error')
if (level === 'warn') return t('common.warning')
return t('common.info')
}
function otgGroupStatusClass(status: OtgCheckGroupStatus): string {
if (status === 'error') return 'bg-red-500'
if (status === 'warn') return 'bg-amber-500'
if (status === 'ok') return 'bg-emerald-500'
return 'bg-muted-foreground/40'
}
function otgGroupStatusText(status: OtgCheckGroupStatus): string {
return t(`settings.otgSelfCheck.status.${status}`)
}
function otgGroupSummary(group: OtgCheckGroup): string {
if (group.items.length === 0) {
return t('settings.otgSelfCheck.notRun')
}
return t('settings.otgSelfCheck.groupCounts', {
ok: group.okCount,
warnings: group.warningCount,
errors: group.errorCount,
})
}
function otgCheckMessage(item: OtgSelfCheckItem): string {
const key = `settings.otgSelfCheck.messages.${item.id}`
const label = te(key) ? t(key) : item.message
const result = otgSelfCheckResult.value
if (!result) return label
const value = (name: string) => t(`settings.otgSelfCheck.values.${name}`)
switch (item.id) {
case 'udc_has_entries':
return `${label}${result.available_udcs.length ? result.available_udcs.join(', ') : value('missing')}`
case 'configured_udc_valid':
if (!result.selected_udc) return `${label}${value('notConfigured')}`
return `${label}${item.ok ? result.selected_udc : `${value('missing')}/${result.selected_udc}`}`
case 'configfs_mounted':
return `${label}${item.ok ? value('mounted') : value('unmounted')}`
case 'usb_gadget_dir_exists':
return `${label}${item.ok ? value('available') : value('unavailable')}`
case 'libcomposite_loaded':
return `${label}${item.ok ? value('available') : value('unavailable')}`
case 'one_kvm_gadget_exists':
return `${label}${item.ok ? value('exists') : value('missing')}`
case 'other_gadgets':
return `${label}${result.other_gadgets.length ? result.other_gadgets.join(', ') : value('none')}`
case 'one_kvm_bound_udc':
return `${label}${result.bound_udc || value('unbound')}`
case 'udc_conflict':
return `${label}${item.ok ? value('noConflict') : value('conflict')}`
case 'udc_state':
return `${label}${result.udc_state || value('unknown')}`
case 'udc_speed':
return `${label}${result.udc_speed || value('unknown')}`
default:
return `${label}${item.ok ? value('normal') : value('abnormal')}`
}
}
function otgCheckHint(item: OtgSelfCheckItem): string {
if (!item.hint) return ''
const key = `settings.otgSelfCheck.hints.${item.id}`
return te(key) ? t(key) : item.hint
}
async function runOtgSelfCheck() {
otgSelfCheckLoading.value = true
otgSelfCheckError.value = ''
try {
otgSelfCheckResult.value = await hidApi.otgSelfCheck()
} catch (e) {
console.error('Failed to run OTG self-check:', e)
otgSelfCheckError.value = t('settings.otgSelfCheck.failed')
} finally {
otgSelfCheckLoading.value = false
}
}
async function onRunOtgSelfCheckClick() {
if (!otgSelfCheckLoading.value) {
otgRunButtonPressed.value = true
window.setTimeout(() => {
otgRunButtonPressed.value = false
}, 160)
}
await runOtgSelfCheck()
}
function alignHidProfileForLowEndpoint() {
if (hidProfileAligned.value) return
if (!configLoaded.value || !devicesLoaded.value) return
@@ -1148,8 +1355,18 @@ async function loadUpdateOverview() {
async function refreshUpdateStatus() {
try {
updateStatus.value = await updateApi.status()
if (updateSawRestarting.value && !updateAutoReloadTriggered.value) {
if (updateSawRequestFailure.value || updateStatus.value.phase === 'idle') {
updateAutoReloadTriggered.value = true
window.location.reload()
}
}
} catch (e) {
console.error('Failed to refresh update status:', e)
if (updateSawRestarting.value) {
updateSawRequestFailure.value = true
}
}
}
@@ -1164,6 +1381,9 @@ function startUpdatePolling() {
if (updateStatusTimer !== null) return
updateStatusTimer = window.setInterval(async () => {
await refreshUpdateStatus()
if (updateStatus.value?.phase === 'restarting') {
updateSawRestarting.value = true
}
if (!updateRunning.value) {
stopUpdatePolling()
await loadUpdateOverview()
@@ -1173,6 +1393,9 @@ function startUpdatePolling() {
async function startOnlineUpgrade() {
try {
updateSawRestarting.value = false
updateSawRequestFailure.value = false
updateAutoReloadTriggered.value = false
await updateApi.upgrade({ channel: updateChannel.value })
await refreshUpdateStatus()
startUpdatePolling()
@@ -1195,6 +1418,25 @@ function updatePhaseText(phase?: string): string {
}
}
function localizeUpdateMessage(message?: string): string | null {
if (!message) return null
if (message === 'Checking for updates') return t('settings.updateMsgChecking')
if (message.startsWith('Downloading binary')) {
return message.replace('Downloading binary', t('settings.updateMsgDownloading'))
}
if (message === 'Verifying sha256') return t('settings.updateMsgVerifying')
if (message === 'Replacing binary') return t('settings.updateMsgInstalling')
if (message === 'Restarting service') return t('settings.updateMsgRestarting')
return message
}
function updateStatusBadgeText(): string {
return localizeUpdateMessage(updateStatus.value?.message)
|| updatePhaseText(updateStatus.value?.phase)
}
async function saveRustdeskConfig() {
loading.value = true
saved.value = false
@@ -1462,11 +1704,17 @@ onMounted(async () => {
if (updateRunning.value) {
startUpdatePolling()
}
await runOtgSelfCheck()
})
watch(updateChannel, async () => {
await loadUpdateOverview()
})
watch(() => config.value.hid_backend, async () => {
await runOtgSelfCheck()
})
</script>
<template>
@@ -1939,6 +2187,105 @@ watch(updateChannel, async () => {
</template>
</CardContent>
</Card>
</div>
<!-- Environment Section -->
<div v-show="activeSection === 'environment'" class="space-y-4 max-w-3xl">
<Card>
<CardHeader class="flex flex-row items-start justify-between space-y-0">
<div class="space-y-1.5">
<CardTitle>{{ t('settings.otgSelfCheck.title') }}</CardTitle>
<CardDescription>{{ t('settings.otgSelfCheck.desc') }}</CardDescription>
</div>
<Button
variant="outline"
size="sm"
:disabled="otgSelfCheckLoading"
:class="[
'transition-all duration-150 active:scale-95 active:brightness-95',
otgRunButtonPressed ? 'scale-95 brightness-95' : ''
]"
@click="onRunOtgSelfCheckClick"
>
<RefreshCw class="h-4 w-4 mr-2" :class="{ 'animate-spin': otgSelfCheckLoading }" />
{{ t('settings.otgSelfCheck.run') }}
</Button>
</CardHeader>
<CardContent class="space-y-3">
<p v-if="otgSelfCheckError" class="text-xs text-red-600 dark:text-red-400">
{{ otgSelfCheckError }}
</p>
<template v-if="otgSelfCheckResult">
<div class="flex flex-wrap gap-2 text-xs">
<Badge
:variant="otgSelfCheckResult.overall_ok ? 'default' : 'destructive'"
class="font-medium"
>
{{ t('settings.otgSelfCheck.overall') }}{{ otgSelfCheckResult.overall_ok ? t('settings.otgSelfCheck.ok') : t('settings.otgSelfCheck.hasIssues') }}
</Badge>
<Badge variant="outline" class="font-normal">
{{ t('settings.otgSelfCheck.counts', { errors: otgSelfCheckResult.error_count, warnings: otgSelfCheckResult.warning_count }) }}
</Badge>
<Badge variant="secondary" class="font-normal">
{{ t('settings.otgSelfCheck.selectedUdc') }}{{ otgSelfCheckResult.selected_udc || '-' }}
</Badge>
<Badge variant="secondary" class="font-normal">
{{ t('settings.otgSelfCheck.boundUdc') }}{{ otgSelfCheckResult.bound_udc || '-' }}
</Badge>
</div>
<div class="rounded-md border divide-y">
<details
v-for="group in otgCheckGroups"
:key="group.id"
:open="group.status === 'error' || group.status === 'warn'"
class="group"
>
<summary class="list-none cursor-pointer px-4 py-3 flex items-center justify-between gap-3 hover:bg-muted/40">
<div class="flex items-center gap-2 min-w-0">
<span class="inline-block h-2 w-2 rounded-full shrink-0" :class="otgGroupStatusClass(group.status)" />
<span class="text-sm font-medium truncate leading-6">{{ t(group.titleKey) }}</span>
</div>
<div class="flex items-center gap-2 shrink-0">
<span class="text-xs text-muted-foreground">{{ otgGroupSummary(group) }}</span>
<Badge variant="outline" class="text-[10px] h-5 px-1.5">{{ otgGroupStatusText(group.status) }}</Badge>
</div>
</summary>
<div v-if="group.items.length > 0" class="border-t bg-muted/20">
<div
v-for="item in group.items"
:key="item.id"
class="px-4 py-3 border-b last:border-b-0"
>
<div class="flex items-start gap-2">
<span class="inline-block h-2 w-2 rounded-full mt-1.5 shrink-0" :class="otgCheckLevelClass(item.level)" />
<div class="min-w-0 space-y-1">
<div class="flex items-center gap-2">
<p class="text-sm leading-5">{{ otgCheckMessage(item) }}</p>
<span class="text-[11px] text-muted-foreground shrink-0">{{ otgCheckStatusText(item.level) }}</span>
</div>
<div class="flex flex-wrap gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
<span v-if="item.hint">{{ otgCheckHint(item) }}</span>
<code v-if="item.path" class="font-mono break-all">{{ item.path }}</code>
</div>
</div>
</div>
</div>
</div>
<div v-else class="border-t bg-muted/20 px-4 py-3 text-xs text-muted-foreground">
{{ t('settings.otgSelfCheck.notRun') }}
</div>
</details>
</div>
</template>
<p v-else-if="otgSelfCheckLoading" class="text-xs text-muted-foreground">
{{ t('common.loading') }}
</p>
</CardContent>
</Card>
</div>
<!-- Access Section -->
@@ -2843,9 +3190,21 @@ watch(updateChannel, async () => {
<!-- About Section -->
<div v-show="activeSection === 'about'" class="space-y-6">
<Card>
<CardHeader>
<CardTitle>{{ t('settings.onlineUpgrade') }}</CardTitle>
<CardDescription>{{ t('settings.onlineUpgradeDesc') }}</CardDescription>
<CardHeader class="flex flex-row items-start justify-between space-y-0">
<div class="space-y-1.5">
<CardTitle>{{ t('settings.onlineUpgrade') }}</CardTitle>
<CardDescription>{{ t('settings.onlineUpgradeDesc') }}</CardDescription>
</div>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
:aria-label="t('common.refresh')"
:disabled="updateRunning || updateLoading"
@click="loadUpdateOverview"
>
<RefreshCw :class="['h-4 w-4', (updateLoading || updateRunning) ? 'animate-spin' : '']" />
</Button>
</CardHeader>
<CardContent class="space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
@@ -2876,9 +3235,9 @@ watch(updateChannel, async () => {
<Badge
variant="outline"
class="max-w-[60%] truncate"
:title="updateStatus?.message || updatePhaseText(updateStatus?.phase)"
:title="updateStatusBadgeText()"
>
{{ updateStatus?.message || updatePhaseText(updateStatus?.phase) }}
{{ updateStatusBadgeText() }}
</Badge>
</div>
<div v-if="updateRunning || updateStatus?.phase === 'failed' || updateStatus?.phase === 'success'" class="w-full h-2 bg-muted rounded overflow-hidden">
@@ -2905,10 +3264,6 @@ watch(updateChannel, async () => {
</div>
<div class="flex justify-end gap-2">
<Button variant="outline" :disabled="updateRunning" @click="loadUpdateOverview">
<RefreshCw class="h-4 w-4 mr-2" />
{{ t('common.refresh') }}
</Button>
<Button
:disabled="updateRunning || !updateOverview?.upgrade_available"
@click="startOnlineUpgrade"