From d5c3283b37cdb4fee6bd6dd39af0af850aae178b Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 14 Mar 2026 16:58:05 +0100 Subject: [PATCH] [fix](trx-frontend-http): restore map history window state Keep map history data cached when the history window is reduced so older APRS, AIS, VDES, FT8, and WSPR items can be shown again when the user expands the window, and add a global decode-history replay overlay with progress updates across the UI. Also update the longest QSO summary to render bidirectional contacts with <-> labels. Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js Verification: git diff --check -- src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js Co-authored-by: OpenAI Codex Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/app.js | 295 ++++++++++++------ .../trx-frontend-http/assets/web/index.html | 6 + .../trx-frontend-http/assets/web/style.css | 40 +++ 3 files changed, 253 insertions(+), 88 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 445fed6..962581e 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -58,6 +58,7 @@ async function authLogout() { authRole = null; // Disconnect and show auth gate without page reload disconnect(); + setDecodeHistoryOverlayVisible(false); document.getElementById("content").style.display = "none"; document.getElementById("loading").style.display = "none"; document.getElementById("auth-passphrase").value = ""; @@ -75,6 +76,7 @@ async function authLogout() { function showAuthGate(allowGuest = false) { if (!authEnabled) return; + setDecodeHistoryOverlayVisible(false); document.getElementById("loading").style.display = "none"; document.getElementById("content").style.display = "none"; const authGate = document.getElementById("auth-gate"); @@ -324,6 +326,9 @@ const rigSubtitle = document.getElementById("rig-subtitle"); const ownerSubtitle = document.getElementById("owner-subtitle"); const loadingTitle = document.getElementById("loading-title"); const loadingSub = document.getElementById("loading-sub"); +const decodeHistoryOverlayEl = document.getElementById("decode-history-overlay"); +const decodeHistoryOverlayTitleEl = document.getElementById("decode-history-overlay-title"); +const decodeHistoryOverlaySubEl = document.getElementById("decode-history-overlay-sub"); const overviewCanvas = document.getElementById("overview-canvas"); const signalOverlayCanvas = document.getElementById("signal-overlay-canvas"); const overviewGl = typeof createTrxWebGlRenderer === "function" @@ -368,6 +373,12 @@ function syncTopBarAccess() { } let overviewDrawPending = false; +function setDecodeHistoryOverlayVisible(visible, title = "", sub = "") { + if (!decodeHistoryOverlayEl) return; + if (title && decodeHistoryOverlayTitleEl) decodeHistoryOverlayTitleEl.textContent = title; + if (decodeHistoryOverlaySubEl) decodeHistoryOverlaySubEl.textContent = sub || ""; + decodeHistoryOverlayEl.classList.toggle("is-hidden", !visible); +} let lastSpectrumData = null; window.lastSpectrumData = null; let lastControl; @@ -2941,6 +2952,7 @@ function disconnect() { clearTimeout(reconnectTimer); reconnectTimer = null; } + setDecodeHistoryOverlayVisible(false); } // Yield the main thread so the browser can paint before heavy async work. @@ -3919,9 +3931,72 @@ function removeMapMarker(marker) { mapMarkers.delete(marker); } +function setRetainedMapMarkerVisible(marker, visible) { + if (!marker) return; + marker.__trxHistoryVisible = visible !== false; + if (!visible) { + if (marker === selectedLocatorMarker) { + setSelectedLocatorMarker(null); + clearMapRadioPath(); + } + if (aprsMap && aprsMap.hasLayer(marker)) marker.removeFrom(aprsMap); + } +} + +function ensureAprsMarker(call, entry) { + if (!aprsMap || !entry || entry.marker || entry.lat == null || entry.lon == null) return; + _aprsAddMarkerToMap(call, entry); +} + +function ensureAisMarker(key, entry) { + if (!aprsMap || !entry || entry.marker || entry?.msg?.lat == null || entry?.msg?.lon == null) return; + const marker = createAisMarker(entry.msg.lat, entry.msg.lon, entry.msg) + .addTo(aprsMap) + .bindPopup(buildAisPopupHtml(entry.msg)); + marker.__trxType = "ais"; + marker._aisMmsi = String(key); + entry.marker = marker; + mapMarkers.add(marker); +} + +function ensureVdesMarker(key, entry) { + if (!aprsMap || !entry || entry.marker || entry?.msg?.lat == null || entry?.msg?.lon == null) return; + const marker = L.circleMarker([entry.msg.lat, entry.msg.lon], { + radius: 5, + color: "#5c394f", + fillColor: "#c46392", + fillOpacity: 0.82, + }).addTo(aprsMap).bindPopup(buildVdesPopupHtml(entry.msg)); + marker.__trxType = "vdes"; + marker._vdesKey = String(key); + entry.marker = marker; + mapMarkers.add(marker); +} + +function ensureDecodeLocatorMarker(entry) { + if (!aprsMap || !entry || entry.marker || !entry.grid || (entry.sourceType !== "ft8" && entry.sourceType !== "wspr")) return; + const bounds = maidenheadToBounds(entry.grid); + if (!bounds) return; + const count = Math.max(entry.stationDetails?.size || 0, entry.stations?.size || 0, 1); + const tooltipHtml = buildDecodeLocatorTooltipHtml(entry.grid, entry, entry.sourceType); + const marker = L.rectangle(bounds, locatorStyleForEntry(entry, count)) + .addTo(aprsMap) + .bindPopup(tooltipHtml); + marker.__trxType = entry.sourceType; + sendLocatorOverlayToBack(marker); + assignLocatorMarkerMeta(marker, entry.sourceType, entry.bandMeta); + entry.marker = marker; + mapMarkers.add(marker); +} + function pruneAprsEntry(call, entry, cutoffMs) { const pktTsMs = Number(entry?.pkt?._tsMs); - if (!Number.isFinite(pktTsMs) || pktTsMs < cutoffMs) { + const visible = Number.isFinite(pktTsMs) && pktTsMs >= cutoffMs; + entry.visibleInHistoryWindow = visible; + entry.trackPoints = trimTrackHistory(entry.trackHistory, cutoffMs, APRS_TRACK_MAX_POINTS) + .map((point) => [point.lat, point.lon]); + refreshAprsTrack(call, entry); + if (!visible) { if (selectedAprsTrackCall && String(selectedAprsTrackCall) === String(call)) { selectedAprsTrackCall = null; } @@ -3929,19 +4004,26 @@ function pruneAprsEntry(call, entry, cutoffMs) { entry.track.remove(); entry.track = null; } - removeMapMarker(entry?.marker); - stationMarkers.delete(call); + setRetainedMapMarkerVisible(entry?.marker, false); return false; } - entry.trackHistory = trimTrackHistory(entry.trackHistory, cutoffMs, APRS_TRACK_MAX_POINTS); - entry.trackPoints = entry.trackHistory.map((point) => [point.lat, point.lon]); - refreshAprsTrack(call, entry); + ensureAprsMarker(call, entry); + setRetainedMapMarkerVisible(entry?.marker, true); + if (entry?.marker) { + entry.marker.setLatLng([entry.lat, entry.lon]); + entry.marker.setPopupContent(buildAprsPopupHtml(call, entry.lat, entry.lon, entry.info || "", entry.pkt)); + } return true; } function pruneAisEntry(key, entry, cutoffMs) { const msgTsMs = Number(entry?.msg?._tsMs); - if (!Number.isFinite(msgTsMs) || msgTsMs < cutoffMs) { + const visible = Number.isFinite(msgTsMs) && msgTsMs >= cutoffMs; + entry.visibleInHistoryWindow = visible; + entry.trackPoints = trimTrackHistory(entry.trackHistory, cutoffMs, AIS_TRACK_MAX_POINTS) + .map((point) => [point.lat, point.lon]); + refreshAisTrack(key, entry); + if (!visible) { if (selectedAisTrackMmsi && String(selectedAisTrackMmsi) === String(key)) { selectedAisTrackMmsi = null; } @@ -3949,28 +4031,37 @@ function pruneAisEntry(key, entry, cutoffMs) { entry.track.remove(); entry.track = null; } - removeMapMarker(entry?.marker); - aisMarkers.delete(key); + setRetainedMapMarkerVisible(entry?.marker, false); return false; } - entry.trackHistory = trimTrackHistory(entry.trackHistory, cutoffMs, AIS_TRACK_MAX_POINTS); - entry.trackPoints = entry.trackHistory.map((point) => [point.lat, point.lon]); - refreshAisTrack(key, entry); + ensureAisMarker(key, entry); + setRetainedMapMarkerVisible(entry?.marker, true); + if (entry?.marker) { + updateAisMarker(entry.marker, entry.msg, buildAisPopupHtml(entry.msg)); + } return true; } function pruneLocatorEntry(key, entry, cutoffMs) { if (!entry || (entry.sourceType !== "ft8" && entry.sourceType !== "wspr")) return true; + if (!(entry.allStationDetails instanceof Map)) { + entry.allStationDetails = entry.stationDetails instanceof Map + ? new Map(entry.stationDetails) + : new Map(); + } const nextDetails = new Map(); - for (const [detailKey, detail] of entry.stationDetails instanceof Map ? entry.stationDetails.entries() : []) { + for (const [detailKey, detail] of entry.allStationDetails.entries()) { const tsMs = Number(detail?.ts_ms); if (Number.isFinite(tsMs) && tsMs >= cutoffMs) { nextDetails.set(detailKey, detail); } } + entry.visibleInHistoryWindow = nextDetails.size > 0; if (nextDetails.size === 0) { - removeMapMarker(entry.marker); - locatorMarkers.delete(key); + entry.stationDetails = new Map(); + entry.stations = new Set(); + entry.bandMeta = new Map(); + setRetainedMapMarkerVisible(entry.marker, false); return false; } const nextStations = new Set(); @@ -3984,6 +4075,8 @@ function pruneLocatorEntry(key, entry, cutoffMs) { Array.from(nextDetails.values()).map((detail) => Number(detail?.freq_hz)) ); const count = Math.max(nextDetails.size, nextStations.size || 0, 1); + ensureDecodeLocatorMarker(entry); + setRetainedMapMarkerVisible(entry.marker, true); if (entry.marker) { entry.marker.setStyle(locatorStyleForEntry(entry, count)); entry.marker.setPopupContent(buildDecodeLocatorTooltipHtml(entry.grid, entry, entry.sourceType)); @@ -3994,19 +4087,28 @@ function pruneLocatorEntry(key, entry, cutoffMs) { function pruneMapHistory() { const cutoffMs = mapHistoryCutoffMs(); - for (const [call, entry] of Array.from(stationMarkers.entries())) { + for (const [call, entry] of stationMarkers.entries()) { pruneAprsEntry(call, entry, cutoffMs); } - for (const [key, entry] of Array.from(aisMarkers.entries())) { + for (const [key, entry] of aisMarkers.entries()) { pruneAisEntry(key, entry, cutoffMs); } - for (const [key, entry] of Array.from(vdesMarkers.entries())) { + for (const [key, entry] of vdesMarkers.entries()) { const tsMs = Number(entry?.msg?._tsMs); - if (Number.isFinite(tsMs) && tsMs >= cutoffMs) continue; - removeMapMarker(entry?.marker); - vdesMarkers.delete(key); + const visible = Number.isFinite(tsMs) && tsMs >= cutoffMs; + entry.visibleInHistoryWindow = visible; + if (!visible) { + setRetainedMapMarkerVisible(entry?.marker, false); + continue; + } + ensureVdesMarker(key, entry); + setRetainedMapMarkerVisible(entry?.marker, true); + if (entry?.marker) { + entry.marker.setLatLng([entry.msg.lat, entry.msg.lon]); + entry.marker.setPopupContent(buildVdesPopupHtml(entry.msg)); + } } - for (const [key, entry] of Array.from(locatorMarkers.entries())) { + for (const [key, entry] of locatorMarkers.entries()) { pruneLocatorEntry(key, entry, cutoffMs); } rebuildDecodeContactPaths(); @@ -4566,10 +4668,20 @@ function rebuildMapLocatorFilters() { const choiceLabelEl = document.getElementById("map-locator-choice-label"); const availableSources = new Set(); - if (aisMarkers.size > 0) availableSources.add("ais"); - if (vdesMarkers.size > 0) availableSources.add("vdes"); + for (const entry of aisMarkers.values()) { + if (entry?.visibleInHistoryWindow) { + availableSources.add("ais"); + break; + } + } + for (const entry of vdesMarkers.values()) { + if (entry?.visibleInHistoryWindow) { + availableSources.add("vdes"); + break; + } + } for (const entry of stationMarkers.values()) { - if (entry?.type === "aprs" && (entry.marker || (entry.lat != null && entry.lon != null))) { + if (entry?.type === "aprs" && entry?.visibleInHistoryWindow) { availableSources.add("aprs"); break; } @@ -4578,6 +4690,7 @@ function rebuildMapLocatorFilters() { for (const entry of locatorMarkers.values()) { const sourceType = entry?.sourceType; if (!sourceType) continue; + if ((sourceType === "ft8" || sourceType === "wspr") && !entry?.visibleInHistoryWindow) continue; availableSources.add(sourceType); const meta = entry?.bandMeta instanceof Map ? entry.bandMeta : new Map(); for (const [label, hz] of meta.entries()) { @@ -5050,8 +5163,8 @@ function initAprsMap() { // Materialise any stations that were buffered before the map was ready for (const [call, entry] of stationMarkers) { - if (entry.type === "aprs" && !entry.marker && entry.lat != null && entry.lon != null) { - _aprsAddMarkerToMap(call, entry); + if (entry.type === "aprs" && entry.visibleInHistoryWindow) { + ensureAprsMarker(call, entry); } } for (const [key, entry] of locatorMarkers) { @@ -5614,10 +5727,8 @@ window.aprsMapAddStation = function(call, lat, lon, info, symbolTable, symbolCod }; stationMarkers.set(call, entry); pruneAprsEntry(call, entry, mapHistoryCutoffMs()); - if (aprsMap) { - _aprsAddMarkerToMap(call, entry); - scheduleDecodeMapMaintenance(); - } + if (entry.visibleInHistoryWindow) ensureAprsMarker(call, entry); + if (aprsMap) scheduleDecodeMapMaintenance(); } }; @@ -5724,32 +5835,36 @@ window.aisMapAddVessel = function(msg) { } return; } - if (!aprsMap) return; - const marker = createAisMarker(msg.lat, msg.lon, msg).addTo(aprsMap).bindPopup(popupHtml); - marker.__trxType = "ais"; - marker._aisMmsi = key; - mapMarkers.add(marker); aisMarkers.set(key, { - marker, + marker: null, track: null, trackHistory: [{ lat: msg.lat, lon: msg.lon, tsMs }], trackPoints: [nextPoint], msg, }); pruneAisEntry(key, aisMarkers.get(key), mapHistoryCutoffMs()); + if (aisMarkers.get(key)?.visibleInHistoryWindow) ensureAisMarker(key, aisMarkers.get(key)); scheduleDecodeMapMaintenance(); }; window.vdesMapAddPoint = function(msg) { if (msg == null || msg.lat == null || msg.lon == null) return; - if (Number(msg?._tsMs) < mapHistoryCutoffMs()) return; const key = vdesMarkerKey(msg); if (!key) return; if (!aprsMap) initAprsMap(); const popupHtml = buildVdesPopupHtml(msg); + const visible = Number.isFinite(Number(msg?._tsMs)) + && Number(msg._tsMs) >= mapHistoryCutoffMs(); const existing = vdesMarkers.get(key); if (existing) { existing.msg = msg; + existing.visibleInHistoryWindow = visible; + if (!visible) { + setRetainedMapMarkerVisible(existing.marker, false); + return; + } + ensureVdesMarker(key, existing); + setRetainedMapMarkerVisible(existing.marker, true); if (existing.marker) { existing.marker.setLatLng([msg.lat, msg.lon]); existing.marker.setPopupContent(popupHtml); @@ -5759,23 +5874,15 @@ window.vdesMapAddPoint = function(msg) { const entry = { marker: null, msg, + visibleInHistoryWindow: visible, }; vdesMarkers.set(key, entry); - if (Number(msg._tsMs) < mapHistoryCutoffMs()) { - vdesMarkers.delete(key); - return; + if (!visible) return; + ensureVdesMarker(key, entry); + setRetainedMapMarkerVisible(entry.marker, true); + if (entry.marker) { + entry.marker.setPopupContent(popupHtml); } - if (!aprsMap) return; - const marker = L.circleMarker([msg.lat, msg.lon], { - radius: 5, - color: "#5c394f", - fillColor: "#c46392", - fillOpacity: 0.82, - }).addTo(aprsMap).bindPopup(popupHtml); - marker.__trxType = "vdes"; - marker._vdesKey = key; - entry.marker = marker; - mapMarkers.add(marker); scheduleDecodeMapMaintenance(); }; @@ -5812,7 +5919,10 @@ function applyMapFilter() { if (!aprsMap) return; mapMarkers.forEach((marker) => { const type = marker.__trxType; - const visible = markerPassesSearchFilter(marker) && markerPassesLocatorFilters(marker) && ( + const visible = marker.__trxHistoryVisible !== false + && markerPassesSearchFilter(marker) + && markerPassesLocatorFilters(marker) + && ( (type === "bookmark" && mapFilter.bookmark) || (type === "ais" && mapFilter.ais) || (type === "vdes" && mapFilter.vdes) || @@ -6028,7 +6138,7 @@ function renderMapQsoSummary() { const pair = document.createElement("div"); pair.className = "map-qso-card-pair"; - pair.textContent = `${entry.source || "Unknown"} -> ${entry.target || "Unknown"}`; + pair.textContent = `${entry.source || "Unknown"} <-> ${entry.target || "Unknown"}`; body.appendChild(pair); const meta = document.createElement("div"); @@ -6059,7 +6169,7 @@ function renderMapQsoSummary() { const grids = document.createElement("div"); grids.className = "map-qso-card-grids"; - grids.textContent = `${entry.sourceGrid || "--"} -> ${entry.targetGrid || "--"}`; + grids.textContent = `${entry.sourceGrid || "--"} <-> ${entry.targetGrid || "--"}`; body.appendChild(grids); card.appendChild(head); @@ -6197,46 +6307,38 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null, freq_hz: Number.isFinite(details?.freq_hz) ? Number(details.freq_hz) : null, message: String(details?.message || message || "").trim() || null, }; - if (Number(detailEntry.ts_ms) < mapHistoryCutoffMs()) continue; const detailKey = detailStationId || `${targetId || "decode"}:${detailEntry.message || "decode"}:${detailEntry.ts_ms || Date.now()}`; const key = `${markerType}:${grid}`; const existing = locatorMarkers.get(key); if (existing) { existing.grid = grid; - if (detailStationId) existing.stations.add(detailStationId); - if (!(existing.stationDetails instanceof Map)) existing.stationDetails = new Map(); - existing.stationDetails.set(detailKey, { ...detailEntry }); + if (!(existing.allStationDetails instanceof Map)) { + existing.allStationDetails = existing.stationDetails instanceof Map + ? new Map(existing.stationDetails) + : new Map(); + } + existing.allStationDetails.set(detailKey, { ...detailEntry }); existing.sourceType = markerType; - existing.bandMeta = collectBandMeta( - Array.from(existing.stationDetails.values()).map((detail) => Number(detail?.freq_hz)) - ); - const count = Math.max(existing.stationDetails.size, existing.stations.size || 0, 1); - const tooltipHtml = buildDecodeLocatorTooltipHtml(grid, existing, markerType); - existing.marker.setStyle(locatorStyleForEntry(existing, count)); - existing.marker.setPopupContent(tooltipHtml); - sendLocatorOverlayToBack(existing.marker); - assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta); + pruneLocatorEntry(key, existing, mapHistoryCutoffMs()); + if (existing.marker) sendLocatorOverlayToBack(existing.marker); scheduleDecodeMapMaintenance(); continue; } - const stations = new Set(); - if (detailStationId) stations.add(detailStationId); - const stationDetails = new Map(); - stationDetails.set(detailKey, { ...detailEntry }); - const bandMeta = collectBandMeta( - Array.from(stationDetails.values()).map((detail) => Number(detail?.freq_hz)) - ); - const count = Math.max(stationDetails.size, stations.size || 0, 1); - const tooltipHtml = buildDecodeLocatorTooltipHtml(grid, { stations, stationDetails }, markerType); - const marker = L.rectangle(bounds, locatorStyleForEntry({ sourceType: markerType, bandMeta }, count)) - .addTo(aprsMap) - .bindPopup(tooltipHtml); - marker.__trxType = markerType; - sendLocatorOverlayToBack(marker); - assignLocatorMarkerMeta(marker, markerType, bandMeta); - locatorMarkers.set(key, { marker, grid, stations, stationDetails, sourceType: markerType, bandMeta }); - mapMarkers.add(marker); + const allStationDetails = new Map(); + allStationDetails.set(detailKey, { ...detailEntry }); + const entry = { + marker: null, + grid, + stations: new Set(), + stationDetails: new Map(), + allStationDetails, + sourceType: markerType, + bandMeta: new Map(), + }; + locatorMarkers.set(key, entry); + pruneLocatorEntry(key, entry, mapHistoryCutoffMs()); + if (entry.marker) sendLocatorOverlayToBack(entry.marker); } scheduleDecodeMapMaintenance(); }; @@ -7003,7 +7105,7 @@ function scheduleDecodeHistoryDrainStep(callback) { } } -function drainDecodeHistory(buffer, index, onDone) { +function drainDecodeHistory(buffer, index, onDone, onProgress) { const startedAt = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : 0; @@ -7014,8 +7116,11 @@ function drainDecodeHistory(buffer, index, onDone) { if (nextIndex - index >= DECODE_HISTORY_MAX_BATCH) break; if (startedAt > 0 && (performance.now() - startedAt) >= DECODE_HISTORY_SLICE_BUDGET_MS) break; } + if (typeof onProgress === "function") { + onProgress(nextIndex, buffer.length); + } if (nextIndex < buffer.length) { - scheduleDecodeHistoryDrainStep(() => drainDecodeHistory(buffer, nextIndex, onDone)); + scheduleDecodeHistoryDrainStep(() => drainDecodeHistory(buffer, nextIndex, onDone, onProgress)); } else if (typeof onDone === "function") { onDone(); } @@ -7036,6 +7141,7 @@ function connectDecode() { const liveBuffer = []; function flushLiveBuffer() { historySettled = true; + setDecodeHistoryOverlayVisible(false); for (const msg of liveBuffer) { try { dispatchDecodeMessage(msg); } catch (_) {} } @@ -7043,6 +7149,7 @@ function connectDecode() { } // Safety valve: if the history fetch hangs, unblock after 8 s. const historyTimeout = setTimeout(() => { if (!historySettled) flushLiveBuffer(); }, 8000); + setDecodeHistoryOverlayVisible(true, "Loading decode history…", "Fetching recent decodes from the client buffer"); decodeSource = new EventSource("/decode"); decodeSource.onopen = () => { @@ -7077,7 +7184,19 @@ function connectDecode() { }).then((msgs) => { clearTimeout(historyTimeout); if (Array.isArray(msgs) && msgs.length > 0) { - drainDecodeHistory(msgs, 0, flushLiveBuffer); + setDecodeHistoryOverlayVisible(true, "Loading decode history…", `Replaying 0 / ${msgs.length} decoded messages`); + drainDecodeHistory( + msgs, + 0, + flushLiveBuffer, + (processed, total) => { + setDecodeHistoryOverlayVisible( + true, + "Loading decode history…", + `Replaying ${processed} / ${total} decoded messages` + ); + } + ); } else { flushLiveBuffer(); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index db693a5..e429986 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -897,6 +897,12 @@
Connecting…
+ diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 123a26e..4608ca9 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -1223,6 +1223,46 @@ small { color: var(--text-muted); } .sub-tab { flex-shrink: 0; background: transparent; border: none; border-bottom: 2px solid transparent; border-radius: 0; padding: 0.35rem 0.75rem; color: var(--text-muted); cursor: pointer; font-size: 0.85rem; height: auto; } .sub-tab.active { border-bottom-color: var(--accent-green); color: var(--accent-green); font-weight: 600; } .sub-tab:hover:not(.active) { color: var(--text); } +.decode-history-overlay { + position: fixed; + inset: 0; + z-index: 9500; + display: flex; + align-items: center; + justify-content: center; + padding: 1.2rem; + background: color-mix(in srgb, var(--bg) 36%, transparent); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + pointer-events: none; + opacity: 1; + visibility: visible; + transition: opacity 140ms ease, visibility 140ms ease; +} +.decode-history-overlay.is-hidden { + opacity: 0; + visibility: hidden; +} +.decode-history-overlay-card { + min-width: min(26rem, calc(100vw - 2.4rem)); + max-width: min(30rem, calc(100vw - 2.4rem)); + padding: 0.9rem 1rem; + border-radius: 0.9rem; + border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent); + background: color-mix(in srgb, var(--card-bg) 88%, transparent); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.22); + text-align: center; +} +.decode-history-overlay-title { + font-size: 0.98rem; + font-weight: 800; + color: var(--text-heading); +} +.decode-history-overlay-sub { + margin-top: 0.24rem; + font-size: 0.82rem; + color: var(--text-muted); +} #tab-map { display: flex; flex-direction: column;