DTM TRAFFIC, SESSION EVENTS 로그 개선
- Receiver에 expected packet type 필드 제거 - 로그 아래로 쌓기 - Clear 버튼 추가 - Trasmitter 세션에서는 packet report 숨김
This commit is contained in:
@@ -68,19 +68,32 @@ const DTM_SETUP_CONTROL = {
|
|||||||
5: 'TX/RX select',
|
5: 'TX/RX select',
|
||||||
};
|
};
|
||||||
|
|
||||||
const DTM_PKT_TYPE = {
|
|
||||||
0: 'PRBS9',
|
|
||||||
1: '11110000',
|
|
||||||
2: '10101010',
|
|
||||||
3: 'constant carrier',
|
|
||||||
};
|
|
||||||
|
|
||||||
const dtmChannelLabel = byte0 => {
|
const dtmChannelLabel = byte0 => {
|
||||||
const channel = byte0 & 0x3f;
|
const channel = byte0 & 0x3f;
|
||||||
const mhz = 2402 + 2 * channel;
|
const mhz = 2402 + 2 * channel;
|
||||||
return `ch ${channel} (${mhz} MHz)`;
|
return `ch ${channel} (${mhz} MHz)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DTM_PAYLOAD_LABEL = {
|
||||||
|
0: 'PRBS9',
|
||||||
|
1: '11110000',
|
||||||
|
2: '10101010',
|
||||||
|
3: 'Constant carrier',
|
||||||
|
};
|
||||||
|
|
||||||
|
const dtmTransmitterTestNote = (b0, b1) => {
|
||||||
|
const channel = b0 & 0x3f;
|
||||||
|
const mhz = 2402 + 2 * channel;
|
||||||
|
const lengthField = (b1 >> 2) & 0x3f;
|
||||||
|
const pattern = DTM_PAYLOAD_LABEL[b1 & 0x03] ?? `type ${b1 & 0x03}`;
|
||||||
|
return `Transmitter test - transmit on ch ${channel}(${mhz}MHz), len ${lengthField}, ${pattern}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dtmPacketReportNote = count => {
|
||||||
|
const noun = count === 1 ? 'packet' : 'packets';
|
||||||
|
return `Receiver report - ${count} ${noun} received on active RX channel (LE DTM event)`;
|
||||||
|
};
|
||||||
|
|
||||||
const dtmTxNote = buffer => {
|
const dtmTxNote = buffer => {
|
||||||
const b0 = buffer[0];
|
const b0 = buffer[0];
|
||||||
const b1 = buffer[1];
|
const b1 = buffer[1];
|
||||||
@@ -96,15 +109,11 @@ const dtmTxNote = buffer => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cmdType === 1) {
|
if (cmdType === 1) {
|
||||||
const length = (b1 >> 2) & 0x3f;
|
return `Receiver test - listen on ${dtmChannelLabel(b0)}`;
|
||||||
const pkt = DTM_PKT_TYPE[b1 & 0x03] ?? `type ${b1 & 0x03}`;
|
|
||||||
return `Receiver test — listen on ${dtmChannelLabel(b0)}, len ${length}, ${pkt}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmdType === 2) {
|
if (cmdType === 2) {
|
||||||
const length = (b1 >> 2) & 0x3f;
|
return dtmTransmitterTestNote(b0, b1);
|
||||||
const pkt = DTM_PKT_TYPE[b1 & 0x03] ?? `type ${b1 & 0x03}`;
|
|
||||||
return `Transmitter test — transmit on ${dtmChannelLabel(b0)}, len ${length}, ${pkt}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmdType === 3) {
|
if (cmdType === 3) {
|
||||||
@@ -125,11 +134,6 @@ const dtmRxNote = buffer => {
|
|||||||
const b0 = buffer[0];
|
const b0 = buffer[0];
|
||||||
const b1 = buffer[1];
|
const b1 = buffer[1];
|
||||||
|
|
||||||
const packetCount = parsePacketReportCount(buffer);
|
|
||||||
if (packetCount !== null) {
|
|
||||||
return `Packet report — ${packetCount} packet(s) received on active RX channel (LE DTM event)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((b1 & 0x01) !== 0) {
|
if ((b1 & 0x01) !== 0) {
|
||||||
const rawDbm = (b1 & 0xfe) >> 1;
|
const rawDbm = (b1 & 0xfe) >> 1;
|
||||||
const modifier = b0 & 0x01 ? -128 : 0;
|
const modifier = b0 & 0x01 ? -128 : 0;
|
||||||
@@ -198,15 +202,21 @@ class DTMTransport {
|
|||||||
this.#dataBuffer =
|
this.#dataBuffer =
|
||||||
this.#dataBuffer.length > 2 ? this.#dataBuffer.subarray(2) : undefined;
|
this.#dataBuffer.length > 2 ? this.#dataBuffer.subarray(2) : undefined;
|
||||||
|
|
||||||
|
const packetCount = parsePacketReportCount(response);
|
||||||
|
if (packetCount !== null) {
|
||||||
|
this.#eventEmitter.emit('packetReport', packetCount);
|
||||||
|
this.#eventEmitter.emit('traffic', {
|
||||||
|
direction: 'rx',
|
||||||
|
data: Array.from(response),
|
||||||
|
note: dtmPacketReportNote(packetCount),
|
||||||
|
kind: 'packetReport',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.#eventEmitter.emit('traffic', {
|
this.#eventEmitter.emit('traffic', {
|
||||||
direction: 'rx',
|
direction: 'rx',
|
||||||
data: Array.from(response),
|
data: Array.from(response),
|
||||||
note: dtmRxNote(response),
|
note: dtmRxNote(response),
|
||||||
});
|
});
|
||||||
|
|
||||||
const packetCount = parsePacketReportCount(response);
|
|
||||||
if (packetCount !== null) {
|
|
||||||
this.#eventEmitter.emit('packetReport', packetCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#callback) {
|
if (this.#callback) {
|
||||||
|
|||||||
@@ -189,9 +189,9 @@ const attachEvents = (sessionId, instance) => {
|
|||||||
emitForSession(sessionId, {
|
emitForSession(sessionId, {
|
||||||
type: 'ended',
|
type: 'ended',
|
||||||
channel: event.channel,
|
channel: event.channel,
|
||||||
packets: event.packets ?? 0,
|
|
||||||
mode: event.type,
|
mode: event.type,
|
||||||
data: [...status.lastReceived],
|
data: [...status.lastReceived],
|
||||||
|
...(typeof event.packets === 'number' ? { packets: event.packets } : {}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -210,6 +210,9 @@ const attachEvents = (sessionId, instance) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
instance.onTraffic(event => {
|
instance.onTraffic(event => {
|
||||||
|
if (sessionId === 'transmitter' && event.kind === 'packetReport') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
emitForSession(sessionId, {
|
emitForSession(sessionId, {
|
||||||
type: 'traffic',
|
type: 'traffic',
|
||||||
direction: event.direction,
|
direction: event.direction,
|
||||||
|
|||||||
+162
-80
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import packageJson from '../package.json';
|
import packageJson from '../package.json';
|
||||||
|
|
||||||
@@ -72,14 +72,22 @@ type TrafficEntry = {
|
|||||||
const parseTrafficBytes = (hex: string) =>
|
const parseTrafficBytes = (hex: string) =>
|
||||||
hex === '--' ? [] : hex.split(' ').map(part => parseInt(part, 16)).filter(n => !Number.isNaN(n));
|
hex === '--' ? [] : hex.split(' ').map(part => parseInt(part, 16)).filter(n => !Number.isNaN(n));
|
||||||
|
|
||||||
/** RF-related DTM traffic: RX/TX test commands and LE Packet Report events (not setup/status). */
|
/** RTT direction: host→board (TX) vs board→host (RX), not over-the-air RF. */
|
||||||
|
const trafficDirLabel = (direction: Direction) => {
|
||||||
|
if (direction === 'tx') return 'TX';
|
||||||
|
if (direction === 'rx') return 'RX';
|
||||||
|
if (direction === 'error') return 'ERR';
|
||||||
|
return 'INFO';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** RF-related DTM traffic: test commands and packet-report events (not setup/status). */
|
||||||
const isRfTrafficEntry = (entry: TrafficEntry) => {
|
const isRfTrafficEntry = (entry: TrafficEntry) => {
|
||||||
if (entry.direction === 'info' || entry.direction === 'error') return false;
|
if (entry.direction === 'info' || entry.direction === 'error') return false;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
entry.note.startsWith('Packet report') ||
|
entry.note.startsWith('Receiver test -') ||
|
||||||
entry.note.startsWith('Receiver test') ||
|
entry.note.startsWith('Transmitter test -') ||
|
||||||
entry.note.startsWith('Transmitter test')
|
entry.note.startsWith('Receiver report -')
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -95,6 +103,108 @@ const isRfTrafficEntry = (entry: TrafficEntry) => {
|
|||||||
return cmdType === 0b01 || cmdType === 0b10;
|
return cmdType === 0b01 || cmdType === 0b10;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SCROLL_BOTTOM_THRESHOLD_PX = 12;
|
||||||
|
|
||||||
|
/** Append-newest-at-bottom lists: auto-scroll only while the user is already at the bottom. */
|
||||||
|
function useStickToBottomOnAppend(items: unknown[]) {
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const stickToBottomRef = useRef(true);
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
const el = scrollRef.current;
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stickToBottomRef.current =
|
||||||
|
el.scrollHeight - el.scrollTop - el.clientHeight <= SCROLL_BOTTOM_THRESHOLD_PX;
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = scrollRef.current;
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
el.scrollTop = 0;
|
||||||
|
stickToBottomRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stickToBottomRef.current) {
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
return { scrollRef, onScroll };
|
||||||
|
}
|
||||||
|
|
||||||
|
function TrafficLogList({
|
||||||
|
sessionId,
|
||||||
|
trafficLog,
|
||||||
|
trafficRfOnly,
|
||||||
|
}: {
|
||||||
|
sessionId: SessionId;
|
||||||
|
trafficLog: TrafficEntry[];
|
||||||
|
trafficRfOnly: boolean;
|
||||||
|
}) {
|
||||||
|
const visibleTraffic = useMemo(
|
||||||
|
() => (trafficRfOnly ? trafficLog.filter(isRfTrafficEntry) : trafficLog),
|
||||||
|
[trafficLog, trafficRfOnly]
|
||||||
|
);
|
||||||
|
const { scrollRef, onScroll } = useStickToBottomOnAppend(visibleTraffic);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={scrollRef} onScroll={onScroll} className="traffic-list scroll-panel">
|
||||||
|
{visibleTraffic.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
{trafficLog.length === 0 ? 'No traffic yet.' : 'No RF traffic (setup/status hidden).'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
visibleTraffic.map((entry, index) => (
|
||||||
|
<div
|
||||||
|
key={`${sessionId}-traffic-${entry.time}-${index}`}
|
||||||
|
className={`traffic-item ${entry.direction}`}
|
||||||
|
>
|
||||||
|
<span className="traffic-time">{entry.time}</span>
|
||||||
|
<span className="traffic-dir">{trafficDirLabel(entry.direction)}</span>
|
||||||
|
<span className="traffic-hex">{entry.hex}</span>
|
||||||
|
<span className="traffic-note">{entry.note}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SessionEventLogList({
|
||||||
|
sessionId,
|
||||||
|
eventLog,
|
||||||
|
}: {
|
||||||
|
sessionId: SessionId;
|
||||||
|
eventLog: LogEntry[];
|
||||||
|
}) {
|
||||||
|
const { scrollRef, onScroll } = useStickToBottomOnAppend(eventLog);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={scrollRef} onScroll={onScroll} className="log-list scroll-panel">
|
||||||
|
{eventLog.length === 0 ? (
|
||||||
|
<div className="empty-state">No events yet.</div>
|
||||||
|
) : (
|
||||||
|
eventLog.map((entry, index) => (
|
||||||
|
<div
|
||||||
|
key={`${sessionId}-event-${entry.time}-${index}`}
|
||||||
|
className="log-item"
|
||||||
|
>
|
||||||
|
<span className="log-time">{entry.time}</span>
|
||||||
|
<span className="log-message">{entry.message}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type JlinkPorts = {
|
type JlinkPorts = {
|
||||||
gdb: number;
|
gdb: number;
|
||||||
swo: number;
|
swo: number;
|
||||||
@@ -169,8 +279,8 @@ const formatLogTime = (date = new Date()) => {
|
|||||||
return `${hours}:${minutes}:${seconds}.${millis}`;
|
return `${hours}:${minutes}:${seconds}.${millis}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const prependEvent = (message: string, eventLog: LogEntry[]) =>
|
const appendEvent = (message: string, eventLog: LogEntry[]) =>
|
||||||
[{ time: formatLogTime(), message }, ...eventLog].slice(0, PANEL_LIMIT);
|
[...eventLog, { time: formatLogTime(), message }].slice(-PANEL_LIMIT);
|
||||||
|
|
||||||
const formatSessionMessage = (message: string) =>
|
const formatSessionMessage = (message: string) =>
|
||||||
message
|
message
|
||||||
@@ -288,7 +398,7 @@ function App() {
|
|||||||
|
|
||||||
const nextState: SessionState = {
|
const nextState: SessionState = {
|
||||||
...current,
|
...current,
|
||||||
trafficLog: [entry, ...current.trafficLog].slice(0, TRAFFIC_LIMIT),
|
trafficLog: [...current.trafficLog, entry].slice(-TRAFFIC_LIMIT),
|
||||||
lastTxHex: entry.direction === 'tx' ? entry.hex : current.lastTxHex,
|
lastTxHex: entry.direction === 'tx' ? entry.hex : current.lastTxHex,
|
||||||
lastRxHex: entry.direction === 'rx' ? entry.hex : current.lastRxHex,
|
lastRxHex: entry.direction === 'rx' ? entry.hex : current.lastRxHex,
|
||||||
};
|
};
|
||||||
@@ -316,7 +426,7 @@ function App() {
|
|||||||
received: new Array(40).fill(0),
|
received: new Array(40).fill(0),
|
||||||
txVisited: new Array(40).fill(false),
|
txVisited: new Array(40).fill(false),
|
||||||
currentChannel: null,
|
currentChannel: null,
|
||||||
eventLog: prependEvent('Reset test state', current.eventLog),
|
eventLog: appendEvent('Reset test state', current.eventLog),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +441,7 @@ function App() {
|
|||||||
...current,
|
...current,
|
||||||
currentChannel: channel,
|
currentChannel: channel,
|
||||||
txVisited,
|
txVisited,
|
||||||
eventLog: prependEvent(`Started ${String(event.mode)} on ch ${channel}`, current.eventLog),
|
eventLog: appendEvent(`Started ${String(event.mode)} on ch ${channel}`, current.eventLog),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,14 +455,17 @@ function App() {
|
|||||||
|
|
||||||
if (nextType === 'ended') {
|
if (nextType === 'ended') {
|
||||||
const data = Array.isArray(event.data) ? (event.data as number[]) : current.received;
|
const data = Array.isArray(event.data) ? (event.data as number[]) : current.received;
|
||||||
|
const channel = String(event.channel);
|
||||||
|
const endedMessage =
|
||||||
|
event.mode === 'receiver' && typeof event.packets === 'number'
|
||||||
|
? `Ended receiver on ch ${channel} · ${event.packets} packets received`
|
||||||
|
: `Ended ${String(event.mode)} on ch ${channel}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
received: data,
|
received: data,
|
||||||
currentChannel: null,
|
currentChannel: null,
|
||||||
eventLog: prependEvent(
|
eventLog: appendEvent(endedMessage, current.eventLog),
|
||||||
`Ended ${String(event.mode)} on ch ${String(event.channel)} packets=${String(event.packets ?? 0)}`,
|
|
||||||
current.eventLog
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +481,7 @@ function App() {
|
|||||||
txPowerFeedback:
|
txPowerFeedback:
|
||||||
typeof event.txPower === 'number' ? Number(event.txPower) : current.txPowerFeedback,
|
typeof event.txPower === 'number' ? Number(event.txPower) : current.txPowerFeedback,
|
||||||
message: 'Test finished',
|
message: 'Test finished',
|
||||||
eventLog: prependEvent('Test finished', current.eventLog),
|
eventLog: appendEvent('Test finished', current.eventLog),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,7 +512,7 @@ function App() {
|
|||||||
starting: false,
|
starting: false,
|
||||||
currentChannel: null,
|
currentChannel: null,
|
||||||
message: String(event.message ?? 'Unknown error'),
|
message: String(event.message ?? 'Unknown error'),
|
||||||
eventLog: prependEvent(
|
eventLog: appendEvent(
|
||||||
`Error: ${String(event.message ?? 'Unknown error')}`,
|
`Error: ${String(event.message ?? 'Unknown error')}`,
|
||||||
current.eventLog
|
current.eventLog
|
||||||
),
|
),
|
||||||
@@ -498,7 +611,7 @@ function App() {
|
|||||||
running: false,
|
running: false,
|
||||||
starting: false,
|
starting: false,
|
||||||
message: canStart.reason || 'Cannot start',
|
message: canStart.reason || 'Cannot start',
|
||||||
eventLog: prependEvent(canStart.reason || 'Cannot start', current.eventLog),
|
eventLog: appendEvent(canStart.reason || 'Cannot start', current.eventLog),
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -514,7 +627,7 @@ function App() {
|
|||||||
trafficLog: [],
|
trafficLog: [],
|
||||||
lastTxHex: '--',
|
lastTxHex: '--',
|
||||||
lastRxHex: '--',
|
lastRxHex: '--',
|
||||||
eventLog: prependEvent('Requested start', current.eventLog),
|
eventLog: appendEvent('Requested start', current.eventLog),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -537,7 +650,7 @@ function App() {
|
|||||||
starting: false,
|
starting: false,
|
||||||
currentChannel: null,
|
currentChannel: null,
|
||||||
message: 'Stopped',
|
message: 'Stopped',
|
||||||
eventLog: prependEvent('Requested stop', current.eventLog),
|
eventLog: appendEvent('Requested stop', current.eventLog),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -638,28 +751,6 @@ function App() {
|
|||||||
<div
|
<div
|
||||||
className={`radio-param-grid ${isReceiver ? 'radio-param-grid--rx' : 'radio-param-grid--tx'}`}
|
className={`radio-param-grid ${isReceiver ? 'radio-param-grid--rx' : 'radio-param-grid--tx'}`}
|
||||||
>
|
>
|
||||||
{isReceiver && (
|
|
||||||
<div className="field">
|
|
||||||
<label>Expected packet type</label>
|
|
||||||
<div className="field-control-wrap">
|
|
||||||
<select
|
|
||||||
className="field-control"
|
|
||||||
value={config.bitpattern}
|
|
||||||
onChange={event =>
|
|
||||||
updateConfig(sessionId, 'bitpattern', Number(event.target.value))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{PACKET_OPTIONS.map(option => (
|
|
||||||
<option key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<span className="field-hint">Start 시 Transmitter 설정과 자동 맞춤</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isReceiver && (
|
{!isReceiver && (
|
||||||
<>
|
<>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
@@ -1036,6 +1127,7 @@ function App() {
|
|||||||
<section className="panel dock-panel">
|
<section className="panel dock-panel">
|
||||||
<div className="panel-title">
|
<div className="panel-title">
|
||||||
<span>DTM Traffic</span>
|
<span>DTM Traffic</span>
|
||||||
|
<div className="panel-title-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={
|
className={
|
||||||
@@ -1052,54 +1144,44 @@ function App() {
|
|||||||
>
|
>
|
||||||
RF only
|
RF only
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button
|
||||||
<div className="traffic-list scroll-panel">
|
type="button"
|
||||||
{(() => {
|
className="traffic-filter-btn"
|
||||||
const visibleTraffic = session.trafficRfOnly
|
onClick={() =>
|
||||||
? session.trafficLog.filter(isRfTrafficEntry)
|
setSession(sessionId, current => ({
|
||||||
: session.trafficLog;
|
...current,
|
||||||
|
trafficLog: [],
|
||||||
if (visibleTraffic.length === 0) {
|
}))
|
||||||
return (
|
|
||||||
<div className="empty-state">
|
|
||||||
{session.trafficLog.length === 0
|
|
||||||
? 'No traffic yet.'
|
|
||||||
: 'No RF traffic (setup/status hidden).'}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return visibleTraffic.map((entry, index) => (
|
|
||||||
<div
|
|
||||||
key={`${sessionId}-traffic-${entry.time}-${index}`}
|
|
||||||
className={`traffic-item ${entry.direction}`}
|
|
||||||
>
|
>
|
||||||
<span className="traffic-time">{entry.time}</span>
|
Clear
|
||||||
<span className="traffic-dir">{entry.direction.toUpperCase()}</span>
|
</button>
|
||||||
<span className="traffic-hex">{entry.hex}</span>
|
|
||||||
<span className="traffic-note">{entry.note}</span>
|
|
||||||
</div>
|
</div>
|
||||||
));
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
|
<TrafficLogList
|
||||||
|
sessionId={sessionId}
|
||||||
|
trafficLog={session.trafficLog}
|
||||||
|
trafficRfOnly={session.trafficRfOnly}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="panel dock-panel">
|
<section className="panel dock-panel">
|
||||||
<div className="panel-title">Session Events</div>
|
<div className="panel-title">
|
||||||
<div className="log-list scroll-panel">
|
<span>Session Events</span>
|
||||||
{session.eventLog.length === 0 && (
|
<button
|
||||||
<div className="empty-state">No events yet.</div>
|
type="button"
|
||||||
)}
|
className="traffic-filter-btn"
|
||||||
{session.eventLog.map((entry, index) => (
|
onClick={() =>
|
||||||
<div
|
setSession(sessionId, current => ({
|
||||||
key={`${sessionId}-event-${entry.time}-${index}`}
|
...current,
|
||||||
className="log-item"
|
eventLog: [],
|
||||||
|
}))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="log-time">{entry.time}</span>
|
Clear
|
||||||
<span className="log-message">{entry.message}</span>
|
</button>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
<SessionEventLogList sessionId={sessionId} eventLog={session.eventLog} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1017,6 +1017,36 @@ input:focus {
|
|||||||
align-content: start;
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-title--traffic {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title--traffic .panel-title-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-legend {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.35;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.traffic-filter-btn {
|
.traffic-filter-btn {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
Reference in New Issue
Block a user