mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-07-31 05:53:20 +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>
377 lines
17 KiB
HTML
377 lines
17 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Chat - mc-webui{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<!-- Emoji Picker (local) -->
|
|
<script type="module" src="{{ url_for('static', filename='vendor/emoji-picker-element/index.js') }}"></script>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid d-flex flex-column" style="height: 100%;">
|
|
<!-- Main content: sidebar + chat -->
|
|
<div class="d-flex flex-grow-1 overflow-hidden" style="min-height: 0;">
|
|
<!-- Channel Sidebar (visible on lg+ screens) -->
|
|
<div id="channelSidebar" class="channel-sidebar">
|
|
<div class="channel-sidebar-header">
|
|
<i class="bi bi-broadcast-pin"></i> Channels
|
|
</div>
|
|
<div class="channel-sidebar-list" id="channelSidebarList">
|
|
<!-- Populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
<!-- Chat Area -->
|
|
<div class="flex-grow-1 d-flex flex-column" style="min-width: 0;">
|
|
<!-- Messages Container -->
|
|
<div class="flex-grow-1 position-relative overflow-hidden" style="min-height: 0;">
|
|
<!-- Filter bar overlay -->
|
|
<div id="filterBar" class="filter-bar">
|
|
<div class="filter-bar-inner">
|
|
<div class="filter-input-wrapper">
|
|
<input type="text" id="filterInput" class="filter-bar-input" placeholder="Filter messages..." autocomplete="off">
|
|
<!-- Filter mentions autocomplete popup -->
|
|
<div id="filterMentionsPopup" class="mentions-popup filter-mentions-popup hidden">
|
|
<div class="mentions-list" id="filterMentionsList"></div>
|
|
</div>
|
|
</div>
|
|
<button type="button" id="filterMeBtn" class="filter-bar-btn filter-bar-btn-me" title="Filter my messages">
|
|
<i class="bi bi-person-fill"></i>
|
|
</button>
|
|
<span id="filterMatchCount" class="filter-match-count"></span>
|
|
<button type="button" id="filterClearBtn" class="filter-bar-btn filter-bar-btn-clear" title="Clear">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
<button type="button" id="filterCloseBtn" class="filter-bar-btn filter-bar-btn-close" title="Close">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="messagesContainer" class="messages-container h-100 overflow-auto p-3">
|
|
<div id="messagesList">
|
|
<!-- Messages will be loaded here via JavaScript -->
|
|
<div class="text-center text-muted py-5">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3">Loading messages...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Scroll to bottom button -->
|
|
<button id="scrollToBottomBtn" class="scroll-to-bottom-btn" title="Scroll to bottom">
|
|
<i class="bi bi-chevron-double-down"></i>
|
|
</button>
|
|
</div>
|
|
<!-- Send Message Form -->
|
|
<div class="border-top bg-light">
|
|
<form id="sendMessageForm" class="p-3">
|
|
<div class="emoji-picker-container">
|
|
<div class="input-group">
|
|
<textarea
|
|
id="messageInput"
|
|
class="form-control"
|
|
placeholder="Type a message..."
|
|
rows="2"
|
|
maxlength="500"
|
|
required
|
|
></textarea>
|
|
<button type="button" class="btn btn-outline-secondary" id="emojiBtn" title="Insert emoji">
|
|
<i class="bi bi-emoji-smile"></i>
|
|
</button>
|
|
<button type="submit" class="btn btn-primary px-4" id="sendBtn">
|
|
<i class="bi bi-send"></i>
|
|
</button>
|
|
</div>
|
|
<!-- Emoji picker popup (hidden by default) -->
|
|
<div id="emojiPickerPopup" class="emoji-picker-popup hidden"></div>
|
|
<!-- Mentions autocomplete popup (hidden by default) -->
|
|
<div id="mentionsPopup" class="mentions-popup hidden">
|
|
<div class="mentions-list" id="mentionsList"></div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex justify-content-end">
|
|
<small id="charCounter" class="text-muted">0 / 135</small>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- Status Bar -->
|
|
<div class="border-top">
|
|
<div class="p-2 small text-muted d-flex justify-content-between align-items-center">
|
|
<div class="d-flex gap-2 align-items-center">
|
|
<span id="statusText">
|
|
<i class="bi bi-circle-fill text-secondary"></i> Connecting...
|
|
</span>
|
|
<span id="regionIndicator" class="badge bg-info text-dark d-none" role="button"
|
|
title="Click to change region for this channel">
|
|
<i class="bi bi-pin-map"></i> <span id="regionIndicatorName"></span>
|
|
</span>
|
|
</div>
|
|
<span id="lastRefresh">Updated: Never</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Floating Action Buttons -->
|
|
<div class="fab-container" id="fabContainer">
|
|
<button class="fab fab-toggle" id="fabToggle" title="Hide buttons">
|
|
<i class="bi bi-chevron-right"></i>
|
|
</button>
|
|
<button class="fab fab-filter" id="filterFab" title="Filter Messages">
|
|
<i class="bi bi-funnel-fill"></i>
|
|
</button>
|
|
<button class="fab fab-search" id="globalSearchBtn" data-bs-toggle="modal" data-bs-target="#searchModal" title="Search Messages">
|
|
<i class="bi bi-search"></i>
|
|
</button>
|
|
<button class="fab fab-dm" data-bs-toggle="modal" data-bs-target="#dmModal" title="Direct Messages">
|
|
<i class="bi bi-envelope-fill"></i>
|
|
</button>
|
|
<button class="fab fab-contacts" data-bs-toggle="modal" data-bs-target="#contactsModal" title="Contact Management">
|
|
<i class="bi bi-person-fill"></i>
|
|
</button>
|
|
<button class="fab fab-settings" data-bs-toggle="modal" data-bs-target="#settingsModal" title="Settings">
|
|
<i class="bi bi-gear-fill"></i>
|
|
</button>
|
|
<button class="fab fab-advert d-none" id="fab-advert" title="Send Advert">
|
|
<i class="bi bi-megaphone"></i>
|
|
</button>
|
|
<button class="fab fab-floodadvert d-none" id="fab-floodadvert" title="Flood Advert">
|
|
<i class="bi bi-broadcast"></i>
|
|
</button>
|
|
<button class="fab fab-map d-none" id="fab-map" title="Map">
|
|
<i class="bi bi-map"></i>
|
|
</button>
|
|
<button class="fab fab-console d-none" id="fab-console" data-bs-toggle="modal" data-bs-target="#consoleModal" title="Console">
|
|
<i class="bi bi-terminal"></i>
|
|
</button>
|
|
<button class="fab fab-repeaters d-none" id="fab-repeaters" data-bs-toggle="modal" data-bs-target="#repeatersModal" title="My Repeaters">
|
|
<i class="bi bi-diagram-3"></i>
|
|
</button>
|
|
<button class="fab fab-pathanalyzer d-none" id="fab-pathanalyzer" data-bs-toggle="modal" data-bs-target="#pathAnalyzerModal" title="Path Analyzer">
|
|
<i class="bi bi-signpost-split"></i>
|
|
</button>
|
|
<button class="fab fab-deviceinfo d-none" id="fab-deviceinfo" data-bs-toggle="modal" data-bs-target="#deviceInfoModal" title="Device Info">
|
|
<i class="bi bi-cpu"></i>
|
|
</button>
|
|
<button class="fab fab-syslog d-none" id="fab-syslog" data-bs-toggle="modal" data-bs-target="#logsModal" title="System Log">
|
|
<i class="bi bi-journal-text"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- DM Modal (Full Screen) -->
|
|
<div class="modal fade" id="dmModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-success text-white">
|
|
<h5 class="modal-title"><i class="bi bi-envelope"></i> Direct Messages</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> Close
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="dmFrame" src="/dm" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Management Modal (Full Screen) -->
|
|
<div class="modal fade" id="contactsModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title"><i class="bi bi-person-check"></i> Contact Management</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> Close
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="contactsFrame" src="/contacts/manage" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Console Modal (Full Screen) -->
|
|
<div class="modal fade" id="consoleModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content" style="background-color: #1a1a2e;">
|
|
<div class="modal-header" style="background-color: #16213e; border-bottom: 1px solid #0f3460;">
|
|
<h5 class="modal-title text-white"><i class="bi bi-terminal"></i> mc-webui Console</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> Close
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="consoleFrame" src="/console" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- My Repeaters Modal (Full Screen) -->
|
|
<div class="modal fade" id="repeatersModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-success text-white">
|
|
<h5 class="modal-title"><i class="bi bi-diagram-3"></i> My Repeaters</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> Close
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="repeatersFrame" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Path Analyzer Modal (Full Screen) -->
|
|
<div class="modal fade" id="pathAnalyzerModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content">
|
|
<div class="modal-header pathanalyzer-header text-white">
|
|
<h5 class="modal-title"><i class="bi bi-signpost-split"></i> Path Analyzer</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> Close
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="pathAnalyzerFrame" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Log Modal (Full Screen) -->
|
|
<div class="modal fade" id="logsModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content" style="background-color: #1a1a2e;">
|
|
<div class="modal-header" style="background-color: #16213e; border-bottom: 1px solid #0f3460;">
|
|
<h5 class="modal-title text-white"><i class="bi bi-journal-text"></i> System Log</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> Close
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="logsFrame" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
// Pass configuration from Flask to JavaScript
|
|
window.MC_CONFIG = {
|
|
deviceName: "{{ device_name }}"
|
|
};
|
|
|
|
// Reload iframe content when modals are opened to ensure fresh data
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const dmModal = document.getElementById('dmModal');
|
|
const contactsModal = document.getElementById('contactsModal');
|
|
|
|
if (dmModal) {
|
|
dmModal.addEventListener('show.bs.modal', function () {
|
|
const dmFrame = document.getElementById('dmFrame');
|
|
if (dmFrame) {
|
|
dmFrame.src = dmFrame.src;
|
|
}
|
|
});
|
|
|
|
// Update DM badges when modal is closed (without full page reload)
|
|
dmModal.addEventListener('hidden.bs.modal', async function () {
|
|
try {
|
|
// Reload DM read status from server to sync with app.js automatic updates
|
|
// This ensures dmLastSeenTimestamps in app.js is current
|
|
if (typeof loadDmLastSeenTimestampsFromServer === 'function') {
|
|
await loadDmLastSeenTimestampsFromServer();
|
|
}
|
|
|
|
// Trigger DM badge update (now handled by app.js on FAB button)
|
|
if (typeof checkDmUpdates === 'function') {
|
|
await checkDmUpdates();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating DM badges:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (contactsModal) {
|
|
contactsModal.addEventListener('show.bs.modal', function () {
|
|
const contactsFrame = document.getElementById('contactsFrame');
|
|
if (contactsFrame) {
|
|
contactsFrame.src = contactsFrame.src;
|
|
}
|
|
});
|
|
|
|
// Update pending contacts badge when modal is closed
|
|
contactsModal.addEventListener('hidden.bs.modal', async function () {
|
|
try {
|
|
// Trigger pending contacts badge update (handled by app.js on FAB button)
|
|
if (typeof updatePendingContactsBadge === 'function') {
|
|
await updatePendingContactsBadge();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating pending contacts badge:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Console modal - reload iframe when opened to reset WebSocket connection
|
|
const consoleModal = document.getElementById('consoleModal');
|
|
if (consoleModal) {
|
|
consoleModal.addEventListener('show.bs.modal', function () {
|
|
const consoleFrame = document.getElementById('consoleFrame');
|
|
if (consoleFrame) {
|
|
consoleFrame.src = '/console';
|
|
}
|
|
});
|
|
}
|
|
|
|
// My Repeaters modal - (re)load iframe on open for fresh data
|
|
const repeatersModal = document.getElementById('repeatersModal');
|
|
if (repeatersModal) {
|
|
repeatersModal.addEventListener('show.bs.modal', function () {
|
|
const repeatersFrame = document.getElementById('repeatersFrame');
|
|
if (repeatersFrame) {
|
|
repeatersFrame.src = '/repeaters';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Path Analyzer modal - (re)load iframe on open for fresh data
|
|
const pathAnalyzerModal = document.getElementById('pathAnalyzerModal');
|
|
if (pathAnalyzerModal) {
|
|
pathAnalyzerModal.addEventListener('show.bs.modal', function () {
|
|
const pathAnalyzerFrame = document.getElementById('pathAnalyzerFrame');
|
|
if (pathAnalyzerFrame) {
|
|
pathAnalyzerFrame.src = '/path-analyzer';
|
|
}
|
|
});
|
|
}
|
|
|
|
// System Log modal - load iframe on open, clear on close
|
|
const logsModal = document.getElementById('logsModal');
|
|
if (logsModal) {
|
|
logsModal.addEventListener('show.bs.modal', function () {
|
|
const logsFrame = document.getElementById('logsFrame');
|
|
if (logsFrame) {
|
|
logsFrame.src = '/logs';
|
|
}
|
|
});
|
|
logsModal.addEventListener('hidden.bs.modal', function () {
|
|
const logsFrame = document.getElementById('logsFrame');
|
|
if (logsFrame) {
|
|
logsFrame.src = ''; // disconnect WebSocket when closed
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|