- Replace gray->cool-steel, blue/indigo->cerulean, red->sweet-salmon, yellow/amber->wheat - Dark sidebar: sidebar/footer use cool-steel-800 bg, text lightened for contrast - ~11,600 color replacements across ~99 files - Update tests for new color class names
248 lines
7.2 KiB
Text
248 lines
7.2 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
active_page="coverage"
|
|
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
|
|
>
|
|
<.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-cerulean-200 dark:border-cerulean-900/50 bg-cerulean-50 dark:bg-cerulean-900/20 text-sm">
|
|
<span class="text-cerulean-700 dark:text-cerulean-300 font-medium mr-2">EIRP:</span>
|
|
<span class="text-cerulean-900 dark:text-cerulean-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-cool-steel-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
|
|
:if={@devices != []}
|
|
field={@form[:device_id]}
|
|
type="select"
|
|
label={t("Attach to device (optional)")}
|
|
prompt={t("No specific device")}
|
|
options={Enum.map(@devices, &{device_label(&1), &1.id})}
|
|
/>
|
|
<p
|
|
:if={@form[:site_id].value not in [nil, ""] and @devices == []}
|
|
class="text-xs text-cool-steel-500 dark:text-cool-steel-400"
|
|
>
|
|
{t("This site has no devices yet — coverage will not be attached to a specific device.")}
|
|
</p>
|
|
|
|
<.input
|
|
field={@form[:antenna_slug]}
|
|
type="select"
|
|
label={t("Antenna")}
|
|
prompt={t("Select an antenna")}
|
|
options={@antenna_options}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<%!-- Radio location: draggable marker on a Leaflet map. --%>
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
|
|
{t("Radio location")}
|
|
</h3>
|
|
<button
|
|
:if={radio_overridden?(@form)}
|
|
type="button"
|
|
phx-click="use_site_location"
|
|
class="text-xs text-cerulean-600 hover:underline dark:text-cerulean-400"
|
|
>
|
|
{t("Use site location")}
|
|
</button>
|
|
</div>
|
|
|
|
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
|
|
{t("Drag the marker to where the radio is mounted. Defaults to the site's coordinates.")}
|
|
</p>
|
|
|
|
<div
|
|
id="coverage-location-picker"
|
|
phx-hook="CoverageLocationPicker"
|
|
phx-update="ignore"
|
|
class="h-72 w-full rounded border border-cool-steel-200 dark:border-white/10 bg-cool-steel-100 dark:bg-cool-steel-800"
|
|
data-marker-lat={radio_marker_lat(@form, @sites)}
|
|
data-marker-lon={radio_marker_lon(@form, @sites)}
|
|
>
|
|
</div>
|
|
|
|
<div
|
|
:if={radio_marker_lat(@form, @sites)}
|
|
class="text-xs font-mono text-cool-steel-600 dark:text-cool-steel-400"
|
|
>
|
|
{radio_marker_lat(@form, @sites)}, {radio_marker_lon(@form, @sites)}
|
|
<span
|
|
:if={radio_overridden?(@form)}
|
|
class="ml-2 text-cerulean-600 dark:text-cerulean-400"
|
|
>
|
|
({t("override")})
|
|
</span>
|
|
</div>
|
|
|
|
<.input field={@form[:latitude_override]} type="hidden" />
|
|
<.input field={@form[:longitude_override]} type="hidden" />
|
|
</div>
|
|
|
|
<%!-- Radio --%>
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-semibold text-cool-steel-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-cool-steel-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-cool-steel-900 dark:text-white">
|
|
{t("Environment")}
|
|
</h3>
|
|
|
|
<div>
|
|
<label
|
|
for="coverage_foliage_tuning"
|
|
class="block text-sm font-medium text-cool-steel-900 dark:text-cool-steel-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-cool-steel-500 dark:text-cool-steel-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 border border-cool-steel-300 bg-transparent dark:border-cool-steel-600"
|
|
>
|
|
{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>
|