clean up warnings
This commit is contained in:
parent
46340e008a
commit
566198afa7
41 changed files with 138 additions and 580 deletions
|
|
@ -2,5 +2,6 @@
|
|||
import_deps: [:ecto, :ecto_sql, :phoenix, :stream_data],
|
||||
subdirectories: ["priv/*/migrations"],
|
||||
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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -221,7 +221,11 @@ defmodule Aprsme.CircuitBreaker do
|
|||
}
|
||||
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
|
||||
:half_open
|
||||
else
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ defmodule Aprsme.EncodingUtils do
|
|||
[codepoint] ->
|
||||
# Allow printable characters and common whitespace
|
||||
# Remove C0 controls (0x00-0x1F except tab, newline, carriage return)
|
||||
# Remove C1 controls (0x80-0x9F)
|
||||
# Remove C1 controls (0x80-0x9F)
|
||||
# Remove DEL (0x7F)
|
||||
cond do
|
||||
# Tab
|
||||
|
|
@ -138,8 +138,9 @@ defmodule Aprsme.EncodingUtils do
|
|||
|
||||
# Helper to check if a float is finite (not infinity or NaN)
|
||||
defp finite_float?(float) when is_float(float) do
|
||||
# NaN != NaN
|
||||
float != :infinity and float != :neg_infinity and float == float
|
||||
# In Elixir, we can't have infinity or NaN in regular floats
|
||||
# This function is kept for defensive programming
|
||||
true
|
||||
end
|
||||
|
||||
defp finite_float?(_), do: false
|
||||
|
|
|
|||
|
|
@ -330,10 +330,10 @@ defmodule Aprsme.ErrorHandler do
|
|||
|
||||
defp categorize_and_handle_error(error, context) do
|
||||
cond do
|
||||
is_database_error?(error) ->
|
||||
database_error?(error) ->
|
||||
handle_database_error(error, context)
|
||||
|
||||
is_network_error?(error) ->
|
||||
network_error?(error) ->
|
||||
handle_network_error(error, :unknown_service, context)
|
||||
|
||||
true ->
|
||||
|
|
@ -349,19 +349,19 @@ defmodule Aprsme.ErrorHandler do
|
|||
end
|
||||
end
|
||||
|
||||
defp is_database_error?(%CastError{}), do: true
|
||||
defp is_database_error?(%Ecto.ConstraintError{}), do: true
|
||||
defp is_database_error?(%Postgrex.Error{}), do: true
|
||||
defp is_database_error?(%DBConnection.ConnectionError{}), do: true
|
||||
defp is_database_error?(%Ecto.QueryError{}), do: true
|
||||
defp is_database_error?(_), do: false
|
||||
defp database_error?(%CastError{}), do: true
|
||||
defp database_error?(%Ecto.ConstraintError{}), do: true
|
||||
defp database_error?(%Postgrex.Error{}), do: true
|
||||
defp database_error?(%DBConnection.ConnectionError{}), do: true
|
||||
defp database_error?(%Ecto.QueryError{}), do: true
|
||||
defp database_error?(_), do: false
|
||||
|
||||
defp is_network_error?(%Req.TransportError{}), do: true
|
||||
defp is_network_error?(%{reason: :circuit_open}), do: true
|
||||
defp is_network_error?(%{reason: :timeout}), do: true
|
||||
defp is_network_error?(%RuntimeError{message: "circuit_open"}), do: true
|
||||
defp is_network_error?(%RuntimeError{message: "timeout"}), do: true
|
||||
defp is_network_error?(_), do: false
|
||||
defp network_error?(%Req.TransportError{}), do: true
|
||||
defp network_error?(%{reason: :circuit_open}), do: true
|
||||
defp network_error?(%{reason: :timeout}), do: true
|
||||
defp network_error?(%RuntimeError{message: "circuit_open"}), do: true
|
||||
defp network_error?(%RuntimeError{message: "timeout"}), do: true
|
||||
defp network_error?(_), do: false
|
||||
|
||||
defp store_bad_packet(packet_data, reason) do
|
||||
# This would store the bad packet for later analysis
|
||||
|
|
|
|||
|
|
@ -402,7 +402,12 @@ defmodule Aprsme.Is do
|
|||
|
||||
case connect_to_aprs_is(state.server, state.port) do
|
||||
{: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 ->
|
||||
Logger.info("Successfully reconnected to APRS-IS")
|
||||
timer = create_timer(@aprs_timeout)
|
||||
|
|
|
|||
|
|
@ -192,7 +192,9 @@ defmodule Aprsme.PacketReplay do
|
|||
]
|
||||
|
||||
# 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
|
||||
Endpoint.broadcast(state.replay_topic, "replay_started", %{
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ defmodule Aprsme.Packets do
|
|||
|
||||
packet_attrs =
|
||||
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()
|
||||
|> set_received_at()
|
||||
|> patch_lat_lon_from_data_extended()
|
||||
|
|
|
|||
|
|
@ -195,17 +195,8 @@ defmodule AprsmeWeb.CoreComponents do
|
|||
{@title}
|
||||
</p>
|
||||
<p class="mt-2 text-[0.8125rem] leading-5">{msg}</p>
|
||||
<button
|
||||
:if={@close}
|
||||
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 :if={@close} 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>
|
||||
</div>
|
||||
"""
|
||||
|
|
@ -463,18 +454,10 @@ defmodule AprsmeWeb.CoreComponents do
|
|||
<div class="dropdown dropdown-end">
|
||||
<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">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
|
||||
>
|
||||
<ul 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} />
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -576,11 +559,7 @@ defmodule AprsmeWeb.CoreComponents do
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700">
|
||||
<tr
|
||||
:for={row <- @rows}
|
||||
id={"#{@id}-#{safe_row_id(row)}"}
|
||||
class="relative group hover:bg-zinc-50"
|
||||
>
|
||||
<tr :for={row <- @rows} id={"#{@id}-#{safe_row_id(row)}"} class="relative group hover:bg-zinc-50">
|
||||
<td
|
||||
:for={{col, i} <- Enum.with_index(@col)}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
|
|
@ -653,10 +632,7 @@ defmodule AprsmeWeb.CoreComponents do
|
|||
def back(assigns) do
|
||||
~H"""
|
||||
<div class="mt-16">
|
||||
<.link
|
||||
navigate={@navigate}
|
||||
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
|
||||
>
|
||||
<.link 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" />
|
||||
{render_slot(@inner_block)}
|
||||
</.link>
|
||||
|
|
|
|||
|
|
@ -219,8 +219,7 @@
|
|||
phx-disconnected={show("#disconnected")}
|
||||
phx-connected={hide("#disconnected")}
|
||||
>
|
||||
Attempting to reconnect
|
||||
<.icon name="arrow-path" outline={true} class="ml-1 w-3 h-3 inline animate-spin" />
|
||||
Attempting to reconnect <.icon name="arrow-path" outline={true} class="ml-1 w-3 h-3 inline animate-spin" />
|
||||
</.flash>
|
||||
{@inner_content}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -206,8 +206,7 @@
|
|||
phx-disconnected={show("#disconnected")}
|
||||
phx-connected={hide("#disconnected")}
|
||||
>
|
||||
Attempting to reconnect
|
||||
<.icon name="arrow-path" outline={true} class="ml-1 w-3 h-3 inline animate-spin" />
|
||||
Attempting to reconnect <.icon name="arrow-path" outline={true} class="ml-1 w-3 h-3 inline animate-spin" />
|
||||
</.flash>
|
||||
{@inner_content}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -248,12 +248,7 @@ defmodule AprsmeWeb.ApiDocsLive do
|
|||
</div>
|
||||
|
||||
<div class="alert alert-warning mt-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -578,11 +573,7 @@ defmodule AprsmeWeb.ApiDocsLive do
|
|||
class="input input-bordered flex-1"
|
||||
disabled={@loading}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={@loading or String.trim(@test_callsign) == ""}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<button type="submit" disabled={@loading or String.trim(@test_callsign) == ""} class="btn btn-primary">
|
||||
<%= if @loading do %>
|
||||
<span class="loading loading-spinner loading-sm"></span> Testing...
|
||||
<% else %>
|
||||
|
|
|
|||
|
|
@ -68,12 +68,7 @@
|
|||
<div class="card-body text-center">
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="w-16 h-16 bg-success/20 rounded-full flex items-center justify-center">
|
||||
<svg
|
||||
class="w-8 h-8 text-success"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg class="w-8 h-8 text-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
|
|||
|
|
@ -51,18 +51,8 @@
|
|||
<div class="card-body">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-4 w-4 opacity-70"
|
||||
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"
|
||||
/>
|
||||
<svg class="h-4 w-4 opacity-70" 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
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -187,13 +177,7 @@
|
|||
<div class="card-body">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-4 w-4 opacity-70"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -275,13 +259,7 @@
|
|||
<div class="card-body">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-4 w-4 opacity-70"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -313,10 +291,7 @@
|
|||
<tr>
|
||||
<td class="font-medium">
|
||||
<div class="flex items-center space-x-2">
|
||||
<.link
|
||||
navigate={~p"/info/#{ssid_info.callsign}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
<.link navigate={~p"/info/#{ssid_info.callsign}"} class="link link-primary">
|
||||
{ssid_info.callsign}
|
||||
</.link>
|
||||
<%= if ssid_info.packet do %>
|
||||
|
|
@ -385,13 +360,7 @@
|
|||
<div class="card-body">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-4 w-4 opacity-70"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -473,13 +442,7 @@
|
|||
<div class="card-body">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-4 w-4 opacity-70"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -520,10 +483,7 @@
|
|||
<%= for station <- @heard_by_stations do %>
|
||||
<tr>
|
||||
<td class="font-medium">
|
||||
<.link
|
||||
navigate={~p"/info/#{station.digipeater}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
<.link navigate={~p"/info/#{station.digipeater}"} class="link link-primary">
|
||||
{station.digipeater}
|
||||
</.link>
|
||||
</td>
|
||||
|
|
@ -578,13 +538,7 @@
|
|||
<div class="card-body">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-4 w-4 opacity-70"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<svg class="h-4 w-4 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ defmodule AprsmeWeb.LocaleHook do
|
|||
end
|
||||
|
||||
# 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)}
|
||||
end
|
||||
|
|
@ -35,7 +35,7 @@ defmodule AprsmeWeb.LocaleHook do
|
|||
end
|
||||
end
|
||||
|
||||
defp is_map_page?(socket, _params) do
|
||||
defp map_page?(socket, _params) do
|
||||
# Only check the view module from socket private data
|
||||
case socket.private[:phoenix_live_view] do
|
||||
%{view: AprsmeWeb.MapLive.Index} -> true
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ defmodule AprsmeWeb.MapLive.BoundsManager do
|
|||
"""
|
||||
|
||||
alias AprsmeWeb.Live.Shared.BoundsUtils
|
||||
alias AprsmeWeb.Live.Shared.PacketUtils
|
||||
|
||||
# Delegate to shared utilities
|
||||
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()
|
||||
def filter_packets_by_time_and_bounds(packets, bounds, time_threshold) do
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -157,7 +157,14 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
|
|||
{lat, lon, _} = get_coordinates(packet)
|
||||
|
||||
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
|
||||
distance_meters >= 10.0
|
||||
else
|
||||
|
|
@ -330,7 +337,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
|
|||
weather_link: weather_link
|
||||
}
|
||||
|
||||
# Optimize string conversion - avoid intermediate iodata step
|
||||
# Optimize string conversion - avoid intermediate iodata step
|
||||
popup_data
|
||||
|> PopupComponent.popup()
|
||||
|> Safe.to_iodata()
|
||||
|
|
@ -475,7 +482,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
|
|||
end
|
||||
|
||||
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
|
||||
|> Stream.map(&convert_tuples_to_strings/1)
|
||||
|> Enum.to_list()
|
||||
|
|
|
|||
|
|
@ -585,7 +585,8 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
defp parse_historical_hours(hours), do: SharedPacketUtils.parse_historical_hours(hours)
|
||||
|
||||
@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)
|
||||
|
||||
|
|
@ -710,14 +711,8 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
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"
|
||||
/>
|
||||
<link rel="stylesheet" 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
|
||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
|
|
@ -726,9 +721,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
</script>
|
||||
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js">
|
||||
</script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier-Leaflet/0.2.6/oms.min.js"
|
||||
>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier-Leaflet/0.2.6/oms.min.js">
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
@ -858,19 +851,8 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
</div>
|
||||
</.error_boundary>
|
||||
|
||||
<button
|
||||
class="locate-button"
|
||||
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"
|
||||
>
|
||||
<button class="locate-button" 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" />
|
||||
</svg>
|
||||
</button>
|
||||
|
|
@ -932,10 +914,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
|
||||
<!-- Mobile Backdrop -->
|
||||
<%= if @slideover_open do %>
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-[999] lg:hidden backdrop-blur-sm"
|
||||
phx-click="toggle_slideover"
|
||||
>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 z-[999] lg:hidden backdrop-blur-sm" phx-click="toggle_slideover">
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
|
@ -951,13 +930,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
<!-- 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 space-x-2">
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -1043,12 +1016,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
<!-- Trail Duration -->
|
||||
<div class="space-y-4">
|
||||
<label class="block text-sm font-semibold text-slate-700 flex items-center space-x-2">
|
||||
<svg
|
||||
class="w-4 h-4 text-emerald-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -1083,18 +1051,8 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
</option>
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<svg
|
||||
class="w-4 h-4 text-slate-400"
|
||||
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 class="w-4 h-4 text-slate-400" 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>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -1135,18 +1093,8 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
</option>
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<svg
|
||||
class="w-4 h-4 text-slate-400"
|
||||
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 class="w-4 h-4 text-slate-400" 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>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ defmodule AprsmeWeb.MapLive.PacketManager do
|
|||
visible_packets = get_visible_packets(packet_state)
|
||||
{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)
|
||||
{keep_historical, remove_historical} = Enum.split_with(historical_packets, predicate_fn)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ defmodule AprsmeWeb.MapLive.PacketProcessor do
|
|||
import Phoenix.Component, only: [assign: 3]
|
||||
|
||||
alias Aprsme.GeoUtils
|
||||
alias AprsmeWeb.Live.Shared.BoundsUtils
|
||||
alias AprsmeWeb.Live.Shared.CoordinateUtils
|
||||
alias AprsmeWeb.Live.Shared.PacketUtils, as: SharedPacketUtils
|
||||
alias AprsmeWeb.MapLive.PacketUtils
|
||||
|
|
@ -147,7 +148,7 @@ defmodule AprsmeWeb.MapLive.PacketProcessor do
|
|||
|
||||
# Use shared bounds utility
|
||||
defp within_bounds?(coords, bounds) do
|
||||
AprsmeWeb.Live.Shared.BoundsUtils.within_bounds?(coords, bounds)
|
||||
BoundsUtils.within_bounds?(coords, bounds)
|
||||
end
|
||||
|
||||
# Helper to get locale from socket
|
||||
|
|
|
|||
|
|
@ -21,9 +21,7 @@
|
|||
<tr>
|
||||
<td>
|
||||
<.link
|
||||
navigate={
|
||||
~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"
|
||||
}
|
||||
navigate={~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"}
|
||||
class="link link-primary font-medium"
|
||||
>
|
||||
{Map.get(packet, :sender, Map.get(packet, "sender", ""))}
|
||||
|
|
@ -36,9 +34,7 @@
|
|||
</td>
|
||||
<td>
|
||||
<.link
|
||||
navigate={
|
||||
~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"
|
||||
}
|
||||
navigate={~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"}
|
||||
class="link link-primary font-medium"
|
||||
>
|
||||
{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ defmodule AprsmeWeb.Live.Shared.BoundsUtils do
|
|||
"""
|
||||
@spec valid_bounds?(map()) :: boolean()
|
||||
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
|
||||
north > south and east >= -180 and west <= 180, do: true
|
||||
when is_number(north) and is_number(south) and is_number(east) and is_number(west) and north <= 90 and
|
||||
south >= -90 and north > south and east >= -180 and west <= 180, do: true
|
||||
|
||||
def valid_bounds?(_invalid_bounds), do: false
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ defmodule AprsmeWeb.Live.SharedPacketHandler do
|
|||
device_identifiers =
|
||||
packets
|
||||
|> Enum.map(&get_device_identifier/1)
|
||||
|> Enum.reject(&is_nil_or_empty/1)
|
||||
|> Enum.reject(&nil_or_empty?/1)
|
||||
|> Enum.uniq()
|
||||
|
||||
# Batch lookup devices (DeviceCache is already optimized with caching)
|
||||
|
|
@ -117,9 +117,9 @@ defmodule AprsmeWeb.Live.SharedPacketHandler do
|
|||
end
|
||||
end
|
||||
|
||||
defp is_nil_or_empty(nil), do: true
|
||||
defp is_nil_or_empty(""), do: true
|
||||
defp is_nil_or_empty(_), do: false
|
||||
defp nil_or_empty?(nil), do: true
|
||||
defp nil_or_empty?(""), do: true
|
||||
defp nil_or_empty?(_), do: false
|
||||
|
||||
@doc """
|
||||
Creates a filter function that checks both callsign and weather data.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ defmodule AprsmeWeb.Live.Shared.PacketUtils do
|
|||
Used across multiple LiveView modules for consistent packet handling.
|
||||
"""
|
||||
|
||||
alias AprsmeWeb.Live.Shared.BoundsUtils
|
||||
|
||||
@doc """
|
||||
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
|
||||
packets
|
||||
|> 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)
|
||||
end)
|
||||
|> Map.new()
|
||||
|
|
|
|||
|
|
@ -80,8 +80,9 @@ defmodule AprsmeWeb.Live.Shared.ParamUtils do
|
|||
"""
|
||||
@spec finite?(float() | any()) :: boolean()
|
||||
def finite?(float) when is_float(float) do
|
||||
# NaN != NaN
|
||||
float != :infinity and float != :neg_infinity and float == float
|
||||
# In Elixir, we can't have infinity or NaN in regular floats
|
||||
# This function is kept for API compatibility
|
||||
true
|
||||
end
|
||||
|
||||
def finite?(_), do: false
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<%= if @aprs_status.packet_stats.last_packet_at do %>
|
||||
{AprsmeWeb.StatusLive.Index.format_time_ago(
|
||||
@aprs_status.packet_stats.last_packet_at
|
||||
)}
|
||||
{AprsmeWeb.StatusLive.Index.format_time_ago(@aprs_status.packet_stats.last_packet_at)}
|
||||
<% else %>
|
||||
<span class="opacity-50">None</span>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,11 @@ defmodule AprsmeWeb.StatusLive.Index do
|
|||
def handle_info({:aprs_status_update, status}, socket) do
|
||||
# Handle real-time status updates via PubSub
|
||||
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}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,13 +18,7 @@ defmodule AprsmeWeb.UserConfirmationInstructionsLive do
|
|||
Enter your email address to receive a new confirmation link
|
||||
</p>
|
||||
|
||||
<.simple_form
|
||||
:let={_f}
|
||||
for={%{}}
|
||||
as={:user}
|
||||
id="resend_confirmation_form"
|
||||
phx-submit="send_instructions"
|
||||
>
|
||||
<.simple_form :let={_f} for={%{}} as={:user} id="resend_confirmation_form" phx-submit="send_instructions">
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Email</span>
|
||||
|
|
|
|||
|
|
@ -14,21 +14,11 @@ defmodule AprsmeWeb.UserConfirmationLive do
|
|||
<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>
|
||||
|
||||
<.simple_form
|
||||
:let={_f}
|
||||
for={%{}}
|
||||
as={:user}
|
||||
id="confirmation_form"
|
||||
phx-submit="confirm_account"
|
||||
>
|
||||
<.simple_form :let={_f} for={%{}} as={:user} id="confirmation_form" phx-submit="confirm_account">
|
||||
<input type="hidden" name="user[token]" value={@token} />
|
||||
|
||||
<div class="form-control mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-full"
|
||||
phx-disable-with="Confirming..."
|
||||
>
|
||||
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Confirming...">
|
||||
Confirm my account
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,13 +14,7 @@ defmodule AprsmeWeb.UserForgotPasswordLive do
|
|||
<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>
|
||||
|
||||
<.simple_form
|
||||
:let={_f}
|
||||
id="reset_password_form"
|
||||
for={%{}}
|
||||
as={:user}
|
||||
phx-submit="send_email"
|
||||
>
|
||||
<.simple_form :let={_f} id="reset_password_form" for={%{}} as={:user} phx-submit="send_email">
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Email</span>
|
||||
|
|
|
|||
|
|
@ -76,11 +76,7 @@ defmodule AprsmeWeb.UserLoginLive do
|
|||
</div>
|
||||
|
||||
<div class="form-control mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-full"
|
||||
phx-disable-with="Signing in..."
|
||||
>
|
||||
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Signing in...">
|
||||
Sign in →
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -105,11 +105,7 @@ defmodule AprsmeWeb.UserRegistrationLive do
|
|||
</div>
|
||||
|
||||
<div class="form-control mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-full"
|
||||
phx-disable-with="Creating account..."
|
||||
>
|
||||
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Creating account...">
|
||||
Create an account
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -70,10 +70,7 @@ defmodule AprsmeWeb.UserSettingsLive do
|
|||
placeholder="Enter current password"
|
||||
required
|
||||
/>
|
||||
<label
|
||||
:if={Keyword.has_key?(@email_changeset.errors, :current_password)}
|
||||
class="label"
|
||||
>
|
||||
<label :if={Keyword.has_key?(@email_changeset.errors, :current_password)} class="label">
|
||||
<span class="label-text-alt text-error">
|
||||
{translate_error(Keyword.get(@email_changeset.errors, :current_password))}
|
||||
</span>
|
||||
|
|
@ -143,10 +140,7 @@ defmodule AprsmeWeb.UserSettingsLive do
|
|||
placeholder="Enter current password"
|
||||
required
|
||||
/>
|
||||
<label
|
||||
:if={Keyword.has_key?(@callsign_changeset.errors, :current_password)}
|
||||
class="label"
|
||||
>
|
||||
<label :if={Keyword.has_key?(@callsign_changeset.errors, :current_password)} class="label">
|
||||
<span class="label-text-alt text-error">
|
||||
{translate_error(Keyword.get(@callsign_changeset.errors, :current_password))}
|
||||
</span>
|
||||
|
|
@ -181,11 +175,7 @@ defmodule AprsmeWeb.UserSettingsLive do
|
|||
<span>Oops, something went wrong! Please check the errors below.</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name={Phoenix.HTML.Form.input_name(f, :email)}
|
||||
value={@current_email}
|
||||
/>
|
||||
<input type="hidden" name={Phoenix.HTML.Form.input_name(f, :email)} value={@current_email} />
|
||||
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
|
|
@ -225,14 +215,9 @@ defmodule AprsmeWeb.UserSettingsLive do
|
|||
placeholder="Confirm new password"
|
||||
required
|
||||
/>
|
||||
<label
|
||||
:if={Keyword.has_key?(@password_changeset.errors, :password_confirmation)}
|
||||
class="label"
|
||||
>
|
||||
<label :if={Keyword.has_key?(@password_changeset.errors, :password_confirmation)} class="label">
|
||||
<span class="label-text-alt text-error">
|
||||
{translate_error(
|
||||
Keyword.get(@password_changeset.errors, :password_confirmation)
|
||||
)}
|
||||
{translate_error(Keyword.get(@password_changeset.errors, :password_confirmation))}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -253,10 +238,7 @@ defmodule AprsmeWeb.UserSettingsLive do
|
|||
placeholder="Enter current password"
|
||||
required
|
||||
/>
|
||||
<label
|
||||
:if={Keyword.has_key?(@password_changeset.errors, :current_password)}
|
||||
class="label"
|
||||
>
|
||||
<label :if={Keyword.has_key?(@password_changeset.errors, :current_password)} class="label">
|
||||
<span class="label-text-alt text-error">
|
||||
{translate_error(Keyword.get(@password_changeset.errors, :current_password))}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -62,12 +62,7 @@
|
|||
) %>
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-4 w-4 text-primary"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="h-4 w-4 text-primary" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
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"
|
||||
|
|
@ -84,9 +79,7 @@
|
|||
<dt class="text-xs font-medium opacity-70">{gettext("Last WX report")}</dt>
|
||||
<dd class="mt-1 text-sm font-semibold">
|
||||
<%= if dt do %>
|
||||
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")} ({AprsmeWeb.TimeHelpers.time_ago_in_words(
|
||||
dt
|
||||
)})<br />
|
||||
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")} ({AprsmeWeb.TimeHelpers.time_ago_in_words(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> --%>
|
||||
<% else %>
|
||||
{gettext("Unknown")}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,10 @@ defmodule AprsmeWeb.Telemetry do
|
|||
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.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
|
||||
|
||||
|
|
@ -100,7 +103,9 @@ defmodule AprsmeWeb.Telemetry do
|
|||
counter("aprsme.spatial_pubsub.broadcasts.filtered", description: "Broadcasts filtered by viewport"),
|
||||
counter("aprsme.spatial_pubsub.broadcasts.packets", description: "Total packets processed"),
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 < 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), do: gettext("%{count} years", count: div(seconds, 31_536_000))
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ defmodule Aprsme.GeoUtilsTest do
|
|||
# With 3 meter threshold, should be significant
|
||||
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)
|
||||
|
||||
# With 20 meter threshold, definitely not significant
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ defmodule Aprsme.PacketPipelineIntegrationTest do
|
|||
import ExUnit.CaptureLog
|
||||
|
||||
alias Aprsme.PacketProducer
|
||||
alias Aprsme.Performance.InsertOptimizer
|
||||
alias Aprsme.SystemMonitor
|
||||
|
||||
describe "packet pipeline under load" do
|
||||
test "system adjusts batch sizes based on load" do
|
||||
# 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 <= 800
|
||||
|
||||
|
|
@ -38,13 +40,13 @@ defmodule Aprsme.PacketPipelineIntegrationTest do
|
|||
end
|
||||
|
||||
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 <= 800
|
||||
end
|
||||
|
||||
test "system monitor provides metrics" do
|
||||
metrics = Aprsme.SystemMonitor.get_metrics()
|
||||
metrics = SystemMonitor.get_metrics()
|
||||
|
||||
assert is_map(metrics)
|
||||
assert Map.has_key?(metrics, :memory)
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ defmodule Aprsme.Packets.ClusteringTest do
|
|||
{:heat_map, zoom3_clusters} = Clustering.cluster_packets(packets, 3, %{})
|
||||
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
|
||||
# Let's use points that are further apart
|
||||
far_packets = [
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Add table
Reference in a new issue