Repeater page redesign

This commit is contained in:
Jonathon Leight committed 2026-10-26 16:20:54 -04:00
1 parent 727e627909
commit 9c5c8f4cfe
10 files changed
+61 -100

No files matched your search

+11 -13
View File
@@ -24,29 +24,27 @@
{{/* ---- Step 1: choose connection method ---- */}}
<div class="row row-cards">
<div class="col-md-6">
<div class="card card-link h-100">
<div class="card h-100" data-testid="add-method-serial">
<div class="card-body d-flex flex-column">
<h2 class="card-title">{{template "icon-antenna" "me-1"}}Add a repeater that's already on the network</h2>
<p class="text-secondary">Register a repeater that's already configured and on the mesh, then control it
remotely through a <strong>KISS modem</strong> over the air. You'll grant MeshTender admin yourself first.
This is the usual choice.</p>
<a class="btn btn-primary mt-auto align-self-start" href="/repeaters/add?step=consent&method=kiss">Add existing →</a>
<div class="need-tag mb-2">{{template "icon-plug" "me-1"}}Just the repeater and a USB cable</div>
<h2 class="card-title">Set up a new repeater</h2>
<p class="text-secondary">Plug a freshly flashed repeater into this computer. MeshTender sets it up in one pass from your organization's configuration profile: a new identity, the radio, its name, the region for where it stands, and admin access for MeshTender.</p>
<a class="btn mt-auto align-self-start" href="/repeaters/add?step=consent&method=serial">Set up over USB</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card card-link h-100">
<div class="card h-100" data-testid="add-method-kiss">
<div class="card-body d-flex flex-column">
<h2 class="card-title">{{template "icon-plug" "me-1"}}Set up a brand-new repeater</h2>
<p class="text-secondary">Connect a freshly-flashed repeater over <strong>USB serial</strong> and let
MeshTender generate and run the whole setup — identity, radio, region, name, and the admin grant —
straight down the wire. Best for a repeater that isn't on the network yet.</p>
<a class="btn mt-auto align-self-start" href="/repeaters/add?step=consent&method=serial">Set up over USB →</a>
<div class="need-tag mb-2">{{template "icon-antenna" "me-1"}}Already on the air</div>
<h2 class="card-title">Register an existing repeater</h2>
<p class="text-secondary">For a repeater you've already configured. Register it by its public key after giving MeshTender admin on it. Confirming it and managing it over the air use a <a href="{{.RootURL}}/docs#kiss">KISS modem</a>; its documentation and sharing don't.</p>
<a class="btn mt-auto align-self-start" href="/repeaters/add?step=consent&method=kiss">Register existing</a>
</div>
</div>
</div>
</div>
<div class="btn-list mt-3"><a class="btn" href="/">Cancel</a></div>
<div class="btn-list mt-3"><a class="btn btn-ghost-secondary" href="/repeaters">Cancel</a></div>
{{else if eq .Step "consent"}}
{{/* ---- Step 2: consent (wording adapts to the chosen method) ---- */}}
+2 -10
View File
@@ -2,14 +2,7 @@
{{define "header"}}{{template "repeater-header" .Nav}}{{end}}
{{define "content"}}
{{template "repeater-tabs" .Nav}}
<div class="card">
<div class="card-body">
<p class="text-secondary mb-0">
Commands grouped by console session (one modem connection). “Confirmed” means a reply came back;
no reply doesn't mean the command failed — it may have run without us hearing the response.
</p>
</div>
</div>
<p class="text-secondary">Every command sent from the console, by session. No reply doesn't mean a command failed: it may have run without its reply reaching us.</p>
{{if .Sessions}}
<ul class="timeline mt-3">
{{range .Sessions}}
@@ -46,8 +39,7 @@
{{end}}
{{else}}
<div class="card mt-3">
<div class="card-body"><p class="text-secondary mb-0">No commands have been sent yet.</p></div>
{{template "empty-state" (dict "Compact" true "Title" "No commands yet" "Line" "Commands sent from the console show up here.")}}
</div>
{{end}}
<a class="back-link mt-3" href="/">{{template "icon-arrow-left" "me-1"}}Back to dashboard</a>
{{end}}
+4 -1
View File
@@ -1,7 +1,10 @@
{{define "title"}}Delete {{.Repeater.Name}} · MeshTender{{end}}
{{define "header"}}
<div class="row g-2 align-items-center">
<div class="col"><h1 class="page-title fs-1">Delete “{{.Repeater.Name}}”?</h1></div>
<div class="col">
<ol class="breadcrumb mb-2" aria-label="Breadcrumb"><li class="breadcrumb-item"><a href="/repeaters">Repeaters</a></li><li class="breadcrumb-item"><a href="/repeaters/{{.Repeater.PublicID}}">{{.Repeater.Name}}</a></li><li class="breadcrumb-item active" aria-current="page">Delete</li></ol>
<h1 class="page-title fs-1">Delete “{{.Repeater.Name}}”?</h1>
</div>
</div>
{{end}}
{{define "content"}}
+3 -3
View File
@@ -2,15 +2,15 @@
{{define "header"}}
<div class="row g-2 align-items-center">
<div class="col">
<div class="page-pretitle">Edit repeater</div>
<h1 class="page-title fs-1">{{.Repeater.Name}}</h1>
<ol class="breadcrumb mb-2" aria-label="Breadcrumb"><li class="breadcrumb-item"><a href="/repeaters">Repeaters</a></li><li class="breadcrumb-item"><a href="/repeaters/{{.Repeater.PublicID}}">{{.Repeater.Name}}</a></li><li class="breadcrumb-item active" aria-current="page">Edit</li></ol>
<h1 class="page-title fs-1">Edit {{.Repeater.Name}}</h1>
</div>
</div>
{{end}}
{{define "content"}}
<div class="card">
<div class="card-body">
<p class="text-secondary">The public key identifies the repeater and can't be changed — delete and re-add to change it.</p>
<p class="text-secondary">The public key is the repeater's identity, so it can't be edited here.</p>
<code class="pk">{{.Repeater.PublicKeyHex}}</code>
<form method="post" action="/repeaters/{{.Repeater.PublicID}}/edit" class="mt-3">
+26 -48
View File
@@ -6,60 +6,39 @@
<div class="row row-cards">
<!-- Left column -->
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h2 class="card-title">Details</h2>
</div>
<div class="card-body">
<div class="datagrid">
<div class="datagrid-item">
<div class="datagrid-title">Status</div>
<div class="datagrid-content">
{{if .Repeater.Confirmed}}
{{if .Repeater.Corroborated}}<span class="badge bg-success-lt" title="Independently reached by {{range $i, $n := .Repeater.Corroborators}}{{if $i}}, {{end}}{{$n}}{{end}}">Corroborated</span>
{{else if .Repeater.SelfConfirmed}}<span class="badge bg-azure-lt" title="You reached it from your own modem; no one else has corroborated yet">Self-confirmed</span>
{{else}}<span class="badge bg-success-lt">Confirmed</span>{{end}}
{{else}}<span class="badge bg-yellow-lt" title="No one has reached this repeater through MeshTender yet">Unconfirmed</span>{{end}}
</div>
</div>
<div class="datagrid-item">
<div class="datagrid-title">Access level</div>
<div class="datagrid-content">
{{if .Repeater.AccessKnown}}{{if .Repeater.IsAdmin}}<span class="badge bg-success-lt">admin</span>{{else}}<span class="badge bg-warning-lt">guest only</span>{{end}}{{else}}<span class="text-secondary">unknown</span>{{end}}
</div>
</div>
{{if not .IsOwner}}
<div class="datagrid-item">
<div class="datagrid-title">Owner</div>
<div class="datagrid-content">{{.Repeater.OwnerName}}</div>
</div>
{{end}}
<div class="datagrid-item">
<div class="datagrid-title">Added</div>
<div class="datagrid-content">{{ts .Repeater.CreatedAt "date"}}</div>
</div>
</div>
<div class="mt-3">
<div class="form-label">Radio settings</div>
<div class="font-monospace">{{.Radio}}</div>
</div>
{{if and .IsOwner .Repeater.Confirmed .Repeater.AccessKnown (not .Repeater.IsAdmin)}}
<div class="alert alert-warning mt-3 mb-0">
<div>
MeshTender only has <strong>guest</strong> access here. Guest is available to anyone with a blank password, so there's no point operating at this level — re-run <code>setperm &lt;your key&gt; 3</code> on the repeater to grant admin, then re-test.
{{if and .IsOwner .Repeater.Confirmed .Repeater.AccessKnown (not .Repeater.IsAdmin)}}
<div class="alert alert-danger" role="alert" data-testid="guest-access">
<div>MeshTender only has <strong>guest</strong> access, which anyone gets with a blank password, so it can't manage this repeater. Grant it admin with <a href="{{.RootURL}}/docs#setperm"><code>setperm</code></a>, then connect from the console again.</div>
</div>
{{end}}
<div class="card" data-testid="repeater-details">
<div class="card-header"><h2 class="card-title">Details</h2></div>
<div class="list-group list-group-flush">
<div class="list-group-item">
<div class="d-flex align-items-center gap-2"><span class="text-secondary">Status</span>
<span class="ms-auto">{{if .Repeater.Confirmed}}<span class="badge bg-green-lt">Confirmed</span>{{else}}<span class="badge bg-orange-lt">Not confirmed</span>{{end}}</span></div>
<div class="small text-secondary mt-1">
{{if .Repeater.Corroborated}}Reached over the air by {{range $i, $n := .Repeater.Corroborators}}{{if $i}}, {{end}}{{$n}}{{end}}.
{{else if .Repeater.SelfConfirmed}}You've reached it over the air; no one else has yet.
{{else if .Repeater.Confirmed}}Reached over the air through MeshTender.
{{else}}No one has reached it over the air through MeshTender yet. Connecting to it from the console confirms it.{{end}}
</div>
</div>
<div class="list-group-item d-flex align-items-center gap-2"><span class="text-secondary">MeshTender's access</span>
<span class="ms-auto">{{if .Repeater.AccessKnown}}{{if .Repeater.IsAdmin}}<span class="badge bg-blue-lt">Admin</span>{{else}}<span class="badge bg-red-lt">Guest only</span>{{end}}{{else}}<span class="text-secondary">Not known yet</span>{{end}}</span></div>
{{if not .IsOwner}}
<div class="list-group-item d-flex align-items-center gap-2"><span class="text-secondary">Owner</span><span class="ms-auto">{{.Repeater.OwnerName}}</span></div>
{{end}}
<div class="list-group-item d-flex align-items-center gap-2"><span class="text-secondary">Added</span><span class="ms-auto">{{ts .Repeater.CreatedAt "date"}}</span></div>
<div class="list-group-item d-flex align-items-center gap-2"><span class="text-secondary">Radio</span><span class="ms-auto font-monospace small">{{.Radio}}</span></div>
</div>
</div>
{{if .IsOwner}}
<div class="card mt-3">
<div class="card-header">
<h2 class="card-title">Organizations</h2>
<div class="card-actions"><a href="/repeaters/{{.Repeater.PublicID}}/share" class="btn btn-sm">Manage</a></div>
<div><h2 class="card-title">Organizations</h2><div class="card-subtitle">Each one's recommended configuration for this repeater, for where it stands.</div></div>
<div class="card-actions"><a href="/repeaters/{{.Repeater.PublicID}}/share" class="btn btn-sm">Manage sharing</a></div>
</div>
{{if .Orgs}}
<div class="list-group list-group-flush">
@@ -70,13 +49,13 @@
<a class="fw-bold text-reset" href="/orgs/{{.OrgSlug}}">{{.OrgName}}</a>
</div>
<div class="btn-list flex-nowrap">
<a class="btn btn-sm" href="/orgs/{{.OrgSlug}}/config{{if $.Repeater.Latitude}}?lat={{$.Repeater.Latitude}}&lon={{$.Repeater.Longitude}}{{end}}" title="This organization's recommended config for this repeater">{{template "icon-settings" "me-1"}}Config</a>
<a class="btn btn-sm" href="/orgs/{{.OrgSlug}}/config{{if $.Repeater.Latitude}}?lat={{$.Repeater.Latitude}}&lon={{$.Repeater.Longitude}}{{end}}" >{{template "icon-settings" "me-1"}}Recommended configuration</a>
</div>
</div>
{{end}}
</div>
{{else}}
<div class="card-body"><p class="text-secondary mb-0">Not shared with any organization.</p></div>
{{template "empty-state" (dict "Compact" true "Title" "Not shared with any organization" "Line" "Repeaters are shared with the organizations you join; you can opt one out on its Sharing tab.")}}
{{end}}
</div>
@@ -150,5 +129,4 @@
</div>
</div>
<a class="back-link mt-3" href="/repeaters">{{template "icon-arrow-left" "me-1"}}All repeaters</a>
{{end}}
+2 -9
View File
@@ -10,11 +10,7 @@
<h2 class="card-title">Public documentation {{if .Repeater.ExposePublicPage}}<span class="text-secondary">· visible on the public page</span>{{else}}<span class="text-secondary">· not published yet</span>{{end}}</h2>
</div>
<div class="card-body">
<p class="text-secondary">
What this node is and how to service it — safe for anyone who can reach the public page.
Shown on the public page when one is published.
<strong>Do not put gate codes, lock combinations, or other site-access secrets here</strong> — use the internal section below for those.
</p>
<p class="text-secondary">What it is and how to service it: hardware, antenna, power, firmware. Anyone can read it. <strong>Keep gate codes and other site-access secrets in the internal notes below.</strong> Markdown works.</p>
<textarea class="form-control font-monospace" name="doc_public" rows="10" maxlength="20000" placeholder="Hardware, antenna, power/solar, firmware, how to service…"
aria-label="Public documentation (Markdown)">{{.Repeater.DocPublic}}</textarea>
</div>
@@ -25,10 +21,7 @@
<h2 class="card-title">Internal notes <span class="text-secondary">· people with access only</span></h2>
</div>
<div class="card-body">
<p class="text-secondary">
Sensitive site-access details — gate codes, lock combinations, landlord/contact info, climbing notes.
Only the owner, stewards, and people you've shared with can read this. <strong>Never shown on the public page.</strong>
</p>
<p class="text-secondary">Site access: gate codes, lock combinations, who to call. Only you, its stewards and people you share it with can read these; they're never public.</p>
<textarea class="form-control font-monospace" name="doc_internal" rows="8" maxlength="20000" placeholder="Gate code, lock combo, who to call for site access…"
aria-label="Internal notes (Markdown, not public)">{{.Repeater.DocInternal}}</textarea>
</div>
@@ -6,10 +6,7 @@
<div class="card">
<div class="card-body">
<h2 class="card-title">Log maintenance</h2>
<p class="text-secondary">
Record physical service work — antenna swaps, battery/solar changes, firmware updates, site visits —
so the history outlives any one person. Anyone with access can log an entry.
</p>
<p class="text-secondary">Antenna swaps, battery and solar work, firmware updates, site visits: a history that outlives any one person. Anyone with access can add to it.</p>
<form method="post" action="/repeaters/{{.Repeater.PublicID}}/maintenance">
<div class="row g-2">
<div class="col-12 col-md-3">
@@ -56,7 +53,7 @@
</ul>
{{else}}
<div class="card mt-3">
<div class="card-body"><p class="text-secondary mb-0">No maintenance has been logged yet.</p></div>
{{template "empty-state" (dict "Compact" true "Title" "Nothing logged yet" "Line" "Entries you add above show up here, newest first.")}}
</div>
{{end}}
{{end}}
@@ -121,5 +121,4 @@
</div>
</div>
<a class="back-link mt-3" href="/repeaters/{{.Repeater.PublicID}}/share">{{template "icon-arrow-left" "me-1"}}Back to sharing</a>
{{end}}
+8 -8
View File
@@ -26,7 +26,7 @@
<h2 class="section-kicker">For members</h2>
<section class="row align-items-center g-5 pb-5">
<div class="col-lg-5">
{{template "landing-tag" "companion"}}
{{template "need-tag" "companion"}}
<h3 class="h1 mb-3">Check in from the app you already use</h3>
<p class="fs-4 text-secondary">Register the companion you carry, then check in to your organization's weekly net by sending a flood advert. Follow the roster and the news as it comes in, and read your organization's docs and recommended setup any time.</p>
</div>
@@ -36,7 +36,7 @@
<h2 class="section-kicker">For repeater owners</h2>
<section class="row align-items-center g-5 pb-5 flex-lg-row-reverse">
<div class="col-lg-5">
{{template "landing-tag" "usb"}}
{{template "need-tag" "usb"}}
<h3 class="h1 mb-3">Set up a new repeater to your organization's spec</h3>
<p class="fs-4 text-secondary">Plug a freshly flashed repeater into your computer and pick your organization's profile. MeshTender writes the radio, name, region for where it stands and settings in one pass, like the flasher's Repeater Setup, without typing any of it.</p>
</div>
@@ -44,7 +44,7 @@
</section>
<section class="row align-items-center g-5 pb-5">
<div class="col-lg-5">
{{template "landing-tag" "none"}}
{{template "need-tag" "none"}}
<h3 class="h1 mb-3">Document it, and share it without passwords</h3>
<p class="fs-4 text-secondary">Keep each repeater's notes and maintenance history, publish a public page with its stewards, and decide who may run which commands on it: a single-use invite, a trusted steward, or your organization.</p>
</div>
@@ -54,7 +54,7 @@
<h2 class="section-kicker">For organizations</h2>
<section class="row align-items-center g-5 pb-5 flex-lg-row-reverse">
<div class="col-lg-5">
{{template "landing-tag" "none"}}
{{template "need-tag" "none"}}
<h3 class="h1 mb-3">One configuration for the whole region</h3>
<p class="fs-4 text-secondary">Publish your radio settings and configuration profiles, and draw regions on a map. Members see the exact commands for where their repeater stands, and new repeaters get them at setup.</p>
</div>
@@ -64,7 +64,7 @@
<h2 class="section-kicker">With a KISS modem</h2>
<section class="row align-items-center g-5 pb-5">
<div class="col-lg-5">
{{template "landing-tag" "modem"}}
{{template "need-tag" "modem"}}
<h3 class="h1 mb-3">Manage repeaters over the air</h3>
<p class="fs-4 text-secondary">For admins, stewards and net control: plug a MeshCore radio flashed as a KISS modem into your computer to run CLI commands on repeaters from the browser, confirm they're reachable, apply your organization's configuration, and run a net on the air. Every command and reply is logged.</p>
</div>
@@ -87,10 +87,10 @@
</div>
{{end}}
{{/* landing-tag says what a feature needs. Pass "companion", "usb", "none" or
{{/* need-tag says what a feature needs. Pass "companion", "usb", "none" or
"modem". */}}
{{define "landing-tag"}}
<div class="landing-tag mb-2" data-testid="landing-tag-{{.}}">
{{define "need-tag"}}
<div class="need-tag mb-2" data-testid="need-tag-{{.}}">
{{if eq . "companion"}}{{template "icon-radio" "me-1"}}Just your companion app
{{else if eq . "usb"}}{{template "icon-plug" "me-1"}}Just the repeater and a USB cable
{{else if eq . "modem"}}{{template "icon-antenna" "me-1"}}Needs a KISS modem
+3 -2
View File
@@ -291,8 +291,9 @@ code.pubkey {
background: var(--tblr-primary-lt);
color: var(--tblr-primary-text-emphasis);
}
/* What a landing feature needs ("Just your companion app", "Needs a KISS modem"). */
.landing-tag {
/* What a feature needs ("Just your companion app", "Needs a KISS modem"), on the
landing page and the add-repeater wizard. */
.need-tag {
display: inline-flex;
align-items: center;
font-size: 0.875rem;