Two fixes cover the blank "analysis breakdown" panel the user reported:
1. Bound the point_detail fallback lookback to 24h. Previously
factors_from_fallback_profile would happily use a week-old analysis
profile when no current one existed — now anything older than 24h
returns an empty factor map so the UI can surface "breakdown
unavailable" instead of silently misattributing.
2. Move the Plausible analytics snippet from Layouts.app into
root.html.heex. Full-bleed LiveViews (/map, /contacts/map,
/weather, home) bypass Layouts.app, so the snippet only loaded on
the navbar-wrapped pages. root.html.heex is loaded once per HTTP
document so coverage is now universal.
Added ~30 tests locking both down:
• point_detail fallback: exact-time wins, 24h boundary accepted,
30h-stale rejected, multi-band coverage, missing-cell degrades to
empty factors, default-valid_time path
• analytics_test.exs: Plausible script present on 12 representative
pages, exactly once, loaded async, surviving a login round-trip
Also fixed pre-existing credo issues per standing rule: shortened the
map_live_test ETS match_delete call, extracted a function from the
deeply-nested hrrr_point_enqueuer.enqueue_for_contacts/1 cond, and
swapped Mix.Tasks.Rust.Golden's IO.inspect for Mix.shell().info.
220 lines
7.2 KiB
Elixir
220 lines
7.2 KiB
Elixir
defmodule MicrowavepropWeb.Layouts do
|
|
@moduledoc """
|
|
This module holds layouts and related functionality
|
|
used by your application.
|
|
"""
|
|
use MicrowavepropWeb, :html
|
|
|
|
# Embed all files in layouts/* within this module.
|
|
# The default root.html.heex file contains the HTML
|
|
# skeleton of your application, namely HTML headers
|
|
# and other static content.
|
|
embed_templates "layouts/*"
|
|
|
|
@doc """
|
|
Renders your app layout.
|
|
|
|
This function is typically invoked from every template,
|
|
and it often contains your application menu, sidebar,
|
|
or similar.
|
|
|
|
## Examples
|
|
|
|
<Layouts.app flash={@flash}>
|
|
<h1>Content</h1>
|
|
</Layouts.app>
|
|
|
|
"""
|
|
attr :flash, :map, required: true, doc: "the map of flash messages"
|
|
|
|
attr :current_scope, :map,
|
|
default: nil,
|
|
doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
|
|
|
|
attr :max_width, :string, default: "max-w-2xl", doc: "max width class for the content container"
|
|
slot :inner_block, required: true
|
|
|
|
def app(assigns) do
|
|
~H"""
|
|
<header class="navbar px-4 sm:px-6 lg:px-8 border-b border-base-200">
|
|
<div class="flex-1 gap-4">
|
|
<a href="/" class="font-semibold tracking-tight">Microwave Propagation</a>
|
|
<nav class="flex gap-1">
|
|
<.link navigate="/map" class="btn btn-ghost btn-sm">Map</.link>
|
|
<.link navigate="/path" class="btn btn-ghost btn-sm">Path</.link>
|
|
<.link navigate="/beacons" class="btn btn-ghost btn-sm">Beacons</.link>
|
|
<.link navigate="/submit" class="btn btn-ghost btn-sm">Submit</.link>
|
|
<.link navigate="/contacts" class="btn btn-ghost btn-sm">Contacts</.link>
|
|
<.link navigate="/contacts/map" class="btn btn-ghost btn-sm">Contact Map</.link>
|
|
<.link navigate="/about" class="btn btn-ghost btn-sm">About</.link>
|
|
<div
|
|
:if={@current_scope && @current_scope.user && @current_scope.user.is_admin}
|
|
class="dropdown dropdown-end"
|
|
>
|
|
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
|
Admin <.icon name="hero-chevron-down-micro" class="size-3" />
|
|
</div>
|
|
<ul
|
|
tabindex="0"
|
|
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-44 p-2 shadow-lg"
|
|
>
|
|
<li><.link navigate="/users">Users</.link></li>
|
|
<li><.link navigate="/admin/contact-edits">Contact edits</.link></li>
|
|
<li><.link href="/admin/oban">Oban</.link></li>
|
|
</ul>
|
|
</div>
|
|
<%= if @current_scope && @current_scope.user do %>
|
|
<.link
|
|
navigate={~p"/u/#{@current_scope.user.callsign}"}
|
|
class="btn btn-ghost btn-sm"
|
|
>
|
|
{@current_scope.user.callsign}
|
|
</.link>
|
|
<.link href={~p"/users/settings"} class="btn btn-ghost btn-sm">Settings</.link>
|
|
<.link href={~p"/users/log-out"} method="delete" class="btn btn-ghost btn-sm">
|
|
Log out
|
|
</.link>
|
|
<% else %>
|
|
<.link href={~p"/users/register"} class="btn btn-ghost btn-sm">Register</.link>
|
|
<.link href={~p"/users/log-in"} class="btn btn-ghost btn-sm">Log in</.link>
|
|
<% end %>
|
|
</nav>
|
|
</div>
|
|
<div class="flex-none flex items-center gap-3">
|
|
<.deploy_stamp />
|
|
<.theme_toggle />
|
|
</div>
|
|
</header>
|
|
|
|
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
|
<div class={["mx-auto space-y-4", @max_width]}>
|
|
{render_slot(@inner_block)}
|
|
</div>
|
|
</main>
|
|
|
|
<footer class="px-4 sm:px-6 lg:px-8 pb-6">
|
|
<div class={["mx-auto text-center text-xs opacity-60 space-y-1", @max_width]}>
|
|
<div>
|
|
Built by and for the
|
|
<a href="https://ntms.org" target="_blank" class="link link-hover">
|
|
North Texas Microwave Society
|
|
</a>
|
|
</div>
|
|
<div>
|
|
<.link navigate="/privacy" class="link link-hover">Privacy</.link>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<.flash_group flash={@flash} />
|
|
"""
|
|
end
|
|
|
|
@doc """
|
|
Shows the flash group with standard titles and content.
|
|
|
|
## Examples
|
|
|
|
<.flash_group flash={@flash} />
|
|
"""
|
|
attr :flash, :map, required: true, doc: "the map of flash messages"
|
|
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
|
|
|
|
def flash_group(assigns) do
|
|
~H"""
|
|
<div id={@id} aria-live="polite">
|
|
<.flash kind={:info} flash={@flash} />
|
|
<.flash kind={:error} flash={@flash} />
|
|
|
|
<.flash
|
|
id="client-error"
|
|
kind={:error}
|
|
title={gettext("We can't find the internet")}
|
|
hidden
|
|
>
|
|
{gettext("Attempting to reconnect")}
|
|
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
|
</.flash>
|
|
|
|
<.flash
|
|
id="server-error"
|
|
kind={:error}
|
|
title={gettext("Something went wrong!")}
|
|
hidden
|
|
>
|
|
{gettext("Attempting to reconnect")}
|
|
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
|
</.flash>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
@doc """
|
|
Shows when the running release was deployed, as a compact relative timestamp
|
|
with the full UTC time in the tooltip.
|
|
"""
|
|
def deploy_stamp(assigns) do
|
|
ts = Microwaveprop.Application.build_timestamp()
|
|
iso = Calendar.strftime(ts, "%Y-%m-%d %H:%M UTC")
|
|
assigns = assign(assigns, build_time: ts, iso: iso, ago: format_time_ago(ts))
|
|
|
|
~H"""
|
|
<span
|
|
class="hidden md:inline text-xs opacity-60"
|
|
title={"Last deployed #{@iso}"}
|
|
>
|
|
Deployed {@ago}
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp format_time_ago(%DateTime{} = dt) do
|
|
diff = max(DateTime.diff(DateTime.utc_now(), dt, :second), 0)
|
|
|
|
cond do
|
|
diff < 60 -> "just now"
|
|
diff < 3600 -> "#{div(diff, 60)}m ago"
|
|
diff < 86_400 -> "#{div(diff, 3600)}h ago"
|
|
diff < 2_592_000 -> "#{div(diff, 86_400)}d ago"
|
|
diff < 31_536_000 -> "#{div(diff, 2_592_000)}mo ago"
|
|
true -> "#{div(diff, 31_536_000)}y ago"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Provides dark vs light theme toggle based on themes defined in app.css.
|
|
|
|
See <head> in root.html.heex which applies the theme before page load.
|
|
"""
|
|
def theme_toggle(assigns) do
|
|
~H"""
|
|
<div class="card relative flex flex-row items-center border-2 border-base-300 bg-base-300 rounded-full">
|
|
<div class="absolute w-1/3 h-full rounded-full border-1 border-base-200 bg-base-100 brightness-200 left-0 [[data-theme=light]_&]:left-1/3 [[data-theme=dark]_&]:left-2/3 transition-[left]" />
|
|
|
|
<button
|
|
class="flex p-2 cursor-pointer w-1/3"
|
|
phx-click={JS.dispatch("phx:set-theme")}
|
|
data-phx-theme="system"
|
|
>
|
|
<.icon name="hero-computer-desktop-micro" class="size-4 opacity-75 hover:opacity-100" />
|
|
</button>
|
|
|
|
<button
|
|
class="flex p-2 cursor-pointer w-1/3"
|
|
phx-click={JS.dispatch("phx:set-theme")}
|
|
data-phx-theme="light"
|
|
>
|
|
<.icon name="hero-sun-micro" class="size-4 opacity-75 hover:opacity-100" />
|
|
</button>
|
|
|
|
<button
|
|
class="flex p-2 cursor-pointer w-1/3"
|
|
phx-click={JS.dispatch("phx:set-theme")}
|
|
data-phx-theme="dark"
|
|
>
|
|
<.icon name="hero-moon-micro" class="size-4 opacity-75 hover:opacity-100" />
|
|
</button>
|
|
</div>
|
|
"""
|
|
end
|
|
end
|