feat: M6.44+M6.45 现场补全 — 补齐按扩展名过滤遗漏的 web 前端与 license 合规件
真机 0.8.45 现场组成补全(初次提现场按扩展名过滤漏掉 7 个文本文件), 内容逐字节取自容器现场 /mnt/mb/home/Aska/Project/R3ProII 并 md5 对拍一致: - patch_v2/meowpanel/web/meowpanel.css (+17) 趋势/App 详情卡样式 - patch_v2/meowpanel/web/mp_pages.js (+289) 性能趋势/功耗曲线/App 详情/蓝牙详情 - patch_v2/meowpanel/web/mp_shared.js (+49) 趋势 sparkline 通用逻辑 - patch_v2/reader_app/.gitignore (+2) seg/chapter smoke host - patch_v2/reader_app/NOTICE (+23) FreeType 2.14.3 + zlib 条目 - patch_v2/reader_app/resources/licenses/NOTICE (+23) 同 reader_app/NOTICE(md5 一致) - patch_v2/reader_app/resources/licenses/FTL.txt (新增 6743B) FreeType 许可证全文 面板前端对应已收编的后端 mp_files.c/mp_net.c(文件搜索+蓝牙详情), NOTICE/FTL.txt 为 LGPL/FreeType 合规产物。仅投运现场内容,未改内容。
This commit is contained in:
1 parent
2ffb92c038
commit
20f4727714
7 files changed
+549
-21
No files matched your search
@@ -270,6 +270,9 @@ input[type=range]{ width:100%; accent-color:var(--primary); }
|
|||||||
.app-card .app-pkg{ font-size:12px; color:var(--on-surface-variant); font-family:var(--mono); }
|
.app-card .app-pkg{ font-size:12px; color:var(--on-surface-variant); font-family:var(--mono); }
|
||||||
.app-card .app-meta{ font-size:12px; color:var(--on-surface-variant); }
|
.app-card .app-meta{ font-size:12px; color:var(--on-surface-variant); }
|
||||||
.app-card .app-actions{ display:flex; gap:6px; margin-top:auto; flex-wrap:wrap; }
|
.app-card .app-actions{ display:flex; gap:6px; margin-top:auto; flex-wrap:wrap; }
|
||||||
|
.app-detail-card{ grid-column:1/-1; }
|
||||||
|
.app-detail-card .app-detail-head{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:6px; }
|
||||||
|
.app-detail-card .app-detail-head strong{ font-family:var(--mono); }
|
||||||
|
|
||||||
/* 后台活动列表(批④,与启动器后台管理页观感呼应:MD3 圆角卡 + 行右杀按钮) */
|
/* 后台活动列表(批④,与启动器后台管理页观感呼应:MD3 圆角卡 + 行右杀按钮) */
|
||||||
.bg-list{ display:flex; flex-direction:column; gap:8px; }
|
.bg-list{ display:flex; flex-direction:column; gap:8px; }
|
||||||
@@ -457,6 +460,20 @@ input[type=range]{ width:100%; accent-color:var(--primary); }
|
|||||||
.lvl-spark .grid{ stroke:var(--outline-variant); stroke-width:1; }
|
.lvl-spark .grid{ stroke:var(--outline-variant); stroke-width:1; }
|
||||||
.lvl-note{ color:var(--on-surface-variant); font-size:12px; margin-top:6px; }
|
.lvl-note{ color:var(--on-surface-variant); font-size:12px; margin-top:6px; }
|
||||||
|
|
||||||
|
/* 性能趋势 sparkline(F1-① 多序列:CPU/内存/负载/温度)+ 功耗历史曲线(F1-②) */
|
||||||
|
.trend-spark polyline{ vector-effect:non-scaling-stroke; }
|
||||||
|
.trend-cpu{ stroke:#4a8bf5; }
|
||||||
|
.trend-mem{ stroke:#00bfa5; }
|
||||||
|
.trend-load{ stroke:#ffb300; }
|
||||||
|
.trend-temp{ stroke:#ff7043; }
|
||||||
|
.trend-legend{ display:flex; flex-wrap:wrap; gap:8px 16px; margin-top:12px; font-size:12px; }
|
||||||
|
.trend-legend .sw{ display:inline-block; width:12px; height:12px; border-radius:3px; margin-right:5px; vertical-align:middle; }
|
||||||
|
.trend-legend .seg{ display:inline-flex; align-items:center; }
|
||||||
|
.pwr-chart{ display:block; width:100%; height:60px; margin-top:8px; }
|
||||||
|
.pwr-chart .grid{ stroke:var(--outline-variant); stroke-width:1; }
|
||||||
|
.pwr-mw{ stroke:#ff7043; }
|
||||||
|
.pwr-pct{ stroke:#4a8bf5; }
|
||||||
|
|
||||||
/* 置信度徽标(measured/derived/estimated) */
|
/* 置信度徽标(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{ 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.measured{ background:rgba(76,175,80,.18); color:var(--success); }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
var MP = window.MP = window.MP || {};
|
var MP = window.MP = window.MP || {};
|
||||||
const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode, authRequired, barHtml, bindLogin, bindPwDlg, bindSlider, bindThemeUi, browserMode, confirmDlg, dbPct, dbg, dbgErr, debounce, deciToDb, devStyleLabel, devThemeLabel, doLogin, drawSpark, emptyState, errText, esc, fmtBytes, fmtDb, fmtTime, fmtUptime, hideLogin, homeCard, homeKv, homeUnavailable, initTheme, isAvail, levelMeterHtml, loginErr, modePill, passwordDlg, pct, pushSpark, refreshAuthGate, resetSpark, resolveMode, resolveStyle, saveSetting, setConn, showLogin, slur, sparkPeak, sparkRms, sparklineHtml, sparklinePoints, stat, state, storageDel, storageGet, storageSet, storedMode, storedStyle, toast, toastTimer, updateLevelMeter, volTimer } = MP;
|
const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode, authRequired, barHtml, bindLogin, bindPwDlg, bindSlider, bindThemeUi, browserMode, confirmDlg, dbPct, dbg, dbgErr, debounce, deciToDb, devStyleLabel, devThemeLabel, doLogin, drawSpark, drawTrendSpark, emptyState, errText, esc, fmtBytes, fmtDb, fmtTime, fmtUptime, hideLogin, homeCard, homeKv, homeUnavailable, initTheme, isAvail, levelMeterHtml, loginErr, modePill, passwordDlg, pct, polylinePoints, pushSpark, pushTrend, refreshAuthGate, resetSpark, resetTrend, resolveMode, resolveStyle, saveSetting, setConn, showLogin, slur, sparkPeak, sparkRms, sparklineHtml, sparklinePoints, stat, state, storageDel, storageGet, storageSet, storedMode, storedStyle, toast, toastTimer, trendCpu, trendLoad, trendMem, trendSvgHtml, trendTemp, updateLevelMeter, volTimer } = MP;
|
||||||
|
|
||||||
let audioTimer = null;
|
let audioTimer = null;
|
||||||
let audioVolDrag = false; // 音量滑条拖拽中(暂停 1s 重渲染,防打断交互)
|
let audioVolDrag = false; // 音量滑条拖拽中(暂停 1s 重渲染,防打断交互)
|
||||||
@@ -14,6 +14,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
let powerData = null, powerEvents = null, powerHistory = null;
|
let powerData = null, powerEvents = null, powerHistory = null;
|
||||||
let procTimer = null;
|
let procTimer = null;
|
||||||
let expandedProc = null; // { pkg, file, lines, truncated }:已展开的 App stdout 尾部(重渲染后重开,防轮询清空)
|
let expandedProc = null; // { pkg, file, lines, truncated }:已展开的 App stdout 尾部(重渲染后重开,防轮询清空)
|
||||||
|
let appDetailPkg = null; // F1-⑤ 当前展开「App 详情四源聚合」的包名
|
||||||
let netTimer = null;
|
let netTimer = null;
|
||||||
let netEntityFilter = '';
|
let netEntityFilter = '';
|
||||||
let netLoaded = false; // 是否已成功加载过一次网络状态(用于「首次失败只显示横幅 + 占位」语义)
|
let netLoaded = false; // 是否已成功加载过一次网络状态(用于「首次失败只显示横幅 + 占位」语义)
|
||||||
@@ -92,7 +93,28 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
h += stat('温度', '<span class="stat-value">' + (tmpCpu == null ? '—' : tmpCpu) + '<span class="stat-unit">℃</span></span>',
|
h += stat('温度', '<span class="stat-value">' + (tmpCpu == null ? '—' : tmpCpu) + '<span class="stat-unit">℃</span></span>',
|
||||||
tmp && tmp.board != null ? '板载 ' + (+tmp.board.toFixed(1)) + ' ℃' : '');
|
tmp && tmp.board != null ? '板载 ' + (+tmp.board.toFixed(1)) + ' ℃' : '');
|
||||||
h += '</div></div>';
|
h += '</div></div>';
|
||||||
|
/* F1-① 趋势(2s 节拍,最近 30 点≈1 分钟):CPU%/内存%/负载(1m/核)/温度(20-90℃ 归一) */
|
||||||
|
const loadN = (cpu.load && cpu.load[0] != null && cpu.cores) ? (cpu.load[0] / cpu.cores) * 100 : null;
|
||||||
|
pushTrend(trendCpu, cpu.usage);
|
||||||
|
pushTrend(trendMem, mem.total ? memP : null);
|
||||||
|
pushTrend(trendLoad, loadN);
|
||||||
|
pushTrend(trendTemp, tmpCpu != null ? (tmpCpu - 20) / 70 * 100 : null);
|
||||||
|
const perfTrend = [
|
||||||
|
{ cls: 'trend-cpu', arr: trendCpu },
|
||||||
|
{ cls: 'trend-mem', arr: trendMem },
|
||||||
|
{ cls: 'trend-load', arr: trendLoad },
|
||||||
|
{ cls: 'trend-temp', arr: trendTemp }
|
||||||
|
];
|
||||||
|
h += '<div class="card"><h3 class="card-title">趋势(2s 采样 · 最近 ' + trendCpu.length + ' 点)</h3>' +
|
||||||
|
'<div class="trend-legend">' +
|
||||||
|
'<span class="seg"><span class="sw" style="background:#4a8bf5"></span>CPU ' + (cpu.usage == null ? '—' : (+cpu.usage).toFixed(1) + '%') + '</span>' +
|
||||||
|
'<span class="seg"><span class="sw" style="background:#00bfa5"></span>内存 ' + (mem.total ? memP.toFixed(1) + '%' : '—') + '</span>' +
|
||||||
|
'<span class="seg"><span class="sw" style="background:#ffb300"></span>负载1m/核 ' + (cpu.load && cpu.load[0] != null ? (+cpu.load[0]).toFixed(2) : '—') + '</span>' +
|
||||||
|
'<span class="seg"><span class="sw" style="background:#ff7043"></span>温度 ' + (tmpCpu == null ? '—' : tmpCpu + '℃') + '</span>' +
|
||||||
|
'</div>' + trendSvgHtml(perfTrend) +
|
||||||
|
'<p class="muted">温度曲线按 20–90℃ 归一;负载按 load1/核(100%=单核满载)。进页起累积,切页清零。</p></div>';
|
||||||
page.innerHTML = h;
|
page.innerHTML = h;
|
||||||
|
drawTrendSpark(page, perfTrend);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPerfOnce() {
|
async function fetchPerfOnce() {
|
||||||
@@ -109,6 +131,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
function renderPerf() {
|
function renderPerf() {
|
||||||
$('#content').innerHTML = '<div class="page" id="perf-page"><p class="placeholder">加载中…</p></div>';
|
$('#content').innerHTML = '<div class="page" id="perf-page"><p class="placeholder">加载中…</p></div>';
|
||||||
if (state.perfTimer) stopPerf();
|
if (state.perfTimer) stopPerf();
|
||||||
|
resetTrend(); // 进页重置趋势累积(F1-①)
|
||||||
fetchPerfOnce();
|
fetchPerfOnce();
|
||||||
state.perfTimer = setInterval(fetchPerfOnce, 2000);
|
state.perfTimer = setInterval(fetchPerfOnce, 2000);
|
||||||
}
|
}
|
||||||
@@ -443,11 +466,21 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
const cpu = perf.cpu || {}, mem = perf.memory || {}, tmp = perf.temp || {};
|
const cpu = perf.cpu || {}, mem = perf.memory || {}, tmp = perf.temp || {};
|
||||||
const cpuU = cpu.usage;
|
const cpuU = cpu.usage;
|
||||||
const memP = mem.total ? (mem.used / mem.total) * 100 : 0;
|
const memP = mem.total ? (mem.used / mem.total) * 100 : 0;
|
||||||
|
/* F1-① 首页迷你趋势(5s overview 节拍,CPU/内存/温度) */
|
||||||
|
pushTrend(trendCpu, cpuU);
|
||||||
|
pushTrend(trendMem, mem.total ? memP : null);
|
||||||
|
pushTrend(trendTemp, tmp.cpu != null ? ((+tmp.cpu) - 20) / 70 * 100 : null);
|
||||||
|
const homeTrend = [
|
||||||
|
{ cls: 'trend-cpu', arr: trendCpu },
|
||||||
|
{ cls: 'trend-mem', arr: trendMem },
|
||||||
|
{ cls: 'trend-temp', arr: trendTemp }
|
||||||
|
];
|
||||||
return homeCard('性能小条', '<div class="stat-grid" style="grid-template-columns:repeat(auto-fill,minmax(120px,1fr))">' +
|
return homeCard('性能小条', '<div class="stat-grid" style="grid-template-columns:repeat(auto-fill,minmax(120px,1fr))">' +
|
||||||
'<div class="stat"><div class="stat-label">CPU</div><div class="stat-value">' + (cpuU == null ? '—' : (+cpuU).toFixed(1)) + '<span class="stat-unit">%</span></div>' + barHtml(cpuU || 0) + '</div>' +
|
'<div class="stat"><div class="stat-label">CPU</div><div class="stat-value">' + (cpuU == null ? '—' : (+cpuU).toFixed(1)) + '<span class="stat-unit">%</span></div>' + barHtml(cpuU || 0) + '</div>' +
|
||||||
'<div class="stat"><div class="stat-label">内存</div><div class="stat-value">' + (mem.total ? fmtBytes(mem.used * 1024) : '—') + '</div>' + barHtml(mem.used && mem.total ? memP : 0) + '</div>' +
|
'<div class="stat"><div class="stat-label">内存</div><div class="stat-value">' + (mem.total ? fmtBytes(mem.used * 1024) : '—') + '</div>' + barHtml(mem.used && mem.total ? memP : 0) + '</div>' +
|
||||||
'<div class="stat"><div class="stat-label">温度</div><div class="stat-value">' + (tmp.cpu != null ? (+tmp.cpu).toFixed(1) + '<span class="stat-unit">℃</span>' : '—') + '</div></div>' +
|
'<div class="stat"><div class="stat-label">温度</div><div class="stat-value">' + (tmp.cpu != null ? (+tmp.cpu).toFixed(1) + '<span class="stat-unit">℃</span>' : '—') + '</div></div>' +
|
||||||
'</div>');
|
'</div>' + trendSvgHtml(homeTrend) +
|
||||||
|
'<p class="muted">趋势 5s 采样 · <span style="color:#4a8bf5">CPU</span> / <span style="color:#00bfa5">内存</span> / <span style="color:#ff7043">温度(20–90℃)</span></p>');
|
||||||
}
|
}
|
||||||
|
|
||||||
function powerSummaryCard() {
|
function powerSummaryCard() {
|
||||||
@@ -483,6 +516,11 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
deviceCard() + batteryCard() + audioCard() + netCard() + btCard() + healthCard() + storageCard() + perfCard() +
|
deviceCard() + batteryCard() + audioCard() + netCard() + btCard() + healthCard() + storageCard() + perfCard() +
|
||||||
'</div>';
|
'</div>';
|
||||||
body.innerHTML = h;
|
body.innerHTML = h;
|
||||||
|
drawTrendSpark(body, [
|
||||||
|
{ cls: 'trend-cpu', arr: trendCpu },
|
||||||
|
{ cls: 'trend-mem', arr: trendMem },
|
||||||
|
{ cls: 'trend-temp', arr: trendTemp }
|
||||||
|
]); // F1-① 首页迷你趋势(perfCard 内 .trend-spark)
|
||||||
const ad = homeData.audio;
|
const ad = homeData.audio;
|
||||||
if (isAvail(ad)) updateLevelMeter($('#home-audio-level'), ad);
|
if (isAvail(ad)) updateLevelMeter($('#home-audio-level'), ad);
|
||||||
}
|
}
|
||||||
@@ -522,6 +560,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
function renderHome() {
|
function renderHome() {
|
||||||
stopHome();
|
stopHome();
|
||||||
resetSpark(); // 进入首页重置 sparkline 累积
|
resetSpark(); // 进入首页重置 sparkline 累积
|
||||||
|
resetTrend(); // 进入首页重置性能趋势累积(F1-①)
|
||||||
homeData = null; homeSys = null;
|
homeData = null; homeSys = null;
|
||||||
$('#content').innerHTML =
|
$('#content').innerHTML =
|
||||||
'<div class="page" id="home-page">' +
|
'<div class="page" id="home-page">' +
|
||||||
@@ -645,6 +684,27 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
'<table class="table"><thead><tr><th>类型</th><th>电量</th><th>说明</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
|
'<table class="table"><thead><tr><th>类型</th><th>电量</th><th>说明</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function powerHistoryChart(h) {
|
||||||
|
const samples = (h && h.samples) || []; // [mono, pct, est_mw, chg, scr, bri](30s 环形 ≈4h)
|
||||||
|
if (!samples.length) return '';
|
||||||
|
let maxMw = 0, hasMw = false;
|
||||||
|
samples.forEach(function (s2) { if (s2[2] != null && s2[2] >= 0) { hasMw = true; if (s2[2] > maxMw) maxMw = s2[2]; } });
|
||||||
|
if (!hasMw) return '<p class="muted">本窗口无 est_mw 估算采样(只读模式/未知),不画曲线。</p>';
|
||||||
|
const W = 100, H = 30;
|
||||||
|
const mwVals = samples.map(function (s2) { return (s2[2] != null && s2[2] >= 0 && maxMw > 0) ? s2[2] / maxMw * 100 : null; });
|
||||||
|
const pctVals = samples.map(function (s2) { return (s2[1] != null && s2[1] >= 0) ? s2[1] : null; });
|
||||||
|
return '<div class="trend-legend">' +
|
||||||
|
'<span class="seg"><span class="sw" style="background:#ff7043"></span>est_mw(窗内峰值 ' + maxMw + ' mW)</span>' +
|
||||||
|
'<span class="seg"><span class="sw" style="background:#4a8bf5"></span>电量 %</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<svg class="pwr-chart" viewBox="0 0 100 30" preserveAspectRatio="none" aria-hidden="true">' +
|
||||||
|
'<line class="grid" x1="0" y1="29" x2="100" y2="29"/>' +
|
||||||
|
'<polyline class="pwr-mw" points="' + polylinePoints(mwVals, W, H) + '" fill="none" stroke-width="1.4" vector-effect="non-scaling-stroke"/>' +
|
||||||
|
'<polyline class="pwr-pct" points="' + polylinePoints(pctVals, W, H) + '" fill="none" stroke-width="1.1" vector-effect="non-scaling-stroke"/>' +
|
||||||
|
'</svg>' +
|
||||||
|
'<p class="muted">30s 采样 · 内存环形约 4h · est_mw 为经验估算(confidence 见「功耗估算」卡),非实测功率;X 轴为采样序(等间隔)。</p>';
|
||||||
|
}
|
||||||
|
|
||||||
function powerHistoryCard(h) {
|
function powerHistoryCard(h) {
|
||||||
if (!h) return '';
|
if (!h) return '';
|
||||||
if (h.available === false) return '<div class="card"><h3 class="card-title">功耗历史</h3>' + homeUnavailable() + '</div>';
|
if (h.available === false) return '<div class="card"><h3 class="card-title">功耗历史</h3>' + homeUnavailable() + '</div>';
|
||||||
@@ -654,6 +714,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
row('起止电量', (h.start_pct != null ? h.start_pct : '—') + '% → ' + (h.end_pct != null ? h.end_pct : '—') + '%') +
|
row('起止电量', (h.start_pct != null ? h.start_pct : '—') + '% → ' + (h.end_pct != null ? h.end_pct : '—') + '%') +
|
||||||
row('平均功耗', (h.avg_mw != null ? h.avg_mw + ' mW' : '—')) +
|
row('平均功耗', (h.avg_mw != null ? h.avg_mw + ' mW' : '—')) +
|
||||||
'</tbody></table>';
|
'</tbody></table>';
|
||||||
|
body += powerHistoryChart(h); // F1-② est_mw 4h 曲线(纯前端画既有 samples)
|
||||||
return '<div class="card"><h3 class="card-title">功耗历史</h3>' + body + '</div>';
|
return '<div class="card"><h3 class="card-title">功耗历史</h3>' + body + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1134,18 +1195,44 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
h += row('状态', '<span class="net-state warn">未连接</span>');
|
h += row('状态', '<span class="net-state warn">未连接</span>');
|
||||||
}
|
}
|
||||||
h += '</tbody></table></div>';
|
h += '</tbody></table></div>';
|
||||||
// 蓝牙摘要
|
return h;
|
||||||
const bt = (d && d.bt) || {};
|
}
|
||||||
h += '<div class="card"><h3 class="card-title">蓝牙</h3><table class="table"><tbody>';
|
|
||||||
if (bt.available) {
|
/* F1-③ 蓝牙详情(GET /api/net/bt 只读聚合:status/paired/connected/a2dp/nearby) */
|
||||||
|
function btDevTable(title, list) {
|
||||||
|
let h = '<div class="home-sub" style="margin-top:10px">' + esc(title) + '</div>';
|
||||||
|
if (!list || !list.length) return h + '<div class="empty">无</div>';
|
||||||
|
h += '<table class="table"><thead><tr><th>MAC</th><th>名称</th></tr></thead><tbody>';
|
||||||
|
list.forEach(function (d) { h += '<tr><td class="mono">' + esc(d.mac) + '</td><td>' + esc(d.name || '—') + '</td></tr>'; });
|
||||||
|
return h + '</tbody></table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function netBtCard(bt) {
|
||||||
|
if (!bt || bt.available === false) return '<div class="card"><h3 class="card-title">蓝牙</h3>' + homeUnavailable() + '</div>';
|
||||||
|
let h = '<div class="card"><h3 class="card-title">蓝牙</h3>';
|
||||||
|
h += '<table class="table"><tbody>';
|
||||||
h += row('状态', '<span class="net-state ' + (bt.powered ? 'ok' : 'warn') + '">' + (bt.powered ? '已开启' : '已关闭') + '</span>');
|
h += row('状态', '<span class="net-state ' + (bt.powered ? 'ok' : 'warn') + '">' + (bt.powered ? '已开启' : '已关闭') + '</span>');
|
||||||
h += row('地址', esc(bt.address || '—'));
|
h += row('地址', esc(bt.address || '—'));
|
||||||
h += row('已配对', esc(bt.paired != null ? bt.paired : '—'));
|
h += row('已配对 / 已连接', (bt.paired_n != null ? esc(bt.paired_n) : '—') + ' / ' + (bt.connected_n != null ? esc(bt.connected_n) : '—'));
|
||||||
h += row('DNS', esc(((bt.dns) || []).join(' / ') || '—'));
|
h += '</tbody></table>';
|
||||||
} else {
|
h += btDevTable('已配对设备', bt.paired);
|
||||||
h += row('状态', '<span class="net-state warn">无蓝牙</span>');
|
h += btDevTable('已连接设备(内核 ACL)', bt.connected);
|
||||||
|
const a2 = bt.a2dp || [];
|
||||||
|
h += '<div class="home-sub" style="margin-top:10px">A2DP 传输</div>';
|
||||||
|
if (!a2.length) h += '<div class="empty">无 A2DP 端点</div>';
|
||||||
|
else {
|
||||||
|
h += '<table class="table"><thead><tr><th>设备</th><th>编解码</th><th>采样率</th><th>声道/位深</th><th>传输</th><th>角色</th></tr></thead><tbody>';
|
||||||
|
a2.forEach(function (a) {
|
||||||
|
h += '<tr><td class="mono">' + esc(a.mac) + '</td><td>' + esc(a.codec || '—') + '</td><td>' + esc(a.rate || '—') + '</td>' +
|
||||||
|
'<td class="muted">' + esc(a.channels || '—') + ' / ' + esc(a.bits || '—') + '</td>' +
|
||||||
|
'<td>' + (a.acquired ? '<span class="home-chip ok">RUNNING</span>' : '<span class="home-chip mut">空闲</span>') + '</td>' +
|
||||||
|
'<td class="muted">' + esc(a.role || '—') + '</td></tr>';
|
||||||
|
});
|
||||||
|
h += '</tbody></table>';
|
||||||
}
|
}
|
||||||
h += '</tbody></table></div>';
|
h += btDevTable('附近设备(扫描缓存)', bt.nearby);
|
||||||
|
h += '<p class="muted">只读聚合(netmgrd 0xE6:status/paired/connected/a2dp/devices);配对/连接/开关等写操作走实体机,面板未开放。</p>';
|
||||||
|
h += '</div>';
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1239,8 +1326,8 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
async function fetchNetOnce() {
|
async function fetchNetOnce() {
|
||||||
if (state.route !== 'net') return;
|
if (state.route !== 'net') return;
|
||||||
const stEl = $('#net-status'), cbEl = $('#net-conns-body'), dbEl = $('#net-dns-body'),
|
const stEl = $('#net-status'), cbEl = $('#net-conns-body'), dbEl = $('#net-dns-body'),
|
||||||
ifEl = $('#net-ifaces'), poEl = $('#net-policy');
|
ifEl = $('#net-ifaces'), poEl = $('#net-policy'), btEl = $('#net-bt');
|
||||||
let st, cn, dn, ifd, pol;
|
let st, cn, dn, ifd, pol, bt;
|
||||||
try { st = await api('GET', '/api/net/status'); }
|
try { st = await api('GET', '/api/net/status'); }
|
||||||
catch (e) { netBannerErr(errText(e)); return; }
|
catch (e) { netBannerErr(errText(e)); return; }
|
||||||
netLoaded = true;
|
netLoaded = true;
|
||||||
@@ -1251,9 +1338,11 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
catch (e) { dn = null; }
|
catch (e) { dn = null; }
|
||||||
try { ifd = await api('GET', '/api/net/ifaces'); } catch (e) { ifd = null; }
|
try { ifd = await api('GET', '/api/net/ifaces'); } catch (e) { ifd = null; }
|
||||||
try { pol = await api('GET', '/api/net/policy'); } catch (e) { pol = null; }
|
try { pol = await api('GET', '/api/net/policy'); } catch (e) { pol = null; }
|
||||||
|
try { bt = await api('GET', '/api/net/bt'); } catch (e) { bt = null; } /* F1-③ 蓝牙只读详情 */
|
||||||
if (state.route !== 'net') return;
|
if (state.route !== 'net') return;
|
||||||
if (ifEl) ifEl.innerHTML = netIfaceCards(ifd);
|
if (ifEl) ifEl.innerHTML = netIfaceCards(ifd);
|
||||||
if (stEl) stEl.innerHTML = netStatusCards(st);
|
if (stEl) stEl.innerHTML = netStatusCards(st);
|
||||||
|
if (btEl) btEl.innerHTML = netBtCard(bt);
|
||||||
if (poEl) poEl.innerHTML = netPolicyCard(pol);
|
if (poEl) poEl.innerHTML = netPolicyCard(pol);
|
||||||
if (cbEl) cbEl.innerHTML = netConnsBody(cn);
|
if (cbEl) cbEl.innerHTML = netConnsBody(cn);
|
||||||
if (dbEl) dbEl.innerHTML = netDnsBody(dn);
|
if (dbEl) dbEl.innerHTML = netDnsBody(dn);
|
||||||
@@ -1293,6 +1382,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
'<div id="net-banner" class="page-banner" hidden></div>' +
|
'<div id="net-banner" class="page-banner" hidden></div>' +
|
||||||
'<div id="net-ifaces"><p class="placeholder">加载网络接口…</p></div>' +
|
'<div id="net-ifaces"><p class="placeholder">加载网络接口…</p></div>' +
|
||||||
'<div id="net-status"><p class="placeholder">加载网络状态…</p></div>' +
|
'<div id="net-status"><p class="placeholder">加载网络状态…</p></div>' +
|
||||||
|
'<div id="net-bt"><p class="placeholder">加载蓝牙详情…</p></div>' +
|
||||||
'<div id="net-policy"><p class="placeholder">加载网络策略…</p></div>' +
|
'<div id="net-policy"><p class="placeholder">加载网络策略…</p></div>' +
|
||||||
'<div class="card"><h3 class="card-title">每实体连接表</h3>' +
|
'<div class="card"><h3 class="card-title">每实体连接表</h3>' +
|
||||||
'<div class="field-row" style="margin-bottom:12px">' +
|
'<div class="field-row" style="margin-bottom:12px">' +
|
||||||
@@ -1425,6 +1515,10 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
'<button class="btn" id="file-upload">上传文件</button>' +
|
'<button class="btn" id="file-upload">上传文件</button>' +
|
||||||
'<button class="btn" id="file-refresh">刷新</button>' +
|
'<button class="btn" id="file-refresh">刷新</button>' +
|
||||||
'<input type="file" id="file-input" multiple hidden></div>';
|
'<input type="file" id="file-input" multiple hidden></div>';
|
||||||
|
/* F1-④ 搜索(只读,当前目录起递归限深 4 层) */
|
||||||
|
h += '<div class="field-row wrap" style="margin-bottom:12px">' +
|
||||||
|
'<input type="text" id="file-search" placeholder="按名称搜索本目录(含子目录,限 4 层)" style="max-width:300px">' +
|
||||||
|
'<button class="btn" id="file-search-go">搜索</button></div>';
|
||||||
h += '<div class="card"><table class="table"><thead><tr><th>名称</th><th>大小</th><th>修改时间</th><th style="text-align:right">操作</th></tr></thead><tbody>';
|
h += '<div class="card"><table class="table"><thead><tr><th>名称</th><th>大小</th><th>修改时间</th><th style="text-align:right">操作</th></tr></thead><tbody>';
|
||||||
if (!d.items.length) h += '<tr><td colspan="4" class="empty">空目录</td></tr>';
|
if (!d.items.length) h += '<tr><td colspan="4" class="empty">空目录</td></tr>';
|
||||||
d.items.forEach(function (it) {
|
d.items.forEach(function (it) {
|
||||||
@@ -1450,6 +1544,49 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
|
|
||||||
function renderFiles() { $('#content').innerHTML = '<div class="page" id="files-page"></div>'; navFiles(state.filesRel || '/'); }
|
function renderFiles() { $('#content').innerHTML = '<div class="page" id="files-page"></div>'; navFiles(state.filesRel || '/'); }
|
||||||
|
|
||||||
|
/* F1-④ 搜索结果视图(GET /api/files/search,只读;下载/进入仍走既有端点) */
|
||||||
|
function searchParentRel(it) {
|
||||||
|
const rel = it.rel || '/';
|
||||||
|
if (it.type === 'dir') return rel;
|
||||||
|
const i = rel.lastIndexOf('/');
|
||||||
|
return i > 0 ? rel.slice(0, i) : '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchFiles() {
|
||||||
|
const q = ($('#file-search') ? $('#file-search').value : '').trim();
|
||||||
|
if (!q) return toast('请输入搜索关键字');
|
||||||
|
const page = $('#files-page'); if (!page) return;
|
||||||
|
const abs = state.filesAbs || '/';
|
||||||
|
page.innerHTML = '<p class="placeholder">在 ' + esc(state.filesRel || '/') + ' 内搜索「' + esc(q) + '」…</p>';
|
||||||
|
let d;
|
||||||
|
try { d = await api('GET', '/api/files/search?path=' + encodeURIComponent(abs) + '&name=' + encodeURIComponent(q)); }
|
||||||
|
catch (e) { page.innerHTML = emptyState('搜索失败:' + errText(e), e.code); return; }
|
||||||
|
let h = crumbHtml(segments(d.rel), d.rel);
|
||||||
|
h += '<div class="field-row wrap" style="margin-bottom:12px">' +
|
||||||
|
'<span class="muted">「' + esc(q) + '」在 ' + esc(d.rel) + ' 内命中 ' + (d.count || 0) + ' 条' +
|
||||||
|
(d.truncated ? '(已达上限,结果截断)' : '') + '</span>' +
|
||||||
|
'<button class="btn" id="file-search-back">返回目录</button></div>';
|
||||||
|
h += '<div class="card"><table class="table"><thead><tr><th>名称</th><th>位置</th><th>大小</th><th>修改时间</th><th style="text-align:right">操作</th></tr></thead><tbody>';
|
||||||
|
if (!d.items.length) h += '<tr><td colspan="5" class="empty">无匹配</td></tr>';
|
||||||
|
d.items.forEach(function (it) {
|
||||||
|
const isDir = it.type === 'dir';
|
||||||
|
const icon = isDir ? '📁' : '📄';
|
||||||
|
const parent = searchParentRel(it);
|
||||||
|
h += '<tr><td>' + icon + ' ' + esc(it.name) + '</td>' +
|
||||||
|
'<td class="muted mono">' + esc(parent) + '</td>' +
|
||||||
|
'<td class="muted">' + (isDir ? '—' : fmtBytes(it.size)) + '</td>' +
|
||||||
|
'<td class="muted">' + fmtTime(it.mtime) + '</td>' +
|
||||||
|
'<td><div class="row-actions">' +
|
||||||
|
(isDir
|
||||||
|
? '<button class="btn btn-ghost" data-sopen="' + esc(it.rel) + '">打开</button>'
|
||||||
|
: '<button class="btn btn-ghost" data-sopen="' + esc(parent) + '">所在目录</button>' +
|
||||||
|
'<a class="btn btn-ghost" data-sdl="' + esc(it.rel) + '" data-name="' + esc(it.name) + '" href="#">下载</a>') +
|
||||||
|
'</div></td></tr>';
|
||||||
|
});
|
||||||
|
h += '</tbody></table></div>';
|
||||||
|
page.innerHTML = h;
|
||||||
|
}
|
||||||
|
|
||||||
function absFor(name) { return state.filesAbs ? (state.filesAbs.endsWith('/') ? state.filesAbs + name : state.filesAbs + '/' + name) : null; }
|
function absFor(name) { return state.filesAbs ? (state.filesAbs.endsWith('/') ? state.filesAbs + name : state.filesAbs + '/' + name) : null; }
|
||||||
|
|
||||||
function bindFiles() {
|
function bindFiles() {
|
||||||
@@ -1460,6 +1597,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
const ren = t.closest('[data-ren]'); const del = t.closest('[data-del]');
|
const ren = t.closest('[data-ren]'); const del = t.closest('[data-del]');
|
||||||
const dirlink = t.closest('[data-dir]'); const nav = t.closest('[data-nav]');
|
const dirlink = t.closest('[data-dir]'); const nav = t.closest('[data-nav]');
|
||||||
const crumb = t.closest('[data-rel]');
|
const crumb = t.closest('[data-rel]');
|
||||||
|
const sopen = t.closest('[data-sopen]'); const sdl = t.closest('[data-sdl]');
|
||||||
const btn = t.closest('button[id]');
|
const btn = t.closest('button[id]');
|
||||||
if (crumb) { navFiles(crumb.dataset.rel); return; }
|
if (crumb) { navFiles(crumb.dataset.rel); return; }
|
||||||
if (nav) { navFiles(nav.dataset.nav); return; }
|
if (nav) { navFiles(nav.dataset.nav); return; }
|
||||||
@@ -1469,11 +1607,22 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
doDownload('/api/files/download?path=' + encodeURIComponent(abs), dl.dataset.dl);
|
doDownload('/api/files/download?path=' + encodeURIComponent(abs), dl.dataset.dl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (sopen) { navFiles(sopen.dataset.sopen); return; } /* F1-④ 搜索结果:打开目录/所在目录 */
|
||||||
|
if (sdl) { /* F1-④ 搜索结果:下载(rel 已是卡根相对) */
|
||||||
|
e.preventDefault();
|
||||||
|
doDownload('/api/files/download?path=' + encodeURIComponent(sdl.dataset.sdl), sdl.dataset.name || sdl.dataset.sdl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (ren) { renameFile(ren.dataset.ren); return; }
|
if (ren) { renameFile(ren.dataset.ren); return; }
|
||||||
if (del) { removeFile(del.dataset.del); return; }
|
if (del) { removeFile(del.dataset.del); return; }
|
||||||
if (btn && btn.id === 'file-newdir') { mkdirFile(); return; }
|
if (btn && btn.id === 'file-newdir') { mkdirFile(); return; }
|
||||||
if (btn && btn.id === 'file-upload') { const fi = $('#file-input'); if (fi) { fi.value = ''; fi.click(); } return; }
|
if (btn && btn.id === 'file-upload') { const fi = $('#file-input'); if (fi) { fi.value = ''; fi.click(); } return; }
|
||||||
if (btn && btn.id === 'file-refresh') { navFiles(state.filesRel); return; }
|
if (btn && btn.id === 'file-refresh') { navFiles(state.filesRel); return; }
|
||||||
|
if (btn && btn.id === 'file-search-go') { searchFiles(); return; } /* F1-④ */
|
||||||
|
if (btn && btn.id === 'file-search-back') { navFiles(state.filesRel); return; } /* F1-④ */
|
||||||
|
});
|
||||||
|
c.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Enter' && e.target && e.target.id === 'file-search') { e.preventDefault(); searchFiles(); }
|
||||||
});
|
});
|
||||||
c.addEventListener('change', function (e) {
|
c.addEventListener('change', function (e) {
|
||||||
if (e.target.id === 'file-input') uploadFiles(e.target.files);
|
if (e.target.id === 'file-input') uploadFiles(e.target.files);
|
||||||
@@ -1575,9 +1724,11 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
const install = t.closest('#app-install'); const refresh = t.closest('#app-refresh');
|
const install = t.closest('#app-install'); const refresh = t.closest('#app-refresh');
|
||||||
const info = t.closest('[data-ainfo]'); const exp = t.closest('[data-aexport]');
|
const info = t.closest('[data-ainfo]'); const exp = t.closest('[data-aexport]');
|
||||||
const uninst = t.closest('[data-auninst]'); const goPath = t.closest('#app-install-path-go');
|
const uninst = t.closest('[data-auninst]'); const goPath = t.closest('#app-install-path-go');
|
||||||
|
const aclose = t.closest('[data-aclose]');
|
||||||
if (install) { const fi = $('#app-file'); if (fi) { fi.value = ''; fi.click(); } return; }
|
if (install) { const fi = $('#app-file'); if (fi) { fi.value = ''; fi.click(); } return; }
|
||||||
if (refresh) { renderApps(); return; }
|
if (refresh) { renderApps(); return; }
|
||||||
if (info) { appInfo(info.dataset.ainfo); return; }
|
if (info) { appInfo(info.dataset.ainfo); return; }
|
||||||
|
if (aclose) { appDetailRemove(); return; }
|
||||||
if (exp) {
|
if (exp) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (exp.dataset.busy) return; /* 打包期间防连点 */
|
if (exp.dataset.busy) return; /* 打包期间防连点 */
|
||||||
@@ -1624,14 +1775,110 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
catch (e) { toast('卸载失败:' + errText(e)); }
|
catch (e) { toast('卸载失败:' + errText(e)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function appInfo(pkg) {
|
/* ---- F1-⑤ 单 App 详情四源聚合(FUTURE_MEOWPANEL §2.4,纯前端):apps 页行内展开,
|
||||||
|
* 并发聚合既有端点:①安装域 /api/apps/:pkg ②运行崩溃史 /api/ams/:pkg ③实时连接
|
||||||
|
* /api/net/conns?entity=app:<pkg> ④stdout 尾部(newestStdout + /api/logs/read)。
|
||||||
|
* 单源失败 → 该子块「—/不可用」,绝不拖垮整卡(overview 同款降级)。 ---- */
|
||||||
|
function appDetailRemove() {
|
||||||
|
const el = document.querySelector('.app-detail-card');
|
||||||
|
if (el) el.remove();
|
||||||
|
appDetailPkg = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appStdoutBlock(pkg) {
|
||||||
try {
|
try {
|
||||||
const d = await api('GET', '/api/apps/' + pkg);
|
const file = await newestStdout(pkg);
|
||||||
let txt = '包名: ' + d.package + '\n名称: ' + d.name + '\n版本: ' + d.version + '\nexec: ' + d.exec +
|
if (!file) return '<div class="empty">无 stdout 日志</div>';
|
||||||
'\nsize: ' + fmtBytes(d.size) + '\n路径: ' + d.path;
|
const d = await api('GET', '/api/logs/read?file=' + encodeURIComponent(pkg + '/' + file) + '&lines=200');
|
||||||
if (d.description) txt += '\n描述: ' + d.description;
|
const lines = d.lines || [];
|
||||||
alert(txt);
|
return '<pre class="log-view">' + esc(lines.join('\n') || '(空)') +
|
||||||
} catch (e) { toast('获取详情失败:' + errText(e)); }
|
(d.truncated ? '\n…(超出尾部上限,部分被截断)' : '') + '</pre>' +
|
||||||
|
'<div class="muted">' + esc(pkg + '/' + file) + ' · 末尾 ' + lines.length + ' 行</div>';
|
||||||
|
} catch (e) { return '<div class="empty">stdout 读取失败:' + esc(errText(e)) + '</div>'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appDetailBody(pkg) {
|
||||||
|
const r = await Promise.allSettled([
|
||||||
|
api('GET', '/api/apps/' + pkg),
|
||||||
|
api('GET', '/api/ams/' + pkg),
|
||||||
|
api('GET', '/api/net/conns?entity=' + encodeURIComponent('app:' + pkg)),
|
||||||
|
appStdoutBlock(pkg)
|
||||||
|
]);
|
||||||
|
|
||||||
|
let h = '';
|
||||||
|
/* ① 安装域(app.json) */
|
||||||
|
const ap = r[0].status === 'fulfilled' ? r[0].value : null;
|
||||||
|
if (ap) {
|
||||||
|
h += '<table class="table"><tbody>' +
|
||||||
|
row('名称', esc(ap.name || '—')) +
|
||||||
|
row('版本', esc(ap.version || '—')) +
|
||||||
|
row('执行', '<span class="mono">' + esc(ap.exec || '—') + '</span>') +
|
||||||
|
row('体积', fmtBytes(ap.size)) +
|
||||||
|
row('路径', '<span class="mono muted">' + esc(ap.path || '—') + '</span>') +
|
||||||
|
'</tbody></table>';
|
||||||
|
} else {
|
||||||
|
h += '<div class="empty">安装详情不可用' + (r[0].status === 'rejected' ? ':' + esc(errText(r[0].reason)) : '') + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ② 运行域崩溃/事件史(am history) */
|
||||||
|
h += '<div class="home-sub" style="margin-top:12px">崩溃 / 事件历史(amsd)</div>';
|
||||||
|
const am = r[1].status === 'fulfilled' ? r[1].value : null;
|
||||||
|
const evs = (am && am.available && am.events) || [];
|
||||||
|
if (evs.length) {
|
||||||
|
h += '<table class="table"><thead><tr><th>时间</th><th>类型</th><th>详情</th></tr></thead><tbody>';
|
||||||
|
evs.slice().reverse().slice(0, 10).forEach(function (ev) {
|
||||||
|
h += '<tr><td class="muted">' + fmtTime(ev.ts) + '</td><td>' + esc(ev.kind || '—') + '</td>' +
|
||||||
|
'<td class="muted">' + esc(ev.detail || '—') + '</td></tr>';
|
||||||
|
});
|
||||||
|
h += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
h += '<div class="empty">无事件记录' + (r[1].status === 'rejected' ? '(' + esc(errText(r[1].reason)) + ')' : '') + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ③ 实时连接(app:<pkg>) */
|
||||||
|
h += '<div class="home-sub" style="margin-top:12px">实时连接(app:' + esc(pkg) + ')</div>';
|
||||||
|
const cn = r[2].status === 'fulfilled' ? r[2].value : null;
|
||||||
|
const conns = (cn && cn.conns) || [];
|
||||||
|
if (conns.length) {
|
||||||
|
h += '<table class="table"><thead><tr><th>远端</th><th>本端</th><th>状态</th><th>寿命</th></tr></thead><tbody>';
|
||||||
|
conns.forEach(function (c) {
|
||||||
|
h += '<tr><td class="mono">' + esc(c.domain || c.remote || '—') + '</td>' +
|
||||||
|
'<td class="mono muted">' + esc(c.local || '—') + '</td>' +
|
||||||
|
'<td>' + esc(c.state || '—') + '</td><td class="muted">' + fmtDur(c.age) + '</td></tr>';
|
||||||
|
});
|
||||||
|
h += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
h += '<div class="empty">无连接' + (r[2].status === 'rejected' ? '(' + esc(errText(r[2].reason)) + ')' : '') + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ④ stdout 尾部 */
|
||||||
|
h += '<div class="home-sub" style="margin-top:12px">stdout 尾部</div>';
|
||||||
|
h += (r[3].status === 'fulfilled' && r[3].value) ? r[3].value : '<div class="empty">stdout 读取失败</div>';
|
||||||
|
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appInfo(pkg) {
|
||||||
|
if (appDetailPkg === pkg) { appDetailRemove(); return; } // 已展开 → 收起
|
||||||
|
appDetailRemove(); // 收起上一个
|
||||||
|
const card = document.querySelector('.app-card[data-pkg="' + escSafeAttr(pkg) + '"]');
|
||||||
|
if (!card) return;
|
||||||
|
const box = document.createElement('div');
|
||||||
|
box.className = 'card app-detail-card';
|
||||||
|
box.innerHTML = '<div class="app-detail-head"><strong>' + esc(pkg) + '</strong>' +
|
||||||
|
'<button class="btn btn-ghost" data-aclose="' + esc(pkg) + '">收起</button></div>' +
|
||||||
|
'<p class="placeholder">并发聚合四源详情…</p>';
|
||||||
|
card.after(box);
|
||||||
|
appDetailPkg = pkg;
|
||||||
|
try {
|
||||||
|
box.innerHTML = '<div class="app-detail-head"><strong>' + esc(pkg) + '</strong>' +
|
||||||
|
'<button class="btn btn-ghost" data-aclose="' + esc(pkg) + '">收起</button></div>' + (await appDetailBody(pkg));
|
||||||
|
} catch (e) {
|
||||||
|
box.innerHTML = '<div class="app-detail-head"><strong>' + esc(pkg) + '</strong>' +
|
||||||
|
'<button class="btn btn-ghost" data-aclose="' + esc(pkg) + '">收起</button></div>' +
|
||||||
|
'<div class="empty">详情加载失败:' + esc(errText(e)) + '</div>';
|
||||||
|
appDetailPkg = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFontsPage() {
|
function renderFontsPage() {
|
||||||
|
|||||||
@@ -99,6 +99,53 @@
|
|||||||
if (arr.length > SPARK_MAX) arr.shift();
|
if (arr.length > SPARK_MAX) arr.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- F1-① 性能趋势(FUTURE_MEOWPANEL §2.1):pushSpark 模式泛化到任意 0..100 归一值 ---- */
|
||||||
|
const trendCpu = [], trendMem = [], trendLoad = [], trendTemp = [];
|
||||||
|
|
||||||
|
function pushTrend(arr, v) { // 0..100 归一值入队(<=SPARK_MAX 点);null/NaN 不入队(诚实空缺)
|
||||||
|
if (v == null || isNaN(v)) return;
|
||||||
|
arr.push(Math.max(0, Math.min(100, Number(v))));
|
||||||
|
if (arr.length > SPARK_MAX) arr.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTrend() { trendCpu.length = 0; trendMem.length = 0; trendLoad.length = 0; trendTemp.length = 0; }
|
||||||
|
|
||||||
|
/* series: [{cls,arr}],cls 决定颜色(CSS .trend-cpu/.trend-mem/.trend-load/.trend-temp),
|
||||||
|
* 位置稳定(首页 3 序列、perf 页 4 序列共用同一语义色,不靠序号猜色)。 */
|
||||||
|
function trendSvgHtml(series) {
|
||||||
|
let polys = '';
|
||||||
|
series.forEach(function (s) {
|
||||||
|
polys += '<polyline class="' + s.cls + '" points="" fill="none" stroke-width="1.6" vector-effect="non-scaling-stroke"/>';
|
||||||
|
});
|
||||||
|
return '<svg class="lvl-spark trend-spark" viewBox="0 0 100 46" preserveAspectRatio="none" aria-hidden="true">' +
|
||||||
|
'<line class="grid" x1="0" y1="46" x2="100" y2="46"/>' +
|
||||||
|
'<line class="grid" x1="0" y1="23" x2="100" y2="23"/>' + polys + '</svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTrendSpark(rootEl, series) {
|
||||||
|
const svg = rootEl && rootEl.querySelector('.trend-spark');
|
||||||
|
if (!svg) return;
|
||||||
|
series.forEach(function (s) {
|
||||||
|
const p = svg.querySelector('.' + s.cls);
|
||||||
|
if (p) p.setAttribute('points', sparklinePoints(s.arr));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用折线点串(任意长度数组,值 0..100 归一;null 跳过):x 铺满 w,y 反向映射 h 内(F1-② 功耗 4h 曲线用) */
|
||||||
|
function polylinePoints(vals, w, h) {
|
||||||
|
const n = vals.length;
|
||||||
|
if (!n) return '';
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const v = vals[i];
|
||||||
|
if (v == null || isNaN(v)) continue;
|
||||||
|
const x = n > 1 ? (i / (n - 1)) * w : w / 2;
|
||||||
|
const y = h - 2 - Math.max(0, Math.min(100, v)) / 100 * (h - 4);
|
||||||
|
out.push(x.toFixed(1) + ',' + y.toFixed(1));
|
||||||
|
}
|
||||||
|
return out.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
function drawSpark(rootEl) {
|
function drawSpark(rootEl) {
|
||||||
const svg = rootEl && rootEl.querySelector('.lvl-spark');
|
const svg = rootEl && rootEl.querySelector('.lvl-spark');
|
||||||
if (!svg) return;
|
if (!svg) return;
|
||||||
@@ -463,5 +510,5 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(MP, { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode, authRequired, barHtml, bindLogin, bindPwDlg, bindSlider, bindThemeUi, browserMode, confirmDlg, dbPct, dbg, dbgErr, debounce, deciToDb, devStyleLabel, devThemeLabel, doLogin, drawSpark, emptyState, errText, esc, fmtBytes, fmtDb, fmtTime, fmtUptime, hideLogin, homeCard, homeKv, homeUnavailable, initTheme, isAvail, levelMeterHtml, loginErr, modePill, passwordDlg, pct, pushSpark, refreshAuthGate, resetSpark, resolveMode, resolveStyle, saveSetting, setConn, showLogin, slur, sparkPeak, sparkRms, sparklineHtml, sparklinePoints, stat, state, storageDel, storageGet, storageSet, storedMode, storedStyle, toast, toastTimer, updateLevelMeter, volTimer });
|
Object.assign(MP, { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode, authRequired, barHtml, bindLogin, bindPwDlg, bindSlider, bindThemeUi, browserMode, confirmDlg, dbPct, dbg, dbgErr, debounce, deciToDb, devStyleLabel, devThemeLabel, doLogin, drawSpark, drawTrendSpark, emptyState, errText, esc, fmtBytes, fmtDb, fmtTime, fmtUptime, hideLogin, homeCard, homeKv, homeUnavailable, initTheme, isAvail, levelMeterHtml, loginErr, modePill, passwordDlg, pct, polylinePoints, pushSpark, pushTrend, refreshAuthGate, resetSpark, resetTrend, resolveMode, resolveStyle, saveSetting, setConn, showLogin, slur, sparkPeak, sparkRms, sparklineHtml, sparklinePoints, stat, state, storageDel, storageGet, storageSet, storedMode, storedStyle, toast, toastTimer, trendCpu, trendLoad, trendMem, trendSvgHtml, trendTemp, updateLevelMeter, volTimer });
|
||||||
})();
|
})();
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
/os.meow.reader.mpk
|
/os.meow.reader.mpk
|
||||||
/test/test_reader_core_host
|
/test/test_reader_core_host
|
||||||
/test/loaders_smoke_host
|
/test/loaders_smoke_host
|
||||||
|
/test/seg_smoke_host
|
||||||
|
/test/chapter_smoke_host
|
||||||
/test/rdft_smoke_mipsel
|
/test/rdft_smoke_mipsel
|
||||||
/test/book_smoke_mipsel
|
/test/book_smoke_mipsel
|
||||||
/test/formats_smoke_mipsel
|
/test/formats_smoke_mipsel
|
||||||
|
|||||||
@@ -232,3 +232,26 @@ Library.
|
|||||||
project: https://github.com/nothings/stb
|
project: https://github.com/nothings/stb
|
||||||
Used for image decode + downsample (CBZ pages, covers).
|
Used for image decode + downsample (CBZ pages, covers).
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
4. FreeType — Copyright (c) 1996-2002, 2006, David Turner, Robert
|
||||||
|
Wilhelm, and Werner Lemberg
|
||||||
|
license: The FreeType Project License (FTL)
|
||||||
|
version: 2.14.3 (fetched by build_reader.sh)
|
||||||
|
project: https://freetype.org
|
||||||
|
Used for glyph loading, glyph measurement, and rasterization
|
||||||
|
of the reader's text layout.
|
||||||
|
|
||||||
|
Portions of this software are copyright © 2006 The FreeType
|
||||||
|
Project (https://freetype.org). All rights reserved.
|
||||||
|
|
||||||
|
Full license text (shipped with the package in
|
||||||
|
resources/licenses/):
|
||||||
|
- The FreeType Project License: resources/licenses/FTL.txt
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
5. zlib — Copyright (c) 1995-2024 Jean-loup Gailly and Mark Adler
|
||||||
|
license: zlib license (permissive)
|
||||||
|
project: https://zlib.net
|
||||||
|
Statically linked for compression support (zlib on the
|
||||||
|
target sysroot; also used by the build toolchain).
|
||||||
|
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
The FreeType Project LICENSE
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
2006-Jan-27
|
||||||
|
|
||||||
|
Copyright 1996-2002, 2006 by
|
||||||
|
David Turner, Robert Wilhelm, and Werner Lemberg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
The FreeType Project is distributed in several archive packages;
|
||||||
|
some of them may contain, in addition to the FreeType font engine,
|
||||||
|
various tools and contributions which rely on, or relate to, the
|
||||||
|
FreeType Project.
|
||||||
|
|
||||||
|
This license applies to all files found in such packages, and
|
||||||
|
which do not fall under their own explicit license. The license
|
||||||
|
affects thus the FreeType font engine, the test programs,
|
||||||
|
documentation and makefiles, at the very least.
|
||||||
|
|
||||||
|
This license was inspired by the BSD, Artistic, and IJG
|
||||||
|
(Independent JPEG Group) licenses, which all encourage inclusion
|
||||||
|
and use of free software in commercial and freeware products
|
||||||
|
alike. As a consequence, its main points are that:
|
||||||
|
|
||||||
|
o We don't promise that this software works. However, we will be
|
||||||
|
interested in any kind of bug reports. (`as is' distribution)
|
||||||
|
|
||||||
|
o You can use this software for whatever you want, in parts or
|
||||||
|
full form, without having to pay us. (`royalty-free' usage)
|
||||||
|
|
||||||
|
o You may not pretend that you wrote this software. If you use
|
||||||
|
it, or only parts of it, in a program, you must acknowledge
|
||||||
|
somewhere in your documentation that you have used the
|
||||||
|
FreeType code. (`credits')
|
||||||
|
|
||||||
|
We specifically permit and encourage the inclusion of this
|
||||||
|
software, with or without modifications, in commercial products.
|
||||||
|
We disclaim all warranties covering The FreeType Project and
|
||||||
|
assume no liability related to The FreeType Project.
|
||||||
|
|
||||||
|
|
||||||
|
Finally, many people asked us for a preferred form for a
|
||||||
|
credit/disclaimer to use in compliance with this license. We thus
|
||||||
|
encourage you to use the following text:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Portions of this software are copyright © <year> The FreeType
|
||||||
|
Project (https://freetype.org). All rights reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Please replace <year> with the value from the FreeType version you
|
||||||
|
actually use.
|
||||||
|
|
||||||
|
|
||||||
|
Legal Terms
|
||||||
|
===========
|
||||||
|
|
||||||
|
0. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Throughout this license, the terms `package', `FreeType Project',
|
||||||
|
and `FreeType archive' refer to the set of files originally
|
||||||
|
distributed by the authors (David Turner, Robert Wilhelm, and
|
||||||
|
Werner Lemberg) as the `FreeType Project', be they named as alpha,
|
||||||
|
beta or final release.
|
||||||
|
|
||||||
|
`You' refers to the licensee, or person using the project, where
|
||||||
|
`using' is a generic term including compiling the project's source
|
||||||
|
code as well as linking it to form a `program' or `executable'.
|
||||||
|
This program is referred to as `a program using the FreeType
|
||||||
|
engine'.
|
||||||
|
|
||||||
|
This license applies to all files distributed in the original
|
||||||
|
FreeType Project, including all source code, binaries and
|
||||||
|
documentation, unless otherwise stated in the file in its
|
||||||
|
original, unmodified form as distributed in the original archive.
|
||||||
|
If you are unsure whether or not a particular file is covered by
|
||||||
|
this license, you must contact us to verify this.
|
||||||
|
|
||||||
|
The FreeType Project is copyright (C) 1996-2000 by David Turner,
|
||||||
|
Robert Wilhelm, and Werner Lemberg. All rights reserved except as
|
||||||
|
specified below.
|
||||||
|
|
||||||
|
1. No Warranty
|
||||||
|
--------------
|
||||||
|
|
||||||
|
THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY
|
||||||
|
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO
|
||||||
|
USE, OF THE FREETYPE PROJECT.
|
||||||
|
|
||||||
|
2. Redistribution
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
This license grants a worldwide, royalty-free, perpetual and
|
||||||
|
irrevocable right and license to use, execute, perform, compile,
|
||||||
|
display, copy, create derivative works of, distribute and
|
||||||
|
sublicense the FreeType Project (in both source and object code
|
||||||
|
forms) and derivative works thereof for any purpose; and to
|
||||||
|
authorize others to exercise some or all of the rights granted
|
||||||
|
herein, subject to the following conditions:
|
||||||
|
|
||||||
|
o Redistribution of source code must retain this license file
|
||||||
|
(`FTL.TXT') unaltered; any additions, deletions or changes to
|
||||||
|
the original files must be clearly indicated in accompanying
|
||||||
|
documentation. The copyright notices of the unaltered,
|
||||||
|
original files must be preserved in all copies of source
|
||||||
|
files.
|
||||||
|
|
||||||
|
o Redistribution in binary form must provide a disclaimer that
|
||||||
|
states that the software is based in part of the work of the
|
||||||
|
FreeType Team, in the distribution documentation. We also
|
||||||
|
encourage you to put an URL to the FreeType web page in your
|
||||||
|
documentation, though this isn't mandatory.
|
||||||
|
|
||||||
|
These conditions apply to any software derived from or based on
|
||||||
|
the FreeType Project, not just the unmodified files. If you use
|
||||||
|
our work, you must acknowledge us. However, no fee need be paid
|
||||||
|
to us.
|
||||||
|
|
||||||
|
3. Advertising
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Neither the FreeType authors and contributors nor you shall use
|
||||||
|
the name of the other for commercial, advertising, or promotional
|
||||||
|
purposes without specific prior written permission.
|
||||||
|
|
||||||
|
We suggest, but do not require, that you use one or more of the
|
||||||
|
following phrases to refer to this software in your documentation
|
||||||
|
or advertising materials: `FreeType Project', `FreeType Engine',
|
||||||
|
`FreeType library', or `FreeType Distribution'.
|
||||||
|
|
||||||
|
As you have not signed this license, you are not required to
|
||||||
|
accept it. However, as the FreeType Project is copyrighted
|
||||||
|
material, only this license, or another one contracted with the
|
||||||
|
authors, grants you the right to use, distribute, and modify it.
|
||||||
|
Therefore, by using, distributing, or modifying the FreeType
|
||||||
|
Project, you indicate that you understand and accept all the terms
|
||||||
|
of this license.
|
||||||
|
|
||||||
|
4. Contacts
|
||||||
|
-----------
|
||||||
|
|
||||||
|
There are two mailing lists related to FreeType:
|
||||||
|
|
||||||
|
o freetype@nongnu.org
|
||||||
|
|
||||||
|
Discusses general use and applications of FreeType, as well as
|
||||||
|
future and wanted additions to the library and distribution.
|
||||||
|
If you are looking for support, start in this list if you
|
||||||
|
haven't found anything to help you in the documentation.
|
||||||
|
|
||||||
|
o freetype-devel@nongnu.org
|
||||||
|
|
||||||
|
Discusses bugs, as well as engine internals, design issues,
|
||||||
|
specific licenses, porting, etc.
|
||||||
|
|
||||||
|
Our home page can be found at
|
||||||
|
|
||||||
|
https://freetype.org
|
||||||
|
|
||||||
|
|
||||||
|
--- end of FTL.TXT ---
|
||||||
@@ -232,3 +232,26 @@ Library.
|
|||||||
project: https://github.com/nothings/stb
|
project: https://github.com/nothings/stb
|
||||||
Used for image decode + downsample (CBZ pages, covers).
|
Used for image decode + downsample (CBZ pages, covers).
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
4. FreeType — Copyright (c) 1996-2002, 2006, David Turner, Robert
|
||||||
|
Wilhelm, and Werner Lemberg
|
||||||
|
license: The FreeType Project License (FTL)
|
||||||
|
version: 2.14.3 (fetched by build_reader.sh)
|
||||||
|
project: https://freetype.org
|
||||||
|
Used for glyph loading, glyph measurement, and rasterization
|
||||||
|
of the reader's text layout.
|
||||||
|
|
||||||
|
Portions of this software are copyright © 2006 The FreeType
|
||||||
|
Project (https://freetype.org). All rights reserved.
|
||||||
|
|
||||||
|
Full license text (shipped with the package in
|
||||||
|
resources/licenses/):
|
||||||
|
- The FreeType Project License: resources/licenses/FTL.txt
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
5. zlib — Copyright (c) 1995-2024 Jean-loup Gailly and Mark Adler
|
||||||
|
license: zlib license (permissive)
|
||||||
|
project: https://zlib.net
|
||||||
|
Statically linked for compression support (zlib on the
|
||||||
|
target sysroot; also used by the build toolchain).
|
||||||
|
|
||||||
Reference in new issue
Block a user