mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
feat(paths): add repeater map picker for path configuration
Adds a map button (geo icon) next to the list picker in the path form. Clicking it opens a modal with a Leaflet map showing repeater locations. User clicks a repeater marker, then clicks Add to append its ID prefix to the path hex. Includes Cached toggle to show all DB repeaters vs only device-known ones. Respects current hash size setting (1B/2B/3B). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1890,6 +1890,16 @@ function setupPathFormHandlers(pubkey) {
|
||||
});
|
||||
}
|
||||
|
||||
// Repeater map picker button
|
||||
const mapBtn = document.getElementById('dmPickRepeaterMapBtn');
|
||||
if (mapBtn) {
|
||||
const newMapBtn = mapBtn.cloneNode(true);
|
||||
mapBtn.parentNode.replaceChild(newMapBtn, mapBtn);
|
||||
newMapBtn.addEventListener('click', () => {
|
||||
openRepeaterMapPicker();
|
||||
});
|
||||
}
|
||||
|
||||
// Reset to FLOOD button
|
||||
if (resetFloodBtn) {
|
||||
const newResetBtn = resetFloodBtn.cloneNode(true);
|
||||
@@ -2037,6 +2047,162 @@ function checkUniquenessWarning(repeaters, hashSize) {
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Repeater Map Picker
|
||||
// ================================================================
|
||||
|
||||
let _rptMap = null;
|
||||
let _rptMapMarkers = null;
|
||||
let _rptMapSelectedRepeater = null;
|
||||
|
||||
function openRepeaterMapPicker() {
|
||||
_rptMapSelectedRepeater = null;
|
||||
|
||||
const modalEl = document.getElementById('repeaterMapModal');
|
||||
if (!modalEl) return;
|
||||
|
||||
const addBtn = document.getElementById('rptMapAddBtn');
|
||||
const selectedLabel = document.getElementById('rptMapSelected');
|
||||
if (addBtn) addBtn.disabled = true;
|
||||
if (selectedLabel) selectedLabel.textContent = 'Click a repeater on the map';
|
||||
|
||||
const modal = new bootstrap.Modal(modalEl);
|
||||
|
||||
const onShown = async function () {
|
||||
// Init map once
|
||||
if (!_rptMap) {
|
||||
_rptMap = L.map('rptLeafletMap').setView([52.0, 19.0], 6);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(_rptMap);
|
||||
_rptMapMarkers = L.layerGroup().addTo(_rptMap);
|
||||
}
|
||||
_rptMap.invalidateSize();
|
||||
await loadRepeaterMapMarkers();
|
||||
modalEl.removeEventListener('shown.bs.modal', onShown);
|
||||
};
|
||||
|
||||
// Cached toggle
|
||||
const cachedSwitch = document.getElementById('rptMapCachedSwitch');
|
||||
if (cachedSwitch) {
|
||||
cachedSwitch.onchange = () => loadRepeaterMapMarkers();
|
||||
}
|
||||
|
||||
// Add button
|
||||
if (addBtn) {
|
||||
addBtn.onclick = () => {
|
||||
if (!_rptMapSelectedRepeater) return;
|
||||
const hashSize = parseInt(document.querySelector('input[name="pathHashSize"]:checked').value);
|
||||
const prefix = _rptMapSelectedRepeater.public_key.substring(0, hashSize * 2).toLowerCase();
|
||||
const hexInput = document.getElementById('dmPathHexInput');
|
||||
if (hexInput) {
|
||||
const current = hexInput.value.replace(/[,\s→]/g, '').trim();
|
||||
const newVal = current + prefix;
|
||||
const chunk = hashSize * 2;
|
||||
const parts = [];
|
||||
for (let i = 0; i < newVal.length; i += chunk) {
|
||||
parts.push(newVal.substring(i, i + chunk));
|
||||
}
|
||||
hexInput.value = parts.join(',');
|
||||
if (_repeatersCache) {
|
||||
checkUniquenessWarning(_repeatersCache, hashSize);
|
||||
}
|
||||
}
|
||||
modal.hide();
|
||||
};
|
||||
}
|
||||
|
||||
modalEl.addEventListener('shown.bs.modal', onShown);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async function loadRepeaterMapMarkers() {
|
||||
if (!_rptMapMarkers) return;
|
||||
_rptMapMarkers.clearLayers();
|
||||
|
||||
const cachedSwitch = document.getElementById('rptMapCachedSwitch');
|
||||
const showCached = cachedSwitch && cachedSwitch.checked;
|
||||
const countEl = document.getElementById('rptMapCount');
|
||||
const addBtn = document.getElementById('rptMapAddBtn');
|
||||
const selectedLabel = document.getElementById('rptMapSelected');
|
||||
|
||||
// Reset selection
|
||||
_rptMapSelectedRepeater = null;
|
||||
if (addBtn) addBtn.disabled = true;
|
||||
if (selectedLabel) selectedLabel.textContent = 'Click a repeater on the map';
|
||||
|
||||
// Ensure repeaters cache is loaded
|
||||
if (!_repeatersCache) {
|
||||
try {
|
||||
const response = await fetch('/api/contacts/repeaters');
|
||||
const data = await response.json();
|
||||
if (data.success) _repeatersCache = data.repeaters;
|
||||
} catch (e) {
|
||||
if (countEl) countEl.textContent = 'Failed to load';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter: only those with GPS
|
||||
let repeaters = (_repeatersCache || []).filter(r =>
|
||||
r.adv_lat && r.adv_lon && (r.adv_lat !== 0 || r.adv_lon !== 0)
|
||||
);
|
||||
|
||||
if (!showCached) {
|
||||
// Non-cached: only repeaters that are on the device (have recent advert)
|
||||
// Use detailed contacts to check which are on device
|
||||
try {
|
||||
const response = await fetch('/api/contacts/detailed');
|
||||
const data = await response.json();
|
||||
if (data.success && data.contacts) {
|
||||
const deviceKeys = new Set(data.contacts
|
||||
.filter(c => c.type === 2)
|
||||
.map(c => c.public_key.toLowerCase()));
|
||||
repeaters = repeaters.filter(r => deviceKeys.has(r.public_key.toLowerCase()));
|
||||
}
|
||||
} catch (e) { /* show all on error */ }
|
||||
}
|
||||
|
||||
if (countEl) countEl.textContent = `${repeaters.length} repeaters`;
|
||||
|
||||
const hashSize = parseInt(document.querySelector('input[name="pathHashSize"]:checked')?.value || '1');
|
||||
const bounds = [];
|
||||
|
||||
repeaters.forEach(rpt => {
|
||||
const prefix = rpt.public_key.substring(0, hashSize * 2).toUpperCase();
|
||||
const lastSeen = rpt.last_advert ? formatRelativeTimeDm(rpt.last_advert) : '';
|
||||
|
||||
const marker = L.circleMarker([rpt.adv_lat, rpt.adv_lon], {
|
||||
radius: 10,
|
||||
fillColor: '#4CAF50',
|
||||
color: '#fff',
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
}).addTo(_rptMapMarkers);
|
||||
|
||||
marker.bindPopup(
|
||||
`<b>${rpt.name}</b><br>` +
|
||||
`<code>${prefix}</code>` +
|
||||
(lastSeen ? `<br><small class="text-muted">Last seen: ${lastSeen}</small>` : '')
|
||||
);
|
||||
|
||||
marker.on('click', () => {
|
||||
_rptMapSelectedRepeater = rpt;
|
||||
if (addBtn) addBtn.disabled = false;
|
||||
if (selectedLabel) {
|
||||
selectedLabel.innerHTML = `<code>${prefix}</code> ${rpt.name}`;
|
||||
}
|
||||
});
|
||||
|
||||
bounds.push([rpt.adv_lat, rpt.adv_lon]);
|
||||
});
|
||||
|
||||
if (bounds.length > 0) {
|
||||
_rptMap.fitBounds(bounds, { padding: [20, 20] });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and setup the No Auto Flood toggle for current contact.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
<!-- 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') }}">
|
||||
|
||||
@@ -173,6 +178,10 @@
|
||||
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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -336,9 +345,13 @@
|
||||
<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">
|
||||
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>
|
||||
@@ -393,6 +406,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Repeater Map Picker Modal -->
|
||||
<div class="modal fade" id="repeaterMapModal" tabindex="-1">
|
||||
<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">Cancel</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="rptMapAddBtn" disabled>Add</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">
|
||||
@@ -413,6 +453,11 @@
|
||||
<!-- 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user