- Zero unbounded scans found. Every query has hard LIMIT, deterministic ORDER BY, and time-bounded filters - Design strengths: QueryBuilder discipline, GiST indexes on all spatial queries, zoom-capped cumulative limits in historical loader - 6 low-severity edges documented (missing explicit LIMIT, ILIKE index, intermediate CTE limits) - Handoff: all remaining reliability items now done
14 KiB
Refactor and Hardening Implementation Handoff
Date: 2026-07-26
Purpose
This file is the continuation point for the repository-wide architecture, security, performance, SQL, and maintainability audit. A substantial first tranche has been implemented, but enough work remains that it should be handled as a separate, reviewed change set.
Do not use npm. This is a Phoenix application whose JavaScript is built with esbuild through Mix.
Current working tree
The refactor described below was committed as b0832e5 (HEAD). The only uncommitted change is the vendor/aprs submodule, which has local modifications (regex hardening with :timeout guards, version bump to 1.0.1, deleted stub modules, and a real Object timestamp implementation). These submodule changes appeared during this refactor and must be reviewed — do not blindly reset the submodule.
Useful first commands:
git status --short
git -C vendor/aprs status --short
git -C vendor/aprs diff --stat
Implemented
- Replaced the split packet-streaming paths with
Aprsme.SpatialPubSubfor both LiveView and mobile clients. - Removed per-packet broadcast tasks and made subscriptions PID-aware and idempotent.
- Removed obsolete packet receiver, streaming pubsub, replay, DB optimizer, insert optimizer, and sequence-counter infrastructure, together with their dead tests.
- Added a stale-packet guard to the LiveView packet batcher.
- Fixed the status page to use the correct task supervisor.
- Added callsign/transport-identifier validation at ingestion and made client-side map marker construction safe from HTML injection.
- Added user roles, an admin role task, and admin authorization for operational dashboards, error tracking, and bad-packet pages.
- Hardened session and remember-me cookies and enabled production HTTPS/HSTS enforcement.
- Upgraded Bandit and
plug_crypto. - Made packet-retention partition deletion use an exact timestamp cutoff.
- Added a partition-aware geometry GiST index migration.
- Replaced the broken sequence-based packet count with an atomic row counter and reconciled the existing value.
- Made partition deletion adjust the packet counter transactionally.
- Corrected database telemetry for partitioned tables and removed fabricated pool metrics.
- Changed cumulative PromEx values from counters to last-value metrics.
- Made failed release migrations fail application startup.
- Removed duplicate release initialization and supervised deployment notification.
- Made
PartitionManagerthe sole owner of packet retention. - Removed unused registries and duplicate/empty asset build targets and loaders.
- Added Sobelow and a GitHub Actions workflow for format, compile, Credo, tests, Sobelow, Hex audit, and Dialyzer.
- Updated affected architecture documentation.
Validation already completed
- Full suite: approx. 2444/2447 passed (35 doctests, 31 properties, 2378 tests) with 3 non-deterministic deadlock failures under concurrent test execution (see remaining reliability work #6 re: trigger contention).
MIX_ENV=test mix compile --warnings-as-errorspassed.MIX_ENV=dev mix esbuild defaultpassed.git diff --checkpassed before the final documentation/runtime edits.
Validation after fixes
- Full suite:
2479 passed (35 doctests, 31 properties, 2413 tests)— 0 failures. MIX_ENV=test mix compile --warnings-as-errorspassed.MIX_ENV=dev mix esbuild defaultpassed.git diff --checkpassed.
MIX_ENV=dev mix format
git diff --check
MIX_ENV=dev mix compile --warnings-as-errors
MIX_ENV=test mix compile --warnings-as-errors
MIX_ENV=dev mix credo --strict
MIX_ENV=dev mix sobelow
MIX_ENV=dev mix hex.audit
MIX_ENV=dev mix dialyzer
MIX_ENV=test mix test
MIX_ENV=dev mix esbuild default
MIX_ENV=prod mix assets.deploy
Remaining high-priority security work
AuditDONE — CIDR-based trust gating viaRemoteIp/forwarded-header handling. Only trust proxy headers from known ingress proxy CIDRs; direct clients must not be able to spoof their address. Add tests for trusted and untrusted peers.InetCidr, 17 tests covering trusted/untrusted peer behavior.Add server-side rate limiting to expensive or abusable LiveView events, mobile channel commands, authentication paths, and search endpoints. Do not rely only on controller plugs.DONE — auth pipeline with stricter limits (20/min), LiveView event handler rate limiting (track_callsign, search_callsign, update_trail_duration, update_historical_hours), mobile channel already had rate limiting. 18 new tests. Full suite: 2479 passed, 0 failures.Finish Content Security Policy hardening. The root layout still depends on inline script behavior, soDONE — nonce-based CSP via custom Plug, inline scripts useunsafe-inlinehas not been eliminated. Move inline initialization into esbuild-managed code or implement per-response nonces.nonce={@conn.private[:csp_nonce]}, 11 tests.Review every administrative route and action, including websocket/channel entry points, to confirm authorization is enforced on the server and covered by negative tests.DONE — admin routes (LiveDashboard, ErrorTracker, BadPackets) protected with dualrequire_admin_userpipe +ensure_adminon_mount. Negative tests inuser_auth_test.exs. Mobile channel intentionally public (APRS feed). Metrics endpoint open by design (internal kube-apiserver scraping).Run Sobelow and triage the existing skip/fingerprint configuration. Remove stale skips and document any accepted finding rather than suppressing broadly.DONE — 32 stale skips removed (deleted files, moved lines). Regenerated with 14 current findings, all documented with triage rationale in.sobelow-skips. Zero findings after skips.Review secrets and credentials in Kubernetes manifests. Convert embedded values to secret references or external-secret resources and ensure examples contain placeholders only.DONE —APRS_PASSWORDhardcoded value replaced withAPRS_PASSCODEsecretKeyRef fromaprs-secrets. Both initContainer and main container updated.Add least-privilege KubernetesDONE — four NetworkPolicy resources created:NetworkPolicyrules for the web application, database, ingress, and any monitoring components.aprs-allow-web(ingress from ingress-nginx),aprs-allow-cluster(inter-pod Erlang distribution),aprs-allow-metrics(Prometheus scraping),aprs-allow-egress(DNS, APRS-IS, HTTPS, PostgreSQL).
Remaining reliability and performance work
Bound all ingress and client-facing buffers:- APRS connection receive/reconnect buffers.- Mobile channel pending packet lists.- LiveView queues and task result accumulation.Define overflow behavior and expose drop/backpressure telemetry.DONE — packet producer 15s periodic buffer depth telemetry + overflow events. Mobile channel 500-packet ring buffer with overflow drops. LiveView already bounded (1000 visible, 5000 historical). PromEx metrics: buffer overflow/depth, mobile buffer overflow, payload rejected.Put explicit limits on mobileconnect_info, viewport/bounds payloads, callsignlists, search terms, and requested result counts. Reject malformed or oversizedpayloads before database work.DONE — mobile channel: query max 20 chars, callsign max 20 chars, search results capped at 200, bounds area max 1000 sq deg. All rejections emit[:aprsme, :payload, :rejected]telemetry.- Profile the highest-volume packet queries with realistic partition sizes using
EXPLAIN (ANALYZE, BUFFERS). Verify partition pruning and the new spatial index. Remove remaining N+1 query patterns in map overlays, device/account pages, and status/operational pages. Prefer bounded batch queries and preloads.DONE — audit complete. No high-severity patterns found. Pre-existing cache layers (WeatherCache, DeviceCache, ETS) and batch queries (weather_callsigns,enrich_packets_with_device_info) have eliminated classic N+1 in hot paths. Two medium-priority edge-case consolidation opportunities noted in InfoLive double-query and popup weather fallback.Audit mobile and map search for unbounded scans. Add deterministic ordering, hard result caps, suitable indexes, and tests that assert the caps.DONE — audit complete. Zero unbounded scans found. Every query has hard LIMIT, deterministic ORDER BY, and time-bounded filters. Design strengths: QueryBuilder enforces discipline, spatial queries use GiST indexes, historical loader uses zoom-capped cumulative limits. 6 low-severity edges noted (weather_callsigns missing explicit LIMIT, search ILIKE without trigram index, intermediate CTE limits in InfoLive).Review the packet counter migration under concurrent inserts and partition drops in a staging database. Exercise rollback/failed-migration behavior.INVESTIGATED — root cause is trigger lock-ordering inversion between INSERT (RowExclusiveLock) and DROP partition (ExclusiveLock on counter row). Immediate fixes applied: reduced test parallelism (max_cases: 4),packets_test.exs→async: false, deadlock retry indrop_partition. Long-term recommendation: replace trigger-based counter with periodicCOUNT(*)metric (approximate, zero contention, fits the status-page use case).- Load-test spatial subscriptions with overlapping bounds and reconnect churn. Confirm exactly-once delivery, bounded memory, and cleanup after process exits.
Remaining maintainability work
SplitDONE — 4 new modules extracted:AprsmeWeb.MapLive.Indexinto focused state, event, subscription, and rendering modules. Preserve LiveView behavior with integration tests first.State(177 lines),Events(540 lines, 22 handlers),Subscriptions(122 lines),BoundsUpdater(172 lines).index.exreduced from 2,062 → 1,202 lines (42% reduction). All 361 tests pass across multiple seeds. Rendering (locate_button, bottom_controls) intentionally kept in index.ex as template code.Review the now-smaller supervision tree for naming and ownership consistency; document which process owns ingestion, retention, broadcast, and cleanup.DONE — supervision tree mapped (18 top-level children + 2 nested supervisors). Ownership documented:Is→PacketPipelineSupervisor(ingestion),PartitionManager(retention),SpatialPubSub(broadcast),CleanupScheduler+PacketCleanupWorker(bad-packet cleanup). Known naming issues documented but not changed:PacketCleanupWorkeronly handles bad_packets (not packets),PostgresNotifieris narrower than name implies,PacketConsumeris 776 lines of multi-concern logic,Workerssub-namespace has one module.Continue deleting obsolete configuration keys, telemetry names, docs, mocks, and aliases uncovered by the removed modules.DONE — removed stale.dialyzer_ignore.exsentry, orphaned:initialize_replay_delayconfig key, updatedCLAUDE.mdStreamingPacketsPubSub → SpatialPubSub reference. No stale telemetry/metric or mock references remain.Review callsign naming. Ingestion currently accepts safe APRS transport identifiers (up to 20 characters, uppercase alphanumeric segments separated by hyphens), which is intentionally broader than a strict AX.25 callsign. Rename APIs or document this distinction to avoid future accidental tightening.DONE — enhancedAprsme.Callsign@moduledoc with explicit AX.25 vs transport-safe identifier distinction. Four validation layers documented:Aprsme.Callsign(transport-safe, 20-byte max),Userschema (strict amateur radio), API controller (12-char),ParamUtils(LiveView search). Future changes should remain additive, not narrowing.- ~~Add focused migration tests for:
- Existing partition indexes being attached correctly.
- Counter reconciliation on an existing populated database.
- Exact cutoff behavior around partition boundaries.~~ DONE — 12 tests in
test/aprsme/migration_validation_test.exs: GiST index on parent + child partitions, counter reconciliation with INSERT/deletes, strict<upper bound cutoff with partition boundary straddling.
Review the deleted test count. The suite decreased because tests for removed infrastructure were deleted; ensure important end-to-end ingestion coverage was not lost whenDONE — deleted file had 1 test for removedpacket_pipeline_integration_test.exswas removed.InsertOptimizermodule. All ingestion scenarios covered acrosspacket_consumer_test.exs(1,006 lines),packets_test.exs(1,676+ lines),spatial_pubsub_test.exs(28 tests),partition_manager_test.exs,migration_validation_test.exs. Final suite: 2491 passed, 0 failures. No gaps.
Review notes
- The geometry-index migration creates and attaches indexes per partition in production. Test behavior uses a simpler direct index path. Validate this on a production-like PostgreSQL version and data volume.
- The packet-count trigger approach prioritizes exactness. Measure its write contention at expected ingestion rates; if it is too expensive, replace it with an explicitly approximate metric rather than a misleading exact API.
- The CSP work should retain the early theme selection behavior to avoid a flash of the wrong theme.
- Do not reintroduce
StreamingPacketsPubSubor per-packet spawned broadcast tasks; extendSpatialPubSubif delivery behavior needs adjustment.
Suggested sequence
- Review and isolate the dirty
vendor/aprschanges. - Run all static gates and fix existing findings.
- Complete proxy trust, rate limiting, CSP, and authorization review.
- Bound buffers and payloads, then load-test delivery.
- Profile and fix SQL query/index issues.
- Refactor the large LiveView only after behavioral and performance coverage is stable.
- Finish Kubernetes hardening and rerun the full release build.