Buffer incoming packets in socket.private (no rerender) and flush to
assigns/stream on a 150ms timer, preventing client-side morphdom from
being overwhelmed during traffic bursts. Applies to the global packet
feed (stream) and per-callsign detail view (assigns).
- Session cookie: add encryption_salt and secure flag to endpoint
- CSP: remove unsafe-eval from script-src directive
- /metrics: wrap PromEx endpoint behind rate-limited pipeline
- LiveView signing_salt: move to env var in production config
- LiveView performance: defer heavy aggregation queries past initial mount
- SQL: replace correlated subquery with DISTINCT ON in get_heard_by_stations
- Move require Logger to module top level in SpatialPubSub
- Replace weak is_list assertions with stronger checks
- Remove redundant 'is a list' test in PromEx tests
- Replace apply/3 with direct function call in packet_replay test
- Fix unmatched_return in weather_cache.ex and callsign_view.ex
- 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.
- Update 9 Hex packages (bandit, credo, finch, phoenix, phoenix_live_view,
plug, req, swoosh, tidewave)
- Load bad_packets data unconditionally in mount/3 so crawlers and link
previews see content instead of an empty page
- Fix Accounts doctests: use concrete values instead of unbound variables,
disable illustrative doctests broken by Elixir 1.20.2 strictness
- 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
Coalesce rapid PubSub notifications into single 2-second delayed DB query. Remove tests for removed :update_time_display handler.
Ultraworked with Sisyphus
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Replace full-list assign with stream_insert + phx-update=stream. Add DOM IDs to all packet rows. Skip deprecated phx-update=prepend.
Ultraworked with Sisyphus
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
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>
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>
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.
- 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)
- get_coordinates_from_mic_e: replace two inline `if direction == ...`
rebinds with a reusable apply_hemisphere_sign/3 helper; the final
range check becomes maybe_return_coords/2 with a guarded clause that
yields {lat, lon} or {nil, nil}.
- has_position_data?: the boolean short-circuit collapses from an
explicit if/else to a single `or` expression.
All 37 CoordinateUtils tests (4 properties) still pass.
- parse_path_element_with_link splits its fat `if`/or-chain into three
focused helpers: linkable_callsign?/1 (regex + alias check),
path_alias?/1 (four String.starts_with guards), and
classify_path_element/2 (link vs. text dispatched on the boolean).
- display_for_element/2 chooses the callsign form with/without the
asterisk via two function heads instead of an inline `if`.
Adds 7 tests covering the parse_path_with_links/1 public API — plain
callsigns, heard-via asterisk preservation, non-linkable aliases
(WIDE/TRACE/RELAY/TCPIP), q-constructs, whitespace trimming, and mixed
paths.
- 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.
- Aprsme.Packet.put_phg_fields: dispatch on the shape of the phg value
(map / 4-char binary / anything else) via put_phg_fields_for/2.
- Aprsme.Packet.get_field_value: remove the catch-all nil clause. After
the put_phg_fields refactor, dialyzer can prove every call site passes
a map and flagged the fallback as unreachable — so it's gone, and
a future mistyped caller now fails loudly instead of silently.
- LogSanitizer.sanitize_map: extract redact_field/2 for readability.
- DeviceIdentification.pattern_matches?: match_pattern/3 dispatched on
String.contains?/2 keeps the rescue tight around the regex path.
- InfoLive.Show: get_received_at pattern-matches on key shape;
format_distance splits locale × range via four format_*_distance
helpers; calculate_course pipes through normalize_bearing/1.
Adds 15 tests exercising the InfoLive.Show helpers (imperial vs. metric
formatting, cardinal bearings, haversine symmetry, Decimal inputs).
Coverage 67.43 → 67.72%.
- HealthCheck.check_health(:readiness): collapse the 3-branch cond into
readiness_status/2 dispatched on (health_status, shutting_down?).
- HealthCheck.shutting_down?: the whereis + alive? + try/catch nest
becomes a small pipeline of function heads (ask_shutting_down?/1 +
ask_if_alive/2), with the catch isolated to the single place it fires.
- EncodingUtils.sanitize_string_fields and sanitize_nested_map shared a
three-branch atom-or-string key lookup. Extract update_existing_key/3
+ first_present_key/3 + nil_aware/1 helpers; the original functions
become two-liners.
- StatusLive.Index.format_uptime: splits the cond into
format_uptime_parts/4 clauses with numeric guards.
- StatusLive.Index.calculate_health_score: struct-shape pattern match
on %{connected: false} / %{uptime_seconds: s} guards replaces the cond.
- StatusLive.Index.format_time_ago: pipe the diff through
format_seconds_ago/1 with four clauses.
Adds tests for the four public StatusLive.Index helpers
(format_uptime, format_time_ago, get_health_description, format_number).
Coverage 67.29 → 67.43%.
- SharedPacketHandler.handle_packet_update: dispatch on filter-result,
and use maybe_enrich/2 dispatched on the enrich? boolean instead of
an inner `if`. lookup_device_info/1 is now a trio of function heads.
- Cluster.Topology.child_spec: build_spec/3 dispatches on
(cluster_enabled?, topologies) tuples — no more nested `if`.
- Cluster.PacketDistributor.distribute_packet: maybe_broadcast/2
dispatches on the leader-and-enabled check; the non-leader / disabled
paths return :ok explicitly instead of relying on an `if` expression's
implicit nil.
Adds tests for SharedPacketHandler (filter/enrich/match paths,
callsign_filter + callsign_and_weather_filter factories) and extends
Cluster.Topology tests to cover the nil-topology and opts-merge cases.
Coverage 66.41 → 66.81%.
- 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%.
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%.
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).
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.
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.
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.
- 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.
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.
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
Added proper latitude/longitude normalization to prevent invalid coordinates:
- Longitude now wraps to -180 to 180 range (e.g., 199.0 -> -161.0)
- Latitude now clamps to -90 to 90 range
- Applied normalization before creating geometry in Packet module
Fixed 7 existing packets with invalid coordinates:
- WB6WWJ-N: 199.0 -> -161.0
- BI7MCK-1: 320.3 -> -39.7
This prevents map display issues and ensures PostGIS spatial queries
work correctly.
Objects and items now display with their own names on the map instead of
the originating station's callsign. This prevents confusion where stations
creating objects (like repeaters) appear at multiple locations.
For example, CALGRY creates repeater objects (147.06-WC, 146.85-NH) which
were appearing on the map labeled as "CALGRY" at the repeater locations
instead of showing the actual object names.
Changes:
- Updated map_label() to return object_name for object packets
- Updated map_label() to return item_name for item packets
- Position packets continue to show the sender callsign
- Updated tests to expect object/item names instead of sender
This matches APRS.fi behavior where objects are displayed separately
from their originating station.
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.
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.