Better list linking

This commit is contained in:
Jonathon Leight
2026-06-28 17:56:51 -04:00
parent 0e105cd694
commit eedafc41f9
5 changed files with 29 additions and 123 deletions
+1 -34
View File
@@ -69,41 +69,8 @@
<div class="row row-cards mt-1">
<!-- Left column -->
<div class="col-lg-8">
<!-- Your repeaters -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Your repeaters</h3>
<div class="card-actions"><a href="/repeaters" class="btn btn-sm">View all{{if .OwnedCount}} ({{.OwnedCount}}){{end}}</a></div>
</div>
{{if .Owned}}
<div class="list-group list-group-flush">
{{range .Owned}}
<div class="list-group-item d-flex align-items-center gap-2">
<span class="text-secondary">{{template "icon-antenna" ""}}</span>
<div class="flex-fill text-truncate">
<a class="fw-bold text-reset text-decoration-none" href="/repeaters/{{.PublicID}}">{{.Name}}</a>
{{$c := index $.ShareCounts .ID}}
<div class="text-secondary small text-truncate">{{if or $c.Users $c.Orgs}}Shared with {{$c.Users}} user{{if ne $c.Users 1}}s{{end}} · {{$c.Orgs}} org{{if ne $c.Orgs 1}}s{{end}}{{else}}Not shared{{end}}</div>
</div>
{{template "repstatus" .}}
<a class="btn btn-sm" href="/repeaters/{{.PublicID}}/console">{{template "icon-terminal" "me-1"}}Console</a>
</div>
{{end}}
</div>
{{else}}
<div class="card-body">
<div class="empty">
<div class="empty-icon">{{template "icon-antenna" ""}}</div>
<p class="empty-title">No repeaters yet</p>
<p class="empty-subtitle text-secondary">Add one to grant MeshTender access and test it with a modem.</p>
<div class="empty-action"><a href="/repeaters/add" class="btn btn-primary">{{template "icon-plus" "me-1"}}Add repeater</a></div>
</div>
</div>
{{end}}
</div>
<!-- Recent activity -->
<div class="card mt-3">
<div class="card">
<div class="card-header"><h3 class="card-title">Recent activity</h3></div>
{{if .Recent}}
<div class="table-responsive">
+8 -62
View File
@@ -13,59 +13,38 @@
{{define "content"}}
{{if .Error}}<div class="alert alert-danger" role="alert">{{.Error}}</div>{{end}}
{{if or .Owned .Shared}}
{{if .Shared}}<h3 class="mb-3">Your repeaters</h3>{{end}}
{{if .Owned}}
{{if .Repeaters}}
<div class="row row-cards">
{{range .Owned}}
{{range .Repeaters}}
<div class="col-12">
<div class="card">
<a class="card card-link card-link-pop text-reset" href="/repeaters/{{.PublicID}}">
<div class="card-body py-3">
<div class="d-flex align-items-center gap-3">
<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"><a class="text-reset text-decoration-none" href="/repeaters/{{.PublicID}}">{{.Name}}</a></h3>
<h3 class="m-0 fs-3">{{.Name}}</h3>
{{if .Shared}}<span class="badge bg-purple-lt">Shared</span>{{else}}<span class="badge bg-blue-lt">Owned</span>{{end}}
{{template "repstatus" .}}
</div>
<div class="d-flex align-items-center flex-wrap gap-2 mt-1 text-secondary">
<span class="font-monospace">{{slice .PublicKeyHex 0 8}}…</span>
<button type="button" class="btn btn-icon btn-sm btn-ghost-secondary" title="Copy public key" aria-label="Copy public key" onclick="navigator.clipboard.writeText('{{.PublicKeyHex}}')">{{template "icon-copy" ""}}</button>
<span class="ms-1">·</span>
<span>{{template "sharetext" (index $.ShareCounts .ID)}}</span>
</div>
</div>
<div class="btn-list flex-nowrap">
<a class="btn btn-outline-primary" href="/repeaters/{{.PublicID}}/console">{{template "icon-terminal" "me-1"}}Console</a>
<div class="dropdown">
<button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Manage</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href="/repeaters/{{.PublicID}}/edit">{{template "icon-pencil" "dropdown-item-icon"}}Edit details</a>
<a class="dropdown-item" href="/repeaters/{{.PublicID}}/confirm">{{template "icon-antenna" "dropdown-item-icon"}}Confirm access</a>
<a class="dropdown-item" href="/repeaters/{{.PublicID}}/log">{{template "icon-list" "dropdown-item-icon"}}Logs</a>
<a class="dropdown-item" href="/repeaters/{{.PublicID}}/share">{{template "icon-share" "dropdown-item-icon"}}Sharing</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="/repeaters/{{.PublicID}}/delete">{{template "icon-trash" "dropdown-item-icon"}}Delete repeater</a>
</div>
{{if .Shared}}<span>Shared by {{.OwnerName}}</span>{{else}}<span>{{template "sharetext" (index $.ShareCounts .ID)}}</span>{{end}}
</div>
</div>
</div>
{{if and .Confirmed .AccessKnown (not .IsAdmin)}}
{{if and (not .Shared) .Confirmed .AccessKnown (not .IsAdmin)}}
<div class="alert alert-warning mt-3 mb-0">
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.
</div>
{{end}}
</div>
</div>
</a>
</div>
{{end}}
</div>
{{else}}
<div class="card">
<div class="card-body text-secondary">You haven't added any of your own repeaters yet.</div>
</div>
{{end}}
{{else}}
<div class="empty">
<div class="empty-icon">{{template "icon-antenna" ""}}</div>
<p class="empty-title">No repeaters yet</p>
@@ -75,39 +54,6 @@
</div>
</div>
{{end}}
{{if .Shared}}
<h3 class="mt-4 mb-3">Shared with you</h3>
<div class="row row-cards">
{{range .Shared}}
<div class="col-12">
<div class="card">
<div class="card-body py-3">
<div class="d-flex align-items-center gap-3">
<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"><a class="text-reset text-decoration-none" href="/repeaters/{{.PublicID}}">{{.Name}}</a></h3>
{{template "repstatus" .}}
</div>
<div class="d-flex align-items-center flex-wrap gap-2 mt-1 text-secondary">
<span class="font-monospace">{{slice .PublicKeyHex 0 8}}…</span>
<button type="button" class="btn btn-icon btn-sm btn-ghost-secondary" title="Copy public key" aria-label="Copy public key" onclick="navigator.clipboard.writeText('{{.PublicKeyHex}}')">{{template "icon-copy" ""}}</button>
<span class="ms-1">·</span>
<span>Shared by {{.OwnerName}}</span>
</div>
</div>
<div class="btn-list flex-nowrap">
<a class="btn btn-outline-primary" href="/repeaters/{{.PublicID}}/console">{{template "icon-terminal" "me-1"}}Console</a>
<a class="btn" href="/repeaters/{{.PublicID}}/confirm">Confirm</a>
</div>
</div>
</div>
</div>
</div>
{{end}}
</div>
{{end}}
{{end}}
{{/* sharetext renders a repeater's sharing summary as plain text: e.g.
+6 -20
View File
@@ -198,8 +198,8 @@ func (s *Handlers) pageHome(w http.ResponseWriter, r *http.Request) {
s.pageDashboard(w, r)
}
// pageRepeaters lists every repeater the user owns or has been shared, split
// into owned and shared sections.
// pageRepeaters lists every repeater the user owns or has been shared as one
// combined list, owned first, each tagged owned/shared in the template.
func (s *Handlers) pageRepeaters(w http.ResponseWriter, r *http.Request) {
uid := s.Auth.CurrentUserID(r.Context())
repeaters, err := s.Store.ListRepeatersForUser(r.Context(), uid)
@@ -213,9 +213,11 @@ func (s *Handlers) pageRepeaters(w http.ResponseWriter, r *http.Request) {
return
}
owned, shared := splitOwnedShared(repeaters)
combined := make([]*store.Repeater, 0, len(owned)+len(shared))
combined = append(combined, owned...)
combined = append(combined, shared...)
s.Render(w, r, "repeaters.html", map[string]any{
"Owned": owned,
"Shared": shared,
"Repeaters": combined,
"ShareCounts": shareCounts,
"Error": r.URL.Query().Get("error"),
})
@@ -244,11 +246,6 @@ func (s *Handlers) pageDashboard(w http.ResponseWriter, r *http.Request) {
http.Error(w, "could not load activity", http.StatusInternalServerError)
return
}
shareCounts, err := s.Store.RepeaterSharingCounts(ctx, uid)
if err != nil {
http.Error(w, "could not load sharing", http.StatusInternalServerError)
return
}
confirmed, unconfirmed := 0, 0
var mapped []*store.Repeater
@@ -269,11 +266,8 @@ func (s *Handlers) pageDashboard(w http.ResponseWriter, r *http.Request) {
"OrgCount": len(orgs),
"Confirmed": confirmed,
"Unconfirmed": unconfirmed,
"Owned": first(owned, 5),
"Shared": first(shared, 5),
"Mapped": mapped,
"Recent": recent,
"ShareCounts": shareCounts,
"Error": r.URL.Query().Get("error"),
})
}
@@ -290,14 +284,6 @@ func splitOwnedShared(repeaters []*store.Repeater) (owned, shared []*store.Repea
return owned, shared
}
// first returns the first n elements of s, or all of them if there are fewer.
func first[T any](s []T, n int) []T {
if len(s) > n {
return s[:n]
}
return s
}
// handleLogout signs the user out of the app host, then chains to the auth host's
// /logout to clear the SSO session too — otherwise the surviving SSO session
// would silently re-authenticate on the next request.
+3 -3
View File
@@ -14,14 +14,14 @@
htmx "load more" fragment so appended pages match the first. */}}
{{define "org-rows"}}
{{range .All}}
<div class="list-group-item">
<a class="list-group-item list-group-item-action" href="/orgs/{{.Slug}}">
<div class="d-flex align-items-center gap-2">
<a class="fw-bold text-reset text-decoration-none" href="/orgs/{{.Slug}}">{{.Name}}</a>
<span class="fw-bold">{{.Name}}</span>
{{if .Region}}<span class="badge bg-secondary-lt">{{.Region}}</span>{{end}}
</div>
{{if .Description}}<p class="text-secondary mb-1 mt-1">{{.Description}}</p>{{end}}
<div class="text-secondary small">{{.MemberCount}} member{{if ne .MemberCount 1}}s{{end}} · {{.RepeaterCount}} repeater{{if ne .RepeaterCount 1}}s{{end}}</div>
</div>
</a>
{{end}}
{{end}}
+11 -4
View File
@@ -11,17 +11,24 @@
htmx "show more" fragment so appended pages match the first. */}}
{{define "rep-rows"}}
{{range .Reps.Repeaters}}
{{if $.Reps.Full}}
<a class="list-group-item list-group-item-action d-flex align-items-center gap-2" href="/repeaters/{{.RepeaterPublicID}}">
<span class="text-secondary">{{template "icon-antenna" ""}}</span>
<div class="flex-fill text-truncate">
<span class="fw-bold">{{.Name}}</span>
<div class="text-secondary small">owner {{.OwnerName}}</div>
</div>
</a>
{{else}}
<div class="list-group-item d-flex align-items-center gap-2">
<span class="text-secondary">{{template "icon-antenna" ""}}</span>
<div class="flex-fill text-truncate">
{{if $.Reps.Full}}<a class="fw-bold text-reset text-decoration-none" href="/repeaters/{{.RepeaterPublicID}}">{{.Name}}</a>
<div class="text-secondary small">owner {{.OwnerName}}</div>
{{else}}<span class="fw-bold">{{.Name}}</span>{{end}}
<span class="fw-bold">{{.Name}}</span>
</div>
{{if $.Reps.Full}}<a class="btn btn-sm" href="/repeaters/{{.RepeaterPublicID}}/console">{{template "icon-terminal" "me-1"}}Console</a>{{end}}
</div>
{{end}}
{{end}}
{{end}}
{{/* rep-more is the public list's "show more" control; replaces itself with the
next page's rows + a fresh control (the cursor carries the keyset position). */}}