diff --git a/internal/core/config_profile.go b/internal/core/config_profile.go index 1ab949f..3a6f02f 100644 --- a/internal/core/config_profile.go +++ b/internal/core/config_profile.go @@ -48,7 +48,7 @@ func (s *Handlers) pageOrgConfig(w http.ResponseWriter, r *http.Request) { } role, isMember, err := s.Store.OrgRole(r.Context(), id, uid) if err != nil { - http.Error(w, "could not load org", http.StatusInternalServerError) + http.Error(w, "could not load organization", http.StatusInternalServerError) return } diff --git a/internal/core/confirm.go b/internal/core/confirm.go index a389479..c97d9f0 100644 --- a/internal/core/confirm.go +++ b/internal/core/confirm.go @@ -174,7 +174,11 @@ func (s *Handlers) wsConfirm(w http.ResponseWriter, r *http.Request) { if !lr.IsAdmin { // Guests can't run CLI commands (including get lat/lon), so there's // nothing more to do — stop here rather than fruitlessly querying. - _ = bridge.Status("warning", fmt.Sprintf("Repeater reached, but MeshTender only has GUEST access (permissions=%d). Guest is open to anyone with a blank password, so MeshTender can't administer this repeater — re-run `%s` to grant admin.", lr.Permissions, s.Identity.SetPermCommand())) + msg := fmt.Sprintf("Repeater reached, but MeshTender only has GUEST access (permissions=%d). Guest is open to anyone with a blank password, so MeshTender can't administer this repeater — re-run `%s` to grant admin.", lr.Permissions, s.Identity.SetPermCommand()) + if s.Cfg.RootHost != "" { + msg += " See " + s.Origin(r, s.Cfg.RootHost) + "/docs#setperm for help." + } + _ = bridge.Status("warning", msg) return } _ = bridge.Status("confirmed", "Repeater reached with admin access. ✓") diff --git a/internal/core/docs_page_test.go b/internal/core/docs_page_test.go new file mode 100644 index 0000000..9b9c287 --- /dev/null +++ b/internal/core/docs_page_test.go @@ -0,0 +1,28 @@ +package core + +import ( + "strings" + "testing" +) + +// TestDocsPage renders the public help page on the root host and confirms it +// shows the live setperm command (with the instance's real key filled in) and +// the key sidebar — the whole reason the page exists. +func TestDocsPage(t *testing.T) { + _, _, ts, h := splitServer(t) + body := readBody(t, do(t, ts, h.root, "/docs")) + for _, want := range []string{ + "How MeshTender works", + "Granting MeshTender admin", + "setperm ", // the live command, e.g. "setperm 3" + "MeshTender server public key", + } { + if !strings.Contains(body, want) { + t.Fatalf("docs page missing %q", want) + } + } + // The literal placeholder must NOT appear — the key should be filled in. + if strings.Contains(body, "setperm <MeshTender") || strings.Contains(body, "") { + t.Fatalf("docs page shows a placeholder instead of the real setperm command") + } +} diff --git a/internal/core/orgs.go b/internal/core/orgs.go index 136cc80..04aad29 100644 --- a/internal/core/orgs.go +++ b/internal/core/orgs.go @@ -32,7 +32,7 @@ func (s *Handlers) pageMyOrgs(w http.ResponseWriter, r *http.Request) { uid := s.Auth.CurrentUserID(r.Context()) mine, err := s.Store.ListOrgsForUser(r.Context(), uid) if err != nil { - http.Error(w, "could not load orgs", http.StatusInternalServerError) + http.Error(w, "could not load organizations", http.StatusInternalServerError) return } s.Render(w, r, "my_orgs.html", map[string]any{ @@ -81,7 +81,7 @@ func (s *Handlers) pageOrg(w http.ResponseWriter, r *http.Request) { } role, isMember, err := s.Store.OrgRole(r.Context(), id, uid) if err != nil { - http.Error(w, "could not load org", http.StatusInternalServerError) + http.Error(w, "could not load organization", http.StatusInternalServerError) return } if !isMember || r.URL.Query().Get("view") == "public" { @@ -149,22 +149,22 @@ func (s *Handlers) pageOrg(w http.ResponseWriter, r *http.Request) { func (s *Handlers) renderOrgPublic(w http.ResponseWriter, r *http.Request, org *store.Org, isMember, isAdmin bool) { admins, err := s.Store.ListOrgAdmins(r.Context(), org.ID) if err != nil { - http.Error(w, "could not load org", http.StatusInternalServerError) + http.Error(w, "could not load organization", http.StatusInternalServerError) return } memberCount, repeaterCount, err := s.Store.OrgCounts(r.Context(), org.ID) if err != nil { - http.Error(w, "could not load org", http.StatusInternalServerError) + http.Error(w, "could not load organization", http.StatusInternalServerError) return } pubReps, err := s.Store.ListPublicRepeaters(r.Context(), org.ID) if err != nil { - http.Error(w, "could not load org", http.StatusInternalServerError) + http.Error(w, "could not load organization", http.StatusInternalServerError) return } links, err := s.Store.ListOrgLinks(r.Context(), org.ID) if err != nil { - http.Error(w, "could not load org", http.StatusInternalServerError) + http.Error(w, "could not load organization", http.StatusInternalServerError) return } uid := s.Auth.CurrentUserID(r.Context()) @@ -200,7 +200,7 @@ func (s *Handlers) pageOrgMembers(w http.ResponseWriter, r *http.Request) { } role, isMember, err := s.Store.OrgRole(r.Context(), id, uid) if err != nil { - http.Error(w, "could not load org", http.StatusInternalServerError) + http.Error(w, "could not load organization", http.StatusInternalServerError) return } if !isMember { @@ -239,7 +239,7 @@ func (s *Handlers) pageOrgRepeaters(w http.ResponseWriter, r *http.Request) { } role, isMember, err := s.Store.OrgRole(r.Context(), id, uid) if err != nil { - http.Error(w, "could not load org", http.StatusInternalServerError) + http.Error(w, "could not load organization", http.StatusInternalServerError) return } rv, err := web.BuildRepeatersView(r.Context(), s.Store, id, isMember) @@ -398,7 +398,7 @@ func (s *Handlers) pageJoinOrg(w http.ResponseWriter, r *http.Request) { } _, isMember, err := s.Store.OrgRole(r.Context(), id, uid) if err != nil { - http.Error(w, "could not load org", http.StatusInternalServerError) + http.Error(w, "could not load organization", http.StatusInternalServerError) return } if isMember { @@ -461,7 +461,7 @@ func (s *Handlers) handleSetOrgMember(w http.ResponseWriter, r *http.Request) { return } if errors.Is(err, store.ErrLastAdmin) { - memberErr("That would leave the org with no admin.") + memberErr("That would leave the organization with no admin.") return } if err != nil { diff --git a/internal/core/repeaters.go b/internal/core/repeaters.go index d466d07..f5c29c0 100644 --- a/internal/core/repeaters.go +++ b/internal/core/repeaters.go @@ -158,7 +158,7 @@ func (s *Handlers) pageRepeaterAdded(w http.ResponseWriter, r *http.Request) { } orgs, err := s.Store.ListOrgsForUser(r.Context(), uid) if err != nil { - http.Error(w, "could not load orgs", http.StatusInternalServerError) + http.Error(w, "could not load organizations", http.StatusInternalServerError) return } s.Render(w, r, "repeater_added.html", map[string]any{ diff --git a/internal/core/shares.go b/internal/core/shares.go index bf98b0e..e6e2d33 100644 --- a/internal/core/shares.go +++ b/internal/core/shares.go @@ -34,7 +34,7 @@ func (s *Handlers) pageShare(w http.ResponseWriter, r *http.Request) { // repeater participates (the default) or has been opted out. orgs, err := s.Store.ListRepeaterOrgMemberships(r.Context(), id) if err != nil { - http.Error(w, "could not load orgs", http.StatusInternalServerError) + http.Error(w, "could not load organizations", http.StatusInternalServerError) return } s.Render(w, r, "share.html", map[string]any{ diff --git a/internal/core/templates/add_repeater.html b/internal/core/templates/add_repeater.html index 35b126b..60650aa 100644 --- a/internal/core/templates/add_repeater.html +++ b/internal/core/templates/add_repeater.html @@ -56,6 +56,7 @@

MeshTender controls repeaters through a single MeshTender identity. Before it can do anything with your repeater, that identity must be granted admin access (permission level 3) on the device. + How does this work? →

+ Run it through the repeater console in the MeshCore companion app, or directly over USB serial. {{template "revokedoc" .RevokeCommand}}