mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-06 00:33:09 +02:00
08b972b891
Reset to FLOOD now only resets the device path without deleting configured paths from the database. New Clear Paths button deletes all configured paths from DB without touching the device. This lets users reset to FLOOD to discover new paths while keeping their configured alternatives intact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
492 lines
23 KiB
HTML
492 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<title>Direct Messages - mc-webui</title>
|
|
|
|
<!-- Favicon -->
|
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='images/apple-touch-icon.png') }}">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/favicon-32x32.png') }}">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='images/favicon-16x16.png') }}">
|
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
|
|
|
<!-- Bootstrap 5 CSS (local) -->
|
|
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
|
|
<!-- Bootstrap Icons (local) -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/bootstrap-icons/bootstrap-icons.css') }}">
|
|
|
|
<!-- Leaflet CSS (for repeater map picker) -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin="" />
|
|
|
|
<!-- Custom CSS -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
|
|
<!-- Emoji Picker (local) -->
|
|
<script type="module" src="{{ url_for('static', filename='vendor/emoji-picker-element/index.js') }}"></script>
|
|
<style>
|
|
emoji-picker {
|
|
--emoji-size: 1.5rem;
|
|
--num-columns: 8;
|
|
}
|
|
.emoji-picker-container {
|
|
position: relative;
|
|
}
|
|
.emoji-picker-popup {
|
|
position: absolute;
|
|
bottom: 100%;
|
|
right: 0;
|
|
z-index: 1000;
|
|
margin-bottom: 0.5rem;
|
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
}
|
|
.emoji-picker-popup.hidden {
|
|
display: none;
|
|
}
|
|
|
|
/* Mobile responsive adjustments */
|
|
@media (max-width: 576px) {
|
|
emoji-picker {
|
|
--emoji-size: 1.25rem;
|
|
--num-columns: 6;
|
|
}
|
|
.emoji-picker-popup {
|
|
right: auto;
|
|
left: 0;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
}
|
|
}
|
|
|
|
/* Searchable contact dropdown */
|
|
.dm-contact-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 1050;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
background: #fff;
|
|
border: 1px solid #dee2e6;
|
|
border-top: none;
|
|
border-radius: 0 0 0.375rem 0.375rem;
|
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
|
}
|
|
.dm-contact-item {
|
|
padding: 0.5rem 0.75rem;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
.dm-contact-item:hover,
|
|
.dm-contact-item.active {
|
|
background-color: #e9ecef;
|
|
}
|
|
.dm-contact-item .contact-name {
|
|
flex-grow: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.dm-contact-item .badge {
|
|
font-size: 0.7rem;
|
|
}
|
|
.dm-dropdown-separator {
|
|
padding: 0.25rem 0.75rem;
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
background: #f8f9fa;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Path management styles */
|
|
.path-list-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
padding: 0.35rem 0.5rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.8rem;
|
|
background: #fff;
|
|
}
|
|
.path-list-item.primary {
|
|
border-color: #0d6efd;
|
|
background: #f0f7ff;
|
|
}
|
|
.path-list-item .path-hex {
|
|
font-family: monospace;
|
|
font-size: 0.75rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.path-list-item .path-label {
|
|
color: #6c757d;
|
|
font-size: 0.7rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.path-list-item .path-actions {
|
|
margin-left: auto;
|
|
display: flex;
|
|
gap: 0.15rem;
|
|
flex-shrink: 0;
|
|
}
|
|
.path-list-item .path-actions .btn {
|
|
padding: 0 0.25rem;
|
|
font-size: 0.7rem;
|
|
line-height: 1.2;
|
|
}
|
|
.path-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.path-section-header h6 {
|
|
font-size: 0.85rem;
|
|
margin: 0;
|
|
}
|
|
.repeater-picker-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.4rem 0.5rem;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
font-size: 0.8rem;
|
|
}
|
|
.repeater-picker-item:hover {
|
|
background-color: #e9ecef;
|
|
}
|
|
.repeater-picker-item .badge {
|
|
font-family: monospace;
|
|
font-size: 0.7rem;
|
|
}
|
|
.path-uniqueness-warning {
|
|
color: #dc3545;
|
|
font-size: 0.75rem;
|
|
}
|
|
/* Leaflet z-index fix for Bootstrap modal */
|
|
#rptLeafletMap { z-index: 1; }
|
|
#rptLeafletMap .leaflet-top,
|
|
#rptLeafletMap .leaflet-bottom { z-index: 1000; }
|
|
/* Map modal backdrop stacks above Contact Info modal */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Main Content -->
|
|
<main>
|
|
<div class="container-fluid d-flex flex-column" style="height: 100vh;">
|
|
<!-- Conversation Selector Bar -->
|
|
<div class="row border-bottom bg-light">
|
|
<div class="col-12 p-2">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<!-- Searchable contact selector -->
|
|
<div class="position-relative flex-grow-1" id="dmContactSearchWrapper">
|
|
<input type="text"
|
|
id="dmContactSearchInput"
|
|
class="form-control"
|
|
placeholder="Select chat..."
|
|
autocomplete="off">
|
|
<div id="dmContactDropdown" class="dm-contact-dropdown" style="display: none;"></div>
|
|
</div>
|
|
<!-- Clear search button -->
|
|
<button type="button"
|
|
class="btn btn-outline-secondary flex-shrink-0"
|
|
id="dmClearSearchBtn"
|
|
title="Clear selection"
|
|
style="display: none;">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
<!-- Contact info button -->
|
|
<button type="button"
|
|
class="btn btn-outline-secondary flex-shrink-0"
|
|
id="dmContactInfoBtn"
|
|
title="Contact info"
|
|
disabled>
|
|
<i class="bi bi-info-circle"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Messages Container -->
|
|
<div class="row flex-grow-1 overflow-hidden" style="min-height: 0;">
|
|
<div class="col-12 position-relative" style="height: 100%;">
|
|
<!-- Filter bar overlay -->
|
|
<div id="dmFilterBar" class="filter-bar">
|
|
<div class="filter-bar-inner">
|
|
<input type="text" id="dmFilterInput" class="filter-bar-input" placeholder="Filter messages..." autocomplete="off">
|
|
<span id="dmFilterMatchCount" class="filter-match-count"></span>
|
|
<button type="button" id="dmFilterClearBtn" class="filter-bar-btn filter-bar-btn-clear" title="Clear">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
<button type="button" id="dmFilterCloseBtn" class="filter-bar-btn filter-bar-btn-close" title="Close">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="dmMessagesContainer" class="messages-container h-100 overflow-auto p-3">
|
|
<div id="dmMessagesList">
|
|
<!-- Placeholder shown when no conversation selected -->
|
|
<div class="dm-empty-state">
|
|
<i class="bi bi-envelope"></i>
|
|
<p class="mb-1">Select a conversation</p>
|
|
<small class="text-muted">Choose from the dropdown above or start a new chat from channel messages</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Scroll to bottom button -->
|
|
<button id="dmScrollToBottomBtn" class="scroll-to-bottom-btn" title="Scroll to bottom">
|
|
<i class="bi bi-chevron-double-down"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Send Message Form -->
|
|
<div class="row border-top bg-light">
|
|
<div class="col-12">
|
|
<form id="dmSendForm" class="p-3">
|
|
<div class="emoji-picker-container">
|
|
<div class="input-group">
|
|
<textarea
|
|
id="dmMessageInput"
|
|
class="form-control"
|
|
placeholder="Type a message..."
|
|
rows="2"
|
|
maxlength="500"
|
|
disabled
|
|
></textarea>
|
|
<button type="button" class="btn btn-outline-secondary" id="dmEmojiBtn" title="Insert emoji">
|
|
<i class="bi bi-emoji-smile"></i>
|
|
</button>
|
|
<button type="submit" class="btn btn-success px-4" id="dmSendBtn" disabled>
|
|
<i class="bi bi-send"></i>
|
|
</button>
|
|
</div>
|
|
<!-- Emoji picker popup (hidden by default) -->
|
|
<div id="dmEmojiPickerPopup" class="emoji-picker-popup hidden"></div>
|
|
</div>
|
|
<div class="d-flex justify-content-end">
|
|
<small class="text-muted"><span id="dmCharCounter">0</span> / 150</small>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Bar -->
|
|
<div class="row border-top">
|
|
<div class="col-12">
|
|
<div class="p-2 small text-muted d-flex justify-content-between align-items-center">
|
|
<span id="dmStatusText">
|
|
<i class="bi bi-circle-fill text-secondary"></i> Connecting...
|
|
</span>
|
|
<span id="dmLastRefresh">Updated: Never</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Floating Action Buttons -->
|
|
<div class="fab-container" id="dmFabContainer">
|
|
<button class="fab fab-toggle" id="dmFabToggle" title="Hide buttons">
|
|
<i class="bi bi-chevron-right"></i>
|
|
</button>
|
|
<button class="fab fab-filter" id="dmFilterFab" title="Filter Messages">
|
|
<i class="bi bi-funnel-fill"></i>
|
|
</button>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Contact Info Modal -->
|
|
<div class="modal fade" id="dmContactInfoModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h6 class="modal-title"><i class="bi bi-person-circle"></i> Contact Info</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="dmContactInfoBody"></div>
|
|
<!-- Path management section (populated dynamically) -->
|
|
<div class="modal-body border-top pt-2 pb-1" id="dmPathSection" style="display: none;">
|
|
<div class="path-section-header">
|
|
<h6><i class="bi bi-signpost-split"></i> Paths</h6>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" id="dmAddPathBtn" title="Add path">
|
|
<i class="bi bi-plus-lg"></i>
|
|
</button>
|
|
</div>
|
|
<div id="dmPathList"></div>
|
|
<!-- Path action buttons -->
|
|
<div class="d-flex justify-content-end gap-2 mt-1">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="dmClearPathsBtn"
|
|
title="Delete all configured paths from database">
|
|
<i class="bi bi-trash"></i> Clear Paths
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger btn-sm" id="dmResetFloodBtn"
|
|
title="Reset device path to FLOOD mode">
|
|
<i class="bi bi-broadcast"></i> Reset to FLOOD
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<div class="d-flex align-items-center justify-content-between w-100">
|
|
<div class="d-flex gap-3">
|
|
<div class="form-check form-switch" title="Auto Retry: resend DM if no ACK received">
|
|
<input class="form-check-input" type="checkbox" id="dmAutoRetryToggle" checked>
|
|
<label class="form-check-label small" for="dmAutoRetryToggle">Auto Retry</label>
|
|
</div>
|
|
<div class="form-check form-switch" title="No Flood Fallback: never auto-reset path to FLOOD">
|
|
<input class="form-check-input" type="checkbox" id="dmNoAutoFloodToggle">
|
|
<label class="form-check-label small" for="dmNoAutoFloodToggle">No Flood</label>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Repeater Map Picker Modal -->
|
|
<div class="modal fade" id="repeaterMapModal" tabindex="-1" style="z-index: 1080;">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-geo-alt"></i> Select Repeater from Map</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<div class="d-flex align-items-center gap-2 px-3 py-2 border-bottom bg-light">
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="rptMapCachedSwitch">
|
|
<label class="form-check-label small" for="rptMapCachedSwitch">Cached</label>
|
|
</div>
|
|
<span class="text-muted small ms-auto" id="rptMapCount"></span>
|
|
</div>
|
|
<div id="rptLeafletMap" style="height: 400px; width: 100%;"></div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<span class="me-auto small text-muted" id="rptMapSelected">Click a repeater on the map</span>
|
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="rptMapAddBtn" disabled>Add</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Path Modal -->
|
|
<div class="modal fade" id="addPathModal" tabindex="-1" style="z-index: 1070;">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-signpost-split"></i> Add Path</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="dmAddPathForm">
|
|
<div class="mb-2">
|
|
<label class="form-label small mb-1">Hash Size</label>
|
|
<div class="btn-group btn-group-sm w-100" role="group">
|
|
<input type="radio" class="btn-check" name="pathHashSize" id="pathHash1" value="1" checked>
|
|
<label class="btn btn-outline-secondary" for="pathHash1">1B (max 64)</label>
|
|
<input type="radio" class="btn-check" name="pathHashSize" id="pathHash2" value="2">
|
|
<label class="btn btn-outline-secondary" for="pathHash2">2B (max 32)</label>
|
|
<input type="radio" class="btn-check" name="pathHashSize" id="pathHash3" value="3">
|
|
<label class="btn btn-outline-secondary" for="pathHash3">3B (max 21)</label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small mb-1">Path (hex)</label>
|
|
<div class="input-group input-group-sm">
|
|
<input type="text" class="form-control font-monospace" id="dmPathHexInput"
|
|
placeholder="e.g. 5e,e7 or 5e34,e761" autocomplete="off">
|
|
<button type="button" class="btn btn-outline-secondary" id="dmPickRepeaterBtn"
|
|
title="Pick repeater from list">
|
|
<i class="bi bi-plus-circle"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" id="dmPickRepeaterMapBtn"
|
|
title="Pick repeater from map">
|
|
<i class="bi bi-geo-alt"></i>
|
|
</button>
|
|
</div>
|
|
<div id="dmPathUniquenessWarning" class="path-uniqueness-warning mt-1" style="display: none;"></div>
|
|
</div>
|
|
<!-- Repeater picker (hidden by default) -->
|
|
<div id="dmRepeaterPicker" style="display: none;" class="border rounded mb-2">
|
|
<div class="d-flex border-bottom">
|
|
<div class="btn-group btn-group-sm flex-shrink-0" role="group">
|
|
<input type="radio" class="btn-check" name="repeaterSearchMode" id="rptSearchName" value="name" checked>
|
|
<label class="btn btn-outline-secondary border-0 rounded-0" for="rptSearchName">Name</label>
|
|
<input type="radio" class="btn-check" name="repeaterSearchMode" id="rptSearchId" value="id">
|
|
<label class="btn btn-outline-secondary border-0 rounded-0" for="rptSearchId">ID</label>
|
|
</div>
|
|
<input type="text" class="form-control form-control-sm border-0"
|
|
id="dmRepeaterSearch" placeholder="Search by name..." autocomplete="off">
|
|
</div>
|
|
<div id="dmRepeaterList" style="max-height: 180px; overflow-y: auto;"></div>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small mb-1">Label (optional)</label>
|
|
<input type="text" class="form-control form-control-sm" id="dmPathLabelInput"
|
|
placeholder="e.g. via Mountain RPT" maxlength="50">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="dmCancelPathBtn" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="dmSavePathBtn">Add Path</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast container for notifications -->
|
|
<div class="toast-container position-fixed top-0 start-0 p-3">
|
|
<div id="notificationToast" class="toast" role="alert">
|
|
<div class="toast-header">
|
|
<strong class="me-auto">mc-webui</strong>
|
|
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
<div class="toast-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bootstrap 5 JS Bundle (local) -->
|
|
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
|
|
|
<!-- Message Content Processing Utilities (must load before dm.js) -->
|
|
<script src="{{ url_for('static', filename='js/message-utils.js') }}"></script>
|
|
|
|
<!-- Filter Utilities (must load before dm.js) -->
|
|
<script src="{{ url_for('static', filename='js/filter-utils.js') }}"></script>
|
|
|
|
<!-- Leaflet JS (for repeater map picker) -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""></script>
|
|
|
|
<!-- SocketIO for real-time updates -->
|
|
<script src="{{ url_for('static', filename='vendor/socket.io/socket.io.min.js') }}"></script>
|
|
|
|
<!-- Custom JS -->
|
|
<script src="{{ url_for('static', filename='js/dm.js') }}"></script>
|
|
|
|
<script>
|
|
// Pass configuration from Flask to JavaScript
|
|
window.MC_CONFIG = {
|
|
deviceName: "{{ device_name }}",
|
|
initialConversation: "{{ initial_conversation or '' }}"
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|