mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-02 10:03:43 +00:00
Fix page headings
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Your account</div>
|
||||
<h2 class="page-title">Account settings</h2>
|
||||
<h1 class="page-title fs-1">Account settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -12,7 +12,7 @@
|
||||
{{if .OK}}<div class="alert alert-success">{{.OK}}</div>{{end}}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Username</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Username</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">Your username is your unique handle (3–32 chars: letters, digits, <code>_ . -</code>). You can change it once every 30 days. When you do, your old username is reserved for 90 days before anyone else can take it.</p>
|
||||
<form method="post" action="/account/username">
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Display name</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Display name</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">Your display name is what other people see — on shares and in organizations. It can be anything, and it isn't tied to sign-in.</p>
|
||||
<form method="post" action="/account/profile">
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Time zone</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Time zone</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">Dates and times across MeshTender are shown in this time zone (and in your browser's locale format). Leave it on <strong>Auto-detect</strong> to use whatever zone your browser reports.</p>
|
||||
<form method="post" action="/account/timezone">
|
||||
@@ -82,7 +82,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Public profile</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Public profile</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">These appear on your public page at <a href="{{.RootURL}}/u/{{.User.Username}}" target="_blank" rel="noopener">{{.RootURL}}/u/{{.User.Username}}</a>, which anyone can view. Leave a field blank to keep it off your page.</p>
|
||||
<form method="post" action="/account/profile-fields">
|
||||
@@ -108,7 +108,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Links <span class="text-secondary">(public)</span></h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Links <span class="text-secondary">(public)</span></h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">Ways to reach or find you — social media, a website, or your MeshCore public key (shown as a QR code). Mark one non-MeshCore link as your <strong>primary contact</strong> so people know the best way to reach you.</p>
|
||||
<form method="post" action="/account/links" data-link-editor>
|
||||
@@ -174,7 +174,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Passkeys</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Passkeys</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">Passkeys let you sign in with your device's fingerprint, face, or PIN — no password to remember or leak.</p>
|
||||
{{if .PKErr}}<div class="alert alert-danger">{{.PKErr}}</div>{{end}}
|
||||
@@ -217,7 +217,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Password</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Password</h2></div>
|
||||
<div class="card-body">
|
||||
{{if .HasPassword}}
|
||||
<p class="text-secondary">Change the password you use as a fallback to passkeys.</p>
|
||||
@@ -264,7 +264,7 @@
|
||||
<div class="modal-content">
|
||||
<form method="post" action="/account/passkeys/rename">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Rename passkey</h5>
|
||||
<h2 class="modal-title">Rename passkey</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{{define "content"}}
|
||||
<div class="card card-md">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-center mb-4">Sign in to your account</h2>
|
||||
<h1 class="card-title text-center mb-4">Sign in to your account</h1>
|
||||
{{if .Error}}<div class="alert alert-danger">{{.Error}}</div>{{end}}
|
||||
|
||||
<form method="post" action="/login/password" id="signin-form">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{{define "content"}}
|
||||
<div class="card card-md">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-center mb-2">Create your account</h2>
|
||||
<h1 class="card-title text-center mb-2">Create your account</h1>
|
||||
{{if .Error}}<div class="alert alert-danger">{{.Error}}</div>{{end}}
|
||||
<p class="text-secondary text-center mb-4">No email required. Passkeys are the easiest, most secure option — your device unlocks the account with a fingerprint, face, or PIN.</p>
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jleight/meshtender/internal/store"
|
||||
)
|
||||
|
||||
var headingRe = regexp.MustCompile(`(?i)<h([1-6])\b`)
|
||||
|
||||
// checkHeadingOutline asserts the two properties that make a page navigable by
|
||||
// heading: it has exactly one <h1> naming the page, and no level is skipped on the
|
||||
// way down (an h1 followed by an h3 leaves a hole in the outline that screen-reader
|
||||
// users navigating by level fall through).
|
||||
func checkHeadingOutline(t *testing.T, label, html string) {
|
||||
t.Helper()
|
||||
|
||||
var levels []int
|
||||
for _, m := range headingRe.FindAllStringSubmatch(html, -1) {
|
||||
n, err := strconv.Atoi(m[1])
|
||||
if err != nil {
|
||||
t.Fatalf("%s: unparseable heading %q", label, m[0])
|
||||
}
|
||||
levels = append(levels, n)
|
||||
}
|
||||
if len(levels) == 0 {
|
||||
t.Errorf("%s: page has no headings at all", label)
|
||||
return
|
||||
}
|
||||
|
||||
h1s := 0
|
||||
for _, l := range levels {
|
||||
if l == 1 {
|
||||
h1s++
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case h1s == 0:
|
||||
t.Errorf("%s: no <h1> — the page has no top-level heading (outline starts at h%d)",
|
||||
label, levels[0])
|
||||
case h1s > 1:
|
||||
t.Errorf("%s: %d <h1> elements — exactly one should name the page; sequence: %s",
|
||||
label, h1s, seq(levels))
|
||||
}
|
||||
if levels[0] != 1 {
|
||||
t.Errorf("%s: outline starts at h%d, not h1 (sequence: %s)", label, levels[0], seq(levels))
|
||||
}
|
||||
for i := 1; i < len(levels); i++ {
|
||||
if levels[i] > levels[i-1]+1 {
|
||||
t.Errorf("%s: heading level jumps h%d → h%d (skips h%d); sequence: %s",
|
||||
label, levels[i-1], levels[i], levels[i-1]+1, seq(levels))
|
||||
break // one report per page is enough to act on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func seq(levels []int) string {
|
||||
parts := make([]string, len(levels))
|
||||
for i, l := range levels {
|
||||
parts[i] = fmt.Sprintf("h%d", l)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// TestPageHeadingOutlines walks a representative page from every layout and header
|
||||
// source — the default base header, the repeater-tabs header, the org-tabs header,
|
||||
// the auth layout (which has no page header, so the card title carries it), the
|
||||
// public root layout, and the branded error page — and checks each one's heading
|
||||
// outline.
|
||||
//
|
||||
// Regression for audit findings A1 (28 pages had no <h1>; page titles were
|
||||
// <h2 class="page-title">) and A2 (the landing page ran h1 → h3 → h2, skipping a
|
||||
// level and then jumping backwards).
|
||||
func TestPageHeadingOutlines(t *testing.T) {
|
||||
t.Parallel()
|
||||
st, ctx, ts, h := splitServer(t)
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("cookiejar: %v", err)
|
||||
}
|
||||
user := seedSession(t, ts, st, ctx, jar, "headinguser")
|
||||
cookies := jar.Cookies(mustURL(t, ts.URL))
|
||||
|
||||
rep, err := st.CreateRepeater(ctx, &store.Repeater{
|
||||
OwnerID: user.ID, Name: "Heading Test Relay", PublicKeyHex: strings.Repeat("e", 64),
|
||||
RadioFreqHz: 1, RadioBwHz: 1, RadioSF: 11, RadioCR: 5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create repeater: %v", err)
|
||||
}
|
||||
org, err := st.CreateOrg(ctx, "Heading Test Org", user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("create org: %v", err)
|
||||
}
|
||||
|
||||
pages := []struct {
|
||||
label, host, path string
|
||||
anon bool // fetch without the session cookie
|
||||
}{
|
||||
// Root (public) host — the landing page is A2's subject.
|
||||
{"root landing", h.root, "/", false},
|
||||
{"root org directory", h.root, "/orgs", false},
|
||||
{"root docs", h.root, "/docs", false},
|
||||
{"root public org page", h.root, "/orgs/" + org.Slug, false},
|
||||
{"root 404 (error page)", h.root, "/no-such-page", false},
|
||||
// Auth host — authbase has no page header, so the card title is the heading.
|
||||
{"auth login", h.auth, "/login", true},
|
||||
{"auth signup", h.auth, "/signup", true},
|
||||
// App host — covers base's own header, repeater-tabs and org-tabs headers.
|
||||
{"app dashboard", h.app, "/", false},
|
||||
{"app repeater list", h.app, "/repeaters", false},
|
||||
{"app repeater overview", h.app, "/repeaters/" + rep.PublicID, false},
|
||||
{"app repeater sharing", h.app, "/repeaters/" + rep.PublicID + "/share", false},
|
||||
{"app my orgs", h.app, "/orgs", false},
|
||||
{"app org member view", h.app, "/orgs/" + org.Slug, false},
|
||||
{"app 404 (error page)", h.app, "/no-such-page", false},
|
||||
}
|
||||
|
||||
for _, p := range pages {
|
||||
send := cookies
|
||||
if p.anon {
|
||||
send = nil
|
||||
}
|
||||
resp := do(t, ts, p.host, p.path, send...)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: read body: %v", p.label, err)
|
||||
}
|
||||
// A redirect would mean the fixture didn't grant access; the outline of a
|
||||
// redirect body is meaningless, so surface it rather than silently passing.
|
||||
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
|
||||
t.Fatalf("%s: unexpected %d redirect to %q — fixture problem",
|
||||
p.label, resp.StatusCode, resp.Header.Get("Location"))
|
||||
}
|
||||
checkHeadingOutline(t, p.label, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
// TestModalFragmentHeadings covers the htmx modal fragments separately: they're
|
||||
// swapped into an already-rendered page, so their heading has to fit under that
|
||||
// page's outline (h1 page → h2 dialog title) rather than restart it.
|
||||
func TestModalFragmentHeadings(t *testing.T) {
|
||||
t.Parallel()
|
||||
st, ctx, ts, h := splitServer(t)
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("cookiejar: %v", err)
|
||||
}
|
||||
user := seedSession(t, ts, st, ctx, jar, "modaluser")
|
||||
cookies := jar.Cookies(mustURL(t, ts.URL))
|
||||
rep, err := st.CreateRepeater(ctx, &store.Repeater{
|
||||
OwnerID: user.ID, Name: "Modal Test Relay", PublicKeyHex: strings.Repeat("f", 64),
|
||||
RadioFreqHz: 1, RadioBwHz: 1, RadioSF: 11, RadioCR: 5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create repeater: %v", err)
|
||||
}
|
||||
|
||||
// The invite modal is the representative fragment; all modal titles share the
|
||||
// same convention.
|
||||
resp := do(t, ts, h.app, "/repeaters/"+rep.PublicID+"/share/link/new", cookies...)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("invite modal fragment = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
found := headingRe.FindAllStringSubmatch(string(body), -1)
|
||||
if len(found) == 0 {
|
||||
t.Fatal("invite modal fragment has no heading — the dialog has nothing to label it")
|
||||
}
|
||||
if got := found[0][1]; got != "2" {
|
||||
t.Errorf("modal title is h%s, want h2 (it sits under the page's h1; an h5 there "+
|
||||
"would skip three levels)", got)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "title"}}Add a repeater · MeshTender{{end}}
|
||||
{{define "header"}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col"><h2 class="page-title">Add a repeater</h2></div>
|
||||
<div class="col"><h1 class="page-title fs-1">Add a repeater</h1></div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="col-md-6">
|
||||
<div class="card card-link h-100">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h3 class="card-title">{{template "icon-antenna" "me-1"}}Add a repeater that's already on the network</h3>
|
||||
<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>
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="col-md-6">
|
||||
<div class="card card-link h-100">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h3 class="card-title">{{template "icon-plug" "me-1"}}Set up a brand-new repeater</h3>
|
||||
<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>
|
||||
@@ -64,7 +64,7 @@
|
||||
<div class="d-flex">
|
||||
<div>{{template "icon-alert" "alert-icon"}}</div>
|
||||
<div>
|
||||
<h4 class="alert-title">This grants full admin control to MeshTender</h4>
|
||||
<h3 class="alert-title">This grants full admin control to MeshTender</h3>
|
||||
<ul class="mb-0 text-secondary">
|
||||
<li>This grants admin to <strong>MeshTender itself</strong> — <strong>not</strong> to other
|
||||
MeshTender users. Letting specific people operate the repeater is a separate step you control later
|
||||
@@ -84,7 +84,7 @@
|
||||
<div class="d-flex">
|
||||
<div>{{template "icon-map-pin" "alert-icon"}}</div>
|
||||
<div>
|
||||
<h4 class="alert-title">MeshTender will read the repeater's location</h4>
|
||||
<h3 class="alert-title">MeshTender will read the repeater's location</h3>
|
||||
<p class="text-secondary mb-0">
|
||||
When you confirm this repeater — or use “Fetch location” later — MeshTender reads its coordinates by
|
||||
running <code>get lat</code> / <code>get lon</code> through your modem, <strong>on its own behalf</strong>
|
||||
@@ -251,7 +251,7 @@
|
||||
|
||||
{{/* Review + run */}}
|
||||
<div class="card mt-3" id="review-card" hidden>
|
||||
<div class="card-header"><h3 class="card-title">Step 4 · Review & run</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Step 4 · Review & run</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">These commands will be sent to your repeater over USB, in order. The private key is
|
||||
shown masked; it never leaves your browser.</p>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Administration</div>
|
||||
<h2 class="page-title">Admin</h2>
|
||||
<h1 class="page-title fs-1">Admin</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="col-md-6">
|
||||
<a class="card card-link" href="/admin/catalog">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{{template "icon-list" "me-1"}}Command catalog</h3>
|
||||
<h2 class="card-title">{{template "icon-list" "me-1"}}Command catalog</h2>
|
||||
<p class="text-secondary mb-0">Edit risk tags and the default command sets.</p>
|
||||
</div>
|
||||
</a>
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="col-md-6">
|
||||
<a class="card card-link" href="/admin/users">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{{template "icon-users" "me-1"}}Users & capabilities</h3>
|
||||
<h2 class="card-title">{{template "icon-users" "me-1"}}Users & capabilities</h2>
|
||||
<p class="text-secondary mb-0">Grant or revoke admin capabilities.</p>
|
||||
</div>
|
||||
</a>
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="col-md-6">
|
||||
<a class="card card-link" href="/admin/analytics">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{{template "icon-list" "me-1"}}Traffic analytics</h3>
|
||||
<h2 class="card-title">{{template "icon-list" "me-1"}}Traffic analytics</h2>
|
||||
<p class="text-secondary mb-0">Visits, unique visitors, busiest pages, and traffic by surface.</p>
|
||||
</div>
|
||||
</a>
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="col-md-6">
|
||||
<a class="card card-link" href="/admin/proxy-test">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{{template "icon-world" "me-1"}}Reverse proxy test</h3>
|
||||
<h2 class="card-title">{{template "icon-world" "me-1"}}Reverse proxy test</h2>
|
||||
<p class="text-secondary mb-0">Inspect forwarding headers and the client IP the app records.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Administration</div>
|
||||
<h2 class="page-title">Command catalog</h2>
|
||||
<h1 class="page-title fs-1">Command catalog</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
{{range .Groups}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">{{.Name}}</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">{{.Name}}</h2></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table catalog-table">
|
||||
<thead>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Administration</div>
|
||||
<h2 class="page-title">Username history</h2>
|
||||
<h1 class="page-title fs-1">Username history</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -35,7 +35,7 @@
|
||||
{{/* history-modal is the htmx fragment swapped into the shared modal's content. */}}
|
||||
{{define "history-modal"}}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Username history — {{.Account.Name}} <span class="text-secondary">@{{.Account.Username}}</span></h5>
|
||||
<h2 class="modal-title">Username history — {{.Account.Name}} <span class="text-secondary">@{{.Account.Username}}</span></h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
closes the modal — no extra client JS needed. */}}
|
||||
{{define "permissions-modal"}}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Permissions — {{.Account.Name}} <span class="text-secondary">@{{.Account.Username}}</span></h5>
|
||||
<h2 class="modal-title">Permissions — {{.Account.Name}} <span class="text-secondary">@{{.Account.Username}}</span></h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post" action="/admin/users/{{.Account.ID}}">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Administration</div>
|
||||
<h2 class="page-title">Users & capabilities</h2>
|
||||
<h1 class="page-title fs-1">Users & capabilities</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Administration</div>
|
||||
<h2 class="page-title">Traffic Analytics</h2>
|
||||
<h1 class="page-title fs-1">Traffic Analytics</h1>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-group" role="group" aria-label="Time range">
|
||||
@@ -47,7 +47,7 @@
|
||||
<div class="row row-cards mt-1">
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Requests per day</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Requests per day</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-end gap-1" style="height:140px">
|
||||
{{range .Bars}}<div class="flex-fill rounded-top" style="height:{{.ReqH}}%;min-height:1px;background:var(--tblr-primary)" title="{{.Label}}: {{.Requests}} requests"></div>{{end}}
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Unique visitors per day</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Unique visitors per day</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-end gap-1" style="height:140px">
|
||||
{{range .Bars}}<div class="flex-fill rounded-top" style="height:{{.VisH}}%;min-height:1px;background:var(--tblr-green)" title="{{.Label}}: {{.Visitors}} visitors"></div>{{end}}
|
||||
@@ -71,7 +71,7 @@
|
||||
<div class="row row-cards mt-1">
|
||||
<div class="col-lg-5">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Traffic by surface</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Traffic by surface</h2></div>
|
||||
<div class="card-body">
|
||||
{{if .Surfaces}}{{range .Surfaces}}{{template "value-bar" .}}{{end}}
|
||||
{{else}}<div class="text-secondary">No data.</div>{{end}}
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Busiest pages</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Busiest pages</h2></div>
|
||||
<div class="card-body">
|
||||
{{if .Paths}}{{range .Paths}}{{template "value-bar" .}}{{end}}
|
||||
{{else}}<div class="text-secondary">No data.</div>{{end}}
|
||||
@@ -93,7 +93,7 @@
|
||||
<div class="row row-cards mt-1">
|
||||
<div class="col-lg-5">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Traffic by host</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Traffic by host</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small">“custom” is any request whose host isn't your configured app/auth/root host — e.g. the bare IP, a health probe, or a crawler. These are the actual hosts seen.</p>
|
||||
{{if .Hosts}}{{range .Hosts}}{{template "value-bar" .}}{{end}}
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Top visitors</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Top visitors</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small">Anonymous, daily-rotating IDs (a salted hash of IP + browser). The same person gets a fresh ID each day, so a heavy daily visitor shows once per day.</p>
|
||||
{{if .Visitors}}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="card timeline-event-card">
|
||||
<div class="card-body">
|
||||
<div class="text-secondary float-end">{{ts .StartedAt "datetime"}}{{if .EndedAt}}–{{ts .EndedAt "time"}}{{else}} · in progress{{end}}</div>
|
||||
<h4 class="mb-2">{{if .SenderUsername}}<a class="text-reset" href="{{$.RootURL}}/u/{{.SenderUsername}}">{{.SenderName}}</a>{{else}}{{.SenderName}}{{end}}</h4>
|
||||
<h2 class="mb-2 fs-3">{{if .SenderUsername}}<a class="text-reset" href="{{$.RootURL}}/u/{{.SenderUsername}}">{{.SenderName}}</a>{{else}}{{.SenderName}}{{end}}</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter">
|
||||
<thead><tr><th>When</th><th>Command</th><th>Confirmed</th><th>Response</th></tr></thead>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">{{.Org.Name}}</div>
|
||||
<h2 class="page-title">Edit configuration</h2>
|
||||
<h1 class="page-title fs-1">Edit configuration</h1>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<a class="btn" href="/orgs/{{.Org.Slug}}/config">Back to configuration</a>
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">Profiles</h3>
|
||||
<h2 class="card-title">Profiles</h2>
|
||||
<div class="text-secondary small">Named sets of base settings — e.g. one per board family. Members pick the profile matching their use case.</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">Regions</h3>
|
||||
<h2 class="card-title">Regions</h2>
|
||||
<div class="text-secondary small">Named areas drawn on a map. A repeater is auto-assigned to every region whose area contains it, forming its <code>region def</code>.</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">{{.Org.Name}} · Configuration</div>
|
||||
<h2 class="page-title">{{if .ProfileID}}Edit profile{{else}}Add profile{{end}}</h2>
|
||||
<h1 class="page-title fs-1">{{if .ProfileID}}Edit profile{{else}}Add profile{{end}}</h1>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<a class="btn" href="/orgs/{{.Org.Slug}}/config/edit">Back to configuration</a>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">{{.Org.Name}} · Configuration</div>
|
||||
<h2 class="page-title">Edit regions</h2>
|
||||
<h1 class="page-title fs-1">Edit regions</h1>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<a class="btn" href="/orgs/{{.Org.Slug}}/config/edit">Back to configuration</a>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Console</div>
|
||||
<h2 class="page-title">{{.Repeater.Name}}</h2>
|
||||
<h1 class="page-title fs-1">{{.Repeater.Name}}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -47,7 +47,7 @@ swap needs no new markup. The flex row is nested so the [hidden] attribute wins
|
||||
<div class="row row-cards">
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Console</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Console</h2></div>
|
||||
<div class="card-body">
|
||||
<div id="unsupported" class="alert alert-danger" role="alert" hidden>
|
||||
This browser does not support WebSerial. Use Chrome, Edge, or another Chromium-based browser over HTTPS or on localhost.
|
||||
@@ -75,7 +75,7 @@ swap needs no new markup. The flex row is nested so the [hidden] attribute wins
|
||||
data-config-url="/repeaters/{{.Repeater.PublicID}}/config.json"
|
||||
data-location-url="/repeaters/{{.Repeater.PublicID}}/location">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Apply organization configuration</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Apply organization configuration</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">
|
||||
Pick an organization and profile to see its recommended configuration for this repeater. Every command is
|
||||
@@ -109,7 +109,7 @@ swap needs no new markup. The flex row is nested so the [hidden] attribute wins
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card" data-testid="allowed-commands">
|
||||
<div class="card-header"><h3 class="card-title">Allowed commands</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Allowed commands</h2></div>
|
||||
<div class="card-body">
|
||||
{{if .Commands}}
|
||||
<div class="d-flex flex-wrap gap-1" style="max-height:360px;overflow-y:auto">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Dashboard</div>
|
||||
<h2 class="page-title">Welcome back{{if .UserName}}, {{.UserName}}{{end}}</h2>
|
||||
<h1 class="page-title fs-1">Welcome back{{if .UserName}}, {{.UserName}}{{end}}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -70,7 +70,7 @@
|
||||
{{if .Onboarding}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Get started</h3>
|
||||
<h2 class="card-title">Get started</h2>
|
||||
<div class="card-actions text-secondary small">{{.OnboardingDone}} of {{.OnboardingTotal}} done</div>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
@@ -95,7 +95,7 @@
|
||||
<div class="col-lg-8">
|
||||
<!-- Recent activity -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Recent activity</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Recent activity</h2></div>
|
||||
{{if .Recent}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
@@ -123,7 +123,7 @@
|
||||
<div class="col-lg-4">
|
||||
<!-- Map -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Repeater map</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Repeater map</h2></div>
|
||||
{{if .Mapped}}
|
||||
<div class="card-body p-2">
|
||||
<link rel="stylesheet" href="{{ asset "/static/leaflet.css" }}">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "title"}}Delete {{.Repeater.Name}} · MeshTender{{end}}
|
||||
{{define "header"}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col"><h2 class="page-title">Delete “{{.Repeater.Name}}”?</h2></div>
|
||||
<div class="col"><h1 class="page-title fs-1">Delete “{{.Repeater.Name}}”?</h1></div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="d-flex">
|
||||
<div>{{template "icon-alert" "alert-icon"}}</div>
|
||||
<div>
|
||||
<h4 class="alert-title">This does not revoke MeshTender's access</h4>
|
||||
<h3 class="alert-title">This does not revoke MeshTender's access</h3>
|
||||
<div class="text-secondary">Your repeater still trusts MeshTender's identity at the firmware level. If you want MeshTender to truly lose access, revoke it on the device too — see below.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Edit repeater</div>
|
||||
<h2 class="page-title">{{.Repeater.Name}}</h2>
|
||||
<h1 class="page-title fs-1">{{.Repeater.Name}}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "title"}}Repeater invite · MeshTender{{end}}
|
||||
{{define "header"}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col"><h2 class="page-title">Repeater invite</h2></div>
|
||||
<div class="col"><h1 class="page-title fs-1">Repeater invite</h1></div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Organization</div>
|
||||
<h2 class="page-title">Join {{.Org.Name}}</h2>
|
||||
<h1 class="page-title fs-1">Join {{.Org.Name}}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Your network</div>
|
||||
<h2 class="page-title">Organizations</h2>
|
||||
<h1 class="page-title fs-1">Organizations</h1>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Organizations</div>
|
||||
<h2 class="page-title">Create an organization</h2>
|
||||
<h1 class="page-title fs-1">Create an organization</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">About</h3>
|
||||
<h2 class="card-title">About</h2>
|
||||
{{if .IsAdmin}}
|
||||
<div class="card-actions">
|
||||
<button type="button" class="btn btn-sm" data-testid="edit-profile"
|
||||
@@ -77,7 +77,7 @@
|
||||
{{if or .Links .IsAdmin}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Links</h3>
|
||||
<h2 class="card-title">Links</h2>
|
||||
{{if .IsAdmin}}
|
||||
<div class="card-actions">
|
||||
<button type="button" class="btn btn-sm" data-testid="edit-links"
|
||||
@@ -114,7 +114,7 @@
|
||||
place yet. Re-enable this card (plus the domains data in pageOrg and the
|
||||
/domains routes in web.go) once it is.
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Custom domains</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Custom domains</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">
|
||||
Point your own domain at MeshTender (CNAME it to this app). Once verified, visiting that
|
||||
@@ -169,7 +169,7 @@
|
||||
<div class="col-lg-4">
|
||||
{{if .HasMap}}
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Repeaters</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Repeaters</h2></div>
|
||||
<div class="card-body p-2">
|
||||
<link rel="stylesheet" href="{{ asset "/static/leaflet.css" }}">
|
||||
<div id="map" style="height:260px" role="region" aria-label="Map of organization repeater locations"></div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
from the window.MESHTENDER_LINK_PLATFORMS global set on the page. */}}
|
||||
{{define "org-links-modal"}}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit links</h5>
|
||||
<h2 class="modal-title">Edit links</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
HX-Redirect to reload the org page. The plain action= is the no-JS fallback. */}}
|
||||
{{define "org-profile-modal"}}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit profile</h5>
|
||||
<h2 class="modal-title">Edit profile</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<!-- Left: member list -->
|
||||
<div class="col-lg-8 col-xl-9">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">{{len .Members}} member{{if ne (len .Members) 1}}s{{end}}</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">{{len .Members}} member{{if ne (len .Members) 1}}s{{end}}</h2></div>
|
||||
<div class="list-group list-group-flush" id="member-list">
|
||||
{{range .Members}}
|
||||
<div class="list-group-item d-flex align-items-center flex-wrap gap-2" data-role="{{.Role}}" data-name="{{.Name}} @{{.Username}}">
|
||||
@@ -37,7 +37,7 @@
|
||||
<!-- Right: search + filter sidebar -->
|
||||
<div class="col-lg-4 col-xl-3">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Find members</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Find members</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="member-search">Search</label>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
buttons reference their form via form=. No client JS. */}}
|
||||
{{define "person-access-modal"}}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Manage access — {{.Target.Name}} <span class="text-secondary">@{{.Target.Username}}</span></h5>
|
||||
<h2 class="modal-title">Manage access — {{.Target.Name}} <span class="text-secondary">@{{.Target.Username}}</span></h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Administration</div>
|
||||
<h2 class="page-title">Reverse proxy test</h2>
|
||||
<h1 class="page-title fs-1">Reverse proxy test</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -49,7 +49,7 @@
|
||||
{{end}}
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Trusted-proxy resolution</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Trusted-proxy resolution</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Connecting peer</div>
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Forwarding headers</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Forwarding headers</h2></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead><tr><th>Header</th><th>Value</th></tr></thead>
|
||||
@@ -97,7 +97,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Connection</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Connection</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item"><div class="datagrid-title">Host</div><div class="datagrid-content font-monospace">{{.Host}}</div></div>
|
||||
@@ -110,7 +110,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">All request headers</h3>
|
||||
<div class="card-header"><h2 class="card-title">All request headers</h2>
|
||||
<span class="card-subtitle ms-auto">credentials redacted</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Details</h3>
|
||||
<h2 class="card-title">Details</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="datagrid">
|
||||
@@ -57,7 +57,7 @@
|
||||
{{if .IsOwner}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Organizations</h3>
|
||||
<h2 class="card-title">Organizations</h2>
|
||||
<div class="card-actions"><a href="/repeaters/{{.Repeater.PublicID}}/share" class="btn btn-sm">Manage</a></div>
|
||||
</div>
|
||||
{{if .Orgs}}
|
||||
@@ -85,7 +85,7 @@
|
||||
<!-- Right column -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Identity</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Identity</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="form-label">Public key</div>
|
||||
<div class="d-flex align-items-start gap-2">
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
{{if .IsOwner}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Public page</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Public page</h2></div>
|
||||
<div class="card-body">
|
||||
{{if and .Repeater.ExposePublicPage .PublicPageURL}}
|
||||
<p class="text-secondary small">Anyone with this link (or who taps an NFC tag / scans the QR inside the enclosure) sees the public documentation and stewards.</p>
|
||||
@@ -133,7 +133,7 @@
|
||||
|
||||
{{if .Repeater.Latitude}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Location</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Location</h2></div>
|
||||
<div class="card-body p-2">
|
||||
<link rel="stylesheet" href="{{ asset "/static/leaflet.css" }}">
|
||||
<div id="map" style="height:240px" role="region" aria-label="Map of this repeater's location"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "title"}}Repeater added · MeshTender{{end}}
|
||||
{{define "header"}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col"><h2 class="page-title">“{{.Repeater.Name}}” added</h2></div>
|
||||
<div class="col"><h1 class="page-title fs-1">“{{.Repeater.Name}}” added</h1></div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Confirm it works <span class="text-secondary">(optional)</span></h3>
|
||||
<h2 class="card-title">Confirm it works <span class="text-secondary">(optional)</span></h2>
|
||||
<p class="text-secondary">
|
||||
Connect a MeshCore KISS modem and have MeshTender login to verify it has admin access. Confirming
|
||||
now marks it <span class="badge bg-azure-lt">Self-confirmed</span>. Later, if another member reaches it
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Organizations <span class="text-secondary">(optional)</span></h3>
|
||||
<h2 class="card-title">Organizations <span class="text-secondary">(optional)</span></h2>
|
||||
{{if .Orgs}}
|
||||
<p class="text-secondary">
|
||||
This repeater is now shared with the organizations you belong to, so their admins and members can run
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<form method="post" action="/repeaters/{{.Repeater.PublicID}}/docs">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 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}}</h3>
|
||||
<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">
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Internal notes <span class="text-secondary">· people with access only</span></h3>
|
||||
<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">
|
||||
@@ -39,7 +39,7 @@
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Documentation</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Documentation</h2></div>
|
||||
<div class="card-body">
|
||||
{{if .Repeater.DocPublic}}
|
||||
<div style="white-space: pre-wrap">{{.Repeater.DocPublic}}</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
{{if .Repeater.DocInternal}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Internal notes <span class="text-secondary">· people with access only</span></h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Internal notes <span class="text-secondary">· people with access only</span></h2></div>
|
||||
<div class="card-body">
|
||||
<div style="white-space: pre-wrap">{{.Repeater.DocInternal}}</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Log maintenance</h3>
|
||||
<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.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Your equipment</div>
|
||||
<h2 class="page-title">Repeaters</h2>
|
||||
<h1 class="page-title fs-1">Repeaters</h1>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<a class="btn btn-primary" href="/repeaters/add">{{template "icon-plus" "me-1"}}Add repeater</a>
|
||||
@@ -31,7 +31,7 @@
|
||||
<span class="avatar bg-blue-lt d-none d-sm-flex">{{template "icon-antenna" ""}}</span>
|
||||
<div class="flex-fill" style="min-width:0">
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
<h3 class="m-0 fs-3">{{.Name}}</h3>
|
||||
<h2 class="m-0 fs-3">{{.Name}}</h2>
|
||||
{{if .Shared}}<span class="badge bg-purple-lt">Shared with me</span>{{else}}<span class="badge bg-blue-lt">Owned</span>{{end}}
|
||||
{{template "repstatus" .}}
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Share links</h3>
|
||||
<h2 class="card-title">Share links</h2>
|
||||
<div class="card-actions">
|
||||
<button type="button" class="btn btn-primary" data-testid="new-invite"
|
||||
data-bs-toggle="modal" data-bs-target="#invite-modal"
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">People with access</h3>
|
||||
<h2 class="card-title">People with access</h2>
|
||||
<p class="text-secondary">
|
||||
Mark a trusted person a <strong>steward</strong> to make them a co-operator: they can run
|
||||
<strong>every</strong> command (including risky ones), just like you, and they're listed as a
|
||||
@@ -107,7 +107,7 @@
|
||||
<div class="col-lg-5">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Organizations</h3>
|
||||
<h2 class="card-title">Organizations</h2>
|
||||
<p class="text-secondary">
|
||||
This repeater is shared with every organization you belong to, so their admins and members can run the
|
||||
commands those organizations are permitted to run on it. Use <strong>Manage access</strong> to opt it
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
page), which also closes the modal — no client JS needed. */}}
|
||||
{{define "invite-new-modal"}}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Create single-use link</h5>
|
||||
<h2 class="modal-title">Create single-use link</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
modal — no client JS needed. */}}
|
||||
{{define "org-limits-modal"}}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Manage access — {{.Org.Name}}</h5>
|
||||
<h2 class="modal-title">Manage access — {{.Org.Name}}</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="d-flex">
|
||||
<div class="me-2">{{template "icon-alert" ""}}</div>
|
||||
<div>
|
||||
<h4 class="alert-title">This repeater is opted out of {{.Org.Name}}</h4>
|
||||
<h3 class="alert-title">This repeater is opted out of {{.Org.Name}}</h3>
|
||||
<p class="mb-0">
|
||||
While it's opted out, <strong>no one</strong> in {{.Org.Name}} can run <strong>any</strong>
|
||||
command below. You can still set these limits now — turn on
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Help</div>
|
||||
<h2 class="page-title">How MeshTender works</h2>
|
||||
<h1 class="page-title fs-1">How MeshTender works</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" id="setperm">
|
||||
<div class="card-header"><h3 class="card-title">Granting MeshTender admin (<code>setperm</code>)</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Granting MeshTender admin (<code>setperm</code>)</h2></div>
|
||||
<div class="card-body">
|
||||
<p>MeshTender signs in to a repeater as its server identity — one fixed public key. Until a repeater grants that key <strong>admin</strong> (permission level 3), MeshTender only has <strong>guest</strong> access — open to anyone with a blank password — and can't administer the repeater.</p>
|
||||
<p>Run this on your repeater — through the <strong>repeater console in the MeshCore companion app</strong>, directly over USB serial, or let MeshTender run it for you during USB setup:</p>
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" id="add">
|
||||
<div class="card-header"><h3 class="card-title">Adding a repeater</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Adding a repeater</h2></div>
|
||||
<div class="card-body">
|
||||
<p>There are two ways in, matching how you reach the repeater:</p>
|
||||
<ul class="mb-2">
|
||||
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" id="kiss">
|
||||
<div class="card-header"><h3 class="card-title">What's a KISS modem?</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">What's a KISS modem?</h2></div>
|
||||
<div class="card-body">
|
||||
<p>A <strong>KISS modem</strong> turns a MeshCore device into a plain radio for a host to drive: instead of acting as a repeater or companion, it just relays raw packets to and from whatever's connected to it. That's how MeshTender talks to your mesh <strong>over the air</strong> — you attach a KISS-modem device to your browser over USB serial, and MeshTender uses it as its radio to reach repeaters that are already deployed (the <a href="#add">over-the-air</a> flow above).</p>
|
||||
<p>It's a relatively new addition to MeshCore, and it's a <strong>standard firmware variant</strong> — you flash it right from the standard MeshCore flasher, the same place you'd flash Repeater or a Companion. It's available for a <strong>large number</strong> of MeshCore devices, though <strong>not all</strong> of them — check the flasher for your specific board.</p>
|
||||
@@ -58,7 +58,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" id="config">
|
||||
<div class="card-header"><h3 class="card-title">Organizations</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Organizations</h2></div>
|
||||
<div class="card-body">
|
||||
<p>An <strong>organization</strong> is a community that coordinates repeaters together. It helps you generate the commands to run to configure your repeater properly, and share settings with others in your area.</p>
|
||||
<p class="mb-0">Browse communities on the <a href="/orgs">Organizations</a> page.</p>
|
||||
@@ -66,7 +66,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" id="sharing">
|
||||
<div class="card-header"><h3 class="card-title">Sharing & stewards</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Sharing & stewards</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">You can share a repeater with other people so they can help operate it, and mark trusted helpers as <strong>stewards</strong> — co-maintainers listed on the repeater's public page. Public repeater and organization pages link to each person's <strong>public profile</strong>, so visitors can find a way to reach the people who keep the mesh running.</p>
|
||||
</div>
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">On this page</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">On this page</h2></div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="#setperm">Granting admin (setperm)</a>
|
||||
<a class="list-group-item list-group-item-action" href="#add">Adding a repeater</a>
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
{{if .ServerKey}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">MeshTender's key</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">MeshTender's key</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="form-label">MeshTender server public key</div>
|
||||
<code class="pk d-block">{{.ServerKey}}</code>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="avatar bg-primary-lt text-primary me-3">{{template "icon-key" ""}}</span>
|
||||
<h3 class="card-title m-0">No password sharing</h3>
|
||||
<h2 class="card-title m-0">No password sharing</h2>
|
||||
</div>
|
||||
<p class="text-secondary mb-0">Grant MeshTender access with its own public key — your repeater's admin password never leaves your hands.</p>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="avatar bg-primary-lt text-primary me-3">{{template "icon-settings" ""}}</span>
|
||||
<h3 class="card-title m-0">Recommended configuration</h3>
|
||||
<h2 class="card-title m-0">Recommended configuration</h2>
|
||||
</div>
|
||||
<p class="text-secondary mb-0">Organizations publish recommended configuration profiles — including region-specific settings — so members can bring every repeater in spec and keep it there.</p>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="avatar bg-primary-lt text-primary me-3">{{template "icon-share" ""}}</span>
|
||||
<h3 class="card-title m-0">Share with precision</h3>
|
||||
<h2 class="card-title m-0">Share with precision</h2>
|
||||
</div>
|
||||
<p class="text-secondary mb-0">Hand out single-use links with per-command permissions. Join an organization and your repeaters share automatically — opt any out anytime.</p>
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="avatar bg-primary-lt text-primary me-3">{{template "icon-terminal" ""}}</span>
|
||||
<h3 class="card-title m-0">A console in your browser</h3>
|
||||
<h2 class="card-title m-0">A console in your browser</h2>
|
||||
</div>
|
||||
<p class="text-secondary mb-0">Connect a MeshCore KISS modem and run CLI commands straight from your browser — no drivers and nothing to install locally.</p>
|
||||
</div>
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="avatar bg-primary-lt text-primary me-3">{{template "icon-antenna" ""}}</span>
|
||||
<h3 class="card-title m-0">Confirm reachability</h3>
|
||||
<h2 class="card-title m-0">Confirm reachability</h2>
|
||||
</div>
|
||||
<p class="text-secondary mb-0">Reach a repeater from your modem, and let others corroborate from theirs — proof it's really on the air, not just registered.</p>
|
||||
</div>
|
||||
@@ -88,7 +88,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="avatar bg-primary-lt text-primary me-3">{{template "icon-list" ""}}</span>
|
||||
<h3 class="card-title m-0">Full audit log</h3>
|
||||
<h2 class="card-title m-0">Full audit log</h2>
|
||||
</div>
|
||||
<p class="text-secondary mb-0">Every command is logged and grouped by session — who ran what, when, and whether the repeater replied.</p>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "title"}}Organizations · MeshTender{{end}}
|
||||
{{define "header"}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col"><h2 class="page-title">Organizations</h2></div>
|
||||
<div class="col"><h1 class="page-title fs-1">Organizations</h1></div>
|
||||
{{if .UserName}}
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<a class="btn btn-primary" href="{{.AppURL}}/orgs/new">{{template "icon-plus" "me-1"}}Create organization</a>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Repeater</div>
|
||||
<h2 class="page-title">{{.Repeater.Name}}</h2>
|
||||
<h1 class="page-title fs-1">{{.Repeater.Name}}</h1>
|
||||
</div>
|
||||
{{if .CanManage}}
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
@@ -17,7 +17,7 @@
|
||||
<!-- Left column -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Details</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Details</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item">
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Documentation</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Documentation</h2></div>
|
||||
<div class="card-body">
|
||||
{{if .DocPublicHTML}}
|
||||
<div class="markdown">{{.DocPublicHTML}}</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
<!-- Right column -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Identity</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Identity</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="form-label">Public key</div>
|
||||
<code class="pk d-block">{{.Repeater.PublicKeyHex}}</code>
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
{{if .Repeater.Latitude}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Location</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Location</h2></div>
|
||||
<div class="card-body p-2">
|
||||
<link rel="stylesheet" href="{{ asset "/static/leaflet.css" }}">
|
||||
<div id="map" style="height:240px" role="region" aria-label="Map of this repeater's location"></div>
|
||||
@@ -77,7 +77,7 @@
|
||||
{{end}}
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Stewards</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Stewards</h2></div>
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item d-flex align-items-center gap-2">
|
||||
<span class="text-secondary">{{template "icon-user" ""}}</span>
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
{{if .Orgs}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Organizations</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Organizations</h2></div>
|
||||
<div class="list-group list-group-flush">
|
||||
{{range .Orgs}}
|
||||
<a class="list-group-item list-group-item-action d-flex align-items-center gap-2" href="/orgs/{{.OrgSlug}}">{{template "icon-world" ""}}{{.OrgName}}</a>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Profile</div>
|
||||
<h2 class="page-title">{{.ProfileUser.Name}}</h2>
|
||||
<h1 class="page-title fs-1">{{.ProfileUser.Name}}</h1>
|
||||
<div class="text-secondary">@{{.ProfileUser.Username}}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,7 +14,7 @@
|
||||
<!-- Left: about -->
|
||||
<div class="{{if $hasSidebar}}col-lg-8{{else}}col-12{{end}}">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">About</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">About</h2></div>
|
||||
<div class="card-body">
|
||||
{{if or .Location .Callsign}}
|
||||
<div class="d-flex flex-wrap gap-3 mb-3 text-secondary">
|
||||
@@ -34,7 +34,7 @@
|
||||
{{if $hasSidebar}}
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Links</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Links</h2></div>
|
||||
<div class="list-group list-group-flush">
|
||||
{{range $i, $l := .Links}}
|
||||
{{if $l.IsMeshCore}}
|
||||
|
||||
@@ -135,6 +135,63 @@ func TestTemplatesUseAssetHelper(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplatesHeadingConventions pins the heading levels the shared layouts assume,
|
||||
// so a new page can't quietly reintroduce the outline problems from the audit (A1:
|
||||
// no <h1> anywhere; A2: levels skipped or jumping backwards). The rendered-page
|
||||
// counterpart is core.TestPageHeadingOutlines, which walks real responses; this one
|
||||
// catches a bad heading the moment it's written, in whichever template it lands.
|
||||
//
|
||||
// The rules mirror the page structure: the page title is the h1, cards and modal
|
||||
// dialogs are sections under it (h2), and alerts nest inside cards (h3).
|
||||
func TestTemplatesHeadingConventions(t *testing.T) {
|
||||
t.Parallel()
|
||||
root := moduleRoot(t)
|
||||
|
||||
banned := []struct {
|
||||
pattern *regexp.Regexp
|
||||
why string
|
||||
}{
|
||||
{regexp.MustCompile(`(?i)<h[2-6][^>]*class="[^"]*\bpage-title\b`),
|
||||
`page-title must be <h1> — it names the page`},
|
||||
{regexp.MustCompile(`(?i)<h(?:[13-6])[^>]*class="[^"]*\bcard-title\b`),
|
||||
`card-title must be <h2> — a card is a section under the page's h1 ` +
|
||||
`(auth pages are the exception and are checked by the rendered test)`},
|
||||
{regexp.MustCompile(`(?i)<h(?:1|[3-6])[^>]*class="[^"]*\bmodal-title\b`),
|
||||
`modal-title must be <h2> — a dialog title sits under the page's h1`},
|
||||
{regexp.MustCompile(`(?i)<h(?:[12]|[4-6])[^>]*class="[^"]*\balert-title\b`),
|
||||
`alert-title must be <h3> — alerts nest inside cards`},
|
||||
}
|
||||
// login/signup have no page header, so their card title legitimately carries the
|
||||
// page's h1.
|
||||
authPages := map[string]bool{"login.html": true, "signup.html": true}
|
||||
|
||||
err := filepath.WalkDir(filepath.Join(root, "internal"), func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() || !strings.HasSuffix(path, ".html") {
|
||||
return nil
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
for _, rule := range banned {
|
||||
if authPages[filepath.Base(path)] && strings.Contains(rule.why, "card-title") {
|
||||
continue
|
||||
}
|
||||
if loc := rule.pattern.FindIndex(b); loc != nil {
|
||||
t.Errorf("%s: %s — found %q", rel, rule.why, snippet(b, loc[0]))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk templates: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func snippet(b []byte, at int) string {
|
||||
end := at + 40
|
||||
if end > len(b) {
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
{{block "header" .}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">MeshTender</h2>
|
||||
<h1 class="page-title fs-1">MeshTender</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{{range .Groups}}
|
||||
<div class="card mt-3" data-check-scope>
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<h3 class="card-title mb-0">{{.Name}}</h3>
|
||||
<h2 class="card-title mb-0">{{.Name}}</h2>
|
||||
<div class="btn-list ms-auto">
|
||||
<button type="button" class="btn btn-sm" data-check-all>Select all</button>
|
||||
<button type="button" class="btn btn-sm" data-check-none>Select none</button>
|
||||
|
||||
@@ -2,11 +2,22 @@
|
||||
It renders on whichever base layout the surface defaults to, so the chrome
|
||||
matches the rest of that host. Data: Status (int), Title, Message. */}}
|
||||
{{define "title"}}{{.Status}} · MeshTender{{end}}
|
||||
{{define "header"}}{{end}}
|
||||
{{/* The error's own title is the page heading, rendered through the normal page
|
||||
header so this page's h1 matches every other page's.
|
||||
|
||||
It must be a real (non-empty) definition: Go templates treat a define whose body
|
||||
is only whitespace or comments as EMPTY and do NOT let it replace an existing
|
||||
definition, so the previous `{{define "header"}}{{end}}` silently fell through to
|
||||
base.html's default header — which is why this page used to render a generic
|
||||
"MeshTender" title above the error. */}}
|
||||
{{define "header"}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col"><h1 class="page-title fs-1">{{.Title}}</h1></div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
<div class="empty py-6">
|
||||
<div class="empty-header">{{.Status}}</div>
|
||||
<p class="empty-title">{{.Title}}</p>
|
||||
<p class="empty-subtitle text-secondary">{{.Message}}</p>
|
||||
<div class="empty-action">
|
||||
<a href="{{if .UserName}}{{.AppURL}}{{else}}{{.RootURL}}{{end}}/" class="btn btn-primary">
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
<div class="col-lg-8">
|
||||
{{if .Config.Profiles}}
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">{{.Config.Selected}} · base settings</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">{{.Config.Selected}} · base settings</h2></div>
|
||||
<div class="card-body">{{template "step-list" .Config.SelectedSteps}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Config.PreviewActive}}
|
||||
<div class="card{{if .Config.Profiles}} mt-3{{end}}">
|
||||
<div class="card-header"><h3 class="card-title">Regions</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Regions</h2></div>
|
||||
{{if .Config.RegionDef}}
|
||||
<div class="card-body"><pre class="mb-0"><code>{{range .Config.RegionDef}}{{.}}
|
||||
{{end}}</code></pre></div>
|
||||
@@ -35,7 +35,7 @@
|
||||
<div class="col-lg-4">
|
||||
{{if .Config.Profiles}}
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Profile</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Profile</h2></div>
|
||||
<div class="card-body">
|
||||
<form method="get" action="/orgs/{{.Org.Slug}}/config">
|
||||
<select class="form-select" name="profile" aria-label="Choose a configuration profile" data-autosubmit>
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
{{if .Config.HasRegionShapes}}
|
||||
<div class="card{{if .Config.Profiles}} mt-3{{end}}">
|
||||
<div class="card-header"><h3 class="card-title">Preview a location</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Preview a location</h2></div>
|
||||
<div class="card-body p-2">
|
||||
<link rel="stylesheet" href="{{ asset "/static/leaflet.css" }}">
|
||||
<div id="region-map" style="height:240px" role="region" aria-label="Click the map to preview a repeater's region config"></div>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<!-- Left column: description (grows with content) -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">About</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">About</h2></div>
|
||||
<div class="card-body">
|
||||
{{if .Org.Description}}<div class="markdown">{{markdown .Org.Description}}</div>{{else}}<p class="text-secondary mb-0">This organization hasn't added a description yet.</p>{{end}}
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
<!-- Right column: repeater map, admins, links -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Repeaters</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Repeaters</h2></div>
|
||||
{{if .HasMap}}
|
||||
<div class="card-body p-2">
|
||||
<link rel="stylesheet" href="{{ asset "/static/leaflet.css" }}">
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Admins</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Admins</h2></div>
|
||||
{{if .Admins}}
|
||||
<div class="list-group list-group-flush">
|
||||
{{range .Admins}}
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
{{if .Links}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Links</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Links</h2></div>
|
||||
<div class="list-group list-group-flush">
|
||||
{{range .Links}}
|
||||
{{if .Href}}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<!-- Left: list -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Shared repeaters</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Shared repeaters</h2></div>
|
||||
{{if .Reps.Repeaters}}
|
||||
<div class="list-group list-group-flush" id="rep-list">
|
||||
{{template "rep-rows" .}}
|
||||
@@ -61,7 +61,7 @@
|
||||
<!-- Right: map -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Map</h3></div>
|
||||
<div class="card-header"><h2 class="card-title">Map</h2></div>
|
||||
{{if .Reps.HasMap}}
|
||||
<div class="card-body p-2">
|
||||
<link rel="stylesheet" href="{{ asset "/static/leaflet.css" }}">
|
||||
|
||||
@@ -12,7 +12,7 @@ host (anonymous). */}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Organization{{if and (not .Nav.Manage) .Nav.CanGoToOrg}} · public preview{{end}}</div>
|
||||
<h2 class="page-title">{{.Nav.Name}}</h2>
|
||||
<h1 class="page-title fs-1">{{.Nav.Name}}</h1>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
{{if .Nav.Manage}}
|
||||
|
||||
@@ -7,7 +7,7 @@ for the owner. */}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Repeater{{if not .IsOwner}} · shared by {{.OwnerName}}{{end}}</div>
|
||||
<h2 class="page-title">{{.Name}}</h2>
|
||||
<h1 class="page-title fs-1">{{.Name}}</h1>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
|
||||
Reference in New Issue
Block a user