-
-
+
-
-
-
+
+
+
+
-
-
-
-
+
+
-
-
-
-
-
+
-
-
-
- ...
- ...
-
+
+
-
+
+
+
+ ......
-
-
-
-
-
-
+
+
-
-
- | - |
+
+
-
+
+ --%
+
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- ▾
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
-
-
-
+
-
-
-
+
+
+
+
+
+
-
-
-
+
-
- -
-
-
+
-
+
+
+
+
+
+
- '
@@ -119,26 +152,30 @@ async function fetchSystemInfo() {
*/
async function checkFrontendConnectivity() {
const checks = [
- { id: 'check-fount-server', name: 'fount Server', url: '/api/ping' },
- { id: 'check-esm', name: 'esm.sh', url: 'https://esm.sh' },
- { id: 'check-jsdelivr', name: 'jsDelivr', url: 'https://cdn.jsdelivr.net' },
- { id: 'check-iconify', name: 'Iconify', url: 'https://api.iconify.design' },
- { id: 'check-fount-public', name: 'fount Public', url: 'https://steve02081504.github.io/fount' }
+ { id: 'check-fount-server', name: 'fount Server', icon: serviceIcon('fount Server'), url: '/api/ping' },
+ { id: 'check-esm', name: 'esm.sh', icon: serviceIcon('esm.sh'), url: 'https://esm.sh' },
+ { id: 'check-jsdelivr', name: 'jsDelivr', icon: serviceIcon('jsDelivr'), url: 'https://cdn.jsdelivr.net' },
+ { id: 'check-iconify', name: 'Iconify', icon: serviceIcon('Iconify'), url: 'https://api.iconify.design' },
+ { id: 'check-fount-public', name: 'fount Public', icon: serviceIcon('fount Public'), url: 'https://steve02081504.github.io/fount' }
]
await mountTemplate(frontendChecks, 'connectivity_list', { checks })
-
+ const results = []
for (const check of checks) {
const start = Date.now()
- let status = 'error', duration = 0
+ let status = 'error', duration
try {
await fetch(check.url, { method: 'HEAD', mode: 'no-cors', cache: 'no-store' })
status = 'ok'
duration = Date.now() - start
} catch { /* unreachable */ }
- debugData.connectivity.frontend.push({ ...check, status, duration })
- document.getElementById(check.id).replaceWith(await renderTemplate('connectivity_item', { ...check, status, duration }))
+ const result = { ...check, status, duration }
+ results.push(result)
+ recordLatency(check.id || check.name, duration)
+ document.getElementById(check.id).replaceWith(await renderTemplate('connectivity_item', result))
}
+ debugData.connectivity.frontend = results
+ redrawLatencySparklines(frontendChecks)
}
let testStatusOpen = false
@@ -176,7 +213,7 @@ async function renderTestStatus(status) {
const online = status?.online === true
testStatusCard.classList.toggle('hidden', !online)
if (!online) return
- testStatusBadge.className = `badge badge-lg gap-2 ${status.active ? 'badge-success' : 'badge-ghost'}`
+ setBadgeState(testStatusBadge, status.active ? 'running' : 'idle')
testStatusBadge.dataset.i18n = status.active ? 'debug_info.testStatus.running' : 'debug_info.testStatus.idle'
if (!status.active) {
testStatusList.replaceChildren()
@@ -326,8 +363,8 @@ function stopTestStatusStream() {
testStatusWs = null
}
-const UPDATE_ICON = 'https://api.iconify.design/mdi/update.svg'
-const LOADING_ICON = 'https://api.iconify.design/line-md/loading-twotone-loop.svg'
+const UPDATE_ICON = 'https://api.iconify.design/line-md/backup-restore.svg'
+const LOADING_ICON = 'https://api.iconify.design/line-md/loading-loop.svg'
const UPTODATE_ICON = 'https://api.iconify.design/line-md/confirm.svg'
/**
@@ -412,7 +449,7 @@ let pollTimer = null
*/
async function pollVersionInfo() {
lastVersionCheckTime = Date.now()
- await fetchVersionInfo()
+ await Promise.all([fetchVersionInfo(), fetchSystemInfo(), checkFrontendConnectivity()])
}
/**
@@ -445,7 +482,9 @@ document.addEventListener('visibilitychange', () => {
if (!document.hidden) startPollTimer()
+const redrawSparklines = () => redrawLatencySparklines()
+new ResizeObserver(redrawSparklines).observe(document.documentElement)
+new MutationObserver(redrawSparklines).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] })
+
pollVersionInfo()
-fetchSystemInfo()
-checkFrontendConnectivity()
startTestStatusStream()
diff --git a/src/public/parts/shells/debug_info/public/src/latency_sparkline.mjs b/src/public/parts/shells/debug_info/public/src/latency_sparkline.mjs
new file mode 100644
index 0000000000..0e3285ed82
--- /dev/null
+++ b/src/public/parts/shells/debug_info/public/src/latency_sparkline.mjs
@@ -0,0 +1,73 @@
+const MAX_SAMPLES = 10
+const histories = new Map()
+
+/**
+ * 记录一次真实的连通性延迟。
+ * @param {string} key 稳定的检查标识。
+ * @param {number} duration 延迟毫秒数。
+ */
+export function recordLatency(key, duration) {
+ if (!Number.isFinite(duration) || duration < 0) return
+ const samples = histories.get(key) || []
+ samples.push(duration)
+ if (samples.length > MAX_SAMPLES) samples.shift()
+ histories.set(key, samples)
+}
+
+/**
+ * 根据已有样本绘制单个延迟趋势图。重绘不会新增样本。
+ * @param {HTMLCanvasElement} canvas 目标画布。
+ * @param {string} key 稳定的检查标识。
+ */
+export function drawLatencySparkline(canvas, key) {
+ const samples = histories.get(key) || []
+ const context = canvas.getContext('2d')
+ if (!context) return
+
+ const ratio = window.devicePixelRatio || 1
+ const { width, height } = canvas.getBoundingClientRect()
+ canvas.width = Math.max(1, Math.round(width * ratio))
+ canvas.height = Math.max(1, Math.round(height * ratio))
+ context.scale(ratio, ratio)
+ context.clearRect(0, 0, width, height)
+ if (!samples.length) return
+
+ const color = getComputedStyle(canvas).color
+ context.strokeStyle = color
+ context.fillStyle = color
+ context.lineWidth = Math.max(1.5, Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--border')) || 1)
+ context.lineJoin = 'round'
+ context.lineCap = 'round'
+ if (samples.length < 3) {
+ context.beginPath()
+ context.arc(width / 2, height / 2, context.lineWidth, 0, Math.PI * 2)
+ context.fill()
+ return
+ }
+
+ const min = Math.min(...samples)
+ const max = Math.max(...samples)
+ const range = max - min
+ const points = samples.map((value, index) => ({
+ x: index * width / (samples.length - 1),
+ y: range ? height - 2 - (value - min) / range * (height - 4) : height / 2,
+ }))
+ context.beginPath()
+ context.moveTo(points[0].x, points[0].y)
+ for (let index = 1; index < points.length - 1; index++) {
+ const point = points[index], next = points[index + 1]
+ context.quadraticCurveTo(point.x, point.y, (point.x + next.x) / 2, (point.y + next.y) / 2)
+ }
+ const last = points.at(-1)
+ context.lineTo(last.x, last.y)
+ context.stroke()
+}
+
+/**
+ * 重绘指定根节点中的全部趋势图。
+ * @param {ParentNode} root 查询根节点。
+ */
+export function redrawLatencySparklines(root = document) {
+ for (const canvas of root.querySelectorAll('.latency-sparkline'))
+ drawLatencySparkline(canvas, canvas.dataset.latencyKey)
+}
diff --git a/src/public/parts/shells/debug_info/public/templates/connectivity_item.html b/src/public/parts/shells/debug_info/public/templates/connectivity_item.html
index 4d60203f41..1c132974c1 100644
--- a/src/public/parts/shells/debug_info/public/templates/connectivity_item.html
+++ b/src/public/parts/shells/debug_info/public/templates/connectivity_item.html
@@ -1,6 +1,11 @@
-
- ${name}
-
- ${status === 'ok' ? duration + 'ms' : ''}
-
-
+
+
+
+
+ ${name}
+
+
+
+ ${status === 'ok' ? duration + 'ms' : '—'}
+
+
diff --git a/src/public/parts/shells/debug_info/public/templates/connectivity_list.html b/src/public/parts/shells/debug_info/public/templates/connectivity_list.html
index ff0a55e8e6..a810fb704e 100644
--- a/src/public/parts/shells/debug_info/public/templates/connectivity_list.html
+++ b/src/public/parts/shells/debug_info/public/templates/connectivity_list.html
@@ -1,8 +1,13 @@
${checks.map(check => `
-
- ${check.name}
-
- ${check.status === 'ok' && check.duration != null ? check.duration + 'ms' : ''}
-
-
+
+
+
+
+ ${check.name}
+
+
+
+ ${check.status === 'ok' && check.duration != null ? check.duration + 'ms' : '—'}
+
+
`).join('')}
diff --git a/src/public/parts/shells/debug_info/public/templates/system_info_table.html b/src/public/parts/shells/debug_info/public/templates/system_info_table.html
index c56986c83a..3ec50da2e2 100644
--- a/src/public/parts/shells/debug_info/public/templates/system_info_table.html
+++ b/src/public/parts/shells/debug_info/public/templates/system_info_table.html
@@ -1,6 +1,11 @@
${rows.map(row => `
- ${row.key}
- ${row.val}
+
+
+
+ ${row.key}
+
+
+ ${row.val}
`).join('')}
From b2817805e31faf397dac1a82b2a81e112e048c33 Mon Sep 17 00:00:00 2001
From: Xiaoqiush <1730504362@qq.com>
Date: Wed, 7 Oct 2026 13:12:52 +0800
Subject: [PATCH 2/2] fix(debug-info): reflect connectivity state
Remove the misleading always-healthy header indicator and show active fount network links when no RTT is available. Use emoji-native diagnostic labels and cover the connection-count rendering in the smoke test.
---
src/public/locales/emoji.json | 6 ++---
.../parts/shells/debug_info/public/index.css | 22 -------------------
.../parts/shells/debug_info/public/index.html | 11 +---------
.../public/templates/connectivity_list.html | 2 +-
.../debug_info/test/frontend/smoke.spec.mjs | 8 +++++++
5 files changed, 13 insertions(+), 36 deletions(-)
diff --git a/src/public/locales/emoji.json b/src/public/locales/emoji.json
index 95b4cac5d3..68ef773e3f 100644
--- a/src/public/locales/emoji.json
+++ b/src/public/locales/emoji.json
@@ -6126,9 +6126,9 @@
}
}
},
- "normal": "OK",
- "memoryUsage": "Memory",
- "realtimeLogs": "Live logs",
+ "normal": "✅",
+ "memoryUsage": "🧠📈",
+ "realtimeLogs": "📜🔴",
"update": {
"failed": "❌🔄",
"now": "🆕🔄▶️",
diff --git a/src/public/parts/shells/debug_info/public/index.css b/src/public/parts/shells/debug_info/public/index.css
index 73d2abd16c..db3f41c215 100644
--- a/src/public/parts/shells/debug_info/public/index.css
+++ b/src/public/parts/shells/debug_info/public/index.css
@@ -16,7 +16,6 @@
}
.debug-info-header,
-.header-title-row,
.panel-heading,
.version-heading,
.version-panel,
@@ -37,27 +36,6 @@
padding: .5rem 0 .75rem;
}
-.header-title-row {
- gap: 1rem;
- flex-wrap: wrap;
-}
-
-.page-status {
- display: inline-flex;
- align-items: center;
- gap: .35rem;
- color: var(--text-muted);
- font-size: .8rem;
-}
-
-.status-dot {
- width: .55rem;
- height: .55rem;
- border-radius: var(--radius-selector);
- background: var(--debug-accent);
- box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--debug-accent) 14%, transparent);
-}
-
.debug-info-header h1 {
font-family: var(--font-heading);
font-size: clamp(1.6rem, 3vw, 2.2rem);
diff --git a/src/public/parts/shells/debug_info/public/index.html b/src/public/parts/shells/debug_info/public/index.html
index 99fadb223e..0ef3d1e1f5 100644
--- a/src/public/parts/shells/debug_info/public/index.html
+++ b/src/public/parts/shells/debug_info/public/index.html
@@ -24,16 +24,7 @@
-
diff --git a/src/public/parts/shells/debug_info/public/templates/connectivity_list.html b/src/public/parts/shells/debug_info/public/templates/connectivity_list.html
index a810fb704e..38f2e3aa44 100644
--- a/src/public/parts/shells/debug_info/public/templates/connectivity_list.html
+++ b/src/public/parts/shells/debug_info/public/templates/connectivity_list.html
@@ -7,7 +7,7 @@
- ${check.status === 'ok' && check.duration != null ? check.duration + 'ms' : '—'}
+ ${check.status === 'ok' && check.duration != null ? check.duration + 'ms' : check.status === 'ok' && check.activeLinks != null ? `` : '—'}
`).join('')}
diff --git a/src/public/parts/shells/debug_info/test/frontend/smoke.spec.mjs b/src/public/parts/shells/debug_info/test/frontend/smoke.spec.mjs
index 6092e51a7f..5bd8d6c512 100644
--- a/src/public/parts/shells/debug_info/test/frontend/smoke.spec.mjs
+++ b/src/public/parts/shells/debug_info/test/frontend/smoke.spec.mjs
@@ -5,13 +5,21 @@ import { test, expect } from './fixtures.mjs'
test.describe('debug_info shell smoke', () => {
test('debug info page boots with system table', async ({ page, baseUrl }) => {
+ await page.route('**/api/parts/shells:debug_info/system_info', route => route.fulfill({ json: {
+ os: { platform: 'test', release: '1', arch: 'x64' },
+ cpu: { model: 'test', cores: 1, speed: 1 },
+ memory: { total: 1, free: 0 },
+ connectivity: [{ id: 'fount-network', name: 'fount Network', status: 'ok', activeLinks: 2 }],
+ } }))
await page.goto(`${baseUrl}/parts/shells:debug_info/`, { waitUntil: 'domcontentloaded' })
await expect(page.locator('main')).toBeVisible({ timeout: 30_000 })
await expect(page.locator('#copy-button')).toBeVisible()
await expect(page.locator('h1[data-i18n="debug_info.heading"]')).toBeVisible()
+ await expect(page.locator('.page-status')).toHaveCount(0)
const systemTable = page.locator('#system-info-table')
await expect(systemTable).toBeVisible()
await expect(systemTable.locator('tr').first()).toBeVisible({ timeout: 30_000 })
+ await expect(page.locator('[data-i18n="debug_info.linksCount"][data-count="2"]')).toBeVisible()
})
test('update button surfaces the auto-update-disabled notice instead of staying silently disabled', async ({ page, baseUrl }) => {
-
+
+
+
+
diff --git a/src/public/parts/shells/debug_info/public/index.mjs b/src/public/parts/shells/debug_info/public/index.mjs
index b5f006bc14..40c0abdf6a 100644
--- a/src/public/parts/shells/debug_info/public/index.mjs
+++ b/src/public/parts/shells/debug_info/public/index.mjs
@@ -6,6 +6,7 @@ import { onServerEvent } from '/scripts/endpoints/server_events.mjs'
import { ping } from '/scripts/endpoints/base.mjs'
import { createTestStatusWs, getSystemInfo, postRestart } from './src/endpoints.mjs'
import { mountTemplate, renderTemplate } from './templates.mjs'
+import { recordLatency, redrawLatencySparklines } from './src/latency_sparkline.mjs'
applyTheme()
await initTranslations('debug_info')
@@ -40,9 +41,34 @@ let isUpToDate = null
* @param {number} bytes - 字节数。
* @returns {string} 以 GiB 为单位保留两位小数的字符串。
*/
-const bytesToGiB = bytes => (bytes / 1024 ** 3).toFixed(2)
+const bytesToGiB = bytes => Number.isFinite(bytes) && bytes >= 0 ? (bytes / 1024 ** 3).toFixed(2) : 'Unknown'
+
+function getMemoryStats(memory = {}) {
+ const total = Number(memory.total)
+ const free = Number(memory.free)
+ if (!Number.isFinite(total) || !Number.isFinite(free) || total <= 0) return { used: null, percent: null }
+ const used = Math.max(0, total - Math.min(free, total))
+ return { used, percent: Math.round(used / total * 100) }
+}
+
+function setBadgeState(element, state) {
+ element.dataset.state = state
+}
const FOUNT_REPO_COMMITS = 'https://api.github.com/repos/steve02081504/fount/commits'
+const ICONIFY_LINE = 'https://api.iconify.design/line-md'
+const SERVICE_ICONS = new Map([
+ ['npm Registry', 'square'],
+ ['Deno Land', 'engine'],
+ ['jsDelivr', 'cloud'],
+ ['JSR', 'list-3-filled'],
+ ['fount Network', 'link'],
+ ['fount Server', 'computer'],
+ ['esm.sh', 'cloud-alt-braces'],
+ ['Iconify', 'image'],
+ ['fount Public', 'external-link'],
+])
+const serviceIcon = name => `${ICONIFY_LINE}/${SERVICE_ICONS.get(name) || 'square'}.svg`
/**
* 获取 GitHub 上指定分支的最新提交 SHA。
@@ -75,11 +101,11 @@ async function fetchVersionInfo() {
debugData.version.remote = remoteVer
isUpToDate = localVer === remoteVer
- versionIndicator.className = `badge badge-lg ${isUpToDate ? 'badge-success' : 'badge-error'} gap-2`
+ setBadgeState(versionIndicator, isUpToDate ? 'ok' : 'outdated')
versionIndicator.dataset.i18n = isUpToDate ? 'debug_info.versionStatus.upToDate' : 'debug_info.versionStatus.outdated'
} catch (error) {
console.error('Version check failed:', error)
- versionIndicator.className = 'badge badge-lg badge-warning gap-2'
+ setBadgeState(versionIndicator, 'failed')
versionIndicator.dataset.i18n = 'debug_info.versionStatus.checkFailed'
isUpToDate = null
}
@@ -97,17 +123,24 @@ async function fetchSystemInfo() {
const data = await getSystemInfo()
debugData.system = data
- const { os, cpu, memory, connectivity } = data
+ const { os = {}, cpu = {}, memory = {}, connectivity = [] } = data
+ const { percent } = getMemoryStats(memory)
+ document.getElementById('memory-percent').textContent = percent == null ? '--%' : `${percent}%`
+ document.getElementById('memory-progress').value = percent ?? 0
const rows = [
- { key: 'OS', val: `${os.platform} ${os.release} (${os.arch})` },
- { key: 'CPU', val: `${cpu.model} (${cpu.cores} cores) @ ${cpu.speed}MHz` },
- { key: 'Memory', val: `Total: ${bytesToGiB(memory.total)} GB / Free: ${bytesToGiB(memory.free)} GB` },
+ { key: 'OS', icon: `${ICONIFY_LINE}/monitor.svg`, val: `${os.platform} ${os.release} (${os.arch})` },
+ { key: 'CPU', icon: `${ICONIFY_LINE}/cog.svg`, val: `${cpu.model} (${cpu.cores} cores) @ ${cpu.speed}MHz` },
+ { key: 'Memory', icon: `${ICONIFY_LINE}/engine.svg`, val: `Total: ${bytesToGiB(memory.total)} GB / Free: ${bytesToGiB(memory.free)} GB` },
]
await mountTemplate(systemInfoTable, 'system_info_table', { rows })
debugData.connectivity.backend = connectivity
- await mountTemplate(backendChecks, 'connectivity_list', { checks: connectivity })
+ for (const check of connectivity) recordLatency(check.id || check.name, check.duration)
+ await mountTemplate(backendChecks, 'connectivity_list', {
+ checks: connectivity.map(check => ({ ...check, icon: serviceIcon(check.name) })),
+ })
+ redrawLatencySparklines(backendChecks)
} catch (error) {
console.error('System info fetch failed:', error)
systemInfoTable.innerHTML = '
+
+
-
-
+
-
-
-
- fount
-
-
-
-