clean up warnings

This commit is contained in:
Graham McIntire 2025-07-16 08:55:46 -05:00
parent 46340e008a
commit 566198afa7
No known key found for this signature in database
41 changed files with 138 additions and 580 deletions

View file

@ -2,5 +2,6 @@
import_deps: [:ecto, :ecto_sql, :phoenix, :stream_data], import_deps: [:ecto, :ecto_sql, :phoenix, :stream_data],
subdirectories: ["priv/*/migrations"], subdirectories: ["priv/*/migrations"],
plugins: [Phoenix.LiveView.HTMLFormatter, Styler], plugins: [Phoenix.LiveView.HTMLFormatter, Styler],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"],
line_length: 120
] ]

View file

View file

@ -1,221 +0,0 @@
# INSERT Performance Optimizations for Slow Queries
## Problem Analysis
The server was experiencing slow INSERT queries with durations of 6+ seconds:
```
LOG: duration: 6069.632 ms execute ecto_insert_all_packets: INSERT INTO "packets"
```
## Root Causes Identified
1. **Heavy Index Maintenance**: Multiple indexes were being updated on each INSERT
2. **Inefficient Batch Processing**: Excessive overhead in packet preparation
3. **Suboptimal Batch Sizes**: Fixed batch sizes not adapting to system load
4. **Redundant Field Processing**: Expensive operations during INSERT preparation
## Optimizations Implemented
### 1. Fast Packet Preparation ✅
**File**: `/Users/graham/dev/aprs.me/lib/aprsme/packet_consumer.ex`
**Created optimized packet preparation pipeline**:
```elixir
# Old approach - expensive processing
defp prepare_packet_for_insert(packet_data) do
# ... extensive processing including data extraction,
# normalization, sanitization, etc.
end
# New approach - minimal essential processing
defp prepare_packet_for_insert_fast(packet_data, current_time) do
attrs = if is_struct(packet_data), do: Map.from_struct(packet_data), else: packet_data
attrs
|> Map.put(:received_at, current_time)
|> Map.put(:inserted_at, current_time)
|> Map.put(:updated_at, current_time)
|> extract_essential_fields()
|> create_location_geometry_fast()
|> validate_essential_fields()
end
```
**Benefits**:
- ~70% reduction in packet preparation time
- Single timestamp calculation per batch
- Essential fields only extraction
- Fast validation with pattern matching
### 2. INSERT Performance Optimizer ✅
**File**: `/Users/graham/dev/aprs.me/lib/aprsme/performance/insert_optimizer.ex` (NEW)
**Adaptive batch sizing based on INSERT performance**:
```elixir
# Dynamic batch size optimization
defp calculate_optimal_batch_size(avg_throughput, avg_duration, current_batch_size) do
cond do
# If throughput is low and duration is high, reduce batch size
avg_throughput < 50 and avg_duration > 5000 ->
max(@min_batch_size, round(current_batch_size * 0.8))
# If throughput is good and duration is acceptable, increase batch size
avg_throughput > 200 and avg_duration < 2000 ->
min(@max_batch_size, round(current_batch_size * 1.2))
true -> current_batch_size
end
end
```
**Features**:
- Monitors INSERT throughput (packets/second)
- Adjusts batch size based on performance
- Optimizes INSERT options (returning: false, on_conflict: :nothing)
- Telemetry integration for monitoring
### 3. Optimized INSERT Options ✅
**Before**:
```elixir
Repo.insert_all(Aprsme.Packet, valid_packets, returning: [:id])
```
**After**:
```elixir
insert_options = Aprsme.Performance.InsertOptimizer.get_insert_options()
Repo.insert_all(Aprsme.Packet, valid_packets, insert_options)
# Options include:
%{
returning: false, # Don't return IDs unless needed
on_conflict: :nothing, # Skip conflicts instead of raising
timeout: 15_000 # Appropriate timeout
}
```
**Benefits**:
- Eliminates unnecessary ID return processing
- Handles conflicts gracefully
- Prevents timeout issues
### 4. Index Optimization for INSERT Performance ✅
**File**: `/Users/graham/dev/aprs.me/priv/repo/migrations/20250714210000_optimize_insert_performance.exs`
**Removed heavy indexes that slow INSERTs**:
```sql
-- Dropped expensive covering index
DROP INDEX packets_sender_received_covering_idx;
-- Replaced with lighter, more selective indexes
CREATE INDEX packets_weather_selective_idx ON packets(received_at DESC)
WHERE data_type IN ('weather', 'Weather', 'WX', 'wx');
CREATE INDEX packets_device_recent_idx ON packets(device_identifier, received_at DESC)
WHERE device_identifier IS NOT NULL;
CREATE INDEX packets_location_selective_idx ON packets USING GIST (location)
WHERE has_position = true;
```
**Benefits**:
- Reduced index maintenance overhead during INSERTs
- Maintained query performance for common patterns
- Selective indexes only on relevant data
### 5. Batch Processing Improvements ✅
**Enhanced batch processing**:
```elixir
# Old - fixed batch size
|> Enum.chunk_every(50)
# New - adaptive batch sizing
batch_size = Aprsme.Performance.InsertOptimizer.get_optimal_batch_size()
|> Enum.chunk_every(batch_size)
```
**Optimized reduce operation**:
```elixir
# Single-pass validation and preparation
defp prepare_packets_batch(packets, current_time) do
packets
|> Enum.reduce({[], 0}, fn packet_data, {valid_acc, invalid_count} ->
case prepare_packet_for_insert_fast(packet_data, current_time) do
nil -> {valid_acc, invalid_count + 1}
attrs -> {[attrs | valid_acc], invalid_count}
end
end)
end
```
### 6. Performance Monitoring & Telemetry ✅
**Added comprehensive INSERT performance metrics**:
```elixir
# Telemetry events
:telemetry.execute([:aprsme, :insert_optimizer, :batch_size], %{value: new_batch_size})
:telemetry.execute([:aprsme, :insert_optimizer, :throughput], %{value: avg_throughput})
:telemetry.execute([:aprsme, :insert_optimizer, :duration], %{value: avg_duration})
```
**LiveDashboard integration**:
- INSERT throughput monitoring (packets/second)
- Batch size optimization tracking
- Duration trend analysis
- Performance optimization events
## Performance Impact
### Before Optimization:
- INSERT duration: 6+ seconds
- Fixed batch size: 50 packets
- Heavy index maintenance
- Extensive packet processing overhead
### After Optimization:
- **Expected INSERT duration**: 1-2 seconds (70% improvement)
- **Adaptive batch size**: 100-500 packets based on load
- **Reduced index overhead**: Selective indexes only
- **Minimal processing**: Essential fields only during INSERT
## Implementation Status
**Fast packet preparation** - Implemented with pattern matching optimization
**INSERT performance optimizer** - Adaptive batch sizing with telemetry
**Optimized INSERT options** - Reduced returning overhead
**Index optimization** - Selective indexes for better INSERT performance
**Batch processing improvements** - Single-pass validation and preparation
**Performance monitoring** - Comprehensive telemetry integration
**All tests passing** - 351 tests verified
## Monitoring
Use the following telemetry metrics to monitor INSERT performance:
1. **`aprsme.insert_optimizer.throughput`** - Packets per second throughput
2. **`aprsme.insert_optimizer.duration`** - INSERT duration per batch
3. **`aprsme.insert_optimizer.batch_size`** - Current optimized batch size
4. **`aprsme.insert_optimizer.optimizations`** - Number of adjustments made
## Next Steps
1. **Monitor production performance** - Watch INSERT durations and throughput
2. **Tune thresholds** - Adjust optimization thresholds based on real data
3. **Consider connection pooling** - If needed, optimize database connections
4. **Index maintenance** - Monitor if additional index optimizations are needed
## Files Modified
1. **`lib/aprsme/packet_consumer.ex`** - Fast packet preparation and batch processing
2. **`lib/aprsme/performance/insert_optimizer.ex`** - NEW: Adaptive INSERT optimization
3. **`priv/repo/migrations/20250714210000_optimize_insert_performance.exs`** - Index optimization
4. **`lib/aprsme/application.ex`** - Added InsertOptimizer to supervision tree
5. **`lib/aprsme_web/telemetry.ex`** - INSERT performance telemetry
## Expected Results
These optimizations should reduce INSERT query times from 6+ seconds to 1-2 seconds, representing a **70% performance improvement** in packet insertion workloads.

View file

@ -221,7 +221,11 @@ defmodule Aprsme.CircuitBreaker do
} }
end end
defp calculate_current_state(%{state: :open, last_failure_time: last_failure_time, recovery_timeout: recovery_timeout}) do defp calculate_current_state(%{
state: :open,
last_failure_time: last_failure_time,
recovery_timeout: recovery_timeout
}) do
if last_failure_time && DateTime.diff(DateTime.utc_now(), last_failure_time, :millisecond) >= recovery_timeout do if last_failure_time && DateTime.diff(DateTime.utc_now(), last_failure_time, :millisecond) >= recovery_timeout do
:half_open :half_open
else else

View file

@ -56,7 +56,7 @@ defmodule Aprsme.EncodingUtils do
[codepoint] -> [codepoint] ->
# Allow printable characters and common whitespace # Allow printable characters and common whitespace
# Remove C0 controls (0x00-0x1F except tab, newline, carriage return) # Remove C0 controls (0x00-0x1F except tab, newline, carriage return)
# Remove C1 controls (0x80-0x9F) # Remove C1 controls (0x80-0x9F)
# Remove DEL (0x7F) # Remove DEL (0x7F)
cond do cond do
# Tab # Tab
@ -138,8 +138,9 @@ defmodule Aprsme.EncodingUtils do
# Helper to check if a float is finite (not infinity or NaN) # Helper to check if a float is finite (not infinity or NaN)
defp finite_float?(float) when is_float(float) do defp finite_float?(float) when is_float(float) do
# NaN != NaN # In Elixir, we can't have infinity or NaN in regular floats
float != :infinity and float != :neg_infinity and float == float # This function is kept for defensive programming
true
end end
defp finite_float?(_), do: false defp finite_float?(_), do: false

View file

@ -330,10 +330,10 @@ defmodule Aprsme.ErrorHandler do
defp categorize_and_handle_error(error, context) do defp categorize_and_handle_error(error, context) do
cond do cond do
is_database_error?(error) -> database_error?(error) ->
handle_database_error(error, context) handle_database_error(error, context)
is_network_error?(error) -> network_error?(error) ->
handle_network_error(error, :unknown_service, context) handle_network_error(error, :unknown_service, context)
true -> true ->
@ -349,19 +349,19 @@ defmodule Aprsme.ErrorHandler do
end end
end end
defp is_database_error?(%CastError{}), do: true defp database_error?(%CastError{}), do: true
defp is_database_error?(%Ecto.ConstraintError{}), do: true defp database_error?(%Ecto.ConstraintError{}), do: true
defp is_database_error?(%Postgrex.Error{}), do: true defp database_error?(%Postgrex.Error{}), do: true
defp is_database_error?(%DBConnection.ConnectionError{}), do: true defp database_error?(%DBConnection.ConnectionError{}), do: true
defp is_database_error?(%Ecto.QueryError{}), do: true defp database_error?(%Ecto.QueryError{}), do: true
defp is_database_error?(_), do: false defp database_error?(_), do: false
defp is_network_error?(%Req.TransportError{}), do: true defp network_error?(%Req.TransportError{}), do: true
defp is_network_error?(%{reason: :circuit_open}), do: true defp network_error?(%{reason: :circuit_open}), do: true
defp is_network_error?(%{reason: :timeout}), do: true defp network_error?(%{reason: :timeout}), do: true
defp is_network_error?(%RuntimeError{message: "circuit_open"}), do: true defp network_error?(%RuntimeError{message: "circuit_open"}), do: true
defp is_network_error?(%RuntimeError{message: "timeout"}), do: true defp network_error?(%RuntimeError{message: "timeout"}), do: true
defp is_network_error?(_), do: false defp network_error?(_), do: false
defp store_bad_packet(packet_data, reason) do defp store_bad_packet(packet_data, reason) do
# This would store the bad packet for later analysis # This would store the bad packet for later analysis

View file

@ -402,7 +402,12 @@ defmodule Aprsme.Is do
case connect_to_aprs_is(state.server, state.port) do case connect_to_aprs_is(state.server, state.port) do
{:ok, socket} -> {:ok, socket} ->
case send_login_string(socket, state.login_params.user_id, state.login_params.passcode, state.login_params.filter) do case send_login_string(
socket,
state.login_params.user_id,
state.login_params.passcode,
state.login_params.filter
) do
:ok -> :ok ->
Logger.info("Successfully reconnected to APRS-IS") Logger.info("Successfully reconnected to APRS-IS")
timer = create_timer(@aprs_timeout) timer = create_timer(@aprs_timeout)

View file

@ -192,7 +192,9 @@ defmodule Aprsme.PacketReplay do
] ]
# Log the start of replay with map bounds # Log the start of replay with map bounds
Logger.info("Starting packet replay for user #{state.user_id} in map area #{inspect(state.bounds)} for the last hour") Logger.info(
"Starting packet replay for user #{state.user_id} in map area #{inspect(state.bounds)} for the last hour"
)
# Send notification to client that replay is starting # Send notification to client that replay is starting
Endpoint.broadcast(state.replay_topic, "replay_started", %{ Endpoint.broadcast(state.replay_topic, "replay_started", %{

View file

@ -30,7 +30,9 @@ defmodule Aprsme.Packets do
packet_attrs = packet_attrs =
sanitized_packet_data sanitized_packet_data
|> Packet.extract_additional_data(sanitized_packet_data[:raw_packet] || sanitized_packet_data["raw_packet"] || "") |> Packet.extract_additional_data(
sanitized_packet_data[:raw_packet] || sanitized_packet_data["raw_packet"] || ""
)
|> normalize_packet_attrs() |> normalize_packet_attrs()
|> set_received_at() |> set_received_at()
|> patch_lat_lon_from_data_extended() |> patch_lat_lon_from_data_extended()

View file

@ -195,17 +195,8 @@ defmodule AprsmeWeb.CoreComponents do
{@title} {@title}
</p> </p>
<p class="mt-2 text-[0.8125rem] leading-5">{msg}</p> <p class="mt-2 text-[0.8125rem] leading-5">{msg}</p>
<button <button :if={@close} type="button" class="group absolute top-2 right-1 p-2" aria-label={gettext("close")}>
:if={@close} <.icon name="x-mark" outline={false} class="h-5 w-5 stroke-current opacity-40 group-hover:opacity-70" />
type="button"
class="group absolute top-2 right-1 p-2"
aria-label={gettext("close")}
>
<.icon
name="x-mark"
outline={false}
class="h-5 w-5 stroke-current opacity-40 group-hover:opacity-70"
/>
</button> </button>
</div> </div>
""" """
@ -463,18 +454,10 @@ defmodule AprsmeWeb.CoreComponents do
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden"> <div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg> </svg>
</div> </div>
<ul <ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
tabindex="0"
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
>
<.navigation variant={:vertical} current_user={@current_user} /> <.navigation variant={:vertical} current_user={@current_user} />
</ul> </ul>
</div> </div>
@ -576,11 +559,7 @@ defmodule AprsmeWeb.CoreComponents do
</tr> </tr>
</thead> </thead>
<tbody class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"> <tbody class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700">
<tr <tr :for={row <- @rows} id={"#{@id}-#{safe_row_id(row)}"} class="relative group hover:bg-zinc-50">
:for={row <- @rows}
id={"#{@id}-#{safe_row_id(row)}"}
class="relative group hover:bg-zinc-50"
>
<td <td
:for={{col, i} <- Enum.with_index(@col)} :for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)} phx-click={@row_click && @row_click.(row)}
@ -653,10 +632,7 @@ defmodule AprsmeWeb.CoreComponents do
def back(assigns) do def back(assigns) do
~H""" ~H"""
<div class="mt-16"> <div class="mt-16">
<.link <.link navigate={@navigate} class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700">
navigate={@navigate}
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<.icon name="arrow-left" outline={false} class="w-3 h-3 stroke-current inline" /> <.icon name="arrow-left" outline={false} class="w-3 h-3 stroke-current inline" />
{render_slot(@inner_block)} {render_slot(@inner_block)}
</.link> </.link>

View file

@ -219,8 +219,7 @@
phx-disconnected={show("#disconnected")} phx-disconnected={show("#disconnected")}
phx-connected={hide("#disconnected")} phx-connected={hide("#disconnected")}
> >
Attempting to reconnect Attempting to reconnect <.icon name="arrow-path" outline={true} class="ml-1 w-3 h-3 inline animate-spin" />
<.icon name="arrow-path" outline={true} class="ml-1 w-3 h-3 inline animate-spin" />
</.flash> </.flash>
{@inner_content} {@inner_content}
</div> </div>

View file

@ -206,8 +206,7 @@
phx-disconnected={show("#disconnected")} phx-disconnected={show("#disconnected")}
phx-connected={hide("#disconnected")} phx-connected={hide("#disconnected")}
> >
Attempting to reconnect Attempting to reconnect <.icon name="arrow-path" outline={true} class="ml-1 w-3 h-3 inline animate-spin" />
<.icon name="arrow-path" outline={true} class="ml-1 w-3 h-3 inline animate-spin" />
</.flash> </.flash>
{@inner_content} {@inner_content}
</div> </div>

View file

@ -248,12 +248,7 @@ defmodule AprsmeWeb.ApiDocsLive do
</div> </div>
<div class="alert alert-warning mt-6"> <div class="alert alert-warning mt-6">
<svg <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@ -578,11 +573,7 @@ defmodule AprsmeWeb.ApiDocsLive do
class="input input-bordered flex-1" class="input input-bordered flex-1"
disabled={@loading} disabled={@loading}
/> />
<button <button type="submit" disabled={@loading or String.trim(@test_callsign) == ""} class="btn btn-primary">
type="submit"
disabled={@loading or String.trim(@test_callsign) == ""}
class="btn btn-primary"
>
<%= if @loading do %> <%= if @loading do %>
<span class="loading loading-spinner loading-sm"></span> Testing... <span class="loading loading-spinner loading-sm"></span> Testing...
<% else %> <% else %>

View file

@ -68,12 +68,7 @@
<div class="card-body text-center"> <div class="card-body text-center">
<div class="flex justify-center mb-4"> <div class="flex justify-center mb-4">
<div class="w-16 h-16 bg-success/20 rounded-full flex items-center justify-center"> <div class="w-16 h-16 bg-success/20 rounded-full flex items-center justify-center">
<svg <svg class="w-8 h-8 text-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
class="w-8 h-8 text-success"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"

View file

@ -51,18 +51,8 @@
<div class="card-body"> <div class="card-body">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg <svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
class="h-4 w-4 opacity-70" <path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@ -187,13 +177,7 @@
<div class="card-body"> <div class="card-body">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg <svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@ -275,13 +259,7 @@
<div class="card-body"> <div class="card-body">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg <svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@ -313,10 +291,7 @@
<tr> <tr>
<td class="font-medium"> <td class="font-medium">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<.link <.link navigate={~p"/info/#{ssid_info.callsign}"} class="link link-primary">
navigate={~p"/info/#{ssid_info.callsign}"}
class="link link-primary"
>
{ssid_info.callsign} {ssid_info.callsign}
</.link> </.link>
<%= if ssid_info.packet do %> <%= if ssid_info.packet do %>
@ -385,13 +360,7 @@
<div class="card-body"> <div class="card-body">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg <svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@ -473,13 +442,7 @@
<div class="card-body"> <div class="card-body">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg <svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@ -520,10 +483,7 @@
<%= for station <- @heard_by_stations do %> <%= for station <- @heard_by_stations do %>
<tr> <tr>
<td class="font-medium"> <td class="font-medium">
<.link <.link navigate={~p"/info/#{station.digipeater}"} class="link link-primary">
navigate={~p"/info/#{station.digipeater}"}
class="link link-primary"
>
{station.digipeater} {station.digipeater}
</.link> </.link>
</td> </td>
@ -578,13 +538,7 @@
<div class="card-body"> <div class="card-body">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg <svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"

View file

@ -20,7 +20,7 @@ defmodule AprsmeWeb.LocaleHook do
end end
# Set map_page assign based on the view module # Set map_page assign based on the view module
map_page = is_map_page?(socket, params) map_page = map_page?(socket, params)
{:cont, assign(socket, locale: locale, map_page: map_page)} {:cont, assign(socket, locale: locale, map_page: map_page)}
end end
@ -35,7 +35,7 @@ defmodule AprsmeWeb.LocaleHook do
end end
end end
defp is_map_page?(socket, _params) do defp map_page?(socket, _params) do
# Only check the view module from socket private data # Only check the view module from socket private data
case socket.private[:phoenix_live_view] do case socket.private[:phoenix_live_view] do
%{view: AprsmeWeb.MapLive.Index} -> true %{view: AprsmeWeb.MapLive.Index} -> true

View file

@ -4,6 +4,7 @@ defmodule AprsmeWeb.MapLive.BoundsManager do
""" """
alias AprsmeWeb.Live.Shared.BoundsUtils alias AprsmeWeb.Live.Shared.BoundsUtils
alias AprsmeWeb.Live.Shared.PacketUtils
# Delegate to shared utilities # Delegate to shared utilities
defdelegate calculate_bounds_from_center_and_zoom(center, zoom), to: BoundsUtils defdelegate calculate_bounds_from_center_and_zoom(center, zoom), to: BoundsUtils
@ -19,6 +20,6 @@ defmodule AprsmeWeb.MapLive.BoundsManager do
@spec filter_packets_by_time_and_bounds(map(), map(), DateTime.t()) :: map() @spec filter_packets_by_time_and_bounds(map(), map(), DateTime.t()) :: map()
def filter_packets_by_time_and_bounds(packets, bounds, time_threshold) do def filter_packets_by_time_and_bounds(packets, bounds, time_threshold) do
# Use shared packet utils for this functionality # Use shared packet utils for this functionality
AprsmeWeb.Live.Shared.PacketUtils.filter_packets_by_time_and_bounds(packets, bounds, time_threshold) PacketUtils.filter_packets_by_time_and_bounds(packets, bounds, time_threshold)
end end
end end

View file

@ -157,7 +157,14 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
{lat, lon, _} = get_coordinates(packet) {lat, lon, _} = get_coordinates(packet)
if lat && lon do if lat && lon do
distance_meters = CoordinateUtils.calculate_distance_meters(most_recent_lat, most_recent_lon, lat, lon) distance_meters =
CoordinateUtils.calculate_distance_meters(
most_recent_lat,
most_recent_lon,
lat,
lon
)
# Only show if 10+ meters away # Only show if 10+ meters away
distance_meters >= 10.0 distance_meters >= 10.0
else else
@ -330,7 +337,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
weather_link: weather_link weather_link: weather_link
} }
# Optimize string conversion - avoid intermediate iodata step # Optimize string conversion - avoid intermediate iodata step
popup_data popup_data
|> PopupComponent.popup() |> PopupComponent.popup()
|> Safe.to_iodata() |> Safe.to_iodata()
@ -475,7 +482,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
end end
defp convert_tuples_to_strings(list) when is_list(list) do defp convert_tuples_to_strings(list) when is_list(list) do
# Use Stream for memory efficiency on large lists # Use Stream for memory efficiency on large lists
list list
|> Stream.map(&convert_tuples_to_strings/1) |> Stream.map(&convert_tuples_to_strings/1)
|> Enum.to_list() |> Enum.to_list()

View file

@ -585,7 +585,8 @@ defmodule AprsmeWeb.MapLive.Index do
defp parse_historical_hours(hours), do: SharedPacketUtils.parse_historical_hours(hours) defp parse_historical_hours(hours), do: SharedPacketUtils.parse_historical_hours(hours)
@impl true @impl true
def handle_info({:process_bounds_update, map_bounds}, socket), do: handle_info_process_bounds_update(map_bounds, socket) def handle_info({:process_bounds_update, map_bounds}, socket),
do: handle_info_process_bounds_update(map_bounds, socket)
def handle_info(:initialize_replay, socket), do: handle_info_initialize_replay(socket) def handle_info(:initialize_replay, socket), do: handle_info_initialize_replay(socket)
@ -710,14 +711,8 @@ defmodule AprsmeWeb.MapLive.Index do
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="" crossorigin=""
/> />
<link <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
rel="stylesheet" <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"
/>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"
/>
<script <script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
@ -726,9 +721,7 @@ defmodule AprsmeWeb.MapLive.Index do
</script> </script>
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"> <script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js">
</script> </script>
<script <script src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier-Leaflet/0.2.6/oms.min.js">
src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier-Leaflet/0.2.6/oms.min.js"
>
</script> </script>
<style> <style>
@ -858,19 +851,8 @@ defmodule AprsmeWeb.MapLive.Index do
</div> </div>
</.error_boundary> </.error_boundary>
<button <button class="locate-button" phx-click="locate_me" title={Gettext.gettext(AprsmeWeb.Gettext, "Find my location")}>
class="locate-button" <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="#374151" stroke="none">
phx-click="locate_me"
title={Gettext.gettext(AprsmeWeb.Gettext, "Find my location")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="#374151"
stroke="none"
>
<path d="M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z" /> <path d="M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z" />
</svg> </svg>
</button> </button>
@ -932,10 +914,7 @@ defmodule AprsmeWeb.MapLive.Index do
<!-- Mobile Backdrop --> <!-- Mobile Backdrop -->
<%= if @slideover_open do %> <%= if @slideover_open do %>
<div <div class="fixed inset-0 bg-black bg-opacity-50 z-[999] lg:hidden backdrop-blur-sm" phx-click="toggle_slideover">
class="fixed inset-0 bg-black bg-opacity-50 z-[999] lg:hidden backdrop-blur-sm"
phx-click="toggle_slideover"
>
</div> </div>
<% end %> <% end %>
@ -951,13 +930,7 @@ defmodule AprsmeWeb.MapLive.Index do
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between p-6 border-b border-slate-200 bg-gradient-to-r from-indigo-600 to-purple-600 text-white"> <div class="flex items-center justify-between p-6 border-b border-slate-200 bg-gradient-to-r from-indigo-600 to-purple-600 text-white">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<svg <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@ -1043,12 +1016,7 @@ defmodule AprsmeWeb.MapLive.Index do
<!-- Trail Duration --> <!-- Trail Duration -->
<div class="space-y-4"> <div class="space-y-4">
<label class="block text-sm font-semibold text-slate-700 flex items-center space-x-2"> <label class="block text-sm font-semibold text-slate-700 flex items-center space-x-2">
<svg <svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
class="w-4 h-4 text-emerald-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@ -1083,18 +1051,8 @@ defmodule AprsmeWeb.MapLive.Index do
</option> </option>
</select> </select>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none"> <div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<svg <svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
class="w-4 h-4 text-slate-400" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg> </svg>
</div> </div>
</form> </form>
@ -1135,18 +1093,8 @@ defmodule AprsmeWeb.MapLive.Index do
</option> </option>
</select> </select>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none"> <div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<svg <svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
class="w-4 h-4 text-slate-400" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg> </svg>
</div> </div>
</form> </form>

View file

@ -111,7 +111,7 @@ defmodule AprsmeWeb.MapLive.PacketManager do
visible_packets = get_visible_packets(packet_state) visible_packets = get_visible_packets(packet_state)
{keep_visible, remove_visible} = Enum.split_with(visible_packets, predicate_fn) {keep_visible, remove_visible} = Enum.split_with(visible_packets, predicate_fn)
# Get historical packets and apply predicate # Get historical packets and apply predicate
historical_packets = get_historical_packets(packet_state) historical_packets = get_historical_packets(packet_state)
{keep_historical, remove_historical} = Enum.split_with(historical_packets, predicate_fn) {keep_historical, remove_historical} = Enum.split_with(historical_packets, predicate_fn)

View file

@ -6,6 +6,7 @@ defmodule AprsmeWeb.MapLive.PacketProcessor do
import Phoenix.Component, only: [assign: 3] import Phoenix.Component, only: [assign: 3]
alias Aprsme.GeoUtils alias Aprsme.GeoUtils
alias AprsmeWeb.Live.Shared.BoundsUtils
alias AprsmeWeb.Live.Shared.CoordinateUtils alias AprsmeWeb.Live.Shared.CoordinateUtils
alias AprsmeWeb.Live.Shared.PacketUtils, as: SharedPacketUtils alias AprsmeWeb.Live.Shared.PacketUtils, as: SharedPacketUtils
alias AprsmeWeb.MapLive.PacketUtils alias AprsmeWeb.MapLive.PacketUtils
@ -147,7 +148,7 @@ defmodule AprsmeWeb.MapLive.PacketProcessor do
# Use shared bounds utility # Use shared bounds utility
defp within_bounds?(coords, bounds) do defp within_bounds?(coords, bounds) do
AprsmeWeb.Live.Shared.BoundsUtils.within_bounds?(coords, bounds) BoundsUtils.within_bounds?(coords, bounds)
end end
# Helper to get locale from socket # Helper to get locale from socket

View file

@ -21,9 +21,7 @@
<tr> <tr>
<td> <td>
<.link <.link
navigate={ navigate={~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"}
~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"
}
class="link link-primary font-medium" class="link link-primary font-medium"
> >
{Map.get(packet, :sender, Map.get(packet, "sender", ""))} {Map.get(packet, :sender, Map.get(packet, "sender", ""))}
@ -36,9 +34,7 @@
</td> </td>
<td> <td>
<.link <.link
navigate={ navigate={~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"}
~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"
}
class="link link-primary font-medium" class="link link-primary font-medium"
> >
{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))} {Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}

View file

@ -11,8 +11,8 @@ defmodule AprsmeWeb.Live.Shared.BoundsUtils do
""" """
@spec valid_bounds?(map()) :: boolean() @spec valid_bounds?(map()) :: boolean()
def valid_bounds?(%{north: north, south: south, east: east, west: west}) def valid_bounds?(%{north: north, south: south, east: east, west: west})
when is_number(north) and is_number(south) and is_number(east) and is_number(west) and north <= 90 and south >= -90 and when is_number(north) and is_number(south) and is_number(east) and is_number(west) and north <= 90 and
north > south and east >= -180 and west <= 180, do: true south >= -90 and north > south and east >= -180 and west <= 180, do: true
def valid_bounds?(_invalid_bounds), do: false def valid_bounds?(_invalid_bounds), do: false

View file

@ -85,7 +85,7 @@ defmodule AprsmeWeb.Live.SharedPacketHandler do
device_identifiers = device_identifiers =
packets packets
|> Enum.map(&get_device_identifier/1) |> Enum.map(&get_device_identifier/1)
|> Enum.reject(&is_nil_or_empty/1) |> Enum.reject(&nil_or_empty?/1)
|> Enum.uniq() |> Enum.uniq()
# Batch lookup devices (DeviceCache is already optimized with caching) # Batch lookup devices (DeviceCache is already optimized with caching)
@ -117,9 +117,9 @@ defmodule AprsmeWeb.Live.SharedPacketHandler do
end end
end end
defp is_nil_or_empty(nil), do: true defp nil_or_empty?(nil), do: true
defp is_nil_or_empty(""), do: true defp nil_or_empty?(""), do: true
defp is_nil_or_empty(_), do: false defp nil_or_empty?(_), do: false
@doc """ @doc """
Creates a filter function that checks both callsign and weather data. Creates a filter function that checks both callsign and weather data.

View file

@ -4,6 +4,8 @@ defmodule AprsmeWeb.Live.Shared.PacketUtils do
Used across multiple LiveView modules for consistent packet handling. Used across multiple LiveView modules for consistent packet handling.
""" """
alias AprsmeWeb.Live.Shared.BoundsUtils
@doc """ @doc """
Get unique callsign key from packet. Get unique callsign key from packet.
""" """
@ -81,7 +83,7 @@ defmodule AprsmeWeb.Live.Shared.PacketUtils do
def filter_packets_by_time_and_bounds(packets, bounds, time_threshold) do def filter_packets_by_time_and_bounds(packets, bounds, time_threshold) do
packets packets
|> Enum.filter(fn {_callsign, packet} -> |> Enum.filter(fn {_callsign, packet} ->
AprsmeWeb.Live.Shared.BoundsUtils.within_bounds?(packet, bounds) && BoundsUtils.within_bounds?(packet, bounds) &&
packet_within_time_threshold?(packet, time_threshold) packet_within_time_threshold?(packet, time_threshold)
end) end)
|> Map.new() |> Map.new()

View file

@ -80,8 +80,9 @@ defmodule AprsmeWeb.Live.Shared.ParamUtils do
""" """
@spec finite?(float() | any()) :: boolean() @spec finite?(float() | any()) :: boolean()
def finite?(float) when is_float(float) do def finite?(float) when is_float(float) do
# NaN != NaN # In Elixir, we can't have infinity or NaN in regular floats
float != :infinity and float != :neg_infinity and float == float # This function is kept for API compatibility
true
end end
def finite?(_), do: false def finite?(_), do: false

View file

@ -97,9 +97,7 @@ defmodule AprsmeWeb.StatusLive.ConnectionCardComponent do
<span class="text-sm font-medium opacity-70 mr-2">Last Packet:</span> <span class="text-sm font-medium opacity-70 mr-2">Last Packet:</span>
<span class="text-sm"> <span class="text-sm">
<%= if @aprs_status.packet_stats.last_packet_at do %> <%= if @aprs_status.packet_stats.last_packet_at do %>
{AprsmeWeb.StatusLive.Index.format_time_ago( {AprsmeWeb.StatusLive.Index.format_time_ago(@aprs_status.packet_stats.last_packet_at)}
@aprs_status.packet_stats.last_packet_at
)}
<% else %> <% else %>
<span class="opacity-50">None</span> <span class="opacity-50">None</span>
<% end %> <% end %>

View file

@ -64,7 +64,11 @@ defmodule AprsmeWeb.StatusLive.Index do
def handle_info({:aprs_status_update, status}, socket) do def handle_info({:aprs_status_update, status}, socket) do
# Handle real-time status updates via PubSub # Handle real-time status updates via PubSub
socket = socket =
assign(socket, aprs_status: status, current_time: DateTime.utc_now(), health_score: calculate_health_score(status)) assign(socket,
aprs_status: status,
current_time: DateTime.utc_now(),
health_score: calculate_health_score(status)
)
{:noreply, socket} {:noreply, socket}
end end

View file

@ -18,13 +18,7 @@ defmodule AprsmeWeb.UserConfirmationInstructionsLive do
Enter your email address to receive a new confirmation link Enter your email address to receive a new confirmation link
</p> </p>
<.simple_form <.simple_form :let={_f} for={%{}} as={:user} id="resend_confirmation_form" phx-submit="send_instructions">
:let={_f}
for={%{}}
as={:user}
id="resend_confirmation_form"
phx-submit="send_instructions"
>
<div class="form-control w-full"> <div class="form-control w-full">
<label class="label"> <label class="label">
<span class="label-text">Email</span> <span class="label-text">Email</span>

View file

@ -14,21 +14,11 @@ defmodule AprsmeWeb.UserConfirmationLive do
<h1 class="card-title text-3xl mb-4 justify-center">Confirm your account</h1> <h1 class="card-title text-3xl mb-4 justify-center">Confirm your account</h1>
<p class="text-base-content/70 mb-6">Click the button below to confirm your account</p> <p class="text-base-content/70 mb-6">Click the button below to confirm your account</p>
<.simple_form <.simple_form :let={_f} for={%{}} as={:user} id="confirmation_form" phx-submit="confirm_account">
:let={_f}
for={%{}}
as={:user}
id="confirmation_form"
phx-submit="confirm_account"
>
<input type="hidden" name="user[token]" value={@token} /> <input type="hidden" name="user[token]" value={@token} />
<div class="form-control mt-6"> <div class="form-control mt-6">
<button <button type="submit" class="btn btn-primary w-full" phx-disable-with="Confirming...">
type="submit"
class="btn btn-primary w-full"
phx-disable-with="Confirming..."
>
Confirm my account Confirm my account
</button> </button>
</div> </div>

View file

@ -14,13 +14,7 @@ defmodule AprsmeWeb.UserForgotPasswordLive do
<h1 class="card-title text-3xl mb-4 justify-center">Forgot your password?</h1> <h1 class="card-title text-3xl mb-4 justify-center">Forgot your password?</h1>
<p class="text-base-content/70 mb-6">We'll send a password reset link to your inbox</p> <p class="text-base-content/70 mb-6">We'll send a password reset link to your inbox</p>
<.simple_form <.simple_form :let={_f} id="reset_password_form" for={%{}} as={:user} phx-submit="send_email">
:let={_f}
id="reset_password_form"
for={%{}}
as={:user}
phx-submit="send_email"
>
<div class="form-control w-full"> <div class="form-control w-full">
<label class="label"> <label class="label">
<span class="label-text">Email</span> <span class="label-text">Email</span>

View file

@ -76,11 +76,7 @@ defmodule AprsmeWeb.UserLoginLive do
</div> </div>
<div class="form-control mt-6"> <div class="form-control mt-6">
<button <button type="submit" class="btn btn-primary w-full" phx-disable-with="Signing in...">
type="submit"
class="btn btn-primary w-full"
phx-disable-with="Signing in..."
>
Sign in Sign in
</button> </button>
</div> </div>

View file

@ -105,11 +105,7 @@ defmodule AprsmeWeb.UserRegistrationLive do
</div> </div>
<div class="form-control mt-6"> <div class="form-control mt-6">
<button <button type="submit" class="btn btn-primary w-full" phx-disable-with="Creating account...">
type="submit"
class="btn btn-primary w-full"
phx-disable-with="Creating account..."
>
Create an account Create an account
</button> </button>
</div> </div>

View file

@ -70,10 +70,7 @@ defmodule AprsmeWeb.UserSettingsLive do
placeholder="Enter current password" placeholder="Enter current password"
required required
/> />
<label <label :if={Keyword.has_key?(@email_changeset.errors, :current_password)} class="label">
:if={Keyword.has_key?(@email_changeset.errors, :current_password)}
class="label"
>
<span class="label-text-alt text-error"> <span class="label-text-alt text-error">
{translate_error(Keyword.get(@email_changeset.errors, :current_password))} {translate_error(Keyword.get(@email_changeset.errors, :current_password))}
</span> </span>
@ -143,10 +140,7 @@ defmodule AprsmeWeb.UserSettingsLive do
placeholder="Enter current password" placeholder="Enter current password"
required required
/> />
<label <label :if={Keyword.has_key?(@callsign_changeset.errors, :current_password)} class="label">
:if={Keyword.has_key?(@callsign_changeset.errors, :current_password)}
class="label"
>
<span class="label-text-alt text-error"> <span class="label-text-alt text-error">
{translate_error(Keyword.get(@callsign_changeset.errors, :current_password))} {translate_error(Keyword.get(@callsign_changeset.errors, :current_password))}
</span> </span>
@ -181,11 +175,7 @@ defmodule AprsmeWeb.UserSettingsLive do
<span>Oops, something went wrong! Please check the errors below.</span> <span>Oops, something went wrong! Please check the errors below.</span>
</div> </div>
<input <input type="hidden" name={Phoenix.HTML.Form.input_name(f, :email)} value={@current_email} />
type="hidden"
name={Phoenix.HTML.Form.input_name(f, :email)}
value={@current_email}
/>
<div class="form-control w-full"> <div class="form-control w-full">
<label class="label"> <label class="label">
@ -225,14 +215,9 @@ defmodule AprsmeWeb.UserSettingsLive do
placeholder="Confirm new password" placeholder="Confirm new password"
required required
/> />
<label <label :if={Keyword.has_key?(@password_changeset.errors, :password_confirmation)} class="label">
:if={Keyword.has_key?(@password_changeset.errors, :password_confirmation)}
class="label"
>
<span class="label-text-alt text-error"> <span class="label-text-alt text-error">
{translate_error( {translate_error(Keyword.get(@password_changeset.errors, :password_confirmation))}
Keyword.get(@password_changeset.errors, :password_confirmation)
)}
</span> </span>
</label> </label>
</div> </div>
@ -253,10 +238,7 @@ defmodule AprsmeWeb.UserSettingsLive do
placeholder="Enter current password" placeholder="Enter current password"
required required
/> />
<label <label :if={Keyword.has_key?(@password_changeset.errors, :current_password)} class="label">
:if={Keyword.has_key?(@password_changeset.errors, :current_password)}
class="label"
>
<span class="label-text-alt text-error"> <span class="label-text-alt text-error">
{translate_error(Keyword.get(@password_changeset.errors, :current_password))} {translate_error(Keyword.get(@password_changeset.errors, :current_password))}
</span> </span>

View file

@ -62,12 +62,7 @@
) %> ) %>
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg <svg class="h-4 w-4 text-primary" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
class="h-4 w-4 text-primary"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.75.75 0 00.736-.686L11.477 4.5a.75.75 0 00-1.491-.154L9.477 9.5H9z" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.75.75 0 00.736-.686L11.477 4.5a.75.75 0 00-1.491-.154L9.477 9.5H9z"
@ -84,9 +79,7 @@
<dt class="text-xs font-medium opacity-70">{gettext("Last WX report")}</dt> <dt class="text-xs font-medium opacity-70">{gettext("Last WX report")}</dt>
<dd class="mt-1 text-sm font-semibold"> <dd class="mt-1 text-sm font-semibold">
<%= if dt do %> <%= if dt do %>
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")} ({AprsmeWeb.TimeHelpers.time_ago_in_words( {Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")} ({AprsmeWeb.TimeHelpers.time_ago_in_words(dt)})<br />
dt
)})<br />
<%!-- <span class="text-xs text-gray-500">{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S %Z")} local time at {Map.get(@weather_packet, :region) || Map.get(@weather_packet, "region") || "Unknown location"} [?]</span> --%> <%!-- <span class="text-xs text-gray-500">{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S %Z")} local time at {Map.get(@weather_packet, :region) || Map.get(@weather_packet, "region") || "Unknown location"} [?]</span> --%>
<% else %> <% else %>
{gettext("Unknown")} {gettext("Unknown")}

View file

@ -88,7 +88,10 @@ defmodule AprsmeWeb.Telemetry do
description: "Number of successful inserts per batch" description: "Number of successful inserts per batch"
), ),
summary("aprsme.packet_pipeline.batch.error", unit: :event, description: "Number of errors per batch"), summary("aprsme.packet_pipeline.batch.error", unit: :event, description: "Number of errors per batch"),
summary("aprsme.packet_pipeline.batch.duration_ms", unit: :millisecond, description: "Batch insert duration (ms)"), summary("aprsme.packet_pipeline.batch.duration_ms",
unit: :millisecond,
description: "Batch insert duration (ms)"
),
# Note: SystemMonitor and InsertOptimizer metrics removed after reverting performance optimizations # Note: SystemMonitor and InsertOptimizer metrics removed after reverting performance optimizations
@ -100,7 +103,9 @@ defmodule AprsmeWeb.Telemetry do
counter("aprsme.spatial_pubsub.broadcasts.filtered", description: "Broadcasts filtered by viewport"), counter("aprsme.spatial_pubsub.broadcasts.filtered", description: "Broadcasts filtered by viewport"),
counter("aprsme.spatial_pubsub.broadcasts.packets", description: "Total packets processed"), counter("aprsme.spatial_pubsub.broadcasts.packets", description: "Total packets processed"),
last_value("aprsme.spatial_pubsub.efficiency.ratio", unit: :percent, description: "Broadcast efficiency ratio"), last_value("aprsme.spatial_pubsub.efficiency.ratio", unit: :percent, description: "Broadcast efficiency ratio"),
counter("aprsme.spatial_pubsub.efficiency.saved_broadcasts", description: "Number of broadcasts saved by filtering") counter("aprsme.spatial_pubsub.efficiency.saved_broadcasts",
description: "Number of broadcasts saved by filtering"
)
] ]
end end

View file

@ -31,7 +31,8 @@ defmodule AprsmeWeb.TimeHelpers do
defp format_time_diff(seconds) when seconds < 2_592_000, do: gettext("%{count} days", count: div(seconds, 86_400)) defp format_time_diff(seconds) when seconds < 2_592_000, do: gettext("%{count} days", count: div(seconds, 86_400))
defp format_time_diff(seconds) when seconds < 5_184_000, do: gettext("1 month") defp format_time_diff(seconds) when seconds < 5_184_000, do: gettext("1 month")
defp format_time_diff(seconds) when seconds < 31_536_000, do: gettext("%{count} months", count: div(seconds, 2_592_000)) defp format_time_diff(seconds) when seconds < 31_536_000,
do: gettext("%{count} months", count: div(seconds, 2_592_000))
defp format_time_diff(seconds) when seconds < 63_072_000, do: gettext("1 year") defp format_time_diff(seconds) when seconds < 63_072_000, do: gettext("1 year")
defp format_time_diff(seconds), do: gettext("%{count} years", count: div(seconds, 31_536_000)) defp format_time_diff(seconds), do: gettext("%{count} years", count: div(seconds, 31_536_000))

View file

@ -64,7 +64,7 @@ defmodule Aprsme.GeoUtilsTest do
# With 3 meter threshold, should be significant # With 3 meter threshold, should be significant
assert GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, 3) assert GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, 3)
# With 10 meter threshold (default), should not be significant # With 10 meter threshold (default), should not be significant
refute GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, 10) refute GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, 10)
# With 20 meter threshold, definitely not significant # With 20 meter threshold, definitely not significant

View file

@ -4,11 +4,13 @@ defmodule Aprsme.PacketPipelineIntegrationTest do
import ExUnit.CaptureLog import ExUnit.CaptureLog
alias Aprsme.PacketProducer alias Aprsme.PacketProducer
alias Aprsme.Performance.InsertOptimizer
alias Aprsme.SystemMonitor
describe "packet pipeline under load" do describe "packet pipeline under load" do
test "system adjusts batch sizes based on load" do test "system adjusts batch sizes based on load" do
# Test the actual running system without starting new processes # Test the actual running system without starting new processes
initial_batch_size = Aprsme.SystemMonitor.get_recommended_batch_size() initial_batch_size = SystemMonitor.get_recommended_batch_size()
assert initial_batch_size >= 100 assert initial_batch_size >= 100
assert initial_batch_size <= 800 assert initial_batch_size <= 800
@ -38,13 +40,13 @@ defmodule Aprsme.PacketPipelineIntegrationTest do
end end
test "insert optimizer provides reasonable batch sizes" do test "insert optimizer provides reasonable batch sizes" do
batch_size = Aprsme.Performance.InsertOptimizer.get_optimal_batch_size() batch_size = InsertOptimizer.get_optimal_batch_size()
assert batch_size >= 100 assert batch_size >= 100
assert batch_size <= 800 assert batch_size <= 800
end end
test "system monitor provides metrics" do test "system monitor provides metrics" do
metrics = Aprsme.SystemMonitor.get_metrics() metrics = SystemMonitor.get_metrics()
assert is_map(metrics) assert is_map(metrics)
assert Map.has_key?(metrics, :memory) assert Map.has_key?(metrics, :memory)

View file

@ -96,7 +96,7 @@ defmodule Aprsme.Packets.ClusteringTest do
{:heat_map, zoom3_clusters} = Clustering.cluster_packets(packets, 3, %{}) {:heat_map, zoom3_clusters} = Clustering.cluster_packets(packets, 3, %{})
assert length(zoom3_clusters) == 1 assert length(zoom3_clusters) == 1
# At zoom 8, these should be two clusters (radius 0.039 degrees) # At zoom 8, these should be two clusters (radius 0.039 degrees)
# but our points are only 0.014 degrees apart, so they'll still cluster # but our points are only 0.014 degrees apart, so they'll still cluster
# Let's use points that are further apart # Let's use points that are further apart
far_packets = [ far_packets = [

View file

@ -1,61 +0,0 @@
# Warning Fixes Summary
## Issue
Running `mix compile --warnings-as-errors` was failing due to 23 unused function warnings in the packet consumer module.
## Root Cause
During the INSERT performance optimization, we implemented a new fast packet processing pipeline but left the old implementation functions in place, causing unused function warnings.
## Functions Removed
### Old Packet Processing Functions (Unused after optimization):
- `prepare_packet_for_insert/1` - Old slow packet preparation
- `normalize_packet_attrs/1` - Complex normalization logic
- `set_received_at/1` - Timestamp setting
- `patch_lat_lon_from_data_extended/1` - Position extraction from extended data
- `extract_position/1` - Position extraction logic
- `extract_position_from_data_extended/1` - Extended data position parsing
- `extract_position_from_data_extended_case/1` - Case-based position extraction
- `has_standard_position?/1` - Position validation
- `extract_standard_position/1` - Standard position extraction
- `extract_lat_from_ext_map/1` - Latitude extraction
- `extract_lon_from_ext_map/1` - Longitude extraction
- `set_lat_lon/3` - Coordinate setting and rounding
- `normalize_ssid/1` - SSID normalization
- `create_location_geometry/1` - Old geometry creation
- `valid_coordinates?/2` - Coordinate validation
- `normalize_coordinate/1` - Coordinate normalization
- `create_point/2` - Point geometry creation
- `valid_packet?/1` - Packet validation
- `sanitize_packet_strings/1` - String sanitization
- `to_float/1` - Float conversion
- `normalize_data_type/1` - Data type normalization
- `struct_to_map/1` - Struct conversion
- `truncate_datetimes_to_second/1` - DateTime truncation
- `normalize_numeric_types/1` - Numeric type conversion
### Functions Kept:
- `validate_essential_fields/2` - Used by the new fast pipeline
- `prepare_packet_for_insert_fast/2` - New optimized packet preparation
- `extract_essential_fields/1` - Essential field extraction
- `create_location_geometry_fast/1` - Fast geometry creation
- All new fast helper functions
## Result
- ✅ All 23 unused function warnings eliminated
- ✅ `mix compile --warnings-as-errors` now passes
- ✅ All 351 tests still passing
- ✅ Code properly formatted
- ✅ No functional impact - the new fast pipeline remains intact
## Performance Impact
The cleanup removed ~250 lines of unused legacy code, which:
- Reduces module size and compilation time
- Eliminates confusion about which functions are actually used
- Makes the codebase cleaner and more maintainable
- Ensures only the optimized fast packet processing pipeline is used
## File Modified
- `/Users/graham/dev/aprs.me/lib/aprsme/packet_consumer.ex` - Removed 23 unused functions
The module now only contains the optimized INSERT performance code, making it much cleaner and easier to maintain.