mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-10-23 13:32:53 +00:00
Add repeater QR code
This commit is contained in:
1 parent
4b527b9268
commit
a9bb3b7b29
6 files changed
+97
-40
No files matched your search
@@ -11,6 +11,7 @@ require (
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
github.com/meshcore-go/meshcore-go v1.0.8
|
||||
github.com/pressly/goose/v3 v3.27.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.43.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
)
|
||||
|
||||
@@ -153,6 +153,8 @@ github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9R
|
||||
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"image/color"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
meshcore "github.com/meshcore-go/meshcore-go"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
|
||||
"github.com/jleight/meshtender/internal/config"
|
||||
"github.com/jleight/meshtender/internal/store"
|
||||
@@ -47,11 +52,24 @@ func (s *Handlers) pageRepeater(w http.ResponseWriter, r *http.Request) {
|
||||
isOwner := !rep.Shared
|
||||
radio := fmt.Sprintf("%g MHz / %g kHz / SF%d / CR%d",
|
||||
float64(rep.RadioFreqHz)/1e6, float64(rep.RadioBwHz)/1e3, rep.RadioSF, rep.RadioCR)
|
||||
contactURI := repeaterContactURI(rep)
|
||||
data := map[string]any{
|
||||
"Repeater": rep,
|
||||
"IsOwner": isOwner,
|
||||
"Radio": radio,
|
||||
"Error": r.URL.Query().Get("error"),
|
||||
"Repeater": rep,
|
||||
"IsOwner": isOwner,
|
||||
"Radio": radio,
|
||||
"ContactURI": contactURI,
|
||||
"Error": r.URL.Query().Get("error"),
|
||||
}
|
||||
// QR code that adds the repeater as a MeshCore contact. Embedded as a data
|
||||
// URI so it needs no extra route or asset; if encoding fails the page just
|
||||
// renders without it. Light modules on a transparent quiet zone so it sits on
|
||||
// the dark card instead of a stark white block (scanners decode inverted QR).
|
||||
if qr, err := qrcode.New(contactURI, qrcode.Medium); err == nil {
|
||||
qr.BackgroundColor = color.Transparent
|
||||
qr.ForegroundColor = color.RGBA{R: 0x8a, G: 0x97, B: 0xa8, A: 0xff}
|
||||
if png, err := qr.PNG(256); err == nil {
|
||||
data["ContactQR"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png))
|
||||
}
|
||||
}
|
||||
if isOwner {
|
||||
orgs, err := s.Store.ListRepeaterOrgs(r.Context(), id)
|
||||
@@ -70,6 +88,16 @@ func (s *Handlers) pageRepeater(w http.ResponseWriter, r *http.Request) {
|
||||
s.Render(w, r, "repeater.html", data)
|
||||
}
|
||||
|
||||
// repeaterContactURI builds the meshcore:// deep link that adds the repeater as
|
||||
// a contact in the MeshCore app. type=2 is MeshCore's repeater contact type.
|
||||
func repeaterContactURI(rep *store.Repeater) string {
|
||||
q := url.Values{}
|
||||
q.Set("name", rep.Name)
|
||||
q.Set("public_key", rep.PublicKeyHex)
|
||||
q.Set("type", "2")
|
||||
return "meshcore://contact/add?" + q.Encode()
|
||||
}
|
||||
|
||||
func addErr(w http.ResponseWriter, r *http.Request, msg string) {
|
||||
web.RedirectErr(w, r, "/repeaters/add?step=details", msg)
|
||||
}
|
||||
|
||||
@@ -38,16 +38,6 @@
|
||||
<h3 class="card-title">Details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Public key</div>
|
||||
<code class="pk">{{.Repeater.PublicKeyHex}}</code>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Radio settings</div>
|
||||
<div class="font-monospace">{{.Radio}}</div>
|
||||
</div>
|
||||
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Status</div>
|
||||
@@ -77,6 +67,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="form-label">Radio settings</div>
|
||||
<div class="font-monospace">{{.Radio}}</div>
|
||||
</div>
|
||||
|
||||
{{if and .IsOwner .Repeater.Confirmed .Repeater.AccessKnown (not .Repeater.IsAdmin)}}
|
||||
<div class="alert alert-warning mt-3 mb-0">
|
||||
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.
|
||||
@@ -86,6 +81,29 @@
|
||||
</div>
|
||||
|
||||
{{if .IsOwner}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Organizations</h3>
|
||||
<div class="card-actions"><a href="/repeaters/{{.Repeater.PublicID}}/share" class="btn btn-sm">Manage</a></div>
|
||||
</div>
|
||||
{{if .Orgs}}
|
||||
<div class="list-group list-group-flush">
|
||||
{{range .Orgs}}
|
||||
<div class="list-group-item d-flex align-items-center gap-2">
|
||||
<span class="text-secondary">{{template "icon-world" ""}}</span>
|
||||
<div class="flex-fill text-truncate">
|
||||
<span class="fw-bold">{{.OrgName}}</span>
|
||||
<div class="text-secondary small">consented to v{{.ConsentedVersion}}</div>
|
||||
</div>
|
||||
{{if gt .CurrentVersion .ConsentedVersion}}<span class="badge bg-warning-lt">re-consent</span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="card-body"><p class="text-secondary mb-0">Not contributed to any organization.</p></div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Recent activity</h3>
|
||||
@@ -116,8 +134,29 @@
|
||||
|
||||
<!-- Right column -->
|
||||
<div class="col-lg-4">
|
||||
{{if .Repeater.Latitude}}
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Identity</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-label">Public key</div>
|
||||
<div class="d-flex align-items-start gap-2">
|
||||
<code class="pk flex-fill">{{.Repeater.PublicKeyHex}}</code>
|
||||
<button type="button" class="btn btn-icon btn-ghost-secondary" title="Copy public key" aria-label="Copy public key" onclick="navigator.clipboard.writeText('{{.Repeater.PublicKeyHex}}')">{{template "icon-copy" ""}}</button>
|
||||
</div>
|
||||
{{if .ContactQR}}
|
||||
<hr class="my-3">
|
||||
<button class="btn btn-sm w-100" type="button" data-bs-toggle="collapse" data-bs-target="#contact-qr" aria-expanded="false" aria-controls="contact-qr">{{template "icon-qrcode" "me-1"}}<span class="collapse-show">Show</span><span class="collapse-hide">Hide</span> contact QR code</button>
|
||||
<div class="collapse" id="contact-qr">
|
||||
<div class="text-center mt-3">
|
||||
<img src="{{.ContactQR}}" width="200" height="200" class="img-fluid" style="max-width:200px" alt="MeshCore contact QR code for {{.Repeater.Name}}">
|
||||
<p class="text-secondary small mt-2 mb-0">Scan with the MeshCore app to add this repeater as a contact.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Repeater.Latitude}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Location</h3></div>
|
||||
<div class="card-body p-2">
|
||||
<link rel="stylesheet" href="/static/leaflet.css">
|
||||
@@ -131,31 +170,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .IsOwner}}
|
||||
<div class="card {{if .Repeater.Latitude}}mt-3{{end}}">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Organizations</h3>
|
||||
<div class="card-actions"><a href="/repeaters/{{.Repeater.PublicID}}/share" class="btn btn-sm">Manage</a></div>
|
||||
</div>
|
||||
{{if .Orgs}}
|
||||
<div class="list-group list-group-flush">
|
||||
{{range .Orgs}}
|
||||
<div class="list-group-item d-flex align-items-center gap-2">
|
||||
<span class="text-secondary">{{template "icon-world" ""}}</span>
|
||||
<div class="flex-fill text-truncate">
|
||||
<span class="fw-bold">{{.OrgName}}</span>
|
||||
<div class="text-secondary small">consented to v{{.ConsentedVersion}}</div>
|
||||
</div>
|
||||
{{if gt .CurrentVersion .ConsentedVersion}}<span class="badge bg-warning-lt">re-consent</span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="card-body"><p class="text-secondary mb-0">Not contributed to any organization.</p></div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,6 +39,17 @@ code.pubkey {
|
||||
color: var(--tblr-gray-400);
|
||||
}
|
||||
|
||||
/* ---------- Collapse toggle label (Show/Hide) ---------- */
|
||||
.collapse-hide {
|
||||
display: none;
|
||||
}
|
||||
[aria-expanded="true"] .collapse-show {
|
||||
display: none;
|
||||
}
|
||||
[aria-expanded="true"] .collapse-hide {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* ---------- Live event / console log ---------- */
|
||||
.evlog {
|
||||
list-style: none;
|
||||
|
||||
@@ -27,3 +27,4 @@
|
||||
{{define "icon-eye"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon{{if .}} {{.}}{{end}}"><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"/><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"/></svg>{{end}}
|
||||
{{define "icon-eye-off"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon{{if .}} {{.}}{{end}}"><path d="M10.585 10.587a2 2 0 0 0 2.829 2.828"/><path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"/><path d="M3 3l18 18"/></svg>{{end}}
|
||||
{{define "icon-copy"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon{{if .}} {{.}}{{end}}"><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"/><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"/></svg>{{end}}
|
||||
{{define "icon-qrcode"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon{{if .}} {{.}}{{end}}"><path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/><path d="M7 17l0 .01"/><path d="M14 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/><path d="M7 7l0 .01"/><path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/><path d="M17 7l0 .01"/><path d="M14 14l3 0"/><path d="M20 14l0 .01"/><path d="M14 14l0 3"/><path d="M14 20l3 0"/><path d="M17 17l3 0"/><path d="M20 17l0 3"/></svg>{{end}}
|
||||
Reference in new issue
Block a user