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%.
Adds ~800 new tests (incl. 27 StreamData properties) across 22 modules
that previously had little or no coverage. Highlights:
- Pure utility modules (LogSanitizer, PacketFieldWhitelist,
PacketSanitizer, DeviceParser, CoordinateUtils, BoundsUtils,
ParamUtils, Convert, WeatherUnits) get thorough unit + property
coverage including UTF-8 truncation boundaries, antimeridian
longitude wrap, Haversine symmetry, and unit-conversion inverses.
- JSON view modules (CallsignJSON, ErrorJSON, ChangesetJSON) verified
for envelope shape and error templating.
- Plugs (HealthCheck, RateLimiter, ApiCSRF) exercised for happy-path
and halt paths.
- GenServer modules (RegexCache, CleanupScheduler, DeploymentNotifier)
tested without touching the supervised singleton where possible.
- Drops the orphaned map_live/map_helpers_test.exs whose module name
collided with the new live/shared/coordinate_utils_test.exs.
Three related bugs prevented upsert_devices/1 from ever succeeding in
production:
- The JSON map keys ("identifier", "vendor", etc.) were passed through
to Repo.insert_all, which only accepts atom field names.
- updated_at was a %DateTime{} but the Devices schema uses :naive_datetime.
- inserted_at was never set, violating the NOT NULL constraint.
Fix by whitelisting known string keys and converting them to atoms via
String.to_existing_atom/1 (safe against atom exhaustion), using
NaiveDateTime.utc_now/1, and setting both timestamps explicitly.
Converts the inline state maps in Is, PacketConsumer, LeaderElection, and
CircuitBreaker into structs with @type t specs. Enforced keys catch typos
on state updates and give dialyzer a concrete type to check against
instead of map() in every handle_* clause.
Is' nested login_params and packet_stats are extracted to their own
modules (Aprsme.Is.LoginParams, Aprsme.Is.PacketStats) rather than
defined inline, per the project's no-nested-modules rule.
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.
Dead fields left over from the original parser schema: srccallsign,
dstcallsign, origpacket, header, body, alive, posambiguity,
symboltable, symbolcode, messaging. None are declared in the Packet
schema, none are read anywhere in lib/ or test/, and Packet.strip_dead_fields/1
already removed them from attrs before insert.
In environments where ConsolidatePacketsColumns (20260220230000)
already DROP/RECREATEd the table without these columns the migration
is a safety-net no-op thanks to IF EXISTS; otherwise it ALTERs the
parent partitioned table and Postgres cascades to all partitions.
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.
Stored entries now carry a monotonic access counter refreshed on every
hit. When the cache fills, the 10% least-recently-used entries are
dropped instead of an arbitrary half of the table, which previously
caused thrashing under diverse input by evicting hot entries.
- MobileUserSocket: cap new socket connections per IP at 30/min via
Aprsme.RateLimiter. Denials log and return :error at handshake.
Tests bypass the check (no real peer_data).
- MobileChannel: rate-limit the expensive client-initiated handlers
(subscribe_bounds, subscribe_callsign, search_callsign) at
30/minute per socket. Streaming packet delivery is unaffected.
- MobileChannel: historical packet loads now arrive as a batched
`packets` event (100 per frame) instead of one `packet` frame per
row. Live streaming still uses the single `packet` event.
- Updated docs/mobile-api.md with the `packets` event and rate-limit
semantics; migration note preserves compatibility for clients that
only handle live `packet` events.
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.
The coord-validation and antimeridian-unwrap logic added in the last
pass had been duplicated across map.ts, trail_manager.ts, and
info_map.ts. Move both into map_helpers.ts and import from there so
there is one authoritative implementation.
- Heatmap: filter points with invalid coords or out-of-range intensity
so malformed rows can't distort the layer.
- InfoMap: validate coords for range (not just NaN) and use an epsilon
when comparing positions, so float noise doesn't cause needless
re-renders or accept bogus data.
- TrailManager: getTrailCenter returns null for empty input instead of
the ambiguous (0, 0) null-island; callers guard accordingly.
- Normalize longitude delta in trail_manager Haversine so wraps across
180/-180 no longer compute a near-global distance and spuriously
split the trail into short-segment-filtered pieces.
- Unwrap longitudes before building segments and drawing polylines in
both the per-station TrailManager and the show_trail_line handler so
a date-line crossing renders as a short hop instead of a line that
wraps around the world.
- Raise maxHopDistance (10 -> 100 km) and drop minSegmentPoints (3 -> 2)
so legitimate fast movers (aircraft, balloons) and short valid hops
are drawn instead of being dropped.
- Unbind mousemove/mouseout handlers on a trail before removing it so
queued events can't fire against discarded state.
- Fix self.isValidCoordinate TypeError in updateMarker (function is
module-level, not on the hook) and add coord validation to
show_trail_line.
- Null boundsTimer after clearTimeout for cleanup consistency.
Registration emails were crashing the LiveView because RESEND_API_KEY
lived in aprs-secrets but was never injected into the container env,
so Resend.Swoosh.Adapter raised at delivery time after the user row
was already persisted.
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
Elixir 1.19 deprecates storing regexes in module attributes. Inline
@wx_preamble_pattern and @wx_field_patterns directly into their
respective functions to resolve the --warnings-as-errors CI failure.
https://claude.ai/code/session_01Ps7Zq3wiBur1RtRKN7JM6X
- RegexCache: change ETS table from :public to :protected for consistency
with other tables, preventing uncontrolled writes bypassing GenServer
- MobileChannel: ensure_float now returns nil instead of the original
unparsed string on parse failure, so validate_bounds properly rejects it
- map.ts: remove dead commented-out localStorage code
- map.ts: use instanceof HTMLAnchorElement instead of unsafe type cast
in popup navigation handler
- TrailManager: add destroyed flag to prevent stale RAF and debounce
callbacks from firing after destroy, and clean up hover timer on destroy
https://claude.ai/code/session_01Ps7Zq3wiBur1RtRKN7JM6X