Commit graph

1266 commits

Author SHA1 Message Date
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
FluxCD
cbf9a9bb5c chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776969151-403300f [skip ci] 2026-04-23 18:33:10 +00:00
403300f696
test: broaden unit & property-test coverage (61.8% → 64.7%)
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.
2026-04-23 13:32:09 -05:00
1ad1bef59f
fix(devices): make upsert_devices/1 work with Repo.insert_all
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.
2026-04-23 13:31:49 -05:00
5ef97adaf7
refactor: promote hot-path GenServer state maps to structs
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.
2026-04-21 10:26:20 -05:00
FluxCD
d1409bcc7d chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776784731-256e61e [skip ci] 2026-04-21 15:19:18 +00: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
FluxCD
3a163028e3 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776784530-f225f57 [skip ci] 2026-04-21 15:15:56 +00: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
ead0be4434
db: drop 10 unused legacy columns from packets table
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.
2026-04-21 10:15:04 -05:00
FluxCD
7cd9ccf2a0 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776784048-b8a9b8a [skip ci] 2026-04-21 15:08:46 +00: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
FluxCD
8332e5c53c chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776782437-8a0381d [skip ci] 2026-04-21 14:41:06 +00:00
8a0381d9bd
perf(regex_cache): replace half-wipe eviction with LRU
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.
2026-04-21 09:40:20 -05:00
FluxCD
8fd62fba26 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776782357-09f92c6 [skip ci] 2026-04-21 14:40:12 +00: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
FluxCD
8f64d0e999 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776781963-c64ad4d [skip ci] 2026-04-21 14:33:13 +00: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
2fc4706d3d
fix: stop silent packet loss, timer leak, and seq-scans
- 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.
2026-04-21 09:29:06 -05:00
FluxCD
acd596e172 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776781097-9aa836e [skip ci] 2026-04-21 14:18:41 +00:00
9aa836e462
refactor(map): share isValidCoordinate and unwrapLongitudes helpers
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.
2026-04-21 09:17:52 -05:00
FluxCD
c41e47ed95 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776726355-e90226d [skip ci] 2026-04-20 23:06:18 +00:00
e90226dc21
fix(map): tighten edge cases in heatmap, info map, and trail color
- 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.
2026-04-20 18:05:31 -05:00
FluxCD
52418a7b41 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776726224-f777dcb [skip ci] 2026-04-20 23:04:07 +00:00
f777dcbb35
fix(map): stop trail spidering across antimeridian and close gaps
- 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.
2026-04-20 18:03:25 -05:00
FluxCD
0ed2dd3806 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776724752-4417961 [skip ci] 2026-04-20 22:39:43 +00:00
441796165b
k8s: wire RESEND_API_KEY env and drop to 1 replica
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.
2026-04-20 17:38:50 -05:00
31ea64aa6c
perf: reduce per-packet allocations in ingest pipeline
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.
2026-04-19 13:33:16 -05:00
FluxCD
753e4b3cf0 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776451638-5ab2ffc [skip ci] 2026-04-17 18:48:05 +00:00
5ab2ffc8f3
perf: replace per-row pg_notify trigger with elixir broadcast after batch insert 2026-04-17 13:46:51 -05:00
FluxCD
1b969f808a chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776451361-1788419 [skip ci] 2026-04-17 18:44:19 +00:00
1788419ae0
perf: drop redundant packet indexes, compute has_weather in elixir, tighten on_conflict target 2026-04-17 13:42:19 -05:00
FluxCD
19a71ea162 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776450708-4854157 [skip ci] 2026-04-17 18:32:40 +00:00
4854157ee9
feat: add sitemap.xml, api-catalog, and agent-discovery Link headers 2026-04-17 13:31:21 -05:00
FluxCD
b1afc36ec4 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776450324-0b178d4 [skip ci] 2026-04-17 18:26:13 +00:00
0b178d45ba
fix: unsubscribe from pubsub topics on LiveView terminate; catch-all handle_info in ConnectionManager 2026-04-17 13:24:58 -05:00
FluxCD
177b88c3f3 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776449833-ab29a3d [skip ci] 2026-04-17 18:18:08 +00:00
ab29a3dca5
fix: add handle_info/2 for distributed_packet messages in PacketDistributor 2026-04-17 13:16:46 -05:00
FluxCD
1edfd1d418 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776449330-0c50208 [skip ci] 2026-04-17 18:10:04 +00:00
0c50208e4f
fix: use string interpolation for SET statement_timeout instead of parameterized query 2026-04-17 13:08:25 -05:00
FluxCD
70902559ae chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1776449132-0f0cc0d [skip ci] 2026-04-17 18:05:37 +00:00
0f0cc0db88
fix: use github.token for git push in forgejo workflow 2026-04-17 13:05:04 -05:00
7b725206af
chore: add forgejo workflow for docker build and deploy 2026-04-17 12:59:28 -05:00
9c7c190979
chore: trigger CI rebuild 2026-04-17 12:46:28 -05:00
adaf9599e2
chore: update dependencies and fix pre-existing issues
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
2026-04-15 14:07:29 -05:00
CI
54a7b2c0f2 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1774646325-51d5107 [skip ci] 2026-03-27 21:20:35 +00:00
Claude
51d5107983
fix: inline regex module attributes to fix deprecation warning
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
2026-03-27 16:18:27 -05:00
Claude
a8051ce247
fix: code review refinements
- 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
2026-03-27 16:18:27 -05:00
CI
36a86114a7 chore: update aprs.me image to git.mcintire.me/graham/aprs.me:main-1774568941-2300dcd [skip ci] 2026-03-26 23:50:42 +00:00
2300dcd69c
fix: stabilize moving station tracking 2026-03-26 18:48:43 -05:00