prop/lib/microwaveprop_web/live_table_footer.ex
Graham McIntire 14b90ee9f3
fix(security,perf): address 9 audit findings (access control, DoS, crashes)
- Beacon detail endpoints (LiveView + REST API) now hide unapproved
  beacons from anonymous and unauthorized viewers; only the submitter
  and admins can see pending records before approval. Adds
  Beacons.get_visible_beacon/2 with scope-aware checks.
- API contact pagination now honors per_page end-to-end.
  Radio.list_contacts/1 accepts :per_page and clamps to 200.
- API rate limiter: ETS table is now owned by a long-lived Sweeper
  GenServer (won't die with a request task); Sweeper periodically
  prunes expired-window rows to bound memory; init_table/0 race is
  rescued.
- /scores/cells and /weather/cells: add per-IP rate limiting and a
  shared GridBounds clamp/413 guard so global / oversized viewports
  no longer drive unbounded binary responses.
- NEXRAD PNG unfilter (sub/up/average/paeth): replace acc++[byte]
  + Enum.at(acc, idx-bpp) with O(n) binary recursion. Decode time
  for the 12200x5400 n0q frame goes from quadratic to linear.
- LiveTableFooter.parse_page and ScoresFile.fetch_bound: switch
  String.to_integer/String.to_float to Integer.parse/Float.parse,
  fall back to defaults instead of raising.
- PathLive and MapLive band-event handlers: replace
  String.to_integer(params["band"]) with parse_int / parse_band_param
  so a non-numeric band parameter no longer crashes the LiveView.
2026-05-11 18:53:21 -05:00

104 lines
3.1 KiB
Elixir

defmodule MicrowavepropWeb.LiveTableFooter do
@moduledoc false
use Phoenix.Component
def render(assigns) do
options = assigns.options
table_options = assigns.table_options
paginate? = get_in(options, ["pagination", "paginate?"])
mode = get_in(table_options, [:pagination, :mode])
page = parse_page(get_in(options, ["pagination", "page"]))
has_next? = get_in(options, ["pagination", :has_next_page]) || false
assigns =
assigns
|> assign(:show_pagination?, paginate? && mode != :infinite_scroll)
|> assign(:page, page)
|> assign(:has_next_page?, has_next?)
|> assign(:page_items, page_items(page, has_next?))
~H"""
<nav
:if={@show_pagination?}
class="flex items-center justify-between px-4 py-3 sm:px-6"
aria-label="Pagination"
>
<p class="hidden sm:block text-sm opacity-60">
Page <span class="font-medium text-base-content">{@page}</span>
</p>
<div class="join">
<button
type="button"
class="btn btn-sm join-item"
phx-click="sort"
phx-value-page={@page - 1}
disabled={@page == 1}
aria-label="Previous page"
>
«
</button>
<%= for item <- @page_items do %>
<%= if item == :ellipsis do %>
<button type="button" class="btn btn-sm join-item btn-disabled pointer-events-none">
</button>
<% else %>
<button
type="button"
class={[
"btn btn-sm join-item",
item == @page && "btn-active pointer-events-none"
]}
phx-click="sort"
phx-value-page={item}
aria-label={"Page #{item}"}
aria-current={item == @page && "page"}
>
{item}
</button>
<% end %>
<% end %>
<button
type="button"
class="btn btn-sm join-item"
phx-click="sort"
phx-value-page={@page + 1}
disabled={!@has_next_page?}
aria-label="Next page"
>
»
</button>
</div>
</nav>
"""
end
# Without a total count from live_table we can only show pages we know exist:
# everything up to and including `page` (we got here), plus `page + 1` when
# `has_next?` is true. Collapses a gap to "…" once we're past page 4.
@doc false
@spec page_items(pos_integer(), boolean()) :: [pos_integer() | :ellipsis]
def page_items(page, has_next?) when page >= 1 do
window_start = max(1, page - 2)
window_end = if has_next?, do: page + 1, else: page
window = Enum.to_list(window_start..window_end)
cond do
window_start == 1 -> window
window_start == 2 -> [1 | window]
true -> [1, :ellipsis | window]
end
end
defp parse_page(page) when is_integer(page) and page >= 1, do: page
defp parse_page(page) when is_binary(page) do
case Integer.parse(page) do
{n, ""} when n >= 1 -> n
_ -> 1
end
end
defp parse_page(_), do: 1
end