mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-07-26 19:42:43 +02:00
feat(pathanalyzer): jump-to-map from echo rows, message preview in map sidebar
- Each routed echo line in the Messages table gets a map icon that switches to the Map view with that message + echo pre-selected and its path drawn; the sidebar scrolls the selection into view - Map sidebar entries show the message content under the sender: one truncated line normally, full text when the message is selected; sidebar entry markup switched from innerHTML to DOM building so network-supplied sender/content strings are never parsed as HTML - Fix: path drawing (fitBounds) now runs after invalidateSize in the same deferred step - drawing immediately after unhiding the map container computed bounds against a stale size and left the map at world zoom when jumping from the table Verified live via Playwright: jump selects the right echo (route chips match the sidebar entry), path draws at zoom 10, previews truncate on unselected entries and expand on the selected one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -163,7 +163,7 @@ function paCopyText(text, label) {
|
||||
);
|
||||
}
|
||||
|
||||
function paBuildEchoLine(echo) {
|
||||
function paBuildEchoLine(msg, echo, echoIdx) {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'pa-echo-line';
|
||||
line.title = 'Click to copy route';
|
||||
@@ -203,6 +203,18 @@ function paBuildEchoLine(echo) {
|
||||
meta.textContent = `SNR: ${snr} | ${echo.received_at || ''}`;
|
||||
line.appendChild(meta);
|
||||
|
||||
if (echo.hops > 0) {
|
||||
const mapBtn = document.createElement('i');
|
||||
mapBtn.className = 'bi bi-map pa-echo-mapbtn';
|
||||
mapBtn.title = 'Show this path on the map';
|
||||
mapBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
paMapSelection = { msgId: msg.id, echoIdx: echoIdx };
|
||||
paSwitchView('map');
|
||||
});
|
||||
line.appendChild(mapBtn);
|
||||
}
|
||||
|
||||
line.addEventListener('click', () => {
|
||||
paCopyText(echo.tokens.join(','), 'Route');
|
||||
});
|
||||
@@ -284,9 +296,9 @@ function paRenderTable() {
|
||||
const detailTd = document.createElement('td');
|
||||
detailTd.className = 'pa-echo-cell';
|
||||
detailTd.colSpan = 9;
|
||||
for (const echo of msg.echoView) {
|
||||
detailTd.appendChild(paBuildEchoLine(echo));
|
||||
}
|
||||
msg.echoView.forEach((echo, echoIdx) => {
|
||||
detailTd.appendChild(paBuildEchoLine(msg, echo, echoIdx));
|
||||
});
|
||||
detailTr.appendChild(detailTd);
|
||||
body.appendChild(detailTr);
|
||||
|
||||
@@ -603,19 +615,37 @@ function paRenderMapView() {
|
||||
}
|
||||
|
||||
for (const msg of filtered) {
|
||||
const selected = msg.id === paMapSelection.msgId;
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'pa-map-msg' + (msg.id === paMapSelection.msgId ? ' pa-selected' : '');
|
||||
entry.innerHTML =
|
||||
`<div class="d-flex justify-content-between gap-2">
|
||||
<strong class="text-truncate">${msg.sender || '—'}</strong>
|
||||
<span class="text-muted text-nowrap">${paFormatTime(msg).slice(5, 16)}</span>
|
||||
</div>`;
|
||||
entry.className = 'pa-map-msg' + (selected ? ' pa-selected' : '');
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.className = 'd-flex justify-content-between gap-2';
|
||||
const senderEl = document.createElement('strong');
|
||||
senderEl.className = 'text-truncate';
|
||||
senderEl.textContent = msg.sender || '—';
|
||||
const timeEl = document.createElement('span');
|
||||
timeEl.className = 'text-muted text-nowrap';
|
||||
timeEl.textContent = paFormatTime(msg).slice(5, 16);
|
||||
head.appendChild(senderEl);
|
||||
head.appendChild(timeEl);
|
||||
entry.appendChild(head);
|
||||
|
||||
// Content preview: one truncated line; full text when selected
|
||||
if (msg.content) {
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'pa-map-msg-preview' + (selected ? '' : ' text-truncate');
|
||||
preview.textContent = msg.content;
|
||||
preview.title = msg.content;
|
||||
entry.appendChild(preview);
|
||||
}
|
||||
|
||||
entry.addEventListener('click', () => {
|
||||
paMapSelection = { msgId: msg.id, echoIdx: null };
|
||||
paRenderMapView();
|
||||
});
|
||||
|
||||
if (msg.id === paMapSelection.msgId) {
|
||||
if (selected) {
|
||||
msg.echoView.forEach((echo, idx) => {
|
||||
if (echo.hops === 0) return;
|
||||
const eEl = document.createElement('div');
|
||||
@@ -638,9 +668,17 @@ function paRenderMapView() {
|
||||
}
|
||||
|
||||
paSetView('map');
|
||||
// Leaflet cannot size itself while the container was display:none
|
||||
setTimeout(() => paMap.invalidateSize(), 60);
|
||||
paDrawSelectedEcho();
|
||||
// Leaflet cannot size itself while the container was display:none -
|
||||
// and fitBounds in paDrawSelectedEcho needs the corrected size, so
|
||||
// drawing must happen after invalidateSize in the same deferred step
|
||||
setTimeout(() => {
|
||||
paMap.invalidateSize();
|
||||
paDrawSelectedEcho();
|
||||
}, 60);
|
||||
|
||||
// Keep the selected message visible (e.g. after jumping from the table)
|
||||
const selectedEl = list.querySelector('.pa-map-msg.pa-selected');
|
||||
if (selectedEl) selectedEl.scrollIntoView({ block: 'nearest' });
|
||||
|
||||
const counter = document.getElementById('paCounter');
|
||||
counter.textContent = paFiltersActive()
|
||||
|
||||
@@ -273,6 +273,22 @@
|
||||
color: var(--text-secondary, #6c757d);
|
||||
}
|
||||
|
||||
.pa-echo-mapbtn {
|
||||
cursor: pointer;
|
||||
padding: 0 0.3rem;
|
||||
color: var(--text-secondary, #6c757d);
|
||||
}
|
||||
|
||||
.pa-echo-mapbtn:hover {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.pa-map-msg-preview {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #6c757d);
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.pa-direct {
|
||||
font-size: 0.75rem;
|
||||
font-style: italic;
|
||||
|
||||
Reference in New Issue
Block a user