#552: variant-D round — local mode, unknown member counts, amber reserved (v0.6.211) #567

Merged
Cordy merged 10 commits from fix-vard into main 2026-09-16 05:00:11 +00:00
Owner

The two decided mockup items, plus the bug Nikola's follow-up question surfaced.

1 — A local-only instance has no directory, and now says nothing about one

This is the real find, and it came from the question "would that notice even be displayed on an instance I deliberately run without an IdP?"

Answer: usually not — but only by accident. The guard was

const showDir = d.dirSource === "client" || dir.length > 0;
if (d.dirSource !== "none" && showDir) {  }

which infers "does this instance have a directory?" from "did any group names turn up in the payload?". Two things make that wrong:

  • dirSource: "none" is unreachable. KnownDirNames is assigned unconditionally in main.go (a closure over cfg.Auth.AdminGroups, reassigned but never nil when peering is on), so the server only ever reports "client" or "seen". The !== "none" check is dead code, and the only thing hiding the section was dir.length === 0.
  • dir includes peering allow-list groups. It is built from cfg.Auth.AdminGroups plus every group named in a peering allow-list — and #550's whole point is that allow-lists work with no IdP. So a local-only instance with one stray allow-list name renders a "Directory groups" table and instructs its admin to configure an identity provider it has deliberately not got.

The Accounts tab has always asked the right question one screen away: if (d.authMode !== "local"). The Groups tab now does the same, via a pure showDirSection(authMode, dirSource, dirCount) with ten cases in web/test/dir-section.test.js — including the stray-name case as an explicit regression test.

2 — The dash in Members now says which dash it is

Decided: the lighter option, not the mockup's per-row sentence. A dash in that column read equally as "no members" and "Cairn cannot tell", which are opposite facts, and on a paginated list the explanatory notice is below the fold. In degraded mode the column header now reads Members (unknown) and each dash carries the reason as a tooltip. One word of new copy rather than one sentence per row, and it scales to 200 rows without becoming wallpaper.

3 — Amber means a fault you can fix

Decided: split, not blanket. guDirErr (directory unreachable) gets notebox warn; guSeenNote (no admin client) stays plain. Per #533 a no-IdP instance is a first-class supported deployment, not a degradation — a permanent amber banner would be nagging it for a choice it made on purpose. .notebox.warn joins the existing --warn convention.

A note on the tooling

Two splices in this round failed loudly before writing anything, and both are worth recording because they are the same class of bug this issue keeps producing:

  • The members-cell anchor matched zero lines, because the and · I typed into the splice script were converted to real characters in transit while the file contains the escape sequences. Fixed by rebuilding that line from its own text — the escapes are never retyped. That is the third escaping-layer failure in this feature, now in the tooling rather than the product.
  • It then matched two lines, because the app-owned table uses the same r.append(nm, cell( shape. Disambiguated on String( — only the app-owned row has a real count.

Also: CHANGELOG.md had no v0.6.210 entry — that release shipped without one. My own post-condition caught it by asserting a version that did not exist; both entries are added here, and the guard now checks the captured previous heading rather than a hard-coded version.

Verification

Red witnessed on the runner before implementation (index.html has no // --- pure:dir-section --- block). Full Go suite green, all web tests pass, node --check clean, eleven splices count-asserted, and a post-condition asserting zero \\u sequences anywhere in the page.

The two decided mockup items, plus the bug Nikola's follow-up question surfaced. ## 1 — A local-only instance has no directory, and now says nothing about one This is the real find, and it came from the question "would that notice even be displayed on an instance I deliberately run without an IdP?" Answer: usually not — **but only by accident**. The guard was ```js const showDir = d.dirSource === "client" || dir.length > 0; if (d.dirSource !== "none" && showDir) { … } ``` which infers *"does this instance have a directory?"* from *"did any group names turn up in the payload?"*. Two things make that wrong: - **`dirSource: "none"` is unreachable.** `KnownDirNames` is assigned unconditionally in `main.go` (a closure over `cfg.Auth.AdminGroups`, reassigned but never nil when peering is on), so the server only ever reports `"client"` or `"seen"`. The `!== "none"` check is dead code, and the only thing hiding the section was `dir.length === 0`. - **`dir` includes peering allow-list groups.** It is built from `cfg.Auth.AdminGroups` *plus* every group named in a peering allow-list — and #550's whole point is that allow-lists work with no IdP. So a local-only instance with one stray allow-list name renders a "Directory groups" table and instructs its admin to configure an identity provider it has deliberately not got. The Accounts tab has always asked the right question one screen away: `if (d.authMode !== "local")`. The Groups tab now does the same, via a pure `showDirSection(authMode, dirSource, dirCount)` with ten cases in `web/test/dir-section.test.js` — including the stray-name case as an explicit regression test. ## 2 — The dash in Members now says which dash it is Decided: the lighter option, not the mockup's per-row sentence. A dash in that column read equally as *"no members"* and *"Cairn cannot tell"*, which are opposite facts, and on a paginated list the explanatory notice is below the fold. In degraded mode the column header now reads **Members (unknown)** and each dash carries the reason as a tooltip. One word of new copy rather than one sentence per row, and it scales to 200 rows without becoming wallpaper. ## 3 — Amber means a fault you can fix Decided: split, not blanket. `guDirErr` (directory unreachable) gets `notebox warn`; `guSeenNote` (no admin client) stays plain. Per #533 a no-IdP instance is a first-class supported deployment, not a degradation — a permanent amber banner would be nagging it for a choice it made on purpose. `.notebox.warn` joins the existing `--warn` convention. ## A note on the tooling Two splices in this round failed loudly before writing anything, and both are worth recording because they are the same class of bug this issue keeps producing: - The members-cell anchor matched **zero** lines, because the `—` and `·` I typed into the splice script were converted to real characters in transit while the file contains the escape sequences. Fixed by rebuilding that line **from its own text** — the escapes are never retyped. That is the third escaping-layer failure in this feature, now in the tooling rather than the product. - It then matched **two** lines, because the app-owned table uses the same `r.append(nm, cell(` shape. Disambiguated on `String(` — only the app-owned row has a real count. Also: `CHANGELOG.md` had no `v0.6.210` entry — that release shipped without one. My own post-condition caught it by asserting a version that did not exist; both entries are added here, and the guard now checks the *captured* previous heading rather than a hard-coded version. ## Verification Red witnessed on the runner before implementation (`index.html has no // --- pure:dir-section --- block`). Full Go suite green, all web tests pass, `node --check` clean, eleven splices count-asserted, and a post-condition asserting zero `\\u` sequences anywhere in the page.
Cordy merged commit 52f87014a6 into main 2026-09-16 05:00:11 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
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#567
No description provided.