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
- IS GenServer: add missing :failure_started_at to test build_state/1 map
- IS GenServer: fix stale status server assertion and reconnection test
- IS GenServer: set Logger level to :debug for dispatch parse-error tests
- PacketReplay: remove conflicting Registry start_supervised! from setup
- PacketReplay: update assertion from {:continue_replay} to :start_replay
- Doctests: fix float precision, stale sprite coords, quoting escapes
- API controllers: use string keys for JSON error/postion details
- PacketUtils: fix operator precedence (not is_nil(result).field)
- ThemeManager: update expected dark theme text color
- StatusLive: remove/update stale :loading assign assertions
- Movement: remove {:ok, _v} wrapper from render_hook/3 (returns HTML)
- AprsIsMock: update packet_stats assertion for populated default shape
- 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
Critical:
- Remove :protected from WeatherCache ETS (conflicted with :public)
- Register Aprsme.ReplayRegistry in supervision tree
- Route :continue_replay dead message to :start_replay handler
- Add 5000ms timeout to unbounded :rpc.call calls
- Fix falsy lat/lng check rejecting valid 0 coordinates
Medium:
- Fix live_reload pattern (temp_web -> aprsme_web)
- Remove duplicate ecto_repos config
- Make DB SSL configurable via DB_SSL env var
- Track sizeCheckTimeout on self for cleanup in destroyed()
- Wrap pushEvent calls in try/catch
- Remove unused size variable in marker cluster icon
- Capture touch coords at touchstart instead of stale TouchEvent
- Demote console.log to console.debug in production code
- Add catch-all to normalize_bounds preventing GenServer crash
Style:
- Add missing @impl true annotations in is.ex
- Improve migration failure error message
- Use textContent instead of innerHTML for error messages
- Use proper type for longPressTimer instead of any
- 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>
Bound batch DISTINCT ON queries by input count. Guard Repo.all(Devices) with exists? check. Add trigram GIN index on base_callsign and (has_weather, received_at DESC) partial index.
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>
The unstyled <h1>Packets</h1> template was dead code - no route in the
router renders it (/packets uses PacketsLive.Index LiveView instead).
Removed the template and its test. Added findings.md with all audit
results from comprehensive security and code quality review.
- k8s: 2 replicas, POOL_SIZE 25, cpu 2000m, mem 1Gi (was 1 replica, 5, 500m, 512Mi)
- telemetry poller 10s -> 60s; drop pg_database_size, pg_stat_statements,
pg_table_size and pg_indexes_size (these were ~7100s of cumulative DB time
on their own)
- get_latest_packet_for_callsign: bound by :packet_retention_days so partition
pruning kicks in
- get_nearby_stations_knn: ST_DWithin spatial pre-filter (default 500km) so
the GiST geography index cuts candidates before DISTINCT ON sort
(EXPLAIN: 5793ms -> 400ms)
- New migration: partial functional indexes on upper(object_name) and
upper(item_name), built per-partition CONCURRENTLY then attached to parent.
Lets get_latest_packet_for_callsign use BitmapOr across all 3 upper()
indexes instead of falling back to a scan
When :beam_lib.chunks fails (e.g. a project module hasn't been loaded
into :code.which yet), the existing with returned the raw error tuple
as the 'line' value, which then crashed format/1 via String.Chars.
Add an explicit else clause so the function always returns nil or an
integer.
- Split Analyzer.analyze/1 into compute_state/1 (slow, scans all BEAMs)
and a fast filter step. analyze/1 now accepts :precomputed_state opt.
- Mix.Tasks.Compile.Unused.run/1 picks up precomputed state from process
dict :mix_unused_extra_analyze_opts when present (test escape hatch).
- AnalyzerTest setup_all computes state once; exclude tests pass it
through analyze/1, dropping per-test cost from ~700ms to <1ms.
- UnusedTest setup_all does the same — the consolidated severity-args
test drops from 3.4s to ~5ms.
- Trim further sleeps in HistoricalLoadingTest (100->30ms) and
MovementTest (50->20ms, receive timeouts 100->30ms).
Total suite: 40.6s -> 37.7s; 2488 tests, 0 failures.
- packet_replay: stop_replay test reduces fake-task receive timeout from 5s to 200ms
- broadcast_task_supervisor: scheduler_usage takes configurable sample seconds;
test uses sample-pair API (instant) instead of blocking 1-second sample
- health_check: ask_if_alive timeout is now configurable; unresponsive-pid test
uses 50ms timeout instead of waiting full 5-second default
- mix_unused/analyzer_test: cache analyze() result in setup_all so 4 tests share
one analyze pass instead of running 4 separate scans
- mix/tasks/compile/unused_test: consolidate three slow severity tests into one
- log_sanitizer + packet_field_whitelist: cap property-test runs at 25 (was 100)
- historical_loading: trim 200ms post-event sleeps to 50ms
- movement: refute_push_event timeout from 200ms to 50ms
Total suite time: ~45s -> 40.6s; 2488 tests, 0 failures.
- Make LeaderElection check_interval, max_cluster_wait, and cluster_check_interval
configurable via Application env so tests can override with short values
- Slow LeaderElection tests now use 50-150ms timeouts instead of 3-6s
- Remove :slow tag from unused_test (compile cache makes reruns cheap)
- Drop :slow from test_helper exclude list
All 2490 tests now run in ~45s (up from 2485 with 5 excluded).
Adds a self-contained 'unused' Mix compiler task that finds public
functions in this project no caller in the project ever invokes:
- MixUnused.Analyzer: enumerates public functions across the project's
compiled BEAM files (filtered by app), walks the abstract code in
each BEAM via :beam_lib to collect every remote {Module, fun, arity}
call observed at compile time, and subtracts the call set from the
def set. Exposes :exclude (modules or MFA triples), :compile_path,
and :app options. Behaviour callbacks (@impl true and any listed
in behaviour_info(:callbacks)) and well-known entry points
(start_link, child_spec, mount, render, GenServer callbacks,
__info__, module_info, etc.) are excluded automatically.
- Mix.Tasks.Compile.Unused: a Mix.Task.Compiler that compiles the
project then asks the analyzer for unused defs. Severity is
configurable via the :unused project key; per-file exclusion of
test/ paths is on by default.
Inspired by https://github.com/hauleth/mix_unused — uses BEAM
introspection rather than a compilation tracer to avoid the
chicken-and-egg problem of recompiling the tracer module itself
during analysis.
Wire up prom_ex with built-in Application, Beam, Phoenix, PhoenixLiveView,
and Ecto plugins, plus a custom plugin that exposes the existing
app-specific telemetry (packet pipeline, spatial PubSub, repo pool,
postgres stats, insert optimizer) as Prometheus metrics.
Mount PromEx.Plug at /metrics — internal-only, intended to be scraped
by the cluster's external Prometheus through the kube-apiserver pod
proxy. K8s pod template now carries prometheus.io/{scrape,port,path}
annotations so the existing kubernetes-pods scrape job picks it up
automatically.
PromEx is disabled in the test environment.
Also harden the packet cleanup and partition manager tests with
Repo.delete_all setup hooks so they aren't poisoned by residual rows
committed outside a sandbox transaction in earlier runs (which were
inflating delete counts and triggering ATTACH PARTITION check_violation
errors against packets_default).
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)
Remove faker, floki, igniter, ecto_psql_extras (zero usages), exvcr
(zero usages), and resend. Replace resend with Aprsme.Swoosh.ResendAdapter
using the already-present req dependency. Frees 23 packages total from
the lockfile including hackney, certifi, and the tesla dependency chain.
Fix two pre-existing test isolation bugs:
- signal_handler_test called the real ShutdownHandler.shutdown() via SIGTERM
simulation without restoring state, causing page_controller_test to see
shutting_down: true and return 503
- page_controller_test setup now resets ShutdownHandler state defensively
- 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.
The original get_status/0 had a nested case/try that duplicated the
"disconnected" status map across two branches. Split into:
- status_from/1 with nil + is_pid heads for the "no GenServer" and
"GenServer alive" paths. The alive path isolates the catch :exit, _
fallback for a non-responding process.
- disconnected_status/1 builds the shared status map once, parameterised
on the default server string to preserve the original branch-specific
fallback behavior.
Removes ~20 duplicated lines; all 61 Is tests still pass.
- get_cluster_aprs_status: fetch_cluster_status/1 dispatches on the
clustering flag — true fans out cluster-wide, false goes straight to
the local Aprsme.Is status.
- init/1: extract schedule_initial_election/1 with true/false heads.
The clustered branch sets up the check-and-backstop timers; the
non-clustered branch queues an immediate election. Makes the two
startup paths obvious from the function signatures.
No behavior change; all 17 LeaderElection tests still pass.
- encoding_info/1 (binary clause): extract build_encoding_info/2 with
true/false heads on String.valid?/1, so the valid-UTF-8 and invalid
branches each own their map literal.
- try_utf8_conversion/1: convert_to_utf8/2 dispatches on String.valid?/1
so the latin1 → UTF-8 fallback is a function head, not an inline if.
No behavior change; all 34 Encoding tests still pass.