- CoreComponents.input/1: expanded coverage to include the select,
textarea, generic-text, and unchecked-checkbox branches, plus
error-vs-no-error border styling.
- PostgresNotifier: handle_info forwards pg_notify payload to
Aprsme.PubSub and ignores unrelated messages.
Coverage 69.56 → 70.11%.
- supported_locales/0 returns the declared ~w(en es de fr) list.
- put_locale/1 sets the Gettext process locale for each supported
value and raises FunctionClauseError for unsupported input.
3 tests, async:false because they mutate process-wide Gettext state.
- 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.
- CoreComponents.safe_row_id: four function heads dispatch on the row
shape (atom-key map, string-key map, other map fallback, non-map)
instead of a cond with Map.has_key? checks.
- Api.V1.CallsignController.validate_callsign: check_callsign_format/2
splits the validation success/failure branches into two clauses; the
binary-vs-everything-else dispatch stays in validate_callsign/1.
Adds 5 integration tests for the CallsignController covering invalid,
not-found, success, case-normalization, and length-limit paths. The
test file is async:false because it shares the rate-limiter ETS with
other API tests and could otherwise trip the 100/min per-IP cap.
- 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%.
- TimeHelpers.to_datetime: `cond` with match?/2 guards becomes five
function heads dispatched by input type — DateTime passes through,
NaiveDateTime anchors to UTC, integer is a millisecond Unix epoch,
binary tries ISO 8601, everything else is nil.
- PageController.health: the three-branch `cond` + inner `if db_healthy`
becomes a chain of small respond_* helpers, each pattern-matched on a
single axis (health_status, shutting_down?, db_healthy?). db_healthy?/0
isolates the rescue.
- format_uptime splits the cond into four format_uptime_parts/4 clauses.
Adds tests for the PageController actions (health 200 / 503-draining,
ready, sitemap.xml, .well-known/api-catalog, status.json). Coverage
66.81 → 67.29%.
- 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%.
- DeviceCache.pattern_matches?: replace a 3-branch `cond` with a
two-clause `case` on `String.contains?(pattern, "?")`. The old `*`
branch was dead code (it did the same exact-match compare as the
fallback), so it's removed.
- Telemetry.DatabaseMetrics.collect_postgres_metrics: dispatch on
environment atom via multi-clause private functions instead of
`if Application.get_env(...) == :test`.
New tests:
- DeviceCache wildcard/exact lookup paths exercised via a seeded cache.
- MobileUserSocket connect/3 for x_headers/peer_data/empty info and
rate-limit deny.
- DatabaseMetrics pool-metrics emission captured via :telemetry handler
and the :test-env short-circuit.
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.
- 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.
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.
The IPGeolocation plug was setting session data, but the live_session
wasn't forwarding it to the LiveView mount. Also removed the root-path
restriction so geolocation works on any initial page load.
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.
Implements GET /api/v1/weather/nearby endpoint with comprehensive parameter
validation and error handling. The controller validates required parameters
(lat, lon, radius) and optional parameters (hours, limit) with appropriate
range checking. Integrates with PreparedQueries for efficient database access
and WeatherJSON for response serialization.
Key features:
- Required params: lat (-90 to 90), lon (-180 to 180), radius (0-1000 miles)
- Optional params: hours (1-168, default 6), limit (1-100, default 50)
- Proper error responses via FallbackController (400 for bad requests, 422 for validation)
- Full test coverage (16 tests) including edge cases and error scenarios
Also updates FallbackController to handle {:error, status, message} tuples
and fixes WeatherJSON to support both field name formats (lat/lon and latitude/longitude)
for compatibility with existing tests and actual query results.
- Implements nearby/1 function to format weather station data
- Returns structured JSON with data and meta fields
- Handles null weather fields gracefully
- Includes distance, position, weather data, and symbols
- Formats DateTime to ISO8601 strings
- All tests passing (3/3)
- 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
- 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
- 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.
The email and callsign changesets add "did not change" errors when
created with empty attrs (on mount). Guard all field error displays
on changeset.action being set so errors only appear after user
interaction.
- 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
- 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
- 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).
parse_rf_path was filtering out all paths containing qA* constructs,
which is virtually every APRS-IS packet. The qA* element delimits the
RF path from the APRS-IS metadata — it should be split on, not
discarded. Now properly extracts digipeaters (before qA*) and the
igate (after qA*), while filtering APRS aliases like WIDE/RELAY/TRACE.
Also adds hover handlers to trail polylines so hovering anywhere on a
call's track shows the RF path, not just on the dot markers.
Extracts client IP from CF-Connecting-IP or X-Forwarded-For headers
and sets conn.remote_ip before request logging, so K8s logs show the
actual client address instead of the internal cluster/ingress IP.
Use CF-IPLatitude/CF-IPLongitude headers instead of making HTTP calls
to ip-api.com on every page load. Eliminates ~500ms latency, external
API dependency, and flaky tests.
Also disable test server to prevent port conflicts during development.
- Switch PacketProducer buffer from list to :queue for O(1) overflow handling
- Add cached leadership check via :persistent_term to eliminate GenServer.call
bottleneck in packet distribution hot path
- Simplify packets_live coordinate extraction from 70 lines of nested
conditionals to helper functions (extract_coordinate/2, format_coordinate/1)
- Remove debug logging from hot paths (packets.ex queries, PacketDistributor,
app.js console.logs)
- Remove sobelow dependency (false positives blocking commits)
- Add 25 new tests for PacketProducer, coordinate helpers, and cached leadership
Users could register duplicate callsigns with different casing (e.g.,
"w1aw" and "W1AW") because the database unique index was case-sensitive.
Additionally, live validation showed false "already taken" errors on
every keystroke.
Changes:
- Add case-insensitive unique index using lower(callsign)
- Skip callsign uniqueness validation during live form changes
- Add test verifying case-insensitive uniqueness enforcement
Replace all DaisyUI component classes with pure Tailwind CSS utility
classes following TailwindUI conventions. Use dark: variants for dark
mode instead of DaisyUI's data-theme semantic color system.
- Remove DaisyUI plugin and theme vendor files
- Configure Tailwind v4 custom dark variant for data-theme attribute
- Migrate all components: cards, tables, badges, buttons, forms,
navigation, dropdowns, modals, alerts, star ratings, spinners
- Update all auth pages (login, register, settings, password reset,
confirmation) to TailwindUI card layout
- Update content pages (about, API docs, status, packets, bad packets,
info, weather) to TailwindUI patterns
- Replace opacity-based text styling with semantic gray colors
- Update test assertions to match new class names
APRS object packets (e.g. DAPNET transmitters) were showing the
sender's callsign on the map instead of the object name. For example,
DB0SDA (Germany) sending an object for P-K5SGD (Texas) would display
as "DB0SDA" at the Texas coordinates.
- Add display_name/1 to PacketUtils that returns object_name for
objects, item_name for items, falling back to sender
- Update DataBuilder to use display_name in all callsign display paths
- Fix object/item detection order in packet_consumer to prevent
objects from being incorrectly flagged as items
- Add DataBuilder tests for object/item display name behavior
- Fix flaky PacketPipelineSupervisor test (ensure module loaded)
- Fix duplicate aprs-map div causing LiveView test failures
- Removed duplicate div from bottom_controls function
- Kept only the map_container component in render function
- Add on_error: :warn to all LiveView test live() calls
- Updated 11 test files to suppress duplicate ID warnings
- Allows tests to continue despite duplicate ID issues
- Optimize mobile channel callsign search query
- Changed from inefficient distinct+ILIKE to grouped query
- Added database indexes for sender and base_callsign pattern matching
- Prevents 30+ second timeout on large packet tables
- Add migration for callsign search indexes
- text_pattern_ops indexes for LIKE/ILIKE queries
- Composite index on sender + received_at for sorting
- Uses CONCURRENTLY to avoid blocking production traffic
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Fix unsubscribe_from_bounds -> unsubscribe (function doesn't exist)
- Fix search_callsign query to use 'lon' instead of 'lng' field
- Add comprehensive test suite for mobile channel:
- Join and subscription tests
- Bounds validation tests
- Callsign search tests
- Callsign tracking tests
- Streaming packet tests
- Packet data format tests
- 29 tests total, all passing
Created test/support/channel_case.ex for channel testing infrastructure.
Generated with Claude Code https://claude.com/claude-code
- Fix duplicate bad packet storage in store_packet function
- Changed insert_packet to return specific error tuples
- Handle validation_error and storage_exception separately
- Prevent double storage of bad packets
- Add try/catch wrapper around insert_packet for exception handling
- Properly catch and return storage_exception errors
- Test now correctly expects :storage_exception
- Add helper function for LiveView tests with duplicate IDs
- Added live_with_warn/2 to ConnCase
- Suppress duplicate ID warnings in integration tests
- Fix unused variable warning in insert_packet
All packet tests now pass without failures.