diff --git a/go.mod b/go.mod index 744fdbc..1ac6fee 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 898ce1a..0185eab 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/core/repeaters.go b/internal/core/repeaters.go index c1263a8..1a7da5f 100644 --- a/internal/core/repeaters.go +++ b/internal/core/repeaters.go @@ -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) } diff --git a/internal/core/templates/repeater.html b/internal/core/templates/repeater.html index f6fa65a..6dfe82c 100644 --- a/internal/core/templates/repeater.html +++ b/internal/core/templates/repeater.html @@ -38,16 +38,6 @@

Details

-
-
Public key
- {{.Repeater.PublicKeyHex}} -
- -
-
Radio settings
-
{{.Radio}}
-
-
Status
@@ -77,6 +67,11 @@
+
+
Radio settings
+
{{.Radio}}
+
+ {{if and .IsOwner .Repeater.Confirmed .Repeater.AccessKnown (not .Repeater.IsAdmin)}}
MeshTender only has guest access here. Guest is available to anyone with a blank password, so there's no point operating at this level — re-run setperm <your key> 3 on the repeater to grant admin, then re-test. @@ -86,6 +81,29 @@
{{if .IsOwner}} +
+
+

Organizations

+ +
+ {{if .Orgs}} +
+ {{range .Orgs}} +
+ {{template "icon-world" ""}} +
+ {{.OrgName}} +
consented to v{{.ConsentedVersion}}
+
+ {{if gt .CurrentVersion .ConsentedVersion}}re-consent{{end}} +
+ {{end}} +
+ {{else}} +

Not contributed to any organization.

+ {{end}} +
+

Recent activity

@@ -116,8 +134,29 @@
- {{if .Repeater.Latitude}}
+

Identity

+
+
Public key
+
+ {{.Repeater.PublicKeyHex}} + +
+ {{if .ContactQR}} +
+ +
+
+ MeshCore contact QR code for {{.Repeater.Name}} +

Scan with the MeshCore app to add this repeater as a contact.

+
+
+ {{end}} +
+
+ + {{if .Repeater.Latitude}} +

Location

@@ -131,31 +170,6 @@
{{end}} - - {{if .IsOwner}} -
-
-

Organizations

- -
- {{if .Orgs}} -
- {{range .Orgs}} -
- {{template "icon-world" ""}} -
- {{.OrgName}} -
consented to v{{.ConsentedVersion}}
-
- {{if gt .CurrentVersion .ConsentedVersion}}re-consent{{end}} -
- {{end}} -
- {{else}} -

Not contributed to any organization.

- {{end}} -
- {{end}}
diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 7895169..00140f2 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -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; diff --git a/internal/web/templates/icons.html b/internal/web/templates/icons.html index 189f6bd..da7a676 100644 --- a/internal/web/templates/icons.html +++ b/internal/web/templates/icons.html @@ -27,3 +27,4 @@ {{define "icon-eye"}}{{end}} {{define "icon-eye-off"}}{{end}} {{define "icon-copy"}}{{end}} +{{define "icon-qrcode"}}{{end}}