fix: address 56 findings from code audit #37
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/bug-audit-batch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes all findings from the 2026-09-20 code audit (
bugs.md): 6 high, 30 medium, 20 low acrosslib/andassets/js/.High
{:tcp, ...}/tcp_closed/tcp_errornow matchstate.socket(stale-socket messages ignored) and:reconnectis a no-op while connected — atcp_error+tcp_closedpair can no longer spawn parallel APRS-IS sessions.:cleanup_old_packetsloop is scheduled once via a trackedcleanup_timerassign (cancelled before reschedule) —handle_paramspatches no longer accumulate parallel cleanup chains.{:drain_connections, percent}(1..100) consumed as:rand.uniform(100) <= percent— previously a count was multiplied by 10, dropping every client at 10.pid_alive?/2returns:alive | :dead | :unknown; only confirmed-dead (:nodedown/:noproc) unregisters — a transient RPC timeout no longer deletes a live leader's registration (split-brain). The:noregistration path now callsnotify_leadership_change(false)when stepping down, and:check_leadershipno longer schedules elections during the cluster-formation wait.Medium (selected)
handle_params/3:trail_duration,map_center,map_zoomcompared against pre-assign values (URL nav/back-forward actually applies); tracking state reconciled from thecallsignparam; tracking a callsign preserveslat/lng/z/trail/hist;get_assignsdebug event removed (it crashed the LiveView).packet_batcher: same-second distinct packets accepted (dedupe by packet id).historical_loader:total_batchesderived from the zoom packet limit (was hardcoded 1 → high-zoom history stopped at 500 rows); a fully-filtered page now advances loading instead of stalling to the 15s failsafe.mobile_channel: tracked callsign matchesbase_callsigntoo — object/item packets (BALLOON) now reach subscribers.packets::start_time/:end_timehonored (ApiDocsLive's 30-day window works);limit/hours_backclamped at the context boundary (negativeLIMITno longer reaches Postgres).prepared_queries:get_latest_packet_for_callsignorders byreceived_at DESConly — newest packet wins regardless of position.partition_manager:packets_defaultrows past retention are deleted (protected callsigns preserved first);packets_preserveddedupes on composite(id, received_at)— new migration20260920000000_packets_preserved_composite_identitydrops the id-only unique index and adds the composite one.application:cluster_enabled+ empty topologies logs a warning and falls back to the standaloneIsSupervisor;RateLimiterstarts beforeEndpoint.connection_monitor: registering pids are monitored (:DOWNdecrements — abnormal LiveView exits no longer leak the count);check_loadgathers stats in a Task so the GenServer stays responsive;scheduler_wall_timeenabled so the CPU fallback works.circuit_breaker:record_successonly closes from:half_open; a single probe is admitted while half-open.device_cache: refresh casts are dropped while a load is in flight (no more full-table SELECT stampede).error_notifier: mail delivered viaBroadcastTaskSupervisoroff the telemetry handler (sync when the Swoosh test adapter is configured).device_identification: an empty/malformed feed returns{:error, :empty_device_payload}instead of wiping the devices table.is.ex: liveness/keepalive timers carry tokens so stale in-flight messages are ignored; stats count parsed packets not TCP chunks;get_statususes the exit-safe stats path;admit_bad_packetsurvives a missing rate-limit ETS table;reset_failurekeeps the actually-connected server.broadcast_task_supervisor: the atomics slot is released whenstart_childexits (:noproc).packet_consumer_pool: per-consumermax_demandclamped to ≥ 1.map.ts:lat/lng == 0accepted;removeMarkerWithoutTrailunregisters from OMS; stale_leaflet_idno longer detaches the container; init-retry timers cleared on destroy and gated onisDestroyed; antimeridian pruning wraps marker lng into the bounds range; long-press converts viewport→container coords;updateMarkerwrites back tomarkerStates.weather_charts:updated()guarded until the bundle loads; chart destroyed only after new data validates.app.ts: failed bundle loads reset the loading flag and keep queued callbacks so remounts retry.error_boundary.ts: attribution checks the event target / composedPath against the boundary element.map_drawer.ts: document-level Tab trap redirects focus into the drawer.Low
map.ts:callsignIndexcleared on historical reload;highlight_packetfallback mapsMarkerState→MarkerDatacorrectly.endpoint::user_agentadded to mobile socketconnect_info.user_session_controller: partialuserparams → clean 400.error_boundarycomponent: CSP-safe reload link replaces inlineonclick.aprs_symbol: packet-controlled symbol table/code (and callsign) escaped beforeinnerHTML.callsign_view: merged packet list sorts by real timestamps.cache:ttl <= 0does not store.spatial_pubsub: efficiency metric clamped ≥ 0; unparseable coords skipped instead of broadcasting to Null Island.geo_utils: haversineaclamped to[0,1];maidenhead: boundary/out-of-domain inputs handled.Verification
mix test: 2273/2273 (25 doctests, 27 properties)mix credo --strict: cleanmix format: clean55634ad0Notes
20260920000000_packets_preserved_composite_identityis required for the newON CONFLICT (id, received_at)— it dropspackets_preserved_id_indexand adds the composite unique index.is_test.exs(socket identity, tagged timers, per-packet stats),prepared_queries_test.exs(newest-wins ordering),historical_loader_test.exs(batch counts),cache_test.exs,device_cache_test.exs,device_identification_test.exs,connection_monitor_test.exs(async check_load),circuit_breaker_test.exs(single half-open probe),get_assignstests removed.docs/tracked-callsign-behavior.mdupdated for the new ordering.🤖 Skippy PR review
4 findings — 2 blocking before merge.
lib/aprsme/packets/prepared_queries.ex:35priv/repo/migrations/20260920000000_packets_preserved_composite_identity.exs:30lib/aprsme/error_notifier.ex:54lib/aprsme_web/aprs_symbol.ex:203Reviewed
55634ad0c61c. Commentskippy reviewto re-run.@ -44,0 +51,4 @@if test_mailer?() domaybe_send_error_email(occurrence)elseAprsme.BroadcastTaskSupervisor.async_execute(fn -> maybe_send_error_email(occurrence) end)🔵 Suggestion — Saturated broadcast pool silently swallows the error email
async_execute/1sheds work at its concurrency ceiling and returns:dropped(documented inAprsme.BroadcastTaskSupervisor); the return value is discarded here, so during an error storm, exactly when notifications matter, a first-occurrence email can vanish with no log.occurrence_count == 1is never retried.Log the non-started result, e.g.
case Aprsme.BroadcastTaskSupervisor.async_execute(...) do {:ok, _} -> :ok; other -> Logger.error("Error email not dispatched: #{inspect(other)}") end, or send synchronously on that path.@ -36,3 +35,1 @@desc: fragment("CASE WHEN ? IS NULL THEN 0 ELSE 1 END", p.location),desc: p.received_at],order_by: [desc: p.received_at],🟡 Warning — Newest-wins ordering breaks map centring for tracked callsigns
get_latest_packet_for_callsign/1no longer prefers a positioned row, so it can now return a status/object/item packet whoselat/lonare nil (the select pulls them fromlocation). Every caller that centres on the tracked station needs a position, and all of them silently no-op on nil:Navigation.handle_callsign_tracking/4,Index.center_on_latest_packet/5andEvents.zoom_to_latest_packet/2. The live path still disagrees with the query:Index.preferred_tracked_packet/2explicitly prefersCoordinateUtils.has_position_data?.Concretely: opening
/W5ISP-1(search, refresh, or back/forward) no longer centres or zooms the map whenever that station's newest packet is not a position packet, which is normal for status/object-heavy stations.Keep newest-wins for display, but give the centring paths a positioned fallback, e.g.
Packets.get_latest_positioned_packet_for_callsign/1(same query pluswhere: not is_nil(p.location)) used byhandle_callsign_tracking/4,center_on_latest_packet/5andzoom_to_latest_packet/2.@ -198,3 +202,2 @@background-repeat: no-repeat, no-repeat;image-rendering: pixelated;" title="#{symbol_table}#{symbol_code}">" title="#{title}">🔵 Suggestion — Overlay branch lost image-rendering: pixelated
The escaping change also removed
image-rendering: pixelated;from the overlay branch only. The non-overlay branch below (line 215) still sets it, as doesrender_style/3, so overlay symbols (tablesA-Z/0-9) are now smooth-scaled off the sprite sheet while every other sprite stays crisp. Restore the declaration if the removal was not deliberate.@ -0,0 +27,4 @@enddef down docreate unique_index(:packets_preserved, [:id])🟡 Warning — down/0 cannot run once the archive holds repeat observations
upwidens the idempotency key to(id, received_at)precisely sopackets_preservedcan keep the same raw packet re-received at a different timestamp.downthen recreates the id-only unique index, and it is the first statement in the rollback: as soon as one repeat exists,create unique_index(:packets_preserved, [:id])raises a duplicate-key error andmix ecto.rollbackfails, leaving the DB on the new schema with no way back.Either dedupe first in
down(keep the newestreceived_atperid, which is an explicit data loss), or drop the id-only index recreation and make the function raise/document that the migration is not reversible.Resolved 4 of 4 earlier findings in
0529d68:prepared_queries.ex+ the three centring paths (Navigation.handle_callsign_tracking/4,Index.center_on_latest_packet/5,Events.zoom_to_latest_packet/2) - positioned-packet fallback added.packets_preservedmigrationdown/0- repeats are deduped (newestreceived_atperid) before the id-only index is recreated;received_atisNOT NULLthroughLIKE packets, so no NULL duplicate can slip past thea.received_at < b.received_atpredicate.error_notifier.ex- a:dropped/{:error, _}dispatch is now logged;async_execute/1does return{:ok, pid}on success, so the newcaseis correct.aprs_symbol.ex-image-rendering: pixelated;restored on the overlay branch.Nothing new in
55634ad..0529d68. 0 still open.Reviewed
0529d68c6c7b.