- Add hardware/config fields migration to beacon_monitors table - Update BeaconMonitor schema with provision/config changesets - Add context functions: create_hardware, update_config, list_all_monitors - Remove user-facing monitor creation (browser POST + API POST) - Update settings page: show assigned monitors table with hardware info - Update profile page: show assigned monitors, remove register links - Fix all tests to match new API
267 lines
8.4 KiB
Text
267 lines
8.4 KiB
Text
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
|
<div class="text-center">
|
|
<.header>
|
|
Account Settings
|
|
<:subtitle>Manage your account email address and password settings</:subtitle>
|
|
</.header>
|
|
</div>
|
|
|
|
<.form :let={f} for={@email_changeset} action={~p"/users/settings"} id="update_email">
|
|
<input type="hidden" name="action" value="update_email" />
|
|
|
|
<.input
|
|
field={f[:email]}
|
|
type="email"
|
|
label="Email"
|
|
autocomplete="username"
|
|
spellcheck="false"
|
|
required
|
|
/>
|
|
|
|
<.button variant="primary" phx-disable-with="Changing...">Change Email</.button>
|
|
</.form>
|
|
|
|
<div class="divider" />
|
|
|
|
<.form :let={f} for={@home_qth_changeset} action={~p"/users/settings"} id="update_home_qth">
|
|
<input type="hidden" name="action" value="update_home_qth" />
|
|
|
|
<div class="text-left mb-2">
|
|
<h3 class="font-semibold">Home QTH</h3>
|
|
<p class="text-xs opacity-70">
|
|
Used to anchor the rover map (drive radius, distance/bearing labels).
|
|
Enter a Maidenhead grid (e.g. <code>EM13qc</code>) or explicit lat/lon.
|
|
</p>
|
|
<div
|
|
:if={!@current_scope.user.home_grid}
|
|
class="alert alert-warning text-xs mt-2"
|
|
>
|
|
<.icon name="hero-exclamation-triangle" class="size-4 shrink-0" />
|
|
<span>
|
|
Using the default NTMS-area location (EM13).
|
|
A QRZ lookup for <code>{@current_scope.user.callsign}</code>
|
|
is queued — check back shortly, or set yours manually below.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<.input
|
|
field={f[:home_grid]}
|
|
type="text"
|
|
label="Home grid"
|
|
placeholder="EM13qc"
|
|
autocomplete="off"
|
|
/>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<.input field={f[:home_lat]} type="number" step="any" label="Latitude" />
|
|
<.input field={f[:home_lon]} type="number" step="any" label="Longitude" />
|
|
</div>
|
|
|
|
<.button variant="primary" phx-disable-with="Saving...">Save Home QTH</.button>
|
|
</.form>
|
|
|
|
<div class="divider" />
|
|
|
|
<.form :let={f} for={@password_changeset} action={~p"/users/settings"} id="update_password">
|
|
<input type="hidden" name="action" value="update_password" />
|
|
|
|
<.input
|
|
field={f[:password]}
|
|
type="password"
|
|
label="New password"
|
|
autocomplete="new-password"
|
|
spellcheck="false"
|
|
required
|
|
/>
|
|
<.input
|
|
field={f[:password_confirmation]}
|
|
type="password"
|
|
label="Confirm new password"
|
|
autocomplete="new-password"
|
|
spellcheck="false"
|
|
required
|
|
/>
|
|
<.button variant="primary" phx-disable-with="Changing...">
|
|
Save Password
|
|
</.button>
|
|
</.form>
|
|
|
|
<div class="divider" />
|
|
|
|
<section id="beacon-monitors" class="space-y-4">
|
|
<.header>
|
|
Assigned beacon monitors
|
|
<:subtitle>
|
|
Physical monitor hardware assigned to you by an admin. Each unit
|
|
reports beacon reception data back to Microwaveprop.
|
|
</:subtitle>
|
|
</.header>
|
|
|
|
<%= if @beacon_monitors == [] do %>
|
|
<p class="text-sm opacity-70">No monitors assigned to you yet.</p>
|
|
<% else %>
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Hardware</th>
|
|
<th>Monitoring</th>
|
|
<th>Last seen</th>
|
|
<th class="w-1"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<%= for monitor <- @beacon_monitors do %>
|
|
<tr>
|
|
<td class="font-semibold">{monitor.name}</td>
|
|
<td class="text-sm">
|
|
<%= if monitor.hardware_type do %>
|
|
{monitor.hardware_type}<br />
|
|
<span class="opacity-60 text-xs">{monitor.hardware_id}</span>
|
|
<% else %>
|
|
<span class="opacity-50">—</span>
|
|
<% end %>
|
|
</td>
|
|
<td class="text-sm">
|
|
<%= if monitor.beacon do %>
|
|
<.link
|
|
navigate={~p"/beacons/#{monitor.beacon.id}"}
|
|
class="link link-hover font-mono"
|
|
>
|
|
{monitor.beacon.callsign}
|
|
</.link>
|
|
<span class="opacity-60">
|
|
{monitor.beacon.frequency_mhz} MHz
|
|
</span>
|
|
<% else %>
|
|
<span class="opacity-50">Not configured</span>
|
|
<% end %>
|
|
</td>
|
|
<td class="text-sm opacity-70">
|
|
<%= if monitor.last_seen_at do %>
|
|
{Calendar.strftime(monitor.last_seen_at, "%Y-%m-%d %H:%M UTC")}
|
|
<% else %>
|
|
never
|
|
<% end %>
|
|
</td>
|
|
<td>
|
|
<.link
|
|
href={~p"/users/beacon-monitors/#{monitor.id}"}
|
|
method="delete"
|
|
class="btn btn-ghost btn-xs text-error"
|
|
data-confirm={"Delete monitor '#{monitor.name}'? Its token will stop working immediately."}
|
|
>
|
|
Delete
|
|
</.link>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<% end %>
|
|
</section>
|
|
|
|
<div class="divider" />
|
|
|
|
<section id="api-tokens" class="space-y-4">
|
|
<.header>
|
|
API tokens
|
|
<:subtitle>
|
|
Long-lived bearer tokens for the
|
|
<.link navigate={~p"/docs/api"} class="link">REST API</.link>
|
|
at <code>/api/v1</code>. Each token grants the same
|
|
permissions as your account; treat them like passwords.
|
|
</:subtitle>
|
|
</.header>
|
|
|
|
<%= if Phoenix.Flash.get(@flash, :api_token) do %>
|
|
<div class="alert alert-success text-sm">
|
|
<.icon name="hero-key" class="size-4 shrink-0" />
|
|
<div class="flex-1">
|
|
<p class="font-semibold mb-1">
|
|
New token — copy now, it will not be shown again:
|
|
</p>
|
|
<code class="text-xs break-all select-all block bg-base-200 rounded p-2">
|
|
{Phoenix.Flash.get(@flash, :api_token)}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<.form
|
|
:let={f}
|
|
for={%{}}
|
|
as={:api_token}
|
|
action={~p"/users/api-tokens"}
|
|
id="create_api_token"
|
|
>
|
|
<label for={f[:name].id} class="label mb-1">Token name</label>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
type="text"
|
|
name={f[:name].name}
|
|
id={f[:name].id}
|
|
placeholder="e.g. laptop, scripts, monitor box"
|
|
class="input flex-1"
|
|
required
|
|
/>
|
|
<.button variant="primary" phx-disable-with="Creating...">Create token</.button>
|
|
</div>
|
|
</.form>
|
|
|
|
<%= if @api_tokens == [] do %>
|
|
<p class="text-sm opacity-70">No API tokens yet.</p>
|
|
<% else %>
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Created</th>
|
|
<th>Last used</th>
|
|
<th>Expires</th>
|
|
<th class="w-1"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<%= for token <- @api_tokens do %>
|
|
<tr>
|
|
<td class="font-semibold">{token.name}</td>
|
|
<td class="text-sm opacity-70">
|
|
{Calendar.strftime(token.inserted_at, "%Y-%m-%d")}
|
|
</td>
|
|
<td class="text-sm opacity-70">
|
|
<%= if token.last_used_at do %>
|
|
{Calendar.strftime(token.last_used_at, "%Y-%m-%d %H:%M UTC")}
|
|
<% else %>
|
|
never
|
|
<% end %>
|
|
</td>
|
|
<td class="text-sm opacity-70">
|
|
<%= if token.expires_at do %>
|
|
{Calendar.strftime(token.expires_at, "%Y-%m-%d")}
|
|
<% else %>
|
|
never
|
|
<% end %>
|
|
</td>
|
|
<td>
|
|
<.link
|
|
href={~p"/users/api-tokens/#{token.id}"}
|
|
method="delete"
|
|
class="btn btn-ghost btn-xs text-error"
|
|
data-confirm={"Revoke API token '#{token.name}'? It will stop working immediately."}
|
|
>
|
|
Revoke
|
|
</.link>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<% end %>
|
|
</section>
|
|
</Layouts.app>
|