Form simplification per user feedback. Form now shows only the essentials (Name, Site, Antenna, Frequency, TX power, Range, Height, Azimuth, Tilt, Foliage tuning) with imperial units throughout (ft, mi, GHz). Defaults: TX power 18 dBm, height 100 ft, azimuth 0°, frequency 5.8 GHz, range 4 mi. EIRP is calculated, not entered: shown as a live-updating badge in the form header as `TX power + antenna.gain`. Removed from the form (still on the schema with sensible defaults so existing callers and the worker keep working): cell_size_m (auto- computed from radius / 200, clamped 5–50 m), cable_loss_db, sm_gain_dbi, tx_clearance_m, height_above_rooftop_m, receiver_height_m, rx_threshold_dbm, latitude/longitude_override. Schema: - Six virtual imperial fields (height_agl_ft, height_above_rooftop_ft, receiver_height_ft, tx_clearance_ft, radius_mi, frequency_ghz). The changeset converts to SI before validation. Tests/workers/API keep sending SI directly. - ensure_cell_size_m/1: defaults the cell size when callers omit it. Show + index pages now display GHz / ft / mi. Propagation accuracy upgrade: - Earth-curvature correction (4/3-Earth model, ITU-R P.453) applied per profile sample. Important for paths over ~5 km. - Replaced Bullington single-knife-edge with Deygout three-edge multi-obstacle method (ITU-R P.526 §4.5): catches multi-ridge terrain shadowing that a single dominant obstacle would miss. Tests: 67 coverage tests pass (added Earth-curvature and Deygout multi-obstacle reference checks). Full suite: 10817 tests.
182 lines
4.7 KiB
Text
182 lines
4.7 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
active_page="coverage"
|
|
>
|
|
<.breadcrumb items={[
|
|
%{label: t("Dashboard"), navigate: ~p"/dashboard"},
|
|
%{label: t("Coverage"), navigate: ~p"/coverage"},
|
|
%{label: @page_title}
|
|
]} />
|
|
|
|
<.header>
|
|
{@page_title}
|
|
<:subtitle>
|
|
{t(
|
|
"Configure the radio and antenna. Coverage will be computed in the background once you save."
|
|
)}
|
|
</:subtitle>
|
|
<:actions>
|
|
<span class="inline-flex items-center px-3 py-1.5 rounded-md border border-blue-200 dark:border-blue-900/50 bg-blue-50 dark:bg-blue-900/20 text-sm">
|
|
<span class="text-blue-700 dark:text-blue-300 font-medium mr-2">EIRP:</span>
|
|
<span class="text-blue-900 dark:text-blue-100 font-mono">
|
|
{Float.round(@computed_eirp, 1)} dBm
|
|
</span>
|
|
</span>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<.form
|
|
for={@form}
|
|
id="coverage-form"
|
|
phx-change="validate"
|
|
phx-submit="save"
|
|
class="mt-6 space-y-8"
|
|
>
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<%!-- Identity --%>
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{t("Identity")}
|
|
</h3>
|
|
|
|
<.input
|
|
field={@form[:name]}
|
|
type="text"
|
|
label={t("Name")}
|
|
placeholder={t("e.g. North sector 5 GHz")}
|
|
required
|
|
/>
|
|
|
|
<.input
|
|
field={@form[:site_id]}
|
|
type="select"
|
|
label={t("Site")}
|
|
prompt={t("Select a site")}
|
|
options={Enum.map(@sites, &{&1.name, &1.id})}
|
|
required
|
|
/>
|
|
|
|
<.input
|
|
field={@form[:antenna_slug]}
|
|
type="select"
|
|
label={t("Antenna")}
|
|
prompt={t("Select an antenna")}
|
|
options={@antenna_options}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<%!-- Radio --%>
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{t("Radio")}
|
|
</h3>
|
|
|
|
<.input
|
|
field={@form[:frequency_ghz]}
|
|
type="number"
|
|
label={t("Frequency (GHz)")}
|
|
step="0.001"
|
|
min="0.7"
|
|
max="90"
|
|
required
|
|
/>
|
|
|
|
<.input
|
|
field={@form[:tx_power_dbm]}
|
|
type="number"
|
|
label={t("TX power (dBm)")}
|
|
step="0.1"
|
|
min="-10"
|
|
max="50"
|
|
required
|
|
/>
|
|
|
|
<.input
|
|
field={@form[:radius_mi]}
|
|
type="number"
|
|
label={t("Range (mi)")}
|
|
step="0.1"
|
|
min="0.3"
|
|
max="25"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<%!-- Mounting --%>
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{t("Mounting")}
|
|
</h3>
|
|
|
|
<.input
|
|
field={@form[:height_agl_ft]}
|
|
type="number"
|
|
label={t("Height above ground (ft)")}
|
|
step="1"
|
|
min="3"
|
|
max="650"
|
|
required
|
|
/>
|
|
|
|
<.input
|
|
field={@form[:azimuth_deg]}
|
|
type="number"
|
|
label={t("Azimuth (°, true north)")}
|
|
step="1"
|
|
min="0"
|
|
max="360"
|
|
required
|
|
/>
|
|
|
|
<.input
|
|
field={@form[:downtilt_deg]}
|
|
type="number"
|
|
label={t("Tilt (°, positive = down)")}
|
|
step="0.1"
|
|
min="-10"
|
|
max="30"
|
|
/>
|
|
</div>
|
|
|
|
<%!-- Foliage --%>
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{t("Environment")}
|
|
</h3>
|
|
|
|
<div>
|
|
<label
|
|
for="coverage_foliage_tuning"
|
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-1"
|
|
>
|
|
{t("Foliage tuning")}: {Phoenix.HTML.Form.input_value(@form, :foliage_tuning) || 0}
|
|
</label>
|
|
<input
|
|
type="range"
|
|
id="coverage_foliage_tuning"
|
|
name="coverage[foliage_tuning]"
|
|
value={Phoenix.HTML.Form.input_value(@form, :foliage_tuning) || 0}
|
|
min="0"
|
|
max="100"
|
|
step="1"
|
|
class="w-full"
|
|
/>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
{t("Higher values reduce predicted signal in wooded areas.")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-2">
|
|
<.link navigate={~p"/coverage"} class="btn btn-outline">
|
|
{t("Cancel")}
|
|
</.link>
|
|
<.button type="submit" variant="primary">
|
|
{if @live_action == :new, do: t("Create coverage"), else: t("Save changes")}
|
|
</.button>
|
|
</div>
|
|
</.form>
|
|
</Layouts.authenticated>
|