Commit graph

385 commits

Author SHA1 Message Date
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
edeb8402db
replace codeberg.org references with git.mcintire.me
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
2026-07-25 09:29:16 -05:00
7e1498bf4e
perf: batch high-frequency LiveView updates via socket.private
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 50s
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).
2026-07-22 11:23:09 -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
Graham McInitre
243a98df77 fix: security hardening and query performance improvements
- 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
2026-07-21 10:13:30 -05:00
77d2a6ca30
updat edeps 2026-07-17 16:42:53 -05:00
281aef095c
fix: resolve all credo warnings and pre-existing dialyzer errors
- 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
2026-07-17 16:41:15 -05:00
088fd20711
refactor: FP patterns in packets_live/callsign_view
- mount: extract subscribe_if_connected/2, split assign_valid/assign_invalid
  to eliminate inline if/else for validation + subscription logic.

- enrich_packet_with_device_identifier: extract resolve_canonical_identifier/1
  as multi-clause function (binary guard + catch-all) instead of nested if.

- update_packet_lists: cond → multi-clause do_update/4 with guards.
  Three explicit paths: at-cap no-stored, at-cap with-stored, under-cap.

- get_timestamp_microseconds: case → multi-clause (%DateTime{}, binary,
  catch-all) for cleaner dispatch.
2026-07-01 18:08:56 -05:00
80983e6223
fix: repair all 60 failing tests after refactor and dep updates
- 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
2026-07-01 09:41:10 -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
bc60fec98c
deps: update all dependencies and fix doctests for Elixir 1.20
- 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
2026-07-01 08:40:17 -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
17838f05d3
Add Plausible Analytics server-side tracking and API Prometheus metrics 2026-06-04 19:37:54 -05:00
0c90a47f9a
update stuff 2026-06-04 18:00:03 -05:00
274af1ce93
Add W5ISP attribution and QRZ link to about page 2026-06-04 17:30:56 -05:00
32fca73d3a
Fix JSON punctuation highlighting inside string values 2026-06-04 17:01:36 -05:00
0a4f921fd9
Format JSON examples with proper indentation and syntax highlighting 2026-06-04 16:53:22 -05:00
5569d8e2a0
Add user-agent to request logging metadata 2026-06-04 16:19:47 -05:00
c3921e9277
Add Plausible analytics snippet to root layout 2026-06-04 16:05:24 -05:00
73c6703102
perf: debounce BadPacketsLive refresh, remove stale test cases
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>
2026-06-01 16:50:28 -05:00
3169abf0f0
perf: convert packets_live table to streams for efficient diffs
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>
2026-06-01 16:50:09 -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
3948ae5894
Remove dead packets.html.heex template and create findings.md
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.
2026-05-29 15:50:23 -05:00
d0b9513b41
fix elixir type warnings 2026-05-29 14:00:29 -05:00
24796f98d4
Optimize prod: scale deployment, drop expensive metric queries, fix slow callsign and nearby-stations queries
- 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
2026-05-12 17:48:58 -05:00
4c5c730ee7
Speed up the slowest tests
- 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.
2026-05-09 10:56:03 -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
51e068f3f1
Add PromEx Prometheus exporter and /metrics endpoint
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).
2026-05-08 10:41:06 -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
6f142a7391
fix(weather): use Map.get for fallback comment access; add WeatherLive render tests 2026-04-24 08:15:01 -05:00
c90a76df04
refactor: CoordinateUtils.get_coordinates_from_mic_e uses guarded clauses
- 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.
2026-04-23 15:03:09 -05:00
0cbe93f8db
refactor: MobileChannel callsign search pattern via function dispatch
build_search_pattern/1 picks between SQL-wildcard (when the query
contains `*`) and prefix-match (SSID-friendly) forms via two function
heads instead of an inline `if`. The transform is now isolated and
obviously boolean-dispatched at the call site.
2026-04-23 14:50:03 -05:00
9053eef2c6
refactor: AprsSymbol.normalize_symbol_table pattern-matches on input
- Three-way cond (known-table / overlay-capable char / fallback) becomes
  three function heads: a guard-matched clause for the canonical
  "/"/"\\"/"]" tables, an is_binary clause that decides via regex match
  whether the input is a single alphanumeric overlay character, and a
  catch-all fallback.
2026-04-23 14:38:26 -05:00
1d68868d27
refactor: pattern-match InfoLive.Show path parsing + add tests
- 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.
2026-04-23 14:23:16 -05:00
fc5e91479b
refactor: pattern-match CoreComponents.safe_row_id + CallsignController
- 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.
2026-04-23 14:18:30 -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
35bd098b30
refactor: more pattern matching + drop unreachable get_field_value clause
- 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%.
2026-04-23 14:09:53 -05:00
ee02769d38
refactor: pattern-match in HealthCheck, EncodingUtils, and StatusLive helpers
- 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%.
2026-04-23 14:02:50 -05:00
564d21bb26
refactor: pattern-match in TimeHelpers.to_datetime and PageController
- 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%.
2026-04-23 13:58:42 -05:00
7db78675a2
refactor: collapse nested if/case into pattern-matched helpers
- 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%.
2026-04-23 13:55:11 -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
09f92c6738
feat(mobile): rate-limit socket + batch historical packet delivery
- 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.
2026-04-21 09:38:51 -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