mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-03 02:23:43 +00:00
Better list linking
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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 <your key> 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
@@ -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.
|
||||
|
||||
@@ -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,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). */}}
|
||||
|
||||
Reference in New Issue
Block a user