Add public repeater checkbox when adding repeater

This commit is contained in:
Jonathon Leight
2026-07-08 18:27:28 -04:00
parent 83f41fc530
commit d821309fd8
8 changed files with 135 additions and 23 deletions
+48
View File
@@ -149,6 +149,54 @@ func TestRepeaterCrudPosts(t *testing.T) {
assertRedirect(t, delRep, "/", "delete repeater")
}
// TestAddRepeaterExposePublicPage: the add form's "Publish a public page"
// checkbox is honored at creation (not only on the edit page). Present → the
// repeater publishes a public page; absent → it does not.
func TestAddRepeaterExposePublicPage(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
_, sess := appLogin(t, ts, st, ctx, h.app, "exposer")
exposed := func(t *testing.T, values url.Values) bool {
t.Helper()
resp := post(t, ts, h.app, "/repeaters", values, sess)
resp.Body.Close()
loc, _ := url.Parse(resp.Header.Get("Location"))
parts := strings.Split(loc.Path, "/")
if resp.StatusCode != http.StatusSeeOther || len(parts) < 3 {
t.Fatalf("create repeater = %d %q, want 303 → /repeaters/{id}/added", resp.StatusCode, resp.Header.Get("Location"))
}
var got bool
if err := st.Pool().QueryRow(ctx,
`SELECT expose_public_page FROM repeaters WHERE public_id = $1`, parts[2]).Scan(&got); err != nil {
t.Fatalf("read expose_public_page: %v", err)
}
return got
}
radio := func(v url.Values) url.Values {
v.Set("radio_freq_mhz", "869.525")
v.Set("radio_bw_khz", "250")
v.Set("radio_sf", "11")
v.Set("radio_cr", "5")
return v
}
onID, _ := meshcore.GenerateLocalIdentity(rand.Reader)
if !exposed(t, radio(url.Values{
"name": {"Public Rep"}, "public_key": {onID.String()}, "expose_public_page": {"1"},
})) {
t.Error("expose_public_page=1 at add time did not publish a public page")
}
offID, _ := meshcore.GenerateLocalIdentity(rand.Reader)
if exposed(t, radio(url.Values{
"name": {"Private Rep"}, "public_key": {offID.String()},
})) {
t.Error("repeater added without the checkbox should not publish a public page")
}
}
// #81 create link, #82 delete link, #85 share commands, #86 steward, #83 unshare,
// #87 org participation.
func TestRepeaterSharePosts(t *testing.T) {
+9 -8
View File
@@ -250,14 +250,15 @@ func (s *Handlers) handleSetupComplete(w http.ResponseWriter, r *http.Request) {
}
rep, err := s.Store.CreateRepeater(r.Context(), &store.Repeater{
OwnerID: uid,
Name: name,
PublicKeyHex: pubHex,
RadioFreqHz: freq,
RadioBwHz: bw,
RadioSF: int16(sf),
RadioCR: int16(cr),
ShowOnPublicOrg: r.FormValue("show_on_public_org") != "",
OwnerID: uid,
Name: name,
PublicKeyHex: pubHex,
RadioFreqHz: freq,
RadioBwHz: bw,
RadioSF: int16(sf),
RadioCR: int16(cr),
ShowOnPublicOrg: r.FormValue("show_on_public_org") != "",
ExposePublicPage: r.FormValue("expose_public_page") != "",
})
if errors.Is(err, store.ErrDuplicate) {
http.Error(w, "you already added a repeater with that public key", http.StatusConflict)
+4 -1
View File
@@ -131,7 +131,7 @@ func TestSerialSetupFlow(t *testing.T) {
complete := url.Values{
"name": {"Hilltop"}, "public_key": {pub},
"radio_freq_mhz": {"910.525"}, "radio_bw_khz": {"62.5"}, "radio_sf": {"7"}, "radio_cr": {"5"},
"lat": {"15.0"}, "lon": {"35.0"},
"lat": {"15.0"}, "lon": {"35.0"}, "expose_public_page": {"1"},
}
cResp := post(t, ts, h.app, "/repeaters/setup/complete", complete, sess)
if cResp.StatusCode != http.StatusOK {
@@ -160,6 +160,9 @@ func TestSerialSetupFlow(t *testing.T) {
if got.Latitude == nil || got.Longitude == nil || *got.Latitude != 15.0 || *got.Longitude != 35.0 {
t.Errorf("location not stored: %v, %v", got.Latitude, got.Longitude)
}
if !got.ExposePublicPage {
t.Error("expose_public_page from the setup form was not persisted")
}
}
// TestBuildSetupCommands locks the from-scratch command order (header → profile
+9 -8
View File
@@ -201,14 +201,15 @@ func (s *Handlers) handleAddRepeater(w http.ResponseWriter, r *http.Request) {
}
rep, err := s.Store.CreateRepeater(r.Context(), &store.Repeater{
OwnerID: uid,
Name: name,
PublicKeyHex: pubHex,
RadioFreqHz: freq,
RadioBwHz: bw,
RadioSF: int16(sf),
RadioCR: int16(cr),
ShowOnPublicOrg: r.FormValue("show_on_public_org") != "",
OwnerID: uid,
Name: name,
PublicKeyHex: pubHex,
RadioFreqHz: freq,
RadioBwHz: bw,
RadioSF: int16(sf),
RadioCR: int16(cr),
ShowOnPublicOrg: r.FormValue("show_on_public_org") != "",
ExposePublicPage: r.FormValue("expose_public_page") != "",
})
if errors.Is(err, store.ErrDuplicate) {
addErr(w, r, "You already added a repeater with that public key.")
@@ -237,6 +237,10 @@
<input class="form-check-input" type="checkbox" id="show_on_public_org">
<span class="form-check-label">Show this repeater on your organizations' <strong>public map</strong>.</span>
</label>
<label class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="expose_public_page">
<span class="form-check-label">Publish a <strong>public page</strong> for this repeater (the NFC/QR target) with its docs, stewards, and location.</span>
</label>
<div class="btn-list mt-3">
<button type="button" class="btn btn-primary" id="generate">Generate command list →</button>
@@ -313,6 +317,10 @@
<input class="form-check-input" type="checkbox" name="show_on_public_org" value="1">
<span class="form-check-label">Show this repeater on your organizations' <strong>public map</strong>.</span>
</label>
<label class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="expose_public_page" value="1">
<span class="form-check-label">Publish a <strong>public page</strong> for this repeater (the NFC/QR target) with its docs, stewards, and location.</span>
</label>
<div class="btn-list mt-3">
<button type="submit" class="btn btn-primary">Add repeater</button>
@@ -0,0 +1,47 @@
//go:build browser
package e2e
import (
"context"
"testing"
"time"
cdplog "github.com/chromedp/cdproto/log"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
)
// TestE2EAddRepeaterPublicPageOption verifies the "Publish a public page"
// checkbox is offered on both add-repeater detail steps — the add-existing (kiss)
// form field and the USB-setup (serial) toggle that serial-setup.js reads — so
// the option isn't edit-page-only. Both steps must render clean under the CSP.
func TestE2EAddRepeaterPublicPageOption(t *testing.T) {
srv := newE2EServer(t)
_, cookie := srv.login(t, "pager")
bctx, cancel, watch := startBrowser(t)
defer cancel()
cases := []struct{ name, url, selector string }{
{"add-existing", "/repeaters/add?step=details&method=kiss", `input[name="expose_public_page"]`},
{"usb-setup", "/repeaters/add?step=serial&method=serial", `#expose_public_page`},
}
for _, c := range cases {
runCtx, cancelRun := context.WithTimeout(bctx, 30*time.Second)
// WaitVisible on the checkbox itself proves it rendered and is visible.
err := chromedp.Run(runCtx,
network.Enable(),
cdplog.Enable(),
setSessionCookie(cookie),
chromedp.Navigate(srv.appURL+c.url),
chromedp.WaitVisible(c.selector, chromedp.ByQuery),
)
cancelRun()
if err != nil {
t.Errorf("%s step: public-page checkbox (%s) not visible: %v", c.name, c.selector, err)
}
}
watch.assertClean(t)
}
+5 -5
View File
@@ -87,12 +87,12 @@ func (s *Store) CreateRepeater(ctx context.Context, r *Repeater) (*Repeater, err
}
var out Repeater
err = s.pool.QueryRow(ctx, `
INSERT INTO repeaters (public_id, owner_id, name, public_key_hex, radio_freq_hz, radio_bw_hz, radio_sf, radio_cr, show_on_public_org)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, public_id, owner_id, name, public_key_hex, radio_freq_hz, radio_bw_hz, radio_sf, radio_cr, confirmed, confirmed_at, created_at, show_on_public_org`,
publicID, r.OwnerID, r.Name, r.PublicKeyHex, r.RadioFreqHz, r.RadioBwHz, r.RadioSF, r.RadioCR, r.ShowOnPublicOrg).
INSERT INTO repeaters (public_id, owner_id, name, public_key_hex, radio_freq_hz, radio_bw_hz, radio_sf, radio_cr, show_on_public_org, expose_public_page)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, public_id, owner_id, name, public_key_hex, radio_freq_hz, radio_bw_hz, radio_sf, radio_cr, confirmed, confirmed_at, created_at, show_on_public_org, expose_public_page`,
publicID, r.OwnerID, r.Name, r.PublicKeyHex, r.RadioFreqHz, r.RadioBwHz, r.RadioSF, r.RadioCR, r.ShowOnPublicOrg, r.ExposePublicPage).
Scan(&out.ID, &out.PublicID, &out.OwnerID, &out.Name, &out.PublicKeyHex, &out.RadioFreqHz, &out.RadioBwHz,
&out.RadioSF, &out.RadioCR, &out.Confirmed, &out.ConfirmedAt, &out.CreatedAt, &out.ShowOnPublicOrg)
&out.RadioSF, &out.RadioCR, &out.Confirmed, &out.ConfirmedAt, &out.CreatedAt, &out.ShowOnPublicOrg, &out.ExposePublicPage)
if isUniqueViolation(err) {
return nil, ErrDuplicate
}
+5 -1
View File
@@ -320,7 +320,11 @@
form.set("lat", loc.lat.toFixed(6));
form.set("lon", loc.lon.toFixed(6));
}
if ($("show_on_public_org").checked) form.set("show_on_public_org", "1");
// Optional visibility toggles — guarded so a trimmed-down template can omit them.
var showOrg = $("show_on_public_org");
if (showOrg && showOrg.checked) form.set("show_on_public_org", "1");
var exposePage = $("expose_public_page");
if (exposePage && exposePage.checked) form.set("expose_public_page", "1");
var resp = await fetch("/repeaters/setup/complete", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },