mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-07 01:03:15 +02:00
9668651a61
Full-screen iframe panel wired per the add-fullscreen-panel checklist: - /path-analyzer Flask view + standalone template (theme bootstrap before CSS, local vendor assets, Leaflet included for the stage-5 map view) - path-analyzer.js: days selector (1/3/5/7), message table (time, channel, sender, preview, copyable packet hash, hops, echo count), spinner/empty states, local toast helpers fed from /api/ui/settings - #pathAnalyzerModal in index.html, iframe src set on show.bs.modal for fresh data each open; FAB button + purple gradient class - menu entry in #mainMenu (Tools) + placement radio row in Settings -> Appearance; registered in ITEM_PLACEMENT_DEFS/DEFAULTS (default: menu) - style.css: #pathAnalyzerModal added to all three narrow-viewport fullscreen-override selector lists Verified live via Playwright at 1280px and 390px, light + dark: modal fills the viewport exactly, placement radios move the entry both ways, iframe reloads on every open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
203 lines
7.3 KiB
JavaScript
203 lines
7.3 KiB
JavaScript
// Path Analyzer panel
|
|
// - bulk channel messages across all channels (GET /api/path-analyzer/messages)
|
|
// - stage 2: days selector + flat message table
|
|
// - later stages: path detail rows, filters, per-repeater stats, map view
|
|
|
|
// ================================================================
|
|
// UI settings + toast (same behavior as repeaters.js)
|
|
// ================================================================
|
|
|
|
const PA_UI_SETTINGS_DEFAULTS = {
|
|
toast_timeout_sec: 2,
|
|
toast_no_autoclose: false,
|
|
toast_position: 'top-left'
|
|
};
|
|
|
|
const PA_TOAST_POSITION_CLASSES = {
|
|
'top-left': ['top-0', 'start-0'],
|
|
'top-right': ['top-0', 'end-0'],
|
|
'bottom-left': ['bottom-0', 'start-0'],
|
|
'bottom-right': ['bottom-0', 'end-0'],
|
|
'center': ['top-50', 'start-50', 'translate-middle']
|
|
};
|
|
const PA_ALL_POSITION_CLASSES = ['top-0', 'top-50', 'start-0', 'start-50', 'bottom-0', 'end-0', 'translate-middle'];
|
|
|
|
window.uiSettingsCache = window.uiSettingsCache || { ...PA_UI_SETTINGS_DEFAULTS };
|
|
|
|
function applyToastPosition(position) {
|
|
const classes = PA_TOAST_POSITION_CLASSES[position] || PA_TOAST_POSITION_CLASSES['top-left'];
|
|
document.querySelectorAll('[data-toast-container]').forEach(el => {
|
|
PA_ALL_POSITION_CLASSES.forEach(c => el.classList.remove(c));
|
|
classes.forEach(c => el.classList.add(c));
|
|
});
|
|
}
|
|
|
|
async function loadUiSettings() {
|
|
try {
|
|
const resp = await fetch('/api/ui/settings');
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
window.uiSettingsCache = { ...PA_UI_SETTINGS_DEFAULTS, ...data };
|
|
applyToastPosition(window.uiSettingsCache.toast_position);
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load UI settings:', e);
|
|
}
|
|
}
|
|
|
|
function showNotification(message, type = 'info') {
|
|
const toastEl = document.getElementById('notificationToast');
|
|
if (!toastEl) return;
|
|
|
|
const toastBody = toastEl.querySelector('.toast-body');
|
|
if (toastBody) {
|
|
toastBody.textContent = message;
|
|
}
|
|
|
|
const toastHeader = toastEl.querySelector('.toast-header');
|
|
if (toastHeader) {
|
|
toastHeader.className = 'toast-header';
|
|
if (type === 'success') {
|
|
toastHeader.classList.add('bg-success', 'text-white');
|
|
} else if (type === 'danger') {
|
|
toastHeader.classList.add('bg-danger', 'text-white');
|
|
} else if (type === 'warning') {
|
|
toastHeader.classList.add('bg-warning');
|
|
}
|
|
}
|
|
|
|
const cfg = window.uiSettingsCache || {};
|
|
const noAutoclose = !!cfg.toast_no_autoclose;
|
|
const timeoutSec = parseFloat(cfg.toast_timeout_sec);
|
|
const delay = isFinite(timeoutSec) && timeoutSec > 0 ? Math.round(timeoutSec * 1000) : 2000;
|
|
|
|
const toast = new bootstrap.Toast(toastEl, {
|
|
autohide: !noAutoclose,
|
|
delay: delay
|
|
});
|
|
toast.show();
|
|
}
|
|
|
|
// ================================================================
|
|
// Data loading + table rendering
|
|
// ================================================================
|
|
|
|
let paMessages = [];
|
|
|
|
function paFormatTime(msg) {
|
|
if (!msg.timestamp) return '—';
|
|
const d = new Date(msg.timestamp * 1000);
|
|
const pad = n => String(n).padStart(2, '0');
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
}
|
|
|
|
function paCopyText(text, label) {
|
|
navigator.clipboard.writeText(text).then(
|
|
() => showNotification(`${label} copied to clipboard`, 'success'),
|
|
() => showNotification(`Failed to copy ${label}`, 'danger')
|
|
);
|
|
}
|
|
|
|
function paRenderTable() {
|
|
const body = document.getElementById('paTableBody');
|
|
body.innerHTML = '';
|
|
|
|
for (const msg of paMessages) {
|
|
const tr = document.createElement('tr');
|
|
|
|
const tdTime = document.createElement('td');
|
|
tdTime.className = 'pa-time';
|
|
tdTime.textContent = paFormatTime(msg);
|
|
tr.appendChild(tdTime);
|
|
|
|
const tdChannel = document.createElement('td');
|
|
tdChannel.textContent = msg.channel_name || `#${msg.channel_idx}`;
|
|
tr.appendChild(tdChannel);
|
|
|
|
const tdSender = document.createElement('td');
|
|
tdSender.textContent = msg.is_own ? `${msg.sender || 'Me'} (own)` : (msg.sender || '—');
|
|
tr.appendChild(tdSender);
|
|
|
|
const tdContent = document.createElement('td');
|
|
const preview = document.createElement('div');
|
|
preview.className = 'pa-content-preview';
|
|
preview.textContent = msg.content || '';
|
|
preview.title = msg.content || '';
|
|
tdContent.appendChild(preview);
|
|
tr.appendChild(tdContent);
|
|
|
|
const tdHash = document.createElement('td');
|
|
if (msg.packet_hash) {
|
|
const span = document.createElement('span');
|
|
span.className = 'pa-hash';
|
|
span.textContent = msg.packet_hash;
|
|
span.title = 'Click to copy';
|
|
span.addEventListener('click', () => paCopyText(msg.packet_hash, 'Packet hash'));
|
|
tdHash.appendChild(span);
|
|
} else {
|
|
tdHash.innerHTML = '<span class="text-muted small">no path data</span>';
|
|
}
|
|
tr.appendChild(tdHash);
|
|
|
|
const tdHops = document.createElement('td');
|
|
tdHops.className = 'text-end';
|
|
tdHops.textContent = (msg.hop_count === null || msg.hop_count === undefined) ? '—' : msg.hop_count;
|
|
tr.appendChild(tdHops);
|
|
|
|
const tdEchoes = document.createElement('td');
|
|
tdEchoes.className = 'text-end';
|
|
tdEchoes.textContent = msg.echoes.length;
|
|
tr.appendChild(tdEchoes);
|
|
|
|
body.appendChild(tr);
|
|
}
|
|
|
|
document.getElementById('paCounter').textContent =
|
|
`${paMessages.length} message${paMessages.length === 1 ? '' : 's'}`;
|
|
}
|
|
|
|
function paSetView(state) {
|
|
document.getElementById('paLoading').classList.toggle('d-none', state !== 'loading');
|
|
document.getElementById('paEmpty').classList.toggle('d-none', state !== 'empty');
|
|
document.getElementById('paTableWrap').classList.toggle('d-none', state !== 'table');
|
|
}
|
|
|
|
async function paLoadMessages() {
|
|
const days = document.getElementById('paDaysSelect').value;
|
|
paSetView('loading');
|
|
document.getElementById('paCounter').textContent = '';
|
|
|
|
try {
|
|
const resp = await fetch(`/api/path-analyzer/messages?days=${encodeURIComponent(days)}`);
|
|
const data = await resp.json();
|
|
if (!resp.ok || !data.success) {
|
|
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
}
|
|
// Newest first for the analysis table (API returns ascending)
|
|
paMessages = (data.messages || []).slice().reverse();
|
|
} catch (e) {
|
|
console.error('Failed to load messages:', e);
|
|
showNotification(`Failed to load messages: ${e.message}`, 'danger');
|
|
paMessages = [];
|
|
}
|
|
|
|
if (paMessages.length === 0) {
|
|
paSetView('empty');
|
|
} else {
|
|
paRenderTable();
|
|
paSetView('table');
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// Init
|
|
// ================================================================
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadUiSettings();
|
|
document.getElementById('paDaysSelect').addEventListener('change', paLoadMessages);
|
|
document.getElementById('paRefreshBtn').addEventListener('click', paLoadMessages);
|
|
paLoadMessages();
|
|
});
|