feat: consistent, bug-free, higher-performance UI/UX #42
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/ui-ux-audit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What changed
Cross-cutting UI/UX consistency + performance pass, verified live in the browser and against the full suite.
Server / LiveView
File.readper icon render); caller class now merges into SVGs that already declare one; new<.page_container>gives uniform gutters + max-width; dark-safe flash palette; flash/menu z-index raised above Leaflet.aria-live"Showing N packets" throttled to 5 s (fix:System.monotonic_time(:second)can be negative, which meant the announced count never refreshed on some hosts).stable_packet_key— morphdom was destroying and recreating rows on every flush.PacketBatcherbounded to 5 000 senders + 500-entry prune margin;DisplayManager/index/subscriptionscleanups.page_containereverywhere; divider tokens standardized todivide-gray-200/dark:divide-white/10.import ... only:list gainspage_container: 1— a later limited import replaces the earlier full import, so it was actually missing.Frontend
data.popup.openPopup === true; hover-open debounced; historical eviction queue.tryExtendTrailappends geometry instead of rebuilding the polyline per packet.setPopupContentfor live text..aprs-mapfull-bleed under mobile nav.@sourceentries so Tailwind emits the dark-mode/component utilities the new classes need.adapter-wrapper,bundle-complete,combined,with-adapter,date-adapter-fixed,vendor-bundle);chart-bundle-srcaligned.Verified
mix test: 2286 passed ·mix credo --strict: clean ·mix dialyzer: clean/,/packets,/packets/:callsign,/packets/bad,/about,/status,/api,/info/:callsign,/weather/:callsign— zero JS errors; aria-live region announcing correctly.Built on main @
181e414f(includes the nix devshell fix).🤖 Skippy PR review
4 findings — 4 blocking before merge.
lib/aprsme_web/components/core_components.ex:37assets/js/features/trail_manager.ts:597assets/js/features/trail_manager.ts:236assets/js/map.ts:2081Reviewed
5b68791fd5d3. Commentskippy reviewto re-run.@ -231,0 +233,4 @@if (!isDuplicate &&trailState.trail &&this.tryExtendTrail(trailState)🟠 High — Extending an already-removed polyline permanently kills a station's trail
updateTrailVisualizationremoves the current polyline at the top and then returns without clearingtrailState.trailwhenever it decides not to redraw (positions.length < 2,totalDistance < this.minMovementThresholdat line 446, or no drawable segment). This fast path treats that non-nil field as "the trail is on the map": it appends to the detached polyline and returns, soscheduleTrailUpdatenever runs again for that callsign. Concretely, a station whose trail was drawn, then parked (or dropped to one retained position), and then moved, keeps extending a polyline that is no longer on the layer - the trail never reappears for the rest of the session. Either verify membership (this.trailLayer.hasLayer(trail)) or settrailState.trail = undefinedwhere the old line is removed.@ -549,0 +594,4 @@const unwrappedLng =last.lng + Math.round((end.lng - last.lng) / 360) * 360;trail.addLatLng([last.lat, unwrappedLng]);🟠 High — Trail fast path appends to the first segment, not the one it validated
The endpoint check reads the LAST ring (
latlngs[latlngs.length - 1]) but the append calls one-argumentaddLatLng/1, which Leaflet routes to the default shape -_defaultShape/0is_latlngs[0]once the polyline is multi-segment (addLatLng:function(t,e){return e=e||this._defaultShape(),...}in the vendoredassets/vendor/js/leaflet.js).updateTrailVisualizationbuildsL.polyline(drawableSegments, ...), i.e. one ring per >100 km hop, so on any trail containing a hop (a 0,0 or otherwise bad fix, an igate jump) the new point lands on the trail's FIRST segment while the geometry was validated against the last: the map draws a long line from the oldest segment's endpoint to the new position and the final segment stops growing. Pass the ring explicitly:trail.addLatLng([last.lat, unwrappedLng], lastSeg).@ -2072,0 +2078,4 @@// remove the OLDEST entries, not arbitrary Map-order ones. Stale queue// entries (already removed) are skipped cheaply at eviction time.if (data.historical && !data.is_most_recent_for_callsign) {(self.historicalEvictionQueue ||= []).push(data.id);🟡 Warning — Eviction queue is append-only and never pruned on marker removal
historicalEvictionQueuegrows monotonically: every historical add pushes an id, each re-add pushes the same id again, and no removal path (removeMarkerWithoutTrail, the expired-marker sweep, a viewport reload) deletes it. The array therefore tracks all historical adds ever seen rather than the markers actually held, which is the opposite of the bounded memory this change exists for, and the eviction loop paysqueue.shift()(O(n)) per removal exactly when the array is largest. Drop the id inremoveMarkerWithoutTrail, or keep a head offset and compact, so the queue mirrorsmarkers.@ -33,0 +34,4 @@# flash/error/title, so a File.read per call was disk I/O in the render path.icons =for style <- ~w(outline solid), into: %{} dodir = Path.join([File.cwd!(), "priv", "heroicons", "24", style])🟠 High — Icons are read at compile time from cwd, and the release image compiles before priv is copied
File.cwd!()runs when this module compiles, so@iconsis whateverpriv/heroiconshappened to exist at compile time. The production image has nothing there:Dockerfilecopiesconfigthenliband runsmix compile(lines 32-34) beforeCOPY priv priv(line 38), soPath.wildcard/1returns[],@iconssettles as%{"outline" => %{}, "solid" => %{}}, andmix releasedoes not recompile afterwards. CI tests run withprivpresent, so nothing goes red - every deploy just ships<!-- icon not found: ... -->for the flash info/error icon, the flash closex-mark, and the layout spinner (layouts/app.html.heex:60). Fix by reading the icons at runtime from:code.priv_dir(:aprsme)(memoize in:persistent_termif the per-render I/O is the concern), or at minimum moveCOPY priv privaboveRUN mix compile.🤖 Skippy PR review
1 finding — 1 blocking before merge.
assets/js/map.ts:2106Resolved 4 earlier findings fixed in
8ff6db3c: icons now memoize from Application.app_dir at runtime, the trail fast path checks hasLayer and appends to the validated ring, and the eviction queue is compacted past 20k entries.Reviewed
8ff6db3c6c1b. Commentskippy reviewto re-run.@ -2079,0 +2103,4 @@while (removed < 1000 && queue.length > 0 && self.markers.size > 9000) {const id = queue.shift()!;const state = self.markerStates.get(id);if (state?.historical && !state.is_most_recent_for_callsign) {🟡 Warning — Eviction only sees queued markers, but demoted live markers are eligible and never queued
This loop now takes ids solely from
historicalEvictionQueue, but the queue is only fed ataddMarkerfor markers that ARRIVE historical (data.historical && !data.is_most_recent_for_callsign). Markers that arrive live and are later demoted are flagged historical in place -existingState.historical = true; existingState.is_most_recent_for_callsign = falseat lines 1188-1189, 1294-1295 and 1916-1917 - and never pushed, so they satisfy this loop'sstate.historical && !state.is_most_recent_for_callsigntest yet are unreachable through the queue. Base main scannedmarkerStatesfor exactly that predicate, so the rewrite silently lost a class of evictable markers: on a map left open on a busy area (24 h trail window), every live packet demotes the previous marker per callsign,markers.sizeclimbs past 10 000, the loop drains the queued historical ids, and thenqueue.length === 0leavesself.markers.size > 9000while the cap does nothing for the rest of the session. Fix at the demotion sites: push the demotedidonto(self.historicalEvictionQueue ||= [])(the compaction at line 2097 dedupes), or fall back to amarkerStatesscan when the queue empties.Resolved 1 of 1 earlier findings — all three demotion sites (
new_packet, batchednew_packets, and theaddMarkermost-recent conversion) now push the demoted marker id ontohistoricalEvictionQueue, so the 10 000-marker cap can reach markers that arrived live and were demoted later.Nothing new in
8ff6db3c..d457f2e4.