From cd4f0b91dc145473d4bd21baa5bc77ffa109b3fe Mon Sep 17 00:00:00 2001 From: Louis King Date: Mon, 8 Dec 2025 22:07:46 +0000 Subject: [PATCH] Various UI improvements --- src/meshcore_hub/web/static/js/utils.js | 63 ++++++++++++++ .../web/templates/advertisements.html | 86 ++++++------------- src/meshcore_hub/web/templates/base.html | 3 + src/meshcore_hub/web/templates/map.html | 42 +-------- src/meshcore_hub/web/templates/messages.html | 66 ++++---------- src/meshcore_hub/web/templates/nodes.html | 33 +++---- 6 files changed, 123 insertions(+), 170 deletions(-) create mode 100644 src/meshcore_hub/web/static/js/utils.js diff --git a/src/meshcore_hub/web/static/js/utils.js b/src/meshcore_hub/web/static/js/utils.js new file mode 100644 index 0000000..ec46f59 --- /dev/null +++ b/src/meshcore_hub/web/static/js/utils.js @@ -0,0 +1,63 @@ +/** + * MeshCore Hub - Common JavaScript Utilities + */ + +/** + * Format a timestamp as relative time (e.g., "2m", "1h", "2d") + * @param {string|Date} timestamp - ISO timestamp string or Date object + * @returns {string} Relative time string, or empty string if invalid + */ +function formatRelativeTime(timestamp) { + if (!timestamp) return ''; + + const date = timestamp instanceof Date ? timestamp : new Date(timestamp); + if (isNaN(date.getTime())) return ''; + + const now = new Date(); + const diffMs = now - date; + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHour = Math.floor(diffMin / 60); + const diffDay = Math.floor(diffHour / 24); + + if (diffDay > 0) return `${diffDay}d`; + if (diffHour > 0) return `${diffHour}h`; + if (diffMin > 0) return `${diffMin}m`; + return '<1m'; +} + +/** + * Populate all elements with data-timestamp attribute with relative time + */ +function populateRelativeTimestamps() { + document.querySelectorAll('[data-timestamp]:not([data-receiver-tooltip])').forEach(el => { + const timestamp = el.dataset.timestamp; + if (timestamp) { + el.textContent = formatRelativeTime(timestamp); + } + }); +} + +/** + * Populate receiver tooltip elements with name and relative time + */ +function populateReceiverTooltips() { + document.querySelectorAll('[data-receiver-tooltip]').forEach(el => { + const name = el.dataset.name || ''; + const timestamp = el.dataset.timestamp; + const relTime = timestamp ? formatRelativeTime(timestamp) : ''; + + // Build tooltip: "NodeName (2m ago)" or just "NodeName" or just "2m ago" + let tooltip = name; + if (relTime) { + tooltip = name ? `${name} (${relTime} ago)` : `${relTime} ago`; + } + el.title = tooltip; + }); +} + +// Auto-populate when DOM is ready +document.addEventListener('DOMContentLoaded', () => { + populateRelativeTimestamps(); + populateReceiverTooltips(); +}); diff --git a/src/meshcore_hub/web/templates/advertisements.html b/src/meshcore_hub/web/templates/advertisements.html index 650eed7..9d078fc 100644 --- a/src/meshcore_hub/web/templates/advertisements.html +++ b/src/meshcore_hub/web/templates/advertisements.html @@ -39,82 +39,46 @@ Node - Type - Received By Time + Receivers {% for ad in advertisements %} - - {% if ad.node_tag_name or ad.node_name or ad.name %} -
{{ ad.node_tag_name or ad.node_name or ad.name }}
-
{{ ad.public_key[:16] }}...
- {% else %} - {{ ad.public_key[:16] }}... - {% endif %} +
+ {% if ad.adv_type and ad.adv_type|lower == 'chat' %}💬{% elif ad.adv_type and ad.adv_type|lower == 'repeater' %}📡{% elif ad.adv_type and ad.adv_type|lower == 'room' %}🪧{% else %}📍{% endif %} +
+ {% if ad.node_tag_name or ad.node_name or ad.name %} +
{{ ad.node_tag_name or ad.node_name or ad.name }}
+
{{ ad.public_key[:16] }}...
+ {% else %} + {{ ad.public_key[:16] }}... + {% endif %} +
- - {% if ad.adv_type and ad.adv_type|lower == 'chat' %} - 💬 - {% elif ad.adv_type and ad.adv_type|lower == 'repeater' %} - 📡 - {% elif ad.adv_type and ad.adv_type|lower == 'room' %} - 🪧 - {% elif ad.adv_type %} - 📍 - {% else %} - - - {% endif %} - - - {% if ad.receivers and ad.receivers|length > 1 %} - - {% elif ad.receivers and ad.receivers|length == 1 %} - - {% if ad.receivers[0].tag_name or ad.receivers[0].name %} -
{{ ad.receivers[0].tag_name or ad.receivers[0].name }}
-
{{ ad.receivers[0].public_key[:16] }}...
- {% else %} - {{ ad.receivers[0].public_key[:16] }}... - {% endif %} -
- {% elif ad.received_by %} - - {% if ad.receiver_tag_name or ad.receiver_name %} -
{{ ad.receiver_tag_name or ad.receiver_name }}
-
{{ ad.received_by[:16] }}...
- {% else %} - {{ ad.received_by[:16] }}... - {% endif %} -
- {% else %} - - - {% endif %} - {{ ad.received_at[:19].replace('T', ' ') if ad.received_at else '-' }} + + {% if ad.receivers and ad.receivers|length >= 1 %} +
+ {% for recv in ad.receivers %} + 📡 + {% endfor %} +
+ {% elif ad.received_by %} + 📡 + {% else %} + - + {% endif %} + {% else %} - No advertisements found. + No advertisements found. {% endfor %} diff --git a/src/meshcore_hub/web/templates/base.html b/src/meshcore_hub/web/templates/base.html index 7650fba..3b6c66c 100644 --- a/src/meshcore_hub/web/templates/base.html +++ b/src/meshcore_hub/web/templates/base.html @@ -121,6 +121,9 @@ + + + {% block extra_scripts %}{% endblock %} diff --git a/src/meshcore_hub/web/templates/map.html b/src/meshcore_hub/web/templates/map.html index 5329e26..73d733f 100644 --- a/src/meshcore_hub/web/templates/map.html +++ b/src/meshcore_hub/web/templates/map.html @@ -52,12 +52,6 @@ -
- -
@@ -88,14 +82,10 @@ 📍 Other -
- 📡 - Infrastructure (gold glow) -
-

Nodes are placed on the map based on their lat and lon tags. Infrastructure nodes are tagged with role: infra.

+

Nodes are placed on the map based on their lat and lon tags.

{% endblock %} @@ -120,23 +110,7 @@ return type ? type.toLowerCase() : null; } - // Format relative time (e.g., "2m", "1h", "2d") - function formatRelativeTime(lastSeenStr) { - if (!lastSeenStr) return null; - - const lastSeen = new Date(lastSeenStr); - const now = new Date(); - const diffMs = now - lastSeen; - const diffSec = Math.floor(diffMs / 1000); - const diffMin = Math.floor(diffSec / 60); - const diffHour = Math.floor(diffMin / 60); - const diffDay = Math.floor(diffHour / 24); - - if (diffDay > 0) return `${diffDay}d`; - if (diffHour > 0) return `${diffHour}h`; - if (diffMin > 0) return `${diffMin}m`; - return '<1m'; - } + // formatRelativeTime is provided by /static/js/utils.js // Get emoji marker based on node type function getNodeEmoji(node) { @@ -159,14 +133,13 @@ // Create marker icon for a node function createNodeIcon(node) { const emoji = getNodeEmoji(node); - const infraGlow = node.is_infra ? 'filter: drop-shadow(0 0 4px gold);' : ''; const displayName = node.name || ''; const relativeTime = formatRelativeTime(node.last_seen); const timeDisplay = relativeTime ? ` (${relativeTime})` : ''; return L.divIcon({ className: 'custom-div-icon', html: `
- ${emoji} + ${emoji} ${displayName}${timeDisplay}
`, iconSize: [82, 28], @@ -186,8 +159,7 @@ let roleHtml = ''; if (node.role) { - const roleClass = node.is_infra ? 'badge-warning' : 'badge-ghost'; - roleHtml = `

Role: ${node.role}

`; + roleHtml = `

Role: ${node.role}

`; } const emoji = getNodeEmoji(node); @@ -219,16 +191,12 @@ function applyFiltersCore() { const typeFilter = document.getElementById('filter-type').value; const ownerFilter = document.getElementById('filter-owner').value; - const infraOnly = document.getElementById('filter-infra').checked; // Filter nodes const filteredNodes = allNodes.filter(node => { // Type filter (case-insensitive) if (typeFilter && normalizeType(node.adv_type) !== typeFilter) return false; - // Infrastructure filter - if (infraOnly && !node.is_infra) return false; - // Owner filter if (ownerFilter) { if (!node.owner || node.owner.public_key !== ownerFilter) return false; @@ -314,14 +282,12 @@ function clearFilters() { document.getElementById('filter-type').value = ''; document.getElementById('filter-owner').value = ''; - document.getElementById('filter-infra').checked = false; applyFilters(); } // Event listeners for filters document.getElementById('filter-type').addEventListener('change', applyFilters); document.getElementById('filter-owner').addEventListener('change', applyFilters); - document.getElementById('filter-infra').addEventListener('change', applyFilters); document.getElementById('clear-filters').addEventListener('click', clearFilters); // Fetch and display nodes diff --git a/src/meshcore_hub/web/templates/messages.html b/src/meshcore_hub/web/templates/messages.html index 008767f..04413e0 100644 --- a/src/meshcore_hub/web/templates/messages.html +++ b/src/meshcore_hub/web/templates/messages.html @@ -57,19 +57,15 @@ Time From/Channel Message - Receiver - SNR + SNR + Receivers {% for msg in messages %} - - {% if msg.message_type == 'channel' %} - Channel - {% else %} - Direct - {% endif %} + + {% if msg.message_type == 'channel' %}📻{% else %}👤{% endif %} {{ msg.received_at[:19].replace('T', ' ') if msg.received_at else '-' }} @@ -86,47 +82,6 @@ {% endif %} {{ msg.text or '-' }} - - {% if msg.receivers and msg.receivers|length > 1 %} - - {% elif msg.receivers and msg.receivers|length == 1 %} - - {% if msg.receivers[0].tag_name or msg.receivers[0].name %} -
{{ msg.receivers[0].tag_name or msg.receivers[0].name }}
-
{{ msg.receivers[0].public_key[:16] }}...
- {% else %} - {{ msg.receivers[0].public_key[:16] }}... - {% endif %} -
- {% elif msg.received_by %} - - {% if msg.receiver_tag_name or msg.receiver_name %} -
{{ msg.receiver_tag_name or msg.receiver_name }}
-
{{ msg.received_by[:16] }}...
- {% else %} - {{ msg.received_by[:16] }}... - {% endif %} -
- {% else %} - - - {% endif %} - {% if msg.snr is not none %} {{ "%.1f"|format(msg.snr) }} @@ -134,6 +89,19 @@ - {% endif %} + + {% if msg.receivers and msg.receivers|length >= 1 %} +
+ {% for recv in msg.receivers %} + 📡 + {% endfor %} +
+ {% elif msg.received_by %} + 📡 + {% else %} + - + {% endif %} + {% else %} diff --git a/src/meshcore_hub/web/templates/nodes.html b/src/meshcore_hub/web/templates/nodes.html index d4bbb77..3d44fb5 100644 --- a/src/meshcore_hub/web/templates/nodes.html +++ b/src/meshcore_hub/web/templates/nodes.html @@ -50,7 +50,6 @@ Node - Type Last Seen Tags @@ -65,28 +64,18 @@ {% endfor %} - - {% if ns.tag_name or node.name %} -
{{ ns.tag_name or node.name }}
-
{{ node.public_key[:16] }}...
- {% else %} - {{ node.public_key[:16] }}... - {% endif %} +
+ {% if node.adv_type and node.adv_type|lower == 'chat' %}💬{% elif node.adv_type and node.adv_type|lower == 'repeater' %}📡{% elif node.adv_type and node.adv_type|lower == 'room' %}🪧{% else %}📍{% endif %} +
+ {% if ns.tag_name or node.name %} +
{{ ns.tag_name or node.name }}
+
{{ node.public_key[:16] }}...
+ {% else %} + {{ node.public_key[:16] }}... + {% endif %} +
- - {% if node.adv_type and node.adv_type|lower == 'chat' %} - 💬 - {% elif node.adv_type and node.adv_type|lower == 'repeater' %} - 📡 - {% elif node.adv_type and node.adv_type|lower == 'room' %} - 🪧 - {% elif node.adv_type %} - 📍 - {% else %} - - - {% endif %} - {% if node.last_seen %} {{ node.last_seen[:19].replace('T', ' ') }} @@ -111,7 +100,7 @@ {% else %} - No nodes found. + No nodes found. {% endfor %}