Explain regions and flooding

This commit is contained in:
Jonathon Leight
2026-08-05 21:21:29 -04:00
parent abc0dbaee8
commit 133c5ec27c
5 changed files with 123 additions and 4 deletions
+19
View File
@@ -361,4 +361,23 @@ func TestConfigRootFloodToggle(t *testing.T) {
if !strings.Contains(page, `name="root_allow_flood" value="1"`) {
t.Fatal("config page should render the root flood switch")
}
// While flooding is allowed there's nothing to warn about.
if strings.Contains(page, `data-testid="config-root-deny-warning"`) {
t.Error("the deny warning should only show while flooding is denied")
}
// Denying is the consequential setting: the wildcard is the only thing that
// repeats unscoped packets (meshcore-go node/region.go, FindFloodMatch), so
// turning it off silently strands anyone who hasn't configured regions. Say so.
resp = post(t, ts, h.app, path, url.Values{}, sess)
resp.Body.Close()
denied := readBody(t, do(t, ts, h.app, "/orgs/"+org.Slug+"/config", sess))
if !strings.Contains(denied, `data-testid="config-root-deny-warning"`) {
t.Error("denying root flood should warn what it does to unscoped traffic")
}
// And the label must not claim the wildcard applies "everywhere" — it governs
// only packets that carry no region.
if strings.Contains(denied, "everywhere") {
t.Error(`the root row should not describe the wildcard as "everywhere"`)
}
}
@@ -95,7 +95,11 @@
<label class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="region_allow_flood" value="1" {{if .Region.AllowFlood}}checked{{end}}>
<span class="form-check-label">Allow flood</span>
<small class="form-hint">Whether flooding is permitted inside this region.</small>
{{/* A repeater repeats a scoped flood packet only if it matches a region that
allows flooding (RegionMap::findMatch, and allowPacketForward in
examples/simple_repeater/MyMesh.cpp), so this is "carry this region's
traffic", not a property of the area itself. */}}
<small class="form-hint">Repeat flood packets scoped to this region. Turn it off and a repeater here drops them.</small>
</label>
<label class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="region_primary" value="1" {{if .Region.Primary}}checked{{end}}>
+45
View File
@@ -217,6 +217,51 @@ func TestE2ERegionIconTooltips(t *testing.T) {
watch.assertClean(t)
}
// TestE2ERootRegionExplainer: the wildcard's info button opens a popover explaining
// what it does and doesn't govern. Bootstrap popovers are opt-in and initialized once
// in ui.js against [data-popover]; the content is the thing people most often misread,
// so it's worth proving it actually reaches the screen.
func TestE2ERootRegionExplainer(t *testing.T) {
srv := newE2EServer(t)
user, cookie := srv.login(t, "e2erootinfo")
org, err := srv.store.CreateOrg(srv.ctx, "Root Info Org", user.ID)
if err != nil {
t.Fatalf("create org: %v", err)
}
bctx, cancel, watch := startBrowser(t)
defer cancel()
var header, body string
cfgURL := srv.appURL + "/orgs/" + org.Slug + "/config"
if err := chromedp.Run(bctx,
network.Enable(),
cdplog.Enable(),
setSessionCookie(cookie),
chromedp.Navigate(cfgURL),
chromedp.WaitVisible(`[data-testid="config-root-info"]`, chromedp.ByQuery),
chromedp.Focus(`[data-testid="config-root-info"]`, chromedp.ByQuery),
chromedp.WaitVisible(`.popover .popover-body`, chromedp.ByQuery),
chromedp.Text(`.popover .popover-header`, &header, chromedp.ByQuery),
chromedp.Text(`.popover .popover-body`, &body, chromedp.ByQuery),
); err != nil {
t.Fatalf("browser run against %s: %v", cfgURL, err)
}
if !strings.Contains(header, "flooding") {
t.Errorf("popover header = %q, want it to mention flooding", header)
}
// The claims that matter, each verified against the firmware
// (filterRecvFloodPacket / allowPacketForward in MyMesh.cpp): the wildcard
// governs unscoped packets, scoped ones are matched against the defined regions,
// and direct-routed packets are exempt from all of it.
for _, want := range []string{"Unscoped", "Scoped", "direct-routed"} {
if !strings.Contains(body, want) {
t.Errorf("popover body missing %q; got %q", want, body)
}
}
watch.assertClean(t)
}
// TestE2ERootFloodSwitchAutosubmits: the root (*) flood switch on the Configuration
// page saves by itself — it's a data-autosubmit checkbox inside its own form nested
// in a list row, so this proves the delegated handler reaches it and that the POST
+16
View File
@@ -23,6 +23,10 @@
// on hover/focus, instead of the browser's slow
// native one. Use it on icon-only controls,
// where the icon alone doesn't say what it does.
// [data-popover] — like data-tooltip but for a paragraph rather
// than a label: title= is the heading and
// data-bs-content= the body. Put it on a
// <button> so it is reachable by keyboard.
//
// It also localizes timestamps: any <time data-fmt="…"> element (emitted by the
// `ts` template func) is rewritten from its machine-readable datetime attribute
@@ -41,6 +45,18 @@
if (window.tabler && window.tabler.Tooltip) {
new window.tabler.Tooltip(document.body, { selector: "[data-tooltip]" });
}
// Popovers carry an explanation too long for a tooltip. Same delegation, and
// "hover focus" means a pointer reveals it while keyboard and touch can still
// reach it — both dismiss on leave/blur, so there is no sticky panel to close.
// Content is server-authored copy, never user data; Bootstrap sanitizes it anyway.
if (window.tabler && window.tabler.Popover) {
new window.tabler.Popover(document.body, {
selector: "[data-popover]",
trigger: "hover focus",
html: true,
placement: "left",
});
}
// --- timestamp localization ---------------------------------------------
// The server emits <time datetime="<RFC3339 UTC>" data-fmt="date|datetime|
+38 -3
View File
@@ -108,6 +108,24 @@
</div>
{{end}}
</div>
{{if and .CanEdit (not .Config.RootAllowFlood)}}
{{/* Denying the wildcard is the one setting here that can quietly take an org off
the air, so it gets a standing alert rather than a line of small print. */}}
<div class="card-body">
<div class="alert alert-danger mb-0" role="alert" data-testid="config-root-deny-warning">
<div>{{template "icon-alert" "alert-icon"}}</div>
<div>
<div class="fw-bold">Unscoped packets are being dropped</div>
<p class="mb-0">
Flooding is denied on <code>*</code>, so a repeater running this configuration
repeats nothing that arrives without a region — including traffic from any
companion whose owner hasn't set up regions yet. Only packets scoped to a
region you allow below get through.
</p>
</div>
</div>
</div>
{{end}}
<div class="card-body p-0">
<div class="row g-0">
<div class="col-lg-8">
@@ -142,11 +160,14 @@
<div class="list-group list-group-flush" style="max-height:520px; overflow-y:auto">
{{if .CanEdit}}
{{/* The root (*) isn't a region row — it lives on the org — so it has no
swatch and its only setting is flood policy, saved on its own. */}}
swatch and its only setting is flood policy, saved on its own.
It is NOT "everywhere": MeshCore consults the wildcard only for packets
that carry no region, and scoped packets are matched against the defined
regions alone (meshcore-go node/region.go, FindFloodMatch). The old
wording invited exactly the wrong reading. */}}
<div class="list-group-item d-flex align-items-center gap-2" data-testid="config-region-root">
<div class="flex-fill" style="min-width:0">
<div class="fw-bold">Root <span class="text-secondary fw-normal">· <code>*</code> · everywhere</span></div>
<div class="text-secondary small">Deny flood to scope flooding to your defined regions.</div>
<div class="fw-bold">Root <span class="text-secondary fw-normal">· <code>*</code></span></div>
</div>
<form method="post" action="/orgs/{{.Org.Slug}}/config/root-flood" class="m-0 flex-shrink-0">
{{/* No hidden shadow input: an unchecked checkbox simply isn't submitted,
@@ -158,6 +179,20 @@
<span class="form-check-label">Allow flood</span>
</label>
</form>
{{/* Sits next to the switch, not the row label: what needs explaining is
allow/deny flooding, not the word "Root". Outside the <label> so a
click opens the popover instead of toggling the switch.
Every claim below is from the firmware — filterRecvFloodPacket and
allowPacketForward in examples/simple_repeater/MyMesh.cpp, and
RegionMap::findMatch. Notably the direct-route exemption is real:
direct packets also get a NULL region, but the region check in
allowPacketForward is guarded by isRouteFlood(), so they pass. */}}
<button type="button" class="btn btn-icon btn-ghost-secondary btn-sm flex-shrink-0 ms-2"
data-popover data-testid="config-root-info"
aria-label="How flooding and regions work"
title="How flooding and regions work"
data-bs-content="A flooded packet either carries a region scope or it doesn&#39;t.&lt;br&gt;&lt;br&gt;&lt;strong&gt;Unscoped&lt;/strong&gt; packets are matched only by &lt;code&gt;*&lt;/code&gt;. &lt;strong&gt;Scoped&lt;/strong&gt; packets are matched only against the regions defined below — define none, and every scoped packet is dropped.&lt;br&gt;&lt;br&gt;&lt;strong&gt;Allow flood&lt;/strong&gt; on a region means a repeater configured with it repeats packets scoped to that region. &lt;strong&gt;Deny flood&lt;/strong&gt; means it drops them — the same as never defining that region.&lt;br&gt;&lt;br&gt;Most meshes allow flood on &lt;code&gt;*&lt;/code&gt;: a companion whose owner hasn&#39;t set up regions sends unscoped, and that is how it reaches the mesh.&lt;br&gt;&lt;br&gt;None of this touches direct-routed packets. Those follow the path recorded in them rather than flooding.">{{template "icon-help" ""}}</button>
</div>
{{end}}
{{range .Config.Regions}}