Commit graph

190 commits

Author SHA1 Message Date
cc88fd4a10
fix: dialyzer errors, pubsub failure handling, cache TTL, stream trim perf, and clearer warnings
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
2026-07-26 16:19:42 -05:00
e1da1716b1
refactor: split MapLive.Index into focused modules
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
- State (177 lines): mount assigns, defaults, slideover, connection status
- Events (540 lines): 22 handle_event handlers, rate limiting, URL helpers
- Subscriptions (122 lines): PubSub setup, spatial registration, cleanup
- BoundsUpdater (172 lines): bounds pipeline, validation, debouncing
- index.ex: 2,062 -> 1,202 lines (42% reduction)
- Credo: initial_historical_completed, bounds_update_timer added to ignored_assigns
  (consumed by extracted modules, invisible to credo)
- All 361 map_live tests pass across multiple random seeds
- Handoff: marked maintainability #1 as done
2026-07-26 15:30:08 -05:00
0f2195ef9d
security: CSP nonces, RemoteIp CIDR gating, rate limiting, NetworkPolicies, deadlock fixes
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
- Content-Security-Policy: nonce-based per-request plug replacing unsafe-inline scripts
- RemoteIp: CIDR-based trust gating via InetCidr, skips forwarded headers from untrusted peers
- Rate limiting: auth pipeline (20/min), LiveView event handlers, existing mobile channel limits
- NetworkPolicy: 4 k8s policies (web ingress, cluster, metrics, egress) for least-privilege networking
- PartitionManager: deadlock retry with exponential backoff in drop_partition
- Tests: reduced parallelism (max_cases 4), packets_test async:false to prevent trigger contention
- k8s: APRS_PASSWORD -> APRS_PASSCODE secretRef, vendor/aprs submodule hardened
2026-07-26 14:04:56 -05:00
b0832e5a9c
refactor: harden packet delivery and operations
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
2026-07-26 13:11:26 -05:00
101dcf4005
bugfixes
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
2026-07-25 15:39:46 -05:00
Graham McInitre
1006fdaefc refactor: remove dead code, duplication, and over-abstractions
- Delete dead config keys and test file (typo'd aprsme_is_*, vacuous disable test)
- Remove duplicate function clauses and identical function pairs
- Rename misleading one_hour_ago variable to one_day_ago
- Strip stale Oban comment
- Remove TestHelpers time function duplicates
- Inline Aprsme.Schema module (only 1 caller)
- Deduplicate prod esbuild config
- Thin SymbolRenderer delegations, inline TimeUtils wrappers
- Remove redundant DeploymentNotifier GenServer polling
- Replace random fallback in get_callsign_key with sentinel
2026-07-21 10:36:49 -05:00
77d2a6ca30
updat edeps 2026-07-17 16:42:53 -05:00
01ecf3d6bc
refactor: apply functional programming patterns across codebase
- accounts.ex: replace validate_user_password (if-based) with with-chain;
  drop unnecessary try/rescue around Repo.get_by. Extract transaction_unwrap
  helper to eliminate 3 copies of the Multi result-unwrap case block.

- packets.ex: deduplicate store_bad_packet by extracting raw_packet_string,
  extract_error_type, and extract_error_message into composable helpers.
  Normalize find_coordinate_value — collapse 12 clauses into a generic
  deep_get/direct_get that handles atom/string keys and nested maps uniformly.

- data_builder.ex: split build_simple_popup into separate data-construction
  (build_simple_popup_data) and rendering (render_popup) phases, eliminating
  duplicated PopupComponent/Safe/IO pipelines.

- param_utils.ex: unify parse_float_in_range and parse_int_in_range via a
  shared parse_in_range that accepts Float.parse/Integer.parse as a function
  parameter — functions-as-values pattern.
2026-07-01 08:45:19 -05:00
b86153cd27
Fix all mix credo --strict warnings (188 → 0)
- Replace apply/2 with direct fully-qualified calls in movement_test
- Fix assert_receive timeouts < 1000ms across 8 test files
- Move nested import statements to module-level scope
- Fix tests with no assertions and add missing doctest
- Replace weak type assertions with specific value checks
- Fix conditional assertions and length/1 expensive patterns
- Disable inappropriate Jump.CredoChecks.AvoidSocketAssignsInTest
- Fix tests not calling application code with credo:disable
- Add various credo:disable comments for legitimate patterns
2026-06-12 16:27:20 -05:00
7f814462ed
perf: reduce traversals, optimize packet field lookups, use has_weather boolean
Remove redundant in-memory sort (DB already orders), shortcut get_packet_field when field found at top level, use pre-computed has_weather boolean instead of field-by-field scan

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-01 16:49:28 -05:00
e02fafc8da
perf: optimize map LiveView hot paths
Remove unused 30s timer, skip heatmap recluster on every single packet, trail dedup O(n^2)->O(n)

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-01 16:49:08 -05:00
ae1194ebb0
perf: fix PacketBatcher O(n) length/1 to O(1) counter
Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-01 16:48:48 -05:00
d0b9513b41
fix elixir type warnings 2026-05-29 14:00:29 -05:00
001af22286
Remove dead code: round_coordinate non-float heads, unreachable get_nearby_stations branch, redundant pattern-match guards 2026-05-08 17:49:23 -05:00
1e32ce7051
Improve test coverage to 87% and point vendor/aprs submodule at codeberg
Coverage:
- Adds focused tests across 13 files covering previously-uncovered
  branches: Packet changeset normalisation (course, wind_direction,
  struct data_extended); Packets store_packet edge cases (nested
  position shapes, ssid handling, string-keyed raw_packet);
  DeviceIdentification HTTP success/404 via Req plug stub; ResendAdapter
  catch-all body, deliver_many, nil/binary formatters; ApiDocsLive
  packets with missing fields; AprsSymbol normalize helpers;
  BadPacketsLive refresh and postgres_notify; MobileChannel handle_info
  catch-all and postgres_packet path; PacketProcessor existing-marker
  movement detection; PacketReplay sanitize_value type clauses;
  InsertOptimizer GenServer lifecycle and negative-duration metric;
  PageController shutdown-handler integration; UrlParams delegated
  helpers.
- mix.exs: configure test_coverage with summary threshold of 87 and
  ignore_modules for test fixtures and macro-only template modules.

Bug fix:
- MapLive.Index handle_info: add a clause for the raw {:new_deployment,
  _} tuple. Phoenix.PubSub.broadcast delivers the raw payload to plain
  subscribers, but the existing handler only matched the %Broadcast{}
  fast-lane wrapper. DeploymentNotifier broadcasts via the raw path,
  which crashed the LiveView under test concurrency.

Submodule:
- Update .gitmodules vendor/aprs URL from
  https://github.com/aprsme/aprs.git to
  ssh://git@codeberg.org/gmcintire/aprs.git.
2026-05-05 15:18:28 -05:00
3f2e0252d5
refactor: dedupe utilities, prefer pattern matching, tighten specs
- consolidate Haversine: CoordinateUtils delegates to GeoUtils;
  InfoLive.haversine/4 reuses it (m -> km)
- AprsSymbol: collapse 3 sprite-info builders onto build_sprite_info/2
  and add a sprite_info @type
- DataBuilder: 3 weather unit converters share convert_unit/4 with
  explicit clauses for "N/A", numeric, and binary inputs
- mobile_channel: do_callsign_match/2 uses pattern-match clauses for
  wildcard handling instead of nested if
- info_live: replace nested case in position_changed? with multi-clause
  coords_changed?/1; format_timestamp_for_display pipelines into a
  guarded build_timestamp_display/1
- api_docs_live: collapse chained single-key assigns into keyword-list
  assign/2 calls
- add strict @spec coverage on all touched helpers; tighten new specs to
  match dialyzer success typing (no contract_supertype warnings)
2026-04-28 14:18:58 -05:00
d879b54fd4
refactor: replace more cond expressions with guarded clauses
- CoordinateUtils.normalize_longitude / normalize_latitude: recursion
  and clamping move into the function head via numeric guards.
- Shared.PacketUtils.map_label: dispatch on data_type + fallback via
  multi-clause helpers (map_label_for/2, prefer_name/2) instead of a
  three-branch cond with inner `if`s.
- MapLive.DataBuilder.get_received_at: pattern-match the atom- and
  string-key packet shapes in the function head.

No behavior change; all 1567 tests and 27 property checks still pass,
dialyzer remains clean.
2026-04-23 14:12:45 -05:00
e43107a25f
refactor(packet_processor): split cond/if into multi-clause helpers
- render_for_zoom/3: zoom ≤ 8 uses heatmap; high zoom with marker_data
  pushes to the client; high zoom + nil marker_data is a no-op. Each
  branch is now a separate function head.
- push_new_packet/3: a boolean-dispatched pair replaces the popup-open
  conditional on the push_event payload.
- prepare_packet_state: the two rebinding `if`s turn into a pipeline
  through prune_if_over_cap/1 (pattern-matched on map_size/1) and
  build_marker_data/5 with a zoom ≤ 8 short-circuit clause, plus
  maybe_tag_convert_from/3 for the replacement-marker tag.
- moved_significantly?/3: the duplicated guard check between
  dispatch_visibility and batch_dispatch collapses into one helper that
  pattern-matches on the coordinate tuple returned by CoordinateUtils.

Adds extra tests covering the new significance / stale-coords branches
and the connection card LiveComponent render paths. Coverage 65.89 → 66.41%.
2026-04-23 13:51:17 -05:00
c5a82b77c1
refactor: pattern-match over conditionals in Navigation and InsertOptimizer
Swaps `if`/`cond` branches for multi-clause function heads or head-pattern
matching, making the control flow visible in the function signatures:

- Navigation.determine_map_location: resolve_location/4 now dispatches on
  the shape of the geolocation input and whether URL params are explicit.
- Navigation.handle_callsign_tracking: per-case clauses for empty callsign
  and explicit-URL-params short-circuits, plus a final clause that does the
  DB lookup and rescues once.
- Navigation.handle_callsign_search: dispatch_callsign_search/3 separates
  the valid/invalid callsign branches.
- InsertOptimizer.optimize_based_on_performance: pattern-match on
  [_, _, _ | _] to require ≥3 samples without calling length/1.
- InsertOptimizer.calculate_insert_options and throughput helpers split
  into guarded clauses instead of inline `if`.
- maybe_emit_optimization uses same-var pattern matching to skip the
  telemetry hit when nothing changed.

Adds unit tests covering every branch of both modules plus SignalHandler,
LocaleHook, and the InsertOptimizer's :noproc fallback path. Coverage
64.69% → 65.72%.
2026-04-23 13:38:24 -05:00
256e61ec76
types: add @type t to last two untyped GenServer structs
Every other defstruct in the app already has a @type t declaration;
these two GenServer states were the last holdouts. Declaring t here
means the whole codebase is ready for the typed-struct milestone of
Elixir's set-theoretic type system (the next phase after the inference
pass already shipping in 1.19).
2026-04-21 10:18:26 -05:00
f225f57044
perf(packets): keyset-based pagination for recent-packet queries
get_recent_packets/1 and get_recent_packets_for_map/1 now accept a
:cursor option (%{received_at: DateTime, id: uuid}) instead of :offset
for deep paging. On the 100M-row partitioned packets table, offset
scanning was linear in page depth; keyset is constant.

Ordering tightened to (received_at DESC, id DESC) so the cursor is
stable across ties. idx_packets_received_desc still covers the lookup;
a dedicated (received_at DESC, id DESC) index isn't worth its size
given microsecond timestamp granularity.

historical_loader migrated from batch_offset*batch_size scheduling to
a sequential cursor chain. The old loader fanned out N Process.send_after
batches up-front which could arrive out-of-order; the cursor version
waits for batch N before scheduling N+1, and also short-circuits the
remaining schedule when an under-full batch signals exhaustion.

mobile_channel load_historical_packets dropped its meaningless
`offset: 0`; callsign history already lacked offset.

No LiveView or WebSocket payload shapes changed. offset: 0 (or omitted)
still behaves as first page; non-zero offsets are silently ignored.
2026-04-21 10:15:04 -05:00
b8a9b8a465
chore(dialyzer): enable stricter flags and fix 97 resulting findings
Turned on :error_handling, :underspecs, and :unmatched_returns in
mix.exs dialyzer config. The 97 warnings this surfaced were fixed in
place rather than suppressed:

- unmatched_return (79): explicit discard with `_ = ...` for
  fire-and-forget side effects (Process.cancel_timer, :ets.new,
  send/2), and pattern-matched `:ok = ...` for control-plane
  Phoenix.PubSub subscribe/unsubscribe/broadcast calls so a future
  return-shape change fails loud.

- contract_supertype (18): tightened @spec arg and return types on
  data_builder, historical_loader, url_params, packet_utils,
  encoding_utils, aprs_symbol, weather_controller, packet_replay to
  match each function's actual success typing.

No behavioural change. mix compile clean, 1008 tests pass, dialyzer
count is now 0.
2026-04-21 10:07:01 -05:00
c64ad4d571
fix(data_builder): clear weather cache in after block
If build_packet_data raises, the :weather_callsigns_cache process-dict
entry would stick around and poison subsequent unrelated calls on the
same LiveView process. Wrap the building pass in try/after so the key
is always removed.
2026-04-21 09:32:17 -05:00
2fc4706d3d
fix: stop silent packet loss, timer leak, and seq-scans
- packet_consumer: when a batch exceeds max_batch_size, carry the
  excess over to the next cycle instead of dropping it. Previously
  any overage was split off into a drop list that was only logged,
  losing packets the producer had already acknowledged.

- map_live: cancel the hover_end_timer in terminate/2 so a user
  disconnecting while a marker is still highlighted doesn't leave
  an orphaned timer behind.

- packet_consumer: rescue/log exceptions inside the async broadcast
  task so PubSub serialization bugs or outages are observable
  instead of silently killing the Task.

- prepared_queries: swap ST_Y/ST_X BETWEEN predicates for the && /
  ST_MakeEnvelope pattern so bounds queries hit the GIST index on
  packets.location instead of sequentially scanning.
2026-04-21 09:29:06 -05:00
31ea64aa6c
perf: reduce per-packet allocations in ingest pipeline
Drop the pre-insert aprs_messages broadcast in Is.dispatch (subscribers
already get a richer payload via postgres:aprsme_packets after insert).
Switch PacketsLive.CallsignView to the per-callsign packets:<CS> topic
so it only receives relevant packets instead of filtering every one.

In PacketConsumer: reuse the received_at stamped in Is.dispatch instead
of calling DateTime.utc_now/0 per packet; drop the duplicate struct_to_map
pass (Is.dispatch already handled it); fold coordinate validation and
Geo.Point construction into set_lat_lon + create_location_geometry so
coords are normalized once; extract device_identifier from the already-
normalized attrs; pre-build the broadcast payload once (identifier pick,
lat/lon aliases, routing callsign) so the async broadcast task no longer
does Map.drop/Map.merge per packet.

Rewrite PacketSanitizer.sanitize_packet / sanitize_data_map with Map.new/2
instead of Enum.reduce + Map.put — one allocation per packet instead of N.

Also compute has_weather in Packet.changeset/2 so direct-changeset inserts
(tests, backfills) populate it the same way the GenStage pipeline does.
2026-04-19 13:33:16 -05:00
0b178d45ba
fix: unsubscribe from pubsub topics on LiveView terminate; catch-all handle_info in ConnectionManager 2026-04-17 13:24:58 -05:00
adaf9599e2
chore: update dependencies and fix pre-existing issues
Dep upgrades (mix deps.update --all):
- phoenix_live_view 1.1.27 → 1.1.28
- swoosh 1.23.1 → 1.25.0
- bandit 1.10.3 → 1.10.4
- credo 1.7.17 → 1.7.18
- hammer 7.2.0 → 7.3.0
- igniter 0.7.6 → 0.7.9
- and minor: fine, lazy_html, meck, mimerl
- removed stale lock entries: geocalc, gettext_pseudolocalize, gridsquare

Bug fixes surfaced during update:
- ETS cache tables were :protected (owned by Application master), preventing
  the Cache GenServer from writing; change to :public + write_concurrency
  so device/symbol/query caches actually work
- historical_dot_html returned Phoenix.HTML.safe tuple instead of plain string;
  symbol_html is JSON-encoded for JS so it must be binary
- String.slice always returns binary so the || "Unknown error" fallback was
  unreachable dead code (dialyzer guard_fail)
- Supervisor.which_children always returns a list so the _ -> branch in
  database_metrics was unreachable dead code (dialyzer pattern_match_cov)
- Add @type t :: %__MODULE__{} to User schema to resolve unknown_type in user_auth spec
- Extract nested if in leader_election to fix Credo nesting depth violation
- Update .dialyzer_ignore.exs: remove 2 stale entries, add false positives for
  Ecto.Multi/Gettext opaque types and Mix.Task PLT limitations
2026-04-15 14:07:29 -05:00
2300dcd69c
fix: stabilize moving station tracking 2026-03-26 18:48:43 -05:00
288b9fbbb2
fix: address security vulnerabilities and concurrency issues
- Fix SQL injection in partition_manager, db_optimizer, and release.ex
- Fix XSS vulnerabilities with proper HTML escaping in LiveViews
- Add proper error handling for email delivery functions
- Fix race conditions with advisory locks and atomic operations
- Replace unsupervised spawn/Task.start with supervised alternatives
- Convert ETS operations to GenServer serialization for thread safety
- Change ETS tables from :public to :protected access
- Add client limits to prevent unbounded memory growth
- Add PubSub cleanup in GenServer terminate callbacks
- Fix device upsert to use atomic Repo.insert_all
2026-03-23 16:59:09 -05:00
21d28afe8e
refactor: reduce map live complexity 2026-03-22 17:20:47 -05:00
a9cf2d3eb8
Add logging to map location determination for debugging 2026-03-22 16:27:43 -05:00
018a9075ab
fix(ios): improve WebSocket stability on iPhone Safari
Increase long-poll fallback timeout to 5s for mobile networks, add
visibility change listener for proper reconnection on app foreground,
and remove backdrop blur that stalls network operations on iOS Safari.
2026-03-11 10:58:16 -05:00
6072eebe5b
vendor heroicons SVGs, remove GitHub dependency
Only the 7 SVGs actually used are vendored into priv/heroicons/.
Also fixes the icon component path resolution (was missing the 24/
size directory) and corrects hero-x-mark/hero-map-pin icon names
that included an erroneous hero- prefix.
2026-03-03 13:34:23 -06:00
9a071b6e3a
feat: refresh trail line when trail duration changes
- Regenerate trail line after trail duration update
- Trail line now respects trail duration setting
2026-02-27 13:49:18 -06:00
464ba84202
feat: clear trail line when clearing callsign tracking
- Push clear_trail_line event when user clears tracking
- Ensures trail line is removed immediately
2026-02-27 13:38:20 -06:00
2919402aa9
feat: show trail line for tracked callsigns at low zoom
- Modified handle_zoom_threshold_crossing to detect tracked_callsign
- Show trail line instead of heat map when tracking at zoom ≤ 8
- Maintain heat map behavior when not tracking
- Maintain marker behavior at zoom > 8
2026-02-27 13:21:37 -06:00
6add35b11a
feat: add send_trail_line_for_tracked_callsign function
- Filters packets by tracked callsign and time threshold
- Sorts packets chronologically by received_at
- Pushes show_trail_line event with coordinates
- Returns socket unchanged when no packets found
2026-02-27 13:10:34 -06:00
f4db38f051
update 2026-02-25 16:29:45 -06:00
d385d1eb75
Default sidebar to open on desktop via CSS
Make the map and panel CSS default to "sidebar open" on desktop
(>= 1024px) without requiring the slideover-open class. Only the
slideover-closed class now hides the sidebar. This prevents a blank
gap when the map and panel states get out of sync after LiveView
reconnects.
2026-02-25 12:25:50 -06:00
f5049a6e2d
updates 2026-02-22 15:33:35 -06:00
d22fc7c661
Fix dialyzer type errors in coordinate validation
- Remove redundant nil check for callsign (spec guarantees String.t())
- Simplify coordinate validation by removing faulty infinity checks
- Remove unreachable catch-all clause in has_weather_packets?

Floats cannot equal atoms :infinity or :neg_infinity in Elixir.
Range checks already ensure finite values, making explicit infinity
checks unnecessary. Reduces dialyzer errors from 40 to 34.
2026-02-20 18:17:59 -06:00
b45993a8be
Fix APRS object display and comment encoding issues
- Strip Mic-E telemetry from comments before database storage
- Show sender callsign on map labels instead of object names for clarity
- Object names still used for grouping to maintain trail organization
- Add comprehensive tests for new encoding utilities

Fixes display issues where weather balloon "X3234025" was shown
instead of station "WA0YMH", and Mic-E telemetry bytes like "!w>`!"
were appearing in comments.
2026-02-20 18:06:32 -06:00
0061c4b169
Apply TailwindCSS Plus design patterns across site
Dark navbar header with radio icon, refined slideover control panel
with branded header and section cards, polished cards/tables/badges
on info, packets, and about pages using ring borders and shadow-xs.
2026-02-20 17:56:06 -06:00
50544c8354
Remove dead PostGIS code, deduplicate utilities, and simplify module hierarchy
- Delete ~170 lines of commented-out PostGIS spatial query functions
- Consolidate coordinate validation into CoordinateUtils
- Replace private normalize_data_type with EncodingUtils delegation
- Inline to_float/to_decimal wrappers at call sites
- Fix has_weather_data? missing wind_gust field
- Remove BoundsManager passthrough delegation module
- Simplify MapLive.PacketUtils by removing pure-passthrough delegations
2026-02-20 13:39:07 -06:00
ca05b497b9
Consolidate 24 packet columns into JSONB data field
Drop 10 dead parser-compat columns (srccallsign, dstcallsign,
origpacket, body, header, alive, posambiguity, symboltable,
symbolcode, messaging) and move 14 display-only columns into a
single `data` JSONB column (PHG, telemetry, radiorange,
information_field, format, posresolution, position_ambiguity,
luminosity, rain_midnight).

Reduces row width from ~70 to ~48 columns. Table is ephemeral
so migration uses DROP/recreate. Also fixes snow_24h -> snow bug
in weather check query and adds null byte stripping for JSONB
string sanitization.
2026-02-20 13:02:47 -06:00
d09e015857
Fix Credo issues and telemetry Decimal coercion
- Extract prepare_markers_for_push/2 to reduce nesting depth in
  handle_info
- Split detect_item_or_object into smaller predicate and application
  functions to reduce cyclomatic complexity
- Coerce Decimal/nil values to native numbers in telemetry metrics
  (PostgreSQL sum() returns numeric type mapped to Decimal by Postgrex)
- Align locate button horizontally with zoom controls
2026-02-20 09:55:56 -06:00
b8d2095346
Fix UI issues: popup positioning, trail/cluster sync, CSS cleanup
- Remove CSS `bottom: 50px !important` on popups that pushed them too
  high above markers
- Hide trails when markers are clustered (zoom < 11) so trails don't
  point to invisible markers inside cluster bubbles
- Fix locate button overlap with zoom controls and keep it always
  light mode
- Remove duplicate slideover/toggle CSS from layout files (single
  source of truth in component)
- Remove dead CSS classes and standardize breakpoints to 1024px
- Add isDestroyed guards on async callbacks to prevent race conditions
- Escape callsigns in marker HTML to prevent XSS
- Replace Leaflet _container private API with getContainer()
- Clean up about page placeholder text
2026-02-20 09:46:45 -06:00
84e65232e5
Batch RF path queries, batch marker removals, and JS callsign index
- Replace N+1 get_latest_packet_for_callsign calls in RF path loading
  with single get_latest_packets_for_callsigns batch query (DISTINCT ON)
- Change batch packet processing to collect marker removal IDs and push
  one remove_markers_batch event instead of individual remove_marker calls
- Add callsignIndex reverse map (callsign_group → Set<markerID>) for O(1)
  lookup in new_packet/new_packets handlers instead of scanning all markers
2026-02-20 09:04:22 -06:00
b4bbfda620
Move client-side logic to server-side LiveView
- Server tracks callsign replacements in prepare_packet_state, sends
  convert_to_historical list so JS skips full markerStates scan
- Historical packets get red dot HTML server-side in build_minimal_packet_data
  instead of JS createMarkerIcon generating it
- Add callsign_group field to all packet data output; server pre-sorts
  by group with chronological order so JS iterates directly
- Normalize heat map intensity in Clustering.cluster_packets (min/50, cap 1.0)
  instead of JS doing it per-point
- Remove buildPopupContent JS fallback and unused escapeHtml import;
  server already provides popup in all data paths
2026-02-20 08:54:34 -06:00
dcabb744df
Batch push_events, RF path queries, and ETS tuning for hot paths
- Batch real-time packet push_events: collect marker data across the
  packet batch and send a single "new_packets" WebSocket message instead
  of one per packet. JS handler processes all live→historical conversions
  before adding new markers to avoid intra-batch conflicts.
- Batch RF path station queries: replace N+1 get_latest_packet_for_callsign
  calls with a single DISTINCT ON query selecting only callsign + lat/lng.
- Add read_concurrency: true to streaming_packets_subscribers and :aprsme
  ETS tables (read-heavy, rarely written).
- Remove dead GC code in PacketConsumer (threshold > 1000 unreachable with
  max batch_size of 100).
2026-02-20 08:30:54 -06:00