Peering admin menu item is inert — clicking it opens nothing #131

Closed
opened 2026-08-07 00:20:21 +00:00 by Cordy · 2 comments
Owner

Reported by Nikola, v0.4.3 dogfood. Signed in as admin, the avatar menu shows "Peering" between Settings and License. Clicking it does nothing — no dialog, no view, no error.

This is the admin surface from #101/#120 (PeeringAPI — issue keys, register peers, allow-lists). The end-user send UI (#105) works and is separate; this is the operator side, where a peering actually gets configured. Without it, peering cannot be set up through the UI at all, which also blocks #106.

Investigating now: the menu item um-peering exists in the markup and is un-hidden on feat.lic (it shares the license gate), but it may have no click handler, or a handler that references a dialog/view that was never built. #120 is titled "completes #101 — Settings panel", so the question is whether the panel was ever wired to this button.

Fix lands in this issue once the cause is confirmed.

**Reported by Nikola, v0.4.3 dogfood.** Signed in as admin, the avatar menu shows "Peering" between Settings and License. Clicking it does nothing — no dialog, no view, no error. This is the admin surface from #101/#120 (`PeeringAPI` — issue keys, register peers, allow-lists). The end-user send UI (#105) works and is separate; this is the operator side, where a peering actually gets configured. Without it, peering cannot be set up through the UI at all, which also blocks #106. Investigating now: the menu item `um-peering` exists in the markup and is un-hidden on `feat.lic` (it shares the license gate), but it may have no click handler, or a handler that references a dialog/view that was never built. #120 is titled "completes #101 — Settings panel", so the question is whether the panel was ever wired to this button. Fix lands in this issue once the cause is confirmed.
Author
Owner

Root cause — confirmed by static analysis

A ReferenceError from a guard that doesn't guard. The click handler:

$("um-peering").onclick = () => { closeUserMenu && closeUserMenu(); openPeeringDialog(); };

closeUserMenu is never declared — it appears at this one call site and nowhere else. Every sibling handler uses closeMenus() (defined line 1262).

The trap: closeUserMenu && … looks defensive, but && only short-circuits a declared falsy value. Referencing an undeclared identifier throws ReferenceError: closeUserMenu is not defined the instant the arrow function runs. openPeeringDialog() is never reached. No dialog, console-only error — precisely the reported symptom.

Everything else is fine: the peerdlg markup, all peerdlg-* ids, peerRefresh (line 1704), openPeeringDialog (1797) — all present, and both <script> blocks parse clean. The handler was simply written by hand in a way that diverged from the codebase's closeMenus() convention, and the divergence is the bug.

A second bug in the same two lines: wrong visibility gate

$("um-peering").hidden = !feat.lic;   // line 2213

Peering visibility is gated on the license feature. That is wrong both ways:

  • A free-tier admin (peering is included in the free tier — Q5) has feat.lic === false, so the button is hidden and they cannot configure peering through the UI at all.
  • A licensed non-admin sees the button, and clicking it hits /api/v1/admin/peering/*, which PeeringAPI.gate refuses with 403.

The admin API is admin-gated server-side, so the button should mirror um-settings: !(me && me.admin).

The fix

Primary (closes the reported symptom): closeUserMenu && closeUserMenu()closeMenus(). Obvious, no alternatives — it just adopts the existing convention.

Gate: !feat.lic!(me && me.admin). Also fairly obvious (mirror Settings).

One design choice for you — the gate has a subtlety worth a decision

On the dogfood, peering.statePath is not set in config.json, so apiH.Peering is nil and the /api/v1/admin/peering/* routes don't exist. So even after both fixes, an admin clicking Peering here would open the dialog and then see peerRefresh fail (404), because the peering subsystem isn't enabled server-side. statePath is a deploy-time prerequisite — the admin UI configures within it, it can't create it.

Two ways to handle that:

  • A — gate on admin only (me.admin). Simplest. The button shows for any admin; if peering isn't configured, the dialog opens but reports it can't load. Honest but slightly ugly on an unconfigured instance.
  • B — gate on admin AND a boot probe of /api/v1/admin/peering/peers (200 = mounted, 404 = not), the same pattern the send UI already uses for feat.peerSend. The button then only appears once statePath is set, so it never opens a dialog that can't work.

I'll ship A now, because it fixes the reported bug and is correct for the configured case, and B is a refinement rather than a correction. If you'd rather have B (button hidden until peering is actually enabled), say so and I'll add the probe — it's ~5 lines and mirrors code that already exists.

## Root cause — confirmed by static analysis **A `ReferenceError` from a guard that doesn't guard.** The click handler: ```js $("um-peering").onclick = () => { closeUserMenu && closeUserMenu(); openPeeringDialog(); }; ``` `closeUserMenu` is **never declared** — it appears at this one call site and nowhere else. Every sibling handler uses `closeMenus()` (defined line 1262). The trap: `closeUserMenu && …` looks defensive, but `&&` only short-circuits a *declared* falsy value. Referencing an **undeclared identifier** throws `ReferenceError: closeUserMenu is not defined` the instant the arrow function runs. `openPeeringDialog()` is never reached. No dialog, console-only error — precisely the reported symptom. Everything else is fine: the `peerdlg` markup, all `peerdlg-*` ids, `peerRefresh` (line 1704), `openPeeringDialog` (1797) — all present, and both `<script>` blocks parse clean. The handler was simply written by hand in a way that diverged from the codebase's `closeMenus()` convention, and the divergence is the bug. ## A second bug in the same two lines: wrong visibility gate ```js $("um-peering").hidden = !feat.lic; // line 2213 ``` Peering visibility is gated on the **license** feature. That is wrong both ways: - A **free-tier admin** (peering is included in the free tier — Q5) has `feat.lic === false`, so the button is hidden and they cannot configure peering through the UI at all. - A **licensed non-admin** sees the button, and clicking it hits `/api/v1/admin/peering/*`, which `PeeringAPI.gate` refuses with 403. The admin API is admin-gated server-side, so the button should mirror `um-settings`: `!(me && me.admin)`. ## The fix **Primary (closes the reported symptom):** `closeUserMenu && closeUserMenu()` → `closeMenus()`. Obvious, no alternatives — it just adopts the existing convention. **Gate:** `!feat.lic` → `!(me && me.admin)`. Also fairly obvious (mirror Settings). ## One design choice for you — the gate has a subtlety worth a decision On the dogfood, `peering.statePath` is **not** set in config.json, so `apiH.Peering` is nil and the `/api/v1/admin/peering/*` routes don't exist. So even after both fixes, an admin clicking Peering here would open the dialog and then see `peerRefresh` fail (404), because the peering subsystem isn't enabled server-side. `statePath` is a deploy-time prerequisite — the admin UI configures *within* it, it can't create it. Two ways to handle that: - **A — gate on admin only (`me.admin`).** Simplest. The button shows for any admin; if peering isn't configured, the dialog opens but reports it can't load. Honest but slightly ugly on an unconfigured instance. - **B — gate on admin AND a boot probe** of `/api/v1/admin/peering/peers` (200 = mounted, 404 = not), the same pattern the send UI already uses for `feat.peerSend`. The button then only appears once `statePath` is set, so it never opens a dialog that can't work. I'll ship **A** now, because it fixes the reported bug and is correct for the configured case, and B is a refinement rather than a correction. If you'd rather have B (button hidden until peering is actually enabled), say so and I'll add the probe — it's ~5 lines and mirrors code that already exists.
Author
Owner

Fixed in PR #132, merged. Shipping in v0.4.4 so it reaches the dogfood.

Both lines corrected: the closeUserMenu ReferenceError → closeMenus(), and the !feat.lic gate → !(me && me.admin).

Reminder for verification: the button will now respond, but on the dogfood the dialog opens onto an empty/erroring panel because peering.statePath is unset — the peering subsystem isn't enabled server-side yet. That is the next step (setting statePath, which is #106's setup), not a regression. The button working and the dialog opening is what this issue was about.

Option B from my diagnosis (hide the button until peering is actually configured) remains open for your call.

Fixed in PR #132, merged. Shipping in v0.4.4 so it reaches the dogfood. Both lines corrected: the `closeUserMenu` ReferenceError → `closeMenus()`, and the `!feat.lic` gate → `!(me && me.admin)`. **Reminder for verification:** the button will now respond, but on the dogfood the dialog opens onto an empty/erroring panel because `peering.statePath` is unset — the peering subsystem isn't enabled server-side yet. That is the *next* step (setting `statePath`, which is #106's setup), not a regression. The button working and the dialog opening is what this issue was about. Option B from my diagnosis (hide the button until peering is actually configured) remains open for your call.
Cordy closed this issue 2026-08-07 00:31:50 +00:00
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#131
No description provided.