diff --git a/patch_v2/meowpanel/web/index.html b/patch_v2/meowpanel/web/index.html index fec98d3..5479279 100644 --- a/patch_v2/meowpanel/web/index.html +++ b/patch_v2/meowpanel/web/index.html @@ -21,6 +21,10 @@ MeowPanel + + + 首页 + 性能 @@ -33,6 +37,10 @@ 音频 + + + 电源 + 网络 @@ -61,6 +69,10 @@ 屏幕 + + + 进程·服务 + 系统 @@ -82,7 +94,7 @@ - 实时性能 + 首页仪表盘 检测中… @@ -141,6 +153,22 @@ + + + + 修改 PIN + + + + + + + 取消 + 确定 + + + + diff --git a/patch_v2/meowpanel/web/meowpanel.css b/patch_v2/meowpanel/web/meowpanel.css index ea590dc..503eb8d 100644 --- a/patch_v2/meowpanel/web/meowpanel.css +++ b/patch_v2/meowpanel/web/meowpanel.css @@ -381,3 +381,83 @@ input[type=range]{ width:100%; accent-color:var(--primary); } .topbar{ padding:0 10px; gap:8px; } .seg-btn{ padding:6px 9px; } } + +/* ================= 批1-B:首页仪表盘 / 电平表 / 电源 / 进程 / PIN / 网络横幅 ================= */ + +/* 首页卡片网格 */ +.home-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:16px; } +.home-grid .card{ margin-bottom:0; } +.home-sub{ color:var(--on-surface-variant); font-size:12px; margin-top:4px; } +.home-dash{ font-size:20px; font-weight:700; } +.home-unit{ font-size:12px; font-weight:400; color:var(--on-surface-variant); margin-left:4px; } +.home-bigval{ font-size:30px; font-weight:700; line-height:1.1; } +.home-bigunit{ font-size:14px; font-weight:400; color:var(--on-surface-variant); margin-left:4px; } +.home-chip{ display:inline-block; padding:1px 8px; border-radius:999px; font-size:11px; vertical-align:middle; } +.home-chip.ok{ background:rgba(76,175,80,.18); color:var(--success); } +.home-chip.warn{ background:rgba(255,179,0,.18); color:var(--warning); } +.home-chip.bad{ background:rgba(207,102,121,.18); color:var(--error); } +.home-chip.mut{ background:var(--surface-variant); color:var(--on-surface-variant); } +.home-card-grid{ display:grid; grid-template-columns:1fr 1fr; gap:6px 16px; } +.home-kv{ display:flex; justify-content:space-between; gap:10px; font-size:13px; padding:3px 0; } +.home-kv .k{ color:var(--on-surface-variant); } +.home-kv .v{ text-align:right; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.dash-strip{ display:flex; align-items:center; gap:8px; margin-top:8px; } + +/* 电平表(rms 青绿 / peak 黄,刻度 -60..0 dBFS) */ +.lvl{ margin-top:8px; } +.lvl-row{ display:flex; align-items:center; gap:10px; margin-bottom:8px; } +.lvl-lbl{ width:44px; color:var(--on-surface-variant); font-size:12px; font-weight:600; } +.lvl-meter{ flex:1; height:14px; border-radius:99px; background:var(--surface-dim); overflow:hidden; border:var(--outline-w) solid var(--outline-variant); } +.lvl-fill{ height:100%; border-radius:99px; transition:width .18s ease; } +.lvl-fill.rms{ background:#00bfa5; } +.lvl-fill.peak{ background:#ffb300; } +.lvl-val{ width:58px; text-align:right; font-family:var(--mono); font-size:12px; color:var(--on-surface-variant); } +.lvl-scale{ display:flex; justify-content:space-between; color:var(--on-surface-variant); font-size:10px; padding:2px 68px 0 54px; } +.lvl-spark{ display:block; width:100%; height:46px; margin-top:8px; } +.lvl-spark .grid{ stroke:var(--outline-variant); stroke-width:1; } +.lvl-note{ color:var(--on-surface-variant); font-size:12px; margin-top:6px; } + +/* 置信度徽标(measured/derived/estimated) */ +.conf-badge{ display:inline-flex; align-items:center; gap:5px; padding:3px 12px; border-radius:999px; font-size:12px; font-weight:600; } +.conf-badge.measured{ background:rgba(76,175,80,.18); color:var(--success); } +.conf-badge.derived{ background:rgba(74,139,245,.18); color:var(--primary); } +.conf-badge.estimated{ background:rgba(255,179,0,.18); color:var(--warning); } +.conf-badge.none{ background:var(--surface-variant); color:var(--on-surface-variant); } + +/* 子系统分解堆叠条 */ +.stack{ display:flex; height:24px; border-radius:99px; overflow:hidden; background:var(--surface-dim); border:var(--outline-w) solid var(--outline-variant); margin-top:10px; } +.stack > span{ height:100%; } +.stack-legend{ display:flex; flex-wrap:wrap; gap:8px 16px; margin-top:12px; font-size:12px; } +.stack-legend .sw{ display:inline-block; width:12px; height:12px; border-radius:3px; margin-right:5px; vertical-align:middle; } +.stack-legend .seg{ display:inline-flex; align-items:center; } +.up-strip{ display:flex; align-items:center; gap:10px; } + +/* 电池展示 */ +.batt{ display:flex; align-items:center; gap:14px; } +.batt-icon{ position:relative; width:40px; height:20px; border:2px solid var(--on-surface-variant); border-radius:4px; overflow:hidden; flex:0 0 40px; } +.batt-icon::after{ content:""; position:absolute; right:-5px; top:5px; width:3px; height:8px; background:var(--on-surface-variant); border-radius:0 2px 2px 0; } +.batt-icon .lvl-fill{ background:var(--success); } +.batt-icon .lvl-fill.warn{ background:var(--warning); } +.batt-icon .lvl-fill.bad{ background:var(--error); } +.batt-icon .lvl-fill.charge{ background:var(--primary); } + +/* 进程/服务页高亮与行内日志 */ +.restart-hi{ color:var(--error); font-weight:700; } +.crash-hi{ color:var(--error); font-weight:700; } +.proc-row{ cursor:pointer; } +.proc-tail{ margin-top:8px; } +.proc-badge{ display:inline-block; padding:1px 7px; border-radius:999px; font-size:11px; font-family:var(--mono); background:var(--surface-variant); color:var(--on-surface-variant); margin-right:4px; } +.proc-badge.board{ background:rgba(74,139,245,.18); color:var(--primary); } +.proc-badge.tf{ background:rgba(255,179,0,.18); color:var(--warning); } + +/* 网络页内联错误横幅(不再整页踢「不可用」占位) */ +.page-banner{ display:flex; align-items:center; gap:12px; padding:10px 14px; border-radius:var(--radius-m); background:rgba(207,102,121,.13); color:var(--error); margin-bottom:14px; border:var(--outline-w) solid var(--outline-variant); font-size:13px; } +.page-banner .ico{ font-size:16px; } +.page-banner button{ margin-left:auto; } + +/* 设置页 PIN 卡 */ +.pin-status{ font-size:13px; } +.pin-status .dot{ display:inline-block; width:10px; height:10px; border-radius:50%; margin-right:6px; vertical-align:middle; } +.pin-status .dot.set{ background:var(--success); } +.pin-status .dot.unset{ background:var(--warning); } +.pin-status .dot.locked{ background:var(--error); } diff --git a/patch_v2/meowpanel/web/meowpanel.js b/patch_v2/meowpanel/web/meowpanel.js index b1ea3a2..76635f5 100644 --- a/patch_v2/meowpanel/web/meowpanel.js +++ b/patch_v2/meowpanel/web/meowpanel.js @@ -35,6 +35,86 @@ function debounce(fn, ms) { let t = null; return function () { const a = arguments, c = this; clearTimeout(t); t = setTimeout(() => fn.apply(c, a), ms); }; } function pct(p) { return Math.max(0, Math.min(100, p)); } + /* ---------------- 音频电平表 / 仪表盘工具(deci-dB → dBFS;-990 = -inf) ---------------- */ + function deciToDb(v) { return (v == null || v <= -990) ? null : v / 10; } + function fmtDb(v) { + const db = deciToDb(v); + if (db == null) return '−∞'; + return (db > 0 ? '+' : '') + db.toFixed(1); + } + function dbPct(v) { // -60..0 dBFS 映射到 0..100% 条宽 + const db = deciToDb(v); + if (db == null) return 0; + return Math.max(0, Math.min(100, (db + 60) / 60 * 100)); + } + function isAvail(b) { return !!(b && b.available !== false); } + const sparkRms = [], sparkPeak = []; + const SPARK_MAX = 30; + function sparklineHtml() { + return '' + + '' + + '' + + '' + + '' + + ''; + } + function sparklinePoints(arr) { + const n = arr.length; + if (!n) return ''; + return arr.map((v, i) => { + const x = (i / (SPARK_MAX - 1)) * 100; + return x.toFixed(1) + ',' + (46 - Math.max(0, Math.min(100, v)) / 100 * 40).toFixed(1); + }).join(' '); + } + function pushSpark(v, arr) { // deci-dB → 0..100 百分比入队(<=30 点) + arr.push(dbPct(v)); + if (arr.length > SPARK_MAX) arr.shift(); + } + function drawSpark(rootEl) { + const svg = rootEl && rootEl.querySelector('.lvl-spark'); + if (!svg) return; + const r = svg.querySelector('.spk-rms'), p = svg.querySelector('.spk-peak'); + if (r) r.setAttribute('points', sparklinePoints(sparkRms)); + if (p) p.setAttribute('points', sparklinePoints(sparkPeak)); + } + function resetSpark() { sparkRms.length = 0; sparkPeak.length = 0; } + function levelMeterHtml() { + return '' + + 'RMS' + + '' + + '−∞' + + 'PEAK' + + '' + + '−∞' + + '-60-40-200 dBFS' + + sparklineHtml() + ''; + } + function updateLevelMeter(rootEl, d) { + if (!rootEl) return; + const rms = (d && d.available) ? d.rms : null; + const peak = (d && d.available) ? d.peak : null; + const fr = rootEl.querySelector('.lvl-fill.rms'); + const fp = rootEl.querySelector('.lvl-fill.peak'); + const vr = rootEl.querySelector('.lvl-val[data-val="rms"]'); + const vp = rootEl.querySelector('.lvl-val[data-val="peak"]'); + if (fr) fr.style.width = dbPct(rms) + '%'; + if (fp) fp.style.width = dbPct(peak) + '%'; + if (vr) vr.textContent = fmtDb(rms); + if (vp) vp.textContent = fmtDb(peak); + pushSpark(rms, sparkRms); pushSpark(peak, sparkPeak); + drawSpark(rootEl); + } + function homeKv(k, v) { + return '' + esc(k) + '' + v + ''; + } + function homeUnavailable() { return '— 数据不可用'; } + function homeCard(title, body) { return '' + esc(title) + '' + body + ''; } + function modePill(m) { + if (!m) return '—'; + const cls = (m === 'meow') ? 'ok' : ((m === 'hiby') ? 'warn' : 'mut'); + return '' + esc(m) + ''; + } + /* ---------------- 全局状态 ---------------- */ const state = { style: 'md3', mode: 'dark', // 已解析的风格/明暗(应用到 data-style/data-mode) @@ -419,7 +499,9 @@ }); } - /* ---------------- 2b. 音频页(meow_audiod 状态快照,5s 轮询,只读) ---------------- */ + /* ---------------- 2b. 音频页(meow_audiod 状态快照,1s 轮询,只读;含 rms/peak 电平可视化) ---------------- + * 电平:rms(青绿) + peak(黄) 双条 dBFS 表(-60..0 刻度)+ deci-dB 数值 + 近 30 采样 sparkline(客户端自累积)。 + * 短流时显示「无流」;audiod 不在 → available:false。 */ let audioTimer = null; function stopAudio() { if (audioTimer) { clearInterval(audioTimer); audioTimer = null; } } function audioPortName(p) { // audiod numid=9 路由口语义(AUDIOD_DESIGN §5) @@ -428,7 +510,7 @@ return m[p] || ('端口 ' + p); } function fmtFrames(n) { return (n == null) ? '—' : Number(n).toLocaleString(); } - function renderAudioData(page, d) { + function audioFlowHtml(d) { let h = ''; if (!d || !d.available) { h += '音频服务(meow_audiod)' + @@ -456,22 +538,454 @@ row('音量', (d.vol != null && d.vol >= 0 ? esc(d.vol) + ' %' : '—')) + ''; } - h += '每 5 秒自动刷新 · 只读展示(测试音请用设备 CLI meow_audio tone,面板不放声)。'; - page.innerHTML = h; + return h; } async function fetchAudioOnce() { - const page = $('#audio-page'); - if (!page || state.route !== 'audio') return; + const flowEl = $('#audio-flow'); + if (!flowEl || state.route !== 'audio') return; let d; try { d = await api('GET', '/api/audio'); } - catch (e) { if (page && state.route === 'audio') page.innerHTML = emptyState('加载失败:' + errText(e), e.code); return; } - if (state.route === 'audio') renderAudioData(page, d); + catch (e) { + if (flowEl && state.route === 'audio') flowEl.innerHTML = '加载失败:' + esc(errText(e)) + ''; + return; + } + if (state.route !== 'audio') return; + flowEl.innerHTML = audioFlowHtml(d); + updateLevelMeter($('#audio-level'), d); + const note = $('#audio-level-note'); + if (note) { + if (!d || !d.available) note.innerHTML = 'audiod 不可用 · 每 1 秒自动刷新 · 只读展示。'; + else if (!d.active) note.innerHTML = '当前无流 · 电平保持 −∞ · 每 1 秒自动刷新。'; + else note.innerHTML = '每 1 秒自动刷新 · 只读展示(测试音请用设备 CLI meow_audio tone,面板不放声)。'; + } } function renderAudio() { - $('#content').innerHTML = '加载音频状态…'; + resetSpark(); // 进入页面重置 sparkline 累积 stopAudio(); + $('#content').innerHTML = + '' + + '加载音频状态…' + + '电平(dBFS)' + + '' + levelMeterHtml() + '' + + '每 1 秒自动刷新 · 只读展示(测试音请用设备 CLI meow_audio tone,面板不放声)。' + + '' + + ''; fetchAudioOnce(); - audioTimer = setInterval(fetchAudioOnce, 5000); + audioTimer = setInterval(fetchAudioOnce, 1000); + } + + /* ---------------- 2d. 首页仪表盘(#/home,默认路由;/api/overview 聚合,5s 节拍 + 电平 1s 子节拍) ---------------- + * 单请求 /api/overview 供全部卡片;另低频补充 /api/system(boot/默认启动,静态)+ /api/net/dns(DNS 上游/缓存)。 + * 任一源 available:false / 缺失 → 该卡显示「—」,绝不拖垮其它卡。 */ + let homeTimer = null, homeLevelTimer = null; + let homeData = null, homeDns = null, homeSys = null; + function stopHome() { + if (homeTimer) { clearInterval(homeTimer); homeTimer = null; } + if (homeLevelTimer) { clearInterval(homeLevelTimer); homeLevelTimer = null; } + } + /* 设备信息卡:版本/hostname/内核/架构/运行模式/默认启动(boot 来自 /api/system 一次性拉取) */ + function deviceCard() { + const p = (homeData && homeData.perf && homeData.perf.system); + const s = homeSys; + const ver = (s && s.version) || (p && p.version) || '—'; + const host = (s && s.hostname) || (p && p.hostname) || '—'; + const ker = (s && s.kernel) || (p && p.kernel) || '—'; + const arch = (s && s.arch) || (p && p.arch) || '—'; + const bootD = (s && s.boot && s.boot.default) || '—'; + const cur = (s && s.boot) ? (s.boot.mode || s.boot.current || null) : null; + let rows = ''; + rows += homeKv('版本', esc(ver)); + rows += homeKv('主机名', esc(host)); + rows += homeKv('内核', esc(ker)); + rows += homeKv('架构', esc(arch)); + rows += homeKv('运行模式', modePill(cur)); + rows += homeKv('默认启动', modePill(bootD)); + return homeCard('设备信息', '' + rows + ''); + } + function batteryCard() { + const pw = homeData && homeData.power; + if (!isAvail(pw)) return homeCard('电量 / 充电', homeUnavailable()); + const b = pw.battery || {}; + const pct = (b.pct != null) ? b.pct : null; + const state = b.state || '—'; + const charging = (state === 'Charging' || state === 'Full'); + const fillCls = (!pct) ? '' : (pct >= 85 ? 'charge' : (pct <= 20 ? 'bad' : '')); + let inner = '' + + '' + (pct == null ? '—' : pct) + '%' + + '' + esc(state) + ''; + inner += '' + + homeKv('电压', b.mv != null ? (b.mv / 1000).toFixed(2) + ' V' : '—') + + homeKv('健康', b.health ? esc(b.health) : '—') + + homeKv('温度', b.temp_c10 != null ? (b.temp_c10 / 10).toFixed(1) + ' ℃' : '—') + + (b.est_minutes_left != null ? homeKv('续航估算', b.est_minutes_left + ' 分钟') : + (b.est_minutes_full != null ? homeKv('充满估算', b.est_minutes_full + ' 分钟') : '')) + + ''; + return homeCard('电量 / 充电', inner); + } + function audioCard() { + const ad = homeData && homeData.audio; + if (!isAvail(ad)) return homeCard('音频', homeUnavailable()); + let body = ''; + if (ad.active) { + body += '' + + homeKv('流', esc(ad.app || '—') + ' · ' + esc(ad.rate) + 'Hz ' + esc(ad.fmt || '')) + + homeKv('输出口', audioPortName(ad.port)) + + homeKv('音量', (ad.vol != null && ad.vol >= 0 ? esc(ad.vol) + ' %' : '—')) + + ''; + } else { + body += '当前无活跃音频流。'; + } + body += '' + levelMeterHtml() + '' + + '' + (ad.active ? '电平 · 1s 子节拍' : '无流') + ''; + return homeCard('音频', body); + } + function netCard() { + const nd = homeData && homeData.net; + if (!isAvail(nd)) return homeCard('WiFi / DNS', homeUnavailable()); + const w = nd.wifi || {}; + const dn = homeDns; + let rows = ''; + rows += homeKv('WiFi', w.connected ? ('已连接 ' + esc(w.ssid || '')) : '未连接'); + if (w.connected) { + rows += homeKv('IP', esc(w.ip || '—')); + rows += homeKv('信号', (w.rssi != null ? esc(w.rssi) + ' dBm' : '—')); + } + if (dn) { + rows += homeKv('DNS 上游', esc(dn.upstream || '—')); + rows += homeKv('DNS 缓存', (dn.cache_hits || 0) + ' / ' + (dn.cache_total || 0) + (dn.enabled ? '' : '(停用)')); + } else { + rows += homeKv('DNS', '—'); + } + return homeCard('WiFi / DNS', '' + rows + ''); + } + function healthCard() { + const ams = homeData && homeData.ams; + const svcd = homeData && homeData.svcd; + let appTxt = '—', svcBad = 0, svcHi = 0, svcTxt = '—'; + if (isAvail(ams)) { + const apps = ams.apps || []; + const run = apps.filter(a => a.state === 'fg' || a.state === 'bg').length; + appTxt = run + ' 在跑 / ' + (ams.count || apps.length) + ' 已装'; + } + let svcHtml; + if (isAvail(svcd)) { + const svcs = svcd.services || []; + svcBad = svcs.filter(s => s.state === 'failed').length; + svcHi = svcs.filter(s => (s.restart || 0) >= 3).length; + svcTxt = svcs.filter(s => s.state === 'running').length + ' 运行中'; + svcHtml = ''; + if (svcBad) svcHtml += '' + svcBad + ' FAILED '; + if (svcHi) svcHtml += '' + svcHi + ' 高重启 '; + if (!svcBad && !svcHi) svcHtml += '正常 '; + } else { + svcHtml = '不可用 '; + svcTxt = '—'; + } + return homeCard('在跑 App 与服务', '' + + homeKv('在跑 App', esc(appTxt)) + homeKv('服务健康', svcHtml + esc(svcTxt)) + ''); + } + function storageCard() { + const perf = homeData && homeData.perf; + if (!isAvail(perf)) return homeCard('存储', homeUnavailable()); + const st = perf.storage || {}, tf = perf.tfcard || {}; + let rows = homeKv('rootfs', (st.used != null ? fmtBytes(st.used * 1024) : '—') + ' / ' + (st.total != null ? fmtBytes(st.total * 1024) : '—')); + rows += (tf.present) ? + homeKv('TF 卡', fmtBytes((tf.used || 0) * 1024) + ' / ' + fmtBytes((tf.total || 0) * 1024)) : + homeKv('TF 卡', '未插卡'); + return homeCard('存储', '' + rows + ''); + } + function perfCard() { + const perf = homeData && homeData.perf; + if (!isAvail(perf)) return homeCard('性能小条', homeUnavailable()); + const cpu = perf.cpu || {}, mem = perf.memory || {}, tmp = perf.temp || {}; + const cpuU = cpu.usage; + const memP = mem.total ? (mem.used / mem.total) * 100 : 0; + return homeCard('性能小条', '' + + 'CPU' + (cpuU == null ? '—' : (+cpuU).toFixed(1)) + '%' + barHtml(cpuU || 0) + '' + + '内存' + (mem.total ? fmtBytes(mem.used * 1024) : '—') + '' + barHtml(mem.used && mem.total ? memP : 0) + '' + + '温度' + (tmp.cpu != null ? (+tmp.cpu).toFixed(1) + '℃' : '—') + '' + + ''); + } + function renderHomeCards() { + const body = $('#home-body'); + if (!body || state.route !== 'home') return; + if (!homeData) { body.innerHTML = '加载总览…'; return; } + let h = '' + + deviceCard() + batteryCard() + audioCard() + netCard() + healthCard() + storageCard() + perfCard() + + ''; + body.innerHTML = h; + const ad = homeData.audio; + if (isAvail(ad)) updateLevelMeter($('#home-audio-level'), ad); + } + async function fetchHomeSystem() { // 静态设备信息(boot/默认启动);进入时拉一次,失败不阻塞 + try { homeSys = await api('GET', '/api/system'); } catch (e) { homeSys = null; } + if (state.route === 'home') renderHomeCards(); + } + async function fetchHomeLevel() { // 电平 1s 子节拍(独立于 overview 5s) + if (state.route !== 'home') return; + const el = $('#home-audio-level'); if (!el) return; + let d; + try { d = await api('GET', '/api/audio'); } catch (e) { return; } + if (state.route !== 'home') return; + updateLevelMeter(el, d); + const note = $('#home-audio-note'); + if (note) note.textContent = (!d || !d.available) ? 'audiod 不可用' : (d.active ? '电平 · 1s 子节拍' : '无流'); + } + async function fetchOverviewOnce() { + if (state.route !== 'home') return; + const r = await Promise.allSettled([api('GET', '/api/net/dns'), api('GET', '/api/overview')]); + if (r[1].status !== 'fulfilled') { + const b = $('#home-banner'); + if (b) { + b.innerHTML = '⚠️总览加载失败:' + esc(errText(r[1].reason)) + '重试'; + b.hidden = false; + } + return; + } + const b = $('#home-banner'); if (b) { b.hidden = true; b.innerHTML = ''; } + homeDns = (r[0].status === 'fulfilled') ? r[0].value : null; + homeData = r[1].value; + renderHomeCards(); + } + function renderHome() { + stopHome(); + resetSpark(); // 进入首页重置 sparkline 累积 + homeData = null; homeDns = null; homeSys = null; + $('#content').innerHTML = + '' + + '' + + '加载总览…' + + ''; + fetchHomeSystem(); + fetchOverviewOnce(); + homeTimer = setInterval(fetchOverviewOnce, 5000); + homeLevelTimer = setInterval(fetchHomeLevel, 1000); + } + function bindHome() { + $('#content').addEventListener('click', function (e) { + if (e.target.closest('#home-retry')) { fetchOverviewOnce(); return; } + }); + } + + /* ---------------- 2e. 电源 / 功耗页(#/power;/api/power,只读) ---------------- + * 电池卡 + 功耗估算卡(est_mw + confidence 徽标,必须如实标注)+ 子系统分解堆叠条。 */ + let powerTimer = null; + function stopPower() { if (powerTimer) { clearInterval(powerTimer); powerTimer = null; } } + function batteryCardBody(b) { + if (!b) return homeUnavailable(); + const pct = (b.pct != null) ? b.pct : null; + const state = b.state || '—'; + const charging = (state === 'Charging' || state === 'Full'); + const fillCls = (!pct) ? '' : (pct >= 85 ? 'charge' : (pct <= 20 ? 'bad' : '')); + let h = '' + + '' + (pct == null ? '—' : pct) + '%' + + '' + esc(state) + ''; + h += '' + + row('电压', b.mv != null ? (b.mv / 1000).toFixed(2) + ' V' : '—') + + row('健康', b.health ? esc(b.health) : '—') + + row('技术', b.technology ? esc(b.technology) : '—') + + row('电池温度', b.temp_c10 != null ? (b.temp_c10 / 10).toFixed(1) + ' ℃' : '—') + + row('充电器温度', b.charger_temp_c10 != null ? (b.charger_temp_c10 / 10).toFixed(1) + ' ℃' : '—') + + row('循环次数', b.cycle_count != null ? b.cycle_count : '—') + + row('放电速率', b.rate_pct_min_x100 != null ? ((b.rate_pct_min_x100 / 100).toFixed(2) + ' %/min') : '—') + + row('续航估算', b.est_minutes_left != null ? (b.est_minutes_left + ' 分钟(剩余)') : (b.est_minutes_full != null ? (b.est_minutes_full + ' 分钟(充满)') : '—')) + + ''; + return h; + } + function estimateCardBody(est) { + if (!est) return homeUnavailable(); + if (est.est_mw == null) return '' + (est.degraded ? '功耗估算不可用(HibyOS 只读模式)' : '—') + ''; + const conf = est.confidence || 'estimated'; + const cls = conf === 'measured' ? 'measured' : (conf === 'derived' ? 'derived' : (conf === 'none' ? 'none' : 'estimated')); + return '' + est.est_mw + 'mW' + + '' + esc(conf) + (est.calibrated ? ' · 已标定' : ' · 经验值') + '' + + 'Model 系数全部为经验初值(未真机标定);confidence 必须如实标注。'; + } + function breakdownCardBody(bd) { + if (!bd) return homeUnavailable(); + if (bd.degraded) return '系统分解不可用(HibyOS 只读模式)'; + const comps = [ + { label: '屏幕(背光)', mw: bd.backlight_mw, color: '#4a8bf5' }, + { label: 'WiFi', mw: bd.wifi_mw, color: '#00bfa5' }, + { label: '蓝牙', mw: bd.bt_mw, color: '#ab47bc' }, + { label: '音频', mw: bd.audio_mw, color: '#ffb300' }, + { label: 'CPU', mw: bd.cpu_mw, color: '#ff7043' }, + { label: '基线', mw: bd.baseline_mw, color: '#8d6e63' } + ].filter(c => c.mw != null && c.mw >= 0); + const total = comps.reduce((a, c) => a + c.mw, 0); + let stack = ''; + comps.forEach(c => { stack += ''; }); + stack += ''; + let legend = ''; + comps.forEach(c => { + legend += '' + esc(c.label) + ' ' + c.mw + ' mW (' + (total > 0 ? (c.mw / total * 100).toFixed(1) : '0') + '%)'; + }); + legend += ''; + if (bd.cpu_util_pct != null || bd.brightness_pct != null || bd.wifi_state || bd.bt_state || bd.audio_state) { + legend += '' + + (bd.cpu_util_pct != null ? 'CPU 利用率 ' + esc(bd.cpu_util_pct) + '% ' : '') + + (bd.brightness_pct != null ? '· 亮度 ' + esc(bd.brightness_pct) + '% ' : '') + + (bd.wifi_state ? '· WiFi ' + esc(bd.wifi_state) : '') + + (bd.bt_state ? ' · BT ' + esc(bd.bt_state) : '') + + (bd.audio_state ? ' · 音频 ' + esc(bd.audio_state) : '') + ''; + } + return '合计 ' + total + ' mW(分解为经验估算)' + stack + legend; + } + function renderPowerBody(d) { + let h = ''; + if (d && d.mode === 'readonly') h += 'ℹ️当前为电源只读观测(HibyOS 模式),仅显示电池采样,无功耗估算/分解。'; + h += '电池' + ((d && d.battery) ? batteryCardBody(d.battery) : homeUnavailable()) + ''; + h += '功耗估算' + ((d && d.estimate) ? estimateCardBody(d.estimate) : homeUnavailable()) + ''; + h += '子系统分解' + ((d && d.breakdown) ? breakdownCardBody(d.breakdown) : homeUnavailable()) + ''; + if (d && d.policy == null) h += '电源策略(profile 四档/低电阈值)为 期3 功能,此处以不可用占位。'; + h += ''; + return h; + } + async function fetchPowerOnce() { + const el = $('#power-page'); + if (!el || state.route !== 'power') return; + let d; + try { d = await api('GET', '/api/power'); } + catch (e) { if (el && state.route === 'power') el.innerHTML = '' + emptyState('加载失败:' + errText(e), e.code) + ''; return; } + if (state.route !== 'power') return; + el.innerHTML = renderPowerBody(d); + } + function renderPower() { + stopPower(); + $('#content').innerHTML = '加载电源数据…'; + fetchPowerOnce(); + powerTimer = setInterval(fetchPowerOnce, 5000); + } + + /* ---------------- 2f. 进程 / 服务页(#/proc;/api/svcd + /api/ams,只读,无 restart/kill) ---------------- + * svcd 服务表(state/pid/restart 计数≥3 高亮/exec/来源 board|tf)+ ams App 表(state/pid/crashes/launches/last_kind); + * 行点开经 /api/files 列 MeowOS/logs// 找最新 stdout_*.log,再用 /api/logs/read 读尾部(崩溃尾巴)。 */ + let procTimer = null; + let expandedProc = null; // { pkg, file, lines, truncated }:已展开的 App stdout 尾部(重渲染后重开,防轮询清空) + function stopProc() { if (procTimer) { clearInterval(procTimer); procTimer = null; } } + function svcStatePill(s) { + const map = { running: ['ok', '运行'], stopped: ['mut', '已停'], stopping: ['warn', '停止中'], disabled: ['mut', '禁用'], failed: ['bad', '失败'] }; + const m = map[s] || ['mut', s || '—']; + return '' + esc(m[1]) + ''; + } + function appStatePill(s) { + const map = { fg: ['ok', '前台'], bg: ['warn', '后台'], stop: ['mut', '已停'] }; + const m = map[s] || ['mut', s || '—']; + return '' + esc(m[1]) + ''; + } + function srcPill(src) { + const cls = (src === 'board') ? 'board' : (src === 'tf' ? 'tf' : ''); + return src ? '' + esc(src) + '' : '—'; + } + function renderProcBody(ams, svcd) { + let h = ''; + h += '服务(meow_svcd)'; + if (svcd && svcd.available) { + const svcs = svcd.services || []; + h += '服务状态PID重启退避触发来源退出执行'; + if (!svcs.length) h += '暂无服务'; + svcs.forEach(s => { + h += '' + esc(s.name) + '' + + '' + svcStatePill(s.state) + '' + + '' + (s.pid > 0 ? esc(s.pid) : '—') + '' + + '' + (s.restart || 0) + ((s.restart || 0) >= 3 ? ' ⚠' : '') + '' + + '' + esc(s.backoff_ms || '—') + '' + + '' + esc(s.trigger || '—') + '' + + '' + srcPill(s.src) + '' + + '' + esc(s.exit || '—') + '' + + '' + esc(s.exec || '—') + ''; + }); + h += ''; + } else h += homeUnavailable(); + h += ''; + h += 'App(meow_amsd,只读)'; + if (ams && ams.available) { + const apps = ams.apps || []; + h += '包名名称状态PID启动崩溃上次退出操作'; + if (!apps.length) h += '无可观测 App'; + apps.forEach(a => { + const openHere = expandedProc && expandedProc.pkg === a.pkg; + h += '' + esc(a.pkg) + '' + esc(a.name || '—') + '' + + '' + appStatePill(a.state) + '' + (a.pid > 0 ? esc(a.pid) : '—') + '' + + '' + esc(a.launches || 0) + '' + + '' + (a.crashes || 0) + ((a.crashes || 0) > 0 ? ' ⚠' : '') + '' + + '' + esc(a.last_kind || '—') + (a.last_code != null && a.last_code !== 0 ? ' (' + esc(a.last_code) + ')' : '') + '' + + '' + (openHere ? '收起' : 'stdout 尾部') + ''; + if (openHere) { + const lines = (expandedProc.lines || []); + h += '' + esc(lines.join('\n') || '(空)') + + (expandedProc.truncated ? '\n…(超出尾部上限,部分被截断)' : '') + '' + + '' + esc(a.pkg + '/' + expandedProc.file) + ' · 末尾 ' + lines.length + ' 行'; + } + }); + h += ''; + h += '点击「stdout 尾部」查看该 App 最新 stdout_*.log 末尾(崩溃尾巴)。仅只读,无 restart/kill。'; + } else h += homeUnavailable(); + h += ''; + return h; + } + async function newestStdout(pkg) { + try { + const d = await api('GET', '/api/files?path=' + encodeURIComponent('/MeowOS/logs/' + pkg + '/')); + const files = (d.items || []).filter(f => f.type === 'file' && f.name && f.name.indexOf('stdout_') === 0); + if (!files.length) return null; + files.sort((a, b) => (b.mtime || 0) - (a.mtime || 0)); + return files[0].name; + } catch (e) { return null; } + } + async function viewAppStdout(pkg, btn) { + const row = btn.closest('tr'); if (!row) return; + const next = row.nextElementSibling; + if (next && next.classList && next.classList.contains('proc-tail')) { next.remove(); expandedProc = null; btn.textContent = 'stdout 尾部'; return; } + if (expandedProc && expandedProc.pkg !== pkg) { // 先收起上一个 opened + const prevTail = document.querySelector('tr.proc-tail'); + if (prevTail) prevTail.remove(); + const prevBtn = document.querySelector('[data-applog="' + escSafeAttr(expandedProc.pkg) + '"]'); + if (prevBtn) prevBtn.textContent = 'stdout 尾部'; + expandedProc = null; + } + btn.textContent = '加载中…'; + const tailTr = document.createElement('tr'); + tailTr.className = 'proc-tail'; + tailTr.innerHTML = '读取 ' + esc(pkg) + ' 最新 stdout…'; + row.after(tailTr); + try { + const file = await newestStdout(pkg); + if (!file) { tailTr.innerHTML = '未找到 ' + esc(pkg) + ' 的 stdout 日志'; btn.textContent = 'stdout 尾部'; return; } + const d = await api('GET', '/api/logs/read?file=' + encodeURIComponent(pkg + '/' + file) + '&lines=200'); + const lines = (d.lines || []); + expandedProc = { pkg: pkg, file: file, lines: lines, truncated: !!d.truncated }; + tailTr.innerHTML = '' + esc(lines.join('\n') || '(空)') + (d.truncated ? '\n…(超出尾部上限,部分被截断)' : '') + '' + + '' + esc(pkg + '/' + file) + ' · 末尾 ' + lines.length + ' 行'; + btn.textContent = '收起'; + } catch (err) { + tailTr.innerHTML = '读取失败:' + esc(errText(err)) + ''; + btn.textContent = 'stdout 尾部'; + } + } + function escSafeAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); } + async function fetchProcOnce() { + const el = $('#proc-page'); + if (!el || state.route !== 'proc') return; + const r = await Promise.allSettled([api('GET', '/api/ams'), api('GET', '/api/svcd')]); + if (state.route !== 'proc') return; + if (r[0].status !== 'fulfilled' && r[1].status !== 'fulfilled') { + el.innerHTML = '' + emptyState('加载失败:' + errText(r[0].reason || r[1].reason)) + ''; + return; + } + el.innerHTML = renderProcBody(r[0].status === 'fulfilled' ? r[0].value : null, r[1].status === 'fulfilled' ? r[1].value : null); + } + function renderProc() { + stopProc(); + $('#content').innerHTML = '加载服务与进程…'; + fetchProcOnce(); + procTimer = setInterval(fetchProcOnce, 5000); + } + function bindProc() { + $('#content').addEventListener('click', function (e) { + const b = e.target.closest('[data-applog]'); if (!b) return; + viewAppStdout(b.dataset.applog, b); + }); } /* ---------------- 2c. 网络管理页(meow_netmgrd 期2 面板侧) ---------------- @@ -586,24 +1100,24 @@ return h; } - function netUnavailable(msg) { - stopNet(); - const m = msg || 'meow_netmgrd 服务未运行或 IPC 不可达。'; - $('#content').innerHTML = '' + - '网络管理' + - '网络管理服务不可用' + - '' + esc(m) + '' + - '面板其它页不受影响;恢复 meow_netmgrd 后点击刷新重试。' + - '重试' + - ''; + // 网络状态(/api/net/status)瞬态失败 → 仅顶部内联错误条,不整页踢「不可用」占位;保留上次成功内容,5s 自动重试。 + function netBannerErr(msg) { + const nb = $('#net-banner'); + if (!nb) return; + nb.innerHTML = '⚠️网络状态获取失败:' + esc(msg) + '(保留上次内容,5 秒后自动重试)' + + '重试'; + nb.hidden = false; } + let netLoaded = false; // 是否已成功加载过一次网络状态(用于「首次失败只显示横幅 + 占位」语义) async function fetchNetOnce() { if (state.route !== 'net') return; const stEl = $('#net-status'), cbEl = $('#net-conns-body'), dbEl = $('#net-dns-body'); let st, cn, dn; try { st = await api('GET', '/api/net/status'); } - catch (e) { netUnavailable(errText(e)); return; } + catch (e) { netBannerErr(errText(e)); return; } + netLoaded = true; + const nb = $('#net-banner'); if (nb) { nb.hidden = true; nb.innerHTML = ''; } try { cn = await api('GET', '/api/net/conns' + (netEntityFilter ? '?entity=' + encodeURIComponent(netEntityFilter) : '')); } catch (e) { cn = null; } try { dn = await api('GET', '/api/net/dns'); } @@ -642,8 +1156,10 @@ async function renderNet() { stopNet(); netEntityFilter = ''; + netLoaded = false; $('#content').innerHTML = '' + + '' + '加载网络状态…' + '每实体连接表' + '' + @@ -658,7 +1174,7 @@ '刷新' + '加载日志…' + ''; - netTimer = setInterval(fetchNetOnce, 5000); // status 失败时 netUnavailable 会 stopNet() 清掉 + netTimer = setInterval(fetchNetOnce, 5000); // status 仅瞬态横幅,不停止轮询 fetchNetOnce(); // 填充 status + conns + dns fetchNetLog(); } @@ -673,7 +1189,7 @@ $('#content').addEventListener('click', async function (e) { if (e.target.closest('#net-conns-refresh')) { fetchNetConns(); return; } if (e.target.closest('#net-log-refresh')) { fetchNetLog(); return; } - if (e.target.closest('#net-retry')) { renderNet(); return; } + if (e.target.closest('#net-banner-retry') || e.target.closest('#net-retry')) { fetchNetOnce(); return; } const flush = e.target.closest('#net-dns-flush'); if (flush) { flush.disabled = true; @@ -1046,6 +1562,9 @@ '浅色' + '深色' + '跟随系统 = 设备 display.theme(当前:' + esc(devThemeLabel()) + ');跟随浏览器 = prefers-color-scheme,随浏览器日/夜自动切换。'; + // 面板 PIN(访问级鉴权):显示状态 + 修改/清空控件(走 /api/auth/* 校验旧 PIN + PUT /api/settings/panel.pin) + h += '面板 PIN(访问鉴权)' + + '查询鉴权状态…'; // 面板鉴权(可选防护)单独一卡 const authItems = items.filter(x => x.key === 'panel.auth_enabled' || x.key === 'panel.auth_pass'); if (authItems.length) { @@ -1072,6 +1591,89 @@ $('#content').innerHTML = h; applyTheme(); // 同步「主题」卡 seg 选中态(applyTheme 按 id 查询,不存在时为空操作) loadFonts(); + loadPinStatus(); + } + async function loadPinStatus() { + const box = $('#pin-status'); + if (!box) return; + let st = null; + try { st = await api('GET', '/api/auth/status'); } catch (e) { box.innerHTML = '无法查询鉴权状态:' + esc(errText(e)) + ''; return; } + let txt, cls; + if (st.locked) { txt = '锁定中(' + Math.ceil((st.lock_remaining_ms || 0) / 1000) + ' 秒后可重试)'; cls = 'locked'; } + else if (st.pin_set) { txt = '已设置'; cls = 'set'; } + else if (st.legacy) { txt = '未设(旧密码屏障生效,建议改设为 PIN)'; cls = 'unset'; } + else { txt = '未设置(当前访问面板无需 PIN)'; cls = 'unset'; } + const canEdit = st.pin_set && !st.locked; + let h = '' + esc(txt) + '' + + (st.session ? ' · 当前会话有效' : ' · 未登录') + ''; + h += '' + + '修改 PIN' + + '清空 PIN' + + (st.locked ? '锁定期间不可修改/清空' : '修改/清空需先验证当前 PIN') + + ''; + h += '4 位数字 PIN,纯加盐哈希存储(不落明文);未设 PIN 时写端点一律拒绝。'; + box.innerHTML = h; + } + function pinEditDlg(mode) { // 返回 {old, n1}(n1=null 表示清空)或 null(取消) + return new Promise(function (resolve) { + const t = $('#pindlg-title'), b = $('#pindlg-body'); + const oldIn = $('#pindlg-old'), n1 = $('#pindlg-new'), n2 = $('#pindlg-new2'); + const err = $('#pindlg-err'); + if (mode === 'change') { + t.textContent = '修改 PIN'; + b.textContent = '请输入当前 PIN 与新 PIN(4 位数字)。'; + oldIn.hidden = false; n1.hidden = false; n2.hidden = false; + n1.placeholder = '新 4 位 PIN'; n2.placeholder = '再次输入新 PIN'; + } else { + t.textContent = '清空 PIN'; + b.textContent = '清空后访问面板将不再要求 PIN。请输入当前 PIN 确认。'; + oldIn.hidden = false; n1.hidden = true; n2.hidden = true; + } + err.hidden = true; oldIn.value = ''; n1.value = ''; n2.value = ''; + oldIn.focus(); + const scrim = $('#pindlg'); scrim.hidden = false; + const ok = $('#pindlg-ok'), cancel = $('#pindlg-cancel'); + const done = function (v) { scrim.hidden = true; ok.onclick = cancel.onclick = scrim.onclick = null; resolve(v); }; + ok.onclick = function () { + const old = oldIn.value.trim(); + if (!old) { err.textContent = '请输入当前 PIN'; err.hidden = false; return; } + if (mode === 'change') { + const np = n1.value.trim(); + if (!/^[0-9]{4}$/.test(np)) { err.textContent = '新 PIN 必须为 4 位数字'; err.hidden = false; return; } + if (np !== n2.value.trim()) { err.textContent = '两次输入的新 PIN 不一致'; err.hidden = false; return; } + done({ old: old, n1: np }); + } else done({ old: old, n1: null }); + }; + cancel.onclick = function () { done(null); }; + scrim.onclick = function (e) { if (e.target === scrim) done(null); }; + }); + } + async function pinEdit(mode) { + const v = await pinEditDlg(mode); + if (!v) return; + try { await api('POST', '/api/auth/login', { pin: v.old }); } // 先验证旧 PIN(客户端复核,后端改 PIN 本身不需旧值) + catch (e2) { + if (e2.code === 'LOCKED' || e2.status === 429) toast('错误次数过多,已锁定,请稍后重试(锁定 5 分钟)'); + else if (e2.status === 401) toast('当前 PIN 验证失败'); + else toast('验证失败:' + errText(e2)); + loadPinStatus(); return; + } + try { + await api('PUT', '/api/settings/panel.pin', { value: (v.n1 !== null ? v.n1 : '') }); + toast(mode === 'change' ? 'PIN 已修改' : 'PIN 已清空'); + loadPinStatus(); + } catch (e3) { toast('操作失败:' + errText(e3)); } + } + function bindPin() { + const c = $('#content'); + c.addEventListener('click', function (e) { + if (e.target.closest('#pin-change')) { pinEdit('change'); return; } + if (e.target.closest('#pin-clear')) { pinEdit('clear'); return; } + }); + ['#pindlg-old', '#pindlg-new', '#pindlg-new2'].forEach(function (sel) { + const el = $(sel); + if (el) el.addEventListener('input', function () { el.value = el.value.replace(/[^0-9]/g, '').slice(0, 4); }); + }); } async function loadFonts() { const box = $('#font-list'); if (!box) return; @@ -1188,17 +1790,35 @@ /* ---------------- 7. 系统信息 / OTA 页 ---------------- */ async function renderSystem() { $('#content').innerHTML = '加载系统信息…'; - let d; + let d, pw = null; try { d = await api('GET', '/api/system'); } catch (e) { if (state.route === 'system') $('#content').innerHTML = emptyState('加载失败:' + errText(e), e.code); return; } + try { pw = await api('GET', '/api/power'); } catch (e) { pw = null; } if (state.route !== 'system') return; const net = d.network || {}; const wf = net.wifi || {}; const bt = net.bluetooth || {}; const ota = d.ota || {}; + const boot = d.boot || {}; + const curMode = boot.mode || boot.current || null; // 后端若暴露 /tmp/meow_boot_mode → boot.mode/current + const bootDef = boot.default || '—'; + const mism = curMode && boot.default && curMode !== boot.default; + const batt = (pw && pw.battery) || {}; + const charging = (batt.state === 'Charging' || batt.state === 'Full'); + let modeHtml = mism + ? '' + esc(curMode) + ' (与默认启动不一致)' + : (curMode ? esc(curMode) : '—'); + let sysRows = row('MeowOS 版本', esc(d.version || '—')) + row('内核', esc(d.kernel || '—')) + row('架构', esc(d.arch || '—')) + + row('主机名', esc(d.hostname || '—')) + row('运行时间', fmtUptime(d.uptime_sec)); + if (pw && pw.battery) { + sysRows += row('电池', '' + esc(batt.state || '—') + ' ' + + (batt.pct != null ? batt.pct + ' %' : '—') + + (batt.mv != null ? ' · ' + (batt.mv / 1000).toFixed(2) + ' V' : '') + + (batt.health ? ' · ' + esc(batt.health) : '')); + } + sysRows += row('当前运行模式', modeHtml); + sysRows += row('默认启动', esc(bootDef)); + sysRows += row('重启原因', esc(boot.reboot_reason || d.reboot_reason || '—')); + sysRows += row('升级状态', esc(boot.upgrade_status || d.upgrade_status || '—')); let h = ''; - h += '系统信息' + - row('MeowOS 版本', esc(d.version || '—')) + row('内核', esc(d.kernel || '—')) + row('架构', esc(d.arch || '—')) + - row('主机名', esc(d.hostname || '—')) + row('运行时间', fmtUptime(d.uptime_sec)) + - row('默认启动', esc((d.boot && d.boot.default) || '—')) + - ''; + h += '系统信息' + sysRows + ''; h += '网络' + row('WiFi', wf.connected ? esc(wf.ssid || '已连接') + ' · ' + esc(wf.ip || '—') + (wf.rssi != null ? ' · ' + wf.rssi + 'dBm' : '') : '未连接') + row('蓝牙', bt.available ? ((bt.powered ? '已开启' : '已关闭') + (bt.connected ? ' · 已连接' : '')) : '无蓝牙') + @@ -1509,6 +2129,7 @@ /* ---------------- 路由 ---------------- */ const ROUTES = { + home: { title: '首页仪表盘', render: renderHome, bind: bindHome }, perf: { title: '实时性能', render: renderPerf, bind: null }, bright: { title: '亮度 · 音量', render: renderBright, bind: null }, audio: { title: '音频状态', render: renderAudio, bind: null }, @@ -1521,7 +2142,9 @@ screen: { title: '实时屏幕', render: renderScreen, bind: bindScreen }, system: { title: '系统信息 / 升级', render: renderSystem, bind: bindSystem }, logs: { title: '系统日志', render: renderLogs, bind: bindLogs }, - notif: { title: '通知中心', render: renderNotif, bind: bindNotif } + notif: { title: '通知中心', render: renderNotif, bind: bindNotif }, + power: { title: '电源 / 功耗', render: renderPower, bind: null }, + proc: { title: '进程 / 服务', render: renderProc, bind: bindProc } }; // 一次性绑定(各菜单 click / 侧栏);页面内交互用 #content 事件委托(已各自绑定) function bindNav() { @@ -1530,13 +2153,16 @@ } function route() { if (state.unauth) { showLogin(authMode); return; } // 未认证 → 登录页(保持当前设置/登录态) - const id = (location.hash || '#/perf').replace('#/', '').split('?')[0] || 'perf'; - const r = ROUTES[id] || ROUTES.perf; + const id = (location.hash || '#/home').replace('#/', '').split('?')[0] || 'home'; + const r = ROUTES[id] || ROUTES.home; state.route = id; stopPerf(); stopAudio(); stopNet(); stopScreenPoll(); + stopHome(); + stopPower(); + stopProc(); $$('.nav-item').forEach(b => b.classList.toggle('active', b.dataset.route === id)); $('#page-title').textContent = r.title; r.render(); @@ -1555,7 +2181,7 @@ bindNav(); bindThemeUi(); bindSlider(); - bindFiles(); bindApps(); bindFonts(); bindSettings(); bindRemote(); bindScreen(); bindSystem(); bindLogs(); bindNotif(); bindNet(); + bindFiles(); bindApps(); bindFonts(); bindSettings(); bindRemote(); bindScreen(); bindSystem(); bindLogs(); bindNotif(); bindNet(); bindHome(); bindProc(); bindPin(); bindLogin(); bindPwDlg(); // 首次进入:访问级 PIN 鉴权——已登录直接进;未设 PIN(且无旧密码)→ 首次设置流程;已设 → 登录
每 5 秒自动刷新 · 只读展示(测试音请用设备 CLI meow_audio tone,面板不放声)。
meow_audio tone
加载音频状态…
每 1 秒自动刷新 · 只读展示(测试音请用设备 CLI meow_audio tone,面板不放声)。
' + (ad.active ? '电平 · 1s 子节拍' : '无流') + '
加载总览…
Model 系数全部为经验初值(未真机标定);confidence 必须如实标注。
' + + (bd.cpu_util_pct != null ? 'CPU 利用率 ' + esc(bd.cpu_util_pct) + '% ' : '') + + (bd.brightness_pct != null ? '· 亮度 ' + esc(bd.brightness_pct) + '% ' : '') + + (bd.wifi_state ? '· WiFi ' + esc(bd.wifi_state) : '') + + (bd.bt_state ? ' · BT ' + esc(bd.bt_state) : '') + + (bd.audio_state ? ' · 音频 ' + esc(bd.audio_state) : '') + '
电源策略(profile 四档/低电阈值)为 期3 功能,此处以不可用占位。
加载电源数据…
' + esc(lines.join('\n') || '(空)') + + (expandedProc.truncated ? '\n…(超出尾部上限,部分被截断)' : '') + '
点击「stdout 尾部」查看该 App 最新 stdout_*.log 末尾(崩溃尾巴)。仅只读,无 restart/kill。
' + esc(lines.join('\n') || '(空)') + (d.truncated ? '\n…(超出尾部上限,部分被截断)' : '') + '
加载服务与进程…
' + esc(m) + '
面板其它页不受影响;恢复 meow_netmgrd 后点击刷新重试。
加载网络状态…
加载日志…
跟随系统 = 设备 display.theme(当前:' + esc(devThemeLabel()) + ');跟随浏览器 = prefers-color-scheme,随浏览器日/夜自动切换。
查询鉴权状态…
无法查询鉴权状态:' + esc(errText(e)) + '
4 位数字 PIN,纯加盐哈希存储(不落明文);未设 PIN 时写端点一律拒绝。
加载系统信息…