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:
AskaEth committed 2026-11-04 20:10:42 +08:00
1 parent 2ffb92c038
commit 20f4727714
7 files changed
+551 -23

No files matched your search

+17
View File
@@ -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-meta{ font-size:12px; color:var(--on-surface-variant); }
.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 圆角卡 + 行右杀按钮) */
.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-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) */
.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); }
+269 -22
View File
@@ -3,7 +3,7 @@
(function () {
'use strict';
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 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 procTimer = null;
let expandedProc = null; // { pkg, file, lines, truncated }:已展开的 App stdout 尾部(重渲染后重开,防轮询清空)
let appDetailPkg = null; // F1-⑤ 当前展开「App 详情四源聚合」的包名
let netTimer = null;
let netEntityFilter = '';
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>',
tmp && tmp.board != null ? '板载 ' + (+tmp.board.toFixed(1)) + ' ℃' : '');
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;
drawTrendSpark(page, perfTrend);
}
async function fetchPerfOnce() {
@@ -109,6 +131,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
function renderPerf() {
$('#content').innerHTML = '<div class="page" id="perf-page"><p class="placeholder">加载中…</p></div>';
if (state.perfTimer) stopPerf();
resetTrend(); // 进页重置趋势累积(F1-①)
fetchPerfOnce();
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 cpuU = cpu.usage;
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))">' +
'<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">' + (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() {
@@ -483,6 +516,11 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
deviceCard() + batteryCard() + audioCard() + netCard() + btCard() + healthCard() + storageCard() + perfCard() +
'</div>';
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;
if (isAvail(ad)) updateLevelMeter($('#home-audio-level'), ad);
}
@@ -522,6 +560,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
function renderHome() {
stopHome();
resetSpark(); // 进入首页重置 sparkline 累积
resetTrend(); // 进入首页重置性能趋势累积(F1-①)
homeData = null; homeSys = null;
$('#content').innerHTML =
'<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>';
}
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) {
if (!h) return '';
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.avg_mw != null ? h.avg_mw + ' mW' : '—')) +
'</tbody></table>';
body += powerHistoryChart(h); // F1-② est_mw 4h 曲线(纯前端画既有 samples)
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 += '</tbody></table></div>';
// 蓝牙摘要
const bt = (d && d.bt) || {};
h += '<div class="card"><h3 class="card-title">蓝牙</h3><table class="table"><tbody>';
if (bt.available) {
h += row('状态', '<span class="net-state ' + (bt.powered ? 'ok' : 'warn') + '">' + (bt.powered ? '已开启' : '已关闭') + '</span>');
h += row('地址', esc(bt.address || '—'));
h += row('已配对', esc(bt.paired != null ? bt.paired : '—'));
h += row('DNS', esc(((bt.dns) || []).join(' / ') || '—'));
} else {
h += row('状态', '<span class="net-state warn">无蓝牙</span>');
return h;
}
/* 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('地址', esc(bt.address || '—'));
h += row('已配对 / 已连接', (bt.paired_n != null ? esc(bt.paired_n) : '—') + ' / ' + (bt.connected_n != null ? esc(bt.connected_n) : '—'));
h += '</tbody></table>';
h += btDevTable('已配对设备', bt.paired);
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;
}
@@ -1239,8 +1326,8 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
async function fetchNetOnce() {
if (state.route !== 'net') return;
const stEl = $('#net-status'), cbEl = $('#net-conns-body'), dbEl = $('#net-dns-body'),
ifEl = $('#net-ifaces'), poEl = $('#net-policy');
let st, cn, dn, ifd, pol;
ifEl = $('#net-ifaces'), poEl = $('#net-policy'), btEl = $('#net-bt');
let st, cn, dn, ifd, pol, bt;
try { st = await api('GET', '/api/net/status'); }
catch (e) { netBannerErr(errText(e)); return; }
netLoaded = true;
@@ -1251,9 +1338,11 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
catch (e) { dn = 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 { bt = await api('GET', '/api/net/bt'); } catch (e) { bt = null; } /* F1-③ 蓝牙只读详情 */
if (state.route !== 'net') return;
if (ifEl) ifEl.innerHTML = netIfaceCards(ifd);
if (stEl) stEl.innerHTML = netStatusCards(st);
if (btEl) btEl.innerHTML = netBtCard(bt);
if (poEl) poEl.innerHTML = netPolicyCard(pol);
if (cbEl) cbEl.innerHTML = netConnsBody(cn);
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-ifaces"><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 class="card"><h3 class="card-title">每实体连接表</h3>' +
'<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-refresh">刷新</button>' +
'<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>';
if (!d.items.length) h += '<tr><td colspan="4" class="empty">空目录</td></tr>';
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 || '/'); }
/* 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 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 dirlink = t.closest('[data-dir]'); const nav = t.closest('[data-nav]');
const crumb = t.closest('[data-rel]');
const sopen = t.closest('[data-sopen]'); const sdl = t.closest('[data-sdl]');
const btn = t.closest('button[id]');
if (crumb) { navFiles(crumb.dataset.rel); 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);
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 (del) { removeFile(del.dataset.del); 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-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) {
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 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 aclose = t.closest('[data-aclose]');
if (install) { const fi = $('#app-file'); if (fi) { fi.value = ''; fi.click(); } return; }
if (refresh) { renderApps(); return; }
if (info) { appInfo(info.dataset.ainfo); return; }
if (aclose) { appDetailRemove(); return; }
if (exp) {
e.preventDefault();
if (exp.dataset.busy) return; /* 打包期间防连点 */
@@ -1624,14 +1775,110 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
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 {
const d = await api('GET', '/api/apps/' + pkg);
let txt = '包名: ' + d.package + '\n名称: ' + d.name + '\n版本: ' + d.version + '\nexec: ' + d.exec +
'\nsize: ' + fmtBytes(d.size) + '\n路径: ' + d.path;
if (d.description) txt += '\n描述: ' + d.description;
alert(txt);
} catch (e) { toast('获取详情失败:' + errText(e)); }
const file = await newestStdout(pkg);
if (!file) return '<div class="empty">无 stdout 日志</div>';
const d = await api('GET', '/api/logs/read?file=' + encodeURIComponent(pkg + '/' + file) + '&lines=200');
const lines = d.lines || [];
return '<pre class="log-view">' + esc(lines.join('\n') || '(空)') +
(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() {
+48 -1
View File
@@ -99,6 +99,53 @@
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) {
const svg = rootEl && rootEl.querySelector('.lvl-spark');
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 });
})();
+2
View File
@@ -4,6 +4,8 @@
/os.meow.reader.mpk
/test/test_reader_core_host
/test/loaders_smoke_host
/test/seg_smoke_host
/test/chapter_smoke_host
/test/rdft_smoke_mipsel
/test/book_smoke_mipsel
/test/formats_smoke_mipsel
+23
View File
@@ -232,3 +232,26 @@ Library.
project: https://github.com/nothings/stb
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
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).