feat(main): 添加CAN 1Hz反馈轮询任务和HTTP接口
- 新增monitor/can_feedback_task.cpp组件,实现CAN 1Hz反馈轮询功能 - 在main.cpp中注册CAN反馈轮询任务,依赖SPI初始化完成后启动 - 在OTA协调器和中继模块中集成CAN反馈任务暂停/恢复机制 - 扩展CANFD桥接层,添加0xC1反馈查询、0xC2模式切换、0xC3ID快照协议支持 - 新增HTTP接口/can/feedback提供三路CAN反馈快照数据 - 新增HTTP接口/can/mode支持查询和切换CAN接收过滤模式 - 更新版本号从0.3.34到0.35 - 扩大SPI缓冲区以支持更大帧传输需求 BREAKING CHANGE: 新增CAN反馈监控功能需要GD32固件支持0xC1/0xC2/0xC3协议扩展
This commit is contained in:
+4
-2
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 CAN 通信控制台</title>
|
||||
<link rel="stylesheet" href="style.css?v=20260624b">
|
||||
<link rel="stylesheet" href="style.css?v=20260628a">
|
||||
</head>
|
||||
<body class="can-page">
|
||||
<div class="container can-container">
|
||||
@@ -23,6 +23,7 @@
|
||||
</div>
|
||||
<div class="header-sub can-header-actions">
|
||||
<a class="pill can-link-pill" href="index.html">返回 OTA 控制台</a>
|
||||
<a class="pill can-link-pill" href="can_monitor.html">CAN 接收监控</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -101,6 +102,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<div class="can-editor-layout">
|
||||
<section class="card can-signal-card">
|
||||
<div class="card-head"><span class="ico">🎚️</span> 信号编辑</div>
|
||||
@@ -204,6 +206,6 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script src="can.js?v=20260624b"></script>
|
||||
<script src="can.js?v=20260628a"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1093,6 +1093,8 @@
|
||||
showToast('DBC 解析失败: ' + err.message, 'error');
|
||||
}
|
||||
|
||||
|
||||
|
||||
function init() {
|
||||
cacheElements();
|
||||
bindEvents();
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 CAN 接收监控</title>
|
||||
<link rel="stylesheet" href="style.css?v=20260628a">
|
||||
</head>
|
||||
<body class="can-page">
|
||||
<div class="container can-container">
|
||||
|
||||
<header class="header-bar can-header">
|
||||
<div class="header-brand">
|
||||
<span class="dot-mark"></span>
|
||||
<strong>ESP32 CAN 接收监控</strong>
|
||||
</div>
|
||||
<div class="header-status">
|
||||
<span class="pill offline" id="monConnPill"><span class="indicator"></span><span id="monConnTxt">未连接</span></span>
|
||||
<span class="pill" id="fbModePill">模式: --</span>
|
||||
<span class="pill" id="fbPollStatus">轮询: 待连接</span>
|
||||
</div>
|
||||
<div class="header-sub can-header-actions">
|
||||
<a class="pill can-link-pill" href="can.html">CAN 发送控制台</a>
|
||||
<a class="pill can-link-pill" href="index.html">返回 OTA 控制台</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ═══ 工作台 ═══ -->
|
||||
<section class="card can-toolbar-card">
|
||||
<div class="card-head"><span class="ico">🧰</span> 连接设置</div>
|
||||
<div class="card-body">
|
||||
<div class="can-toolbar-grid" style="grid-template-columns: minmax(260px, 1.2fr) 1fr;">
|
||||
<div class="can-toolbar-block">
|
||||
<label class="form-label" for="monIp">ESP32 IP</label>
|
||||
<div class="input-group">
|
||||
<input class="input" type="text" id="monIp" value="192.168.31.127" placeholder="输入 IP 地址">
|
||||
<button class="btn btn-outline" id="monTestBtn" type="button">测试</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="can-toolbar-block">
|
||||
<label class="form-label">接收模式</label>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-primary btn-sm" id="btnModeAllPass" type="button">全收模式</button>
|
||||
<button class="btn btn-outline btn-sm" id="btnModeFilter" type="button">过滤模式</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="can-toolbar-foot">
|
||||
<div class="conn-indicator err" id="monConnIndicator">
|
||||
<span class="light"></span>
|
||||
<span id="monConnIndicatorTxt">等待连接测试</span>
|
||||
</div>
|
||||
<div class="helper-note" id="monHelper">
|
||||
页面每 1 秒通过 <code>/can/feedback</code> 拉取三路 CAN 通道快照。无 RX 的通道显示 <b>空(无 RX)</b>。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ 三路反馈 ═══ -->
|
||||
<section class="card can-feedback-card">
|
||||
<div class="card-head"><span class="ico">📊</span> 三路 CAN 通道反馈(1Hz)</div>
|
||||
<div class="card-body">
|
||||
<div class="feedback-grid">
|
||||
|
||||
<div class="feedback-channel" id="fbCh0">
|
||||
<div class="feedback-ch-head"><span class="ch-dot ch0"></span> CAN0</div>
|
||||
<div class="feedback-row"><span class="fb-label">状态</span><span class="fb-val" id="fb0State">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">接收数</span><span class="fb-val" id="fb0Rx">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">发送数</span><span class="fb-val" id="fb0Tx">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">丢帧</span><span class="fb-val" id="fb0Drop">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">错误</span><span class="fb-val" id="fb0Err">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">最新ID</span><span class="fb-val mono" id="fb0LastId">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">DLC</span><span class="fb-val" id="fb0Dlc">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">FD/BRS/ESI</span><span class="fb-val" id="fb0Flags">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">Seq</span><span class="fb-val" id="fb0Seq">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">Δrx/s</span><span class="fb-val" id="fb0Delta">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">最新HEX</span><span class="fb-val mono fb-data-cell" id="fb0LastData">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="feedback-channel" id="fbCh1">
|
||||
<div class="feedback-ch-head"><span class="ch-dot ch1"></span> CAN1</div>
|
||||
<div class="feedback-row"><span class="fb-label">状态</span><span class="fb-val" id="fb1State">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">接收数</span><span class="fb-val" id="fb1Rx">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">发送数</span><span class="fb-val" id="fb1Tx">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">丢帧</span><span class="fb-val" id="fb1Drop">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">错误</span><span class="fb-val" id="fb1Err">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">最新ID</span><span class="fb-val mono" id="fb1LastId">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">DLC</span><span class="fb-val" id="fb1Dlc">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">FD/BRS/ESI</span><span class="fb-val" id="fb1Flags">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">Seq</span><span class="fb-val" id="fb1Seq">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">Δrx/s</span><span class="fb-val" id="fb1Delta">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">最新HEX</span><span class="fb-val mono fb-data-cell" id="fb1LastData">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="feedback-channel" id="fbCh2">
|
||||
<div class="feedback-ch-head"><span class="ch-dot ch2"></span> CAN2</div>
|
||||
<div class="feedback-row"><span class="fb-label">状态</span><span class="fb-val" id="fb2State">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">接收数</span><span class="fb-val" id="fb2Rx">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">发送数</span><span class="fb-val" id="fb2Tx">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">丢帧</span><span class="fb-val" id="fb2Drop">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">错误</span><span class="fb-val" id="fb2Err">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">最新ID</span><span class="fb-val mono" id="fb2LastId">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">DLC</span><span class="fb-val" id="fb2Dlc">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">FD/BRS/ESI</span><span class="fb-val" id="fb2Flags">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">Seq</span><span class="fb-val" id="fb2Seq">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">Δrx/s</span><span class="fb-val" id="fb2Delta">--</span></div>
|
||||
<div class="feedback-row"><span class="fb-label">最新HEX</span><span class="fb-val mono fb-data-cell" id="fb2LastData">--</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast">
|
||||
<span class="toast-ico" id="toastIco">✓</span>
|
||||
<span id="toastMsg">操作成功</span>
|
||||
|
||||
<!-- ═══ 每 ID 快照表 ═══ -->
|
||||
<section class="card" id="idSnapshotSection" style="margin-top:18px;">
|
||||
<div class="card-head"><span class="ico">🔬</span> 每 ID 快照表(1Hz 刷新)
|
||||
<div class="feedback-controls">
|
||||
<select class="input" id="idsnapChannel" style="width:100px;">
|
||||
<option value="0">CAN0</option>
|
||||
<option value="1">CAN1</option>
|
||||
<option value="2">CAN2</option>
|
||||
</select>
|
||||
<span class="pill" id="idsnapStatus">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="idsnap-table-wrap">
|
||||
<table class="idsnap-table" id="idsnapTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th><th>CAN ID</th><th>DLC</th>
|
||||
<th>FD</th><th>BRS</th><th>ESI</th><th>数据 (HEX)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="idsnapBody">
|
||||
<tr><td colspan="7" class="status-empty">等待数据...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="can_monitor.js?v=20260629b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,195 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var state = { fbTimer: null };
|
||||
|
||||
var el = {};
|
||||
|
||||
var ids = [
|
||||
'monIp', 'monTestBtn', 'monConnPill', 'monConnTxt',
|
||||
'monConnIndicator', 'monConnIndicatorTxt', 'monHelper',
|
||||
'fbModePill', 'fbPollStatus',
|
||||
'fb0State', 'fb0Rx', 'fb0Tx', 'fb0Drop', 'fb0Err', 'fb0Seq', 'fb0Delta',
|
||||
'fb1State', 'fb1Rx', 'fb1Tx', 'fb1Drop', 'fb1Err', 'fb1Seq', 'fb1Delta',
|
||||
'fb2State', 'fb2Rx', 'fb2Tx', 'fb2Drop', 'fb2Err', 'fb2Seq', 'fb2Delta',
|
||||
'btnModeAllPass', 'btnModeFilter',
|
||||
'idsnapChannel', 'idsnapStatus', 'idsnapBody',
|
||||
'toast', 'toastIco', 'toastMsg'
|
||||
];
|
||||
|
||||
function cacheElements() {
|
||||
ids.forEach(function (id) { el[id] = document.getElementById(id); });
|
||||
}
|
||||
|
||||
function showToast(msg, type) {
|
||||
type = type || 'success';
|
||||
if (!el.toast) return;
|
||||
el.toast.className = 'toast ' + (type === 'success' ? 'ok' : type === 'error' ? 'err' : 'warn') + ' show';
|
||||
el.toastIco.textContent = type === 'success' ? '\u2713' : type === 'error' ? '\u2715' : '\u26A0';
|
||||
el.toastMsg.textContent = msg;
|
||||
clearTimeout(showToast._t);
|
||||
showToast._t = setTimeout(function () { el.toast.classList.remove('show'); }, 3000);
|
||||
}
|
||||
|
||||
function sanitize(text) {
|
||||
return String(text == null ? '' : text)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function getBaseUrl() {
|
||||
var ip = (el.monIp && el.monIp.value ? el.monIp.value.trim() : '') || '192.168.31.127';
|
||||
return 'http://' + ip;
|
||||
}
|
||||
|
||||
function setConnState(ok, text) {
|
||||
if (el.monConnPill) el.monConnPill.className = 'pill ' + (ok ? 'online' : 'offline');
|
||||
if (el.monConnTxt) el.monConnTxt.textContent = text;
|
||||
if (el.monConnIndicator) el.monConnIndicator.className = 'conn-indicator ' + (ok ? 'ok' : 'err');
|
||||
if (el.monConnIndicatorTxt) el.monConnIndicatorTxt.textContent = text;
|
||||
}
|
||||
|
||||
// ── 1Hz 轮询 /can/feedback ─────────────────────────────────
|
||||
async function pollFeedback() {
|
||||
try {
|
||||
var resp = await fetch(getBaseUrl() + '/can/feedback', { signal: AbortSignal.timeout(3000) });
|
||||
if (!resp.ok) return;
|
||||
var data = await resp.json();
|
||||
var channels = data.channels || [];
|
||||
var selectedCh = el.idsnapChannel ? parseInt(el.idsnapChannel.value) : 0;
|
||||
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var ch = channels[i];
|
||||
if (!ch || !ch.valid) {
|
||||
renderChannelEmpty(i);
|
||||
continue;
|
||||
}
|
||||
renderChannel(i, ch);
|
||||
}
|
||||
|
||||
// 渲染选中通道的 ID 快照表
|
||||
var sel = channels[selectedCh];
|
||||
if (sel && sel.valid && sel.entries) {
|
||||
renderIdTable(sel.entries, sel.count || sel.entries.length);
|
||||
if (el.idsnapStatus) {
|
||||
el.idsnapStatus.textContent = (sel.entries.length) + ' 个 ID';
|
||||
el.idsnapStatus.className = 'pill online';
|
||||
}
|
||||
} else {
|
||||
renderIdTableEmpty();
|
||||
}
|
||||
|
||||
if (el.fbPollStatus) {
|
||||
el.fbPollStatus.textContent = '轮询: 正常';
|
||||
el.fbPollStatus.className = 'pill online';
|
||||
}
|
||||
setConnState(true, '已连接');
|
||||
} catch (e) {
|
||||
if (el.fbPollStatus) {
|
||||
el.fbPollStatus.textContent = '轮询: 离线';
|
||||
el.fbPollStatus.className = 'pill offline';
|
||||
}
|
||||
setConnState(false, '连接失败');
|
||||
}
|
||||
}
|
||||
|
||||
function renderChannel(i, ch) {
|
||||
var p = 'fb' + i;
|
||||
if (el[p + 'State']) {
|
||||
var hasIds = ch.count > 0;
|
||||
el[p + 'State'].textContent = hasIds ? '活跃 (' + ch.count + ' ID)' : '无帧(总线空闲)';
|
||||
el[p + 'State'].className = 'fb-val ' + (hasIds ? 'ok' : 'info');
|
||||
}
|
||||
if (el[p + 'Rx']) el[p + 'Rx'].textContent = ch.rx;
|
||||
if (el[p + 'Tx']) el[p + 'Tx'].textContent = ch.tx;
|
||||
if (el[p + 'Drop']) { el[p + 'Drop'].textContent = ch.drop; el[p + 'Drop'].className = 'fb-val' + (ch.drop > 0 ? ' warn' : ''); }
|
||||
if (el[p + 'Err']) { el[p + 'Err'].textContent = ch.err; el[p + 'Err'].className = 'fb-val' + (ch.err > 0 ? ' err' : ''); }
|
||||
if (el[p + 'Seq']) el[p + 'Seq'].textContent = ch.seq;
|
||||
if (el[p + 'Delta']) { el[p + 'Delta'].textContent = ch.count || 0; el[p + 'Delta'].className = 'fb-val'; }
|
||||
if (i === 0 && el.fbModePill) el.fbModePill.textContent = '模式: ' + (ch.mode === 1 ? '过滤' : '全收');
|
||||
}
|
||||
|
||||
function renderChannelEmpty(i) {
|
||||
var p = 'fb' + i;
|
||||
if (el[p + 'State']) { el[p + 'State'].textContent = 'SPI 离线'; el[p + 'State'].className = 'fb-val err'; }
|
||||
['Rx','Tx','Drop','Err','Seq','Delta'].forEach(function (f) {
|
||||
if (el[p + f]) { el[p + f].textContent = '--'; el[p + f].className = 'fb-val'; }
|
||||
});
|
||||
}
|
||||
function renderIdTable(entries, count) {
|
||||
var tbody = el.idsnapBody;
|
||||
if (!tbody) return;
|
||||
if (!entries || !entries.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="status-empty">无数据(CAN 未收到帧)</td></tr>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var e = entries[i];
|
||||
html += '<tr>';
|
||||
html += '<td>' + (i + 1) + '</td>';
|
||||
html += '<td class="mono">' + sanitize(e.id) + '</td>';
|
||||
html += '<td>' + e.dlc + '</td>';
|
||||
html += '<td class="' + (e.fd ? 'flag-on' : 'flag-off') + '">' + (e.fd ? 'Y' : '-') + '</td>';
|
||||
html += '<td class="' + (e.brs ? 'flag-on' : 'flag-off') + '">' + (e.brs ? 'Y' : '-') + '</td>';
|
||||
html += '<td class="' + (e.esi ? 'flag-on' : 'flag-off') + '">' + (e.esi ? 'Y' : '-') + '</td>';
|
||||
html += '<td class="idsnap-data mono">' + sanitize(e.data || '--') + '</td>';
|
||||
html += '</tr>';
|
||||
}
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderIdTableEmpty() {
|
||||
var tbody = el.idsnapBody;
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="7" class="status-empty">等待数据...</td></tr>';
|
||||
if (el.idsnapStatus) { el.idsnapStatus.textContent = '--'; el.idsnapStatus.className = 'pill'; }
|
||||
}
|
||||
|
||||
// ── 连接测试 ─────────────────────────────────────────────
|
||||
async function testConnection() {
|
||||
try {
|
||||
var resp = await fetch(getBaseUrl() + '/can/feedback', { signal: AbortSignal.timeout(3000) });
|
||||
if (!resp.ok) { setConnState(false, 'HTTP ' + resp.status); return; }
|
||||
setConnState(true, '已连接');
|
||||
showToast('ESP32 已连接', 'success');
|
||||
} catch (e) {
|
||||
setConnState(false, '连接失败');
|
||||
showToast('连接失败: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 模式切换 ─────────────────────────────────────────────
|
||||
async function setCanMode(mode) {
|
||||
try {
|
||||
var resp = await fetch(getBaseUrl() + '/can/mode', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mode: mode }),
|
||||
signal: AbortSignal.timeout(3000)
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
showToast('CAN 模式已切换: ' + (data.actual === 1 ? '过滤' : '全收'), 'success');
|
||||
} else {
|
||||
showToast('模式切换失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('模式切换失败: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 初始化 ───────────────────────────────────────────────
|
||||
function init() {
|
||||
cacheElements();
|
||||
setConnState(false, '未连接');
|
||||
if (el.monTestBtn) el.monTestBtn.addEventListener('click', testConnection);
|
||||
if (el.monIp) el.monIp.addEventListener('change', testConnection);
|
||||
if (el.btnModeAllPass) el.btnModeAllPass.addEventListener('click', function () { setCanMode(0); });
|
||||
if (el.btnModeFilter) el.btnModeFilter.addEventListener('click', function () { setCanMode(1); });
|
||||
if (el.idsnapChannel) el.idsnapChannel.addEventListener('change', pollFeedback);
|
||||
setTimeout(testConnection, 150);
|
||||
pollFeedback();
|
||||
state.fbTimer = setInterval(pollFeedback, 1000);
|
||||
}
|
||||
|
||||
init();
|
||||
})();
|
||||
@@ -359,6 +359,7 @@
|
||||
<div class="can-launch-actions">
|
||||
<a class="btn btn-primary can-launch-primary" href="can.html">进入 CAN 页面</a>
|
||||
<button class="btn btn-outline can-launch-secondary" type="button" onclick="window.open('can.html','_blank','noopener')">新窗口打开</button>
|
||||
<a class="btn btn-outline can-launch-secondary" href="can_monitor.html">CAN 接收监控</a>
|
||||
</div>
|
||||
<div class="can-launch-foot">
|
||||
<span class="can-launch-foot-label">说明</span>
|
||||
|
||||
@@ -1708,3 +1708,68 @@ code { color: #d3f9ff; font-family: var(--font-mono); }
|
||||
transition-duration: .001ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* === CAN 1Hz FEEDBACK === */
|
||||
.can-feedback-card { margin-top: 18px; }
|
||||
.can-feedback-card .card-head {
|
||||
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;
|
||||
}
|
||||
.feedback-controls { display: flex; align-items: center; gap: 8px; }
|
||||
.feedback-grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;
|
||||
}
|
||||
.feedback-channel {
|
||||
background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.07);
|
||||
border-radius: 10px; padding: 14px 16px;
|
||||
}
|
||||
.feedback-ch-head {
|
||||
font-weight: 700; font-size: 15px; margin-bottom: 10px;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.ch-dot {
|
||||
width: 10px; height: 10px; border-radius: 50%; display: inline-block;
|
||||
}
|
||||
.ch-dot.ch0 { background: #22d3ee; }
|
||||
.ch-dot.ch1 { background: #a78bfa; }
|
||||
.ch-dot.ch2 { background: #f59e0b; }
|
||||
.feedback-row {
|
||||
display: flex; justify-content: space-between; padding: 3px 0;
|
||||
font-size: 13px; border-bottom: 1px solid rgba(255,255,255,.04);
|
||||
}
|
||||
.feedback-row:last-child { border-bottom: none; }
|
||||
.fb-label { color: var(--muted, #8892a4); }
|
||||
.fb-val { font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.fb-val.mono { font-family: monospace; }
|
||||
.fb-val.ok { color: #34d399; }
|
||||
.fb-val.warn { color: #f59e0b; }
|
||||
.fb-val.err { color: #f87171; }
|
||||
.fb-val.zero { color: #64748b; }
|
||||
@media (max-width: 900px) {
|
||||
.feedback-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* === ID SNAPSHOT TABLE === */
|
||||
.idsnap-table-wrap { overflow-x: auto; }
|
||||
.idsnap-table {
|
||||
width: 100%; border-collapse: collapse; font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.idsnap-table th {
|
||||
text-align: left; padding: 6px 10px; font-weight: 600;
|
||||
border-bottom: 2px solid rgba(255,255,255,.12); color: var(--muted, #8892a4);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.idsnap-table td {
|
||||
padding: 5px 10px; border-bottom: 1px solid rgba(255,255,255,.05);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.idsnap-table td.mono { font-family: monospace; }
|
||||
.idsnap-table tr:hover td { background: rgba(255,255,255,.03); }
|
||||
.idsnap-table .flag-on { color: #34d399; font-weight: 600; }
|
||||
.idsnap-table .flag-off { color: #475569; }
|
||||
.idsnap-data { font-family: monospace; max-width: 320px; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.fb-data-cell {
|
||||
max-width: 200px; overflow: hidden; text-overflow: ellipsis;
|
||||
font-size: 11px; word-break: break-all; line-height: 1.4;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user