feat: retention rules become editable, space lifecycle reaches the admin UI, membership stops lying about who owns it #333

Merged
Cordy merged 13 commits from feat/cluster-c into main 2026-08-27 19:38:45 +00:00
Owner

Deferred-items cluster C (#311 items 1, 6, 13, 14, 15, 16). Item 2 deferred to #138 by decision.

The headline finding: most of this was never a backend problem

The deferred list described seven items blocked on missing endpoints. Reading the code, four of them were already served and simply never consumed:

  • Item 14auditQuery has always parsed from/to as RFC3339 into audit.Filter. Only the two inputs were missing. Recorded as "endpoint support unconfirmed".
  • Item 13POST .../deactivate, POST .../reactivate and DELETE /api/v1/spaces/{name} have existed since #286. Recorded as "admin API unconfirmed".
  • Item 6/admin/info already returns storageDriver and shareCount. Overview never read them.
  • Item 15RetentionPeriods is a map[string]string, not free text. The textarea is a serialisation of structured data, so there was no grammar to reverse-engineer — which was my stated blocker in slice D, and it was wrong.

So the backend change here is one endpoint and one boolean, not seven endpoints.

Retention rules are now editable rows (item 15)

You picked the control panel over the read-only window, and building it justified the choice: the mockup's typo case is a real bug in today's UI. A mistyped prefix in the textarea is accepted in silence and protects nothing — no error, no warning, and you find out when a file you believed was held gets purged. Rows can validate the path against the instance; a textarea cannot.

Two safeguards, as agreed:

  • The textarea remains the single source of truth and saveSettings is untouched — it still reads $("set-retention").value and parses it. The rows can only ever produce something that round-trips through the format the server already accepts. The textarea becomes read-only and moves into a "Rules as stored" disclosure, which is therefore literally the stored form rather than a rendering of it.
  • Path validation warns, never blocks — a rule may legitimately be written before the space it covers exists.

Durations are a select of named windows plus Custom…, because storage is arbitrary Go durations and a fixed list would quietly make existing rules uneditable.

One endpoint, with one algorithm (item 16)

GET /api/v1/admin/retention/resolve?path= returns the server's own answer. RetentionFor and the new RetentionRuleFor now share one implementation, so the endpoint and the trash sweep can never disagree about a floor. Test-first, and the test is the interesting part: it pins the segment-boundary rule, so /spaces/legal can never silently cover /spaces/legalese, and it asserts RetentionFor and RetentionRuleFor always agree.

The red step is enforced in CI, not just claimed — the workflow fails if the test passes before the implementation exists.

Membership stops offering edits the IdP owns (item 1 / #129)

listMembers already branched on s.Mgr != nil to decide whether membership comes from the directory; it just never told the client. It now returns idpManaged, and the dialog shows the notice that has been shipping hidden since the redesign, disables Add/role/Remove, and stops rendering remove buttons. Previously the dialog offered three actions the identity provider would overwrite.

Controls are disabled rather than hidden, so the notice above them explains why — a row that vanishes teaches nothing.

Space lifecycle (item 13)

Deactivate / Reactivate / Delete on each row of the admin inventory. Delete only appears once a space is deactivated, because the server returns 409 otherwise — better to show the required order than to let someone discover it as an error. Deletion is confirmed by typing the space name, since it moves every file into the trash and reserves the name for the retention window.

Not done, deliberately

  • Item 2 (per-item notification read state) — deferred to #138. It needs per-user persistence, and every existing state file is what #138 is moving into the storage backend. Adding another /data JSON now means handing that milestone one more thing to migrate.
  • Audit event count on Overview — there is no count endpoint, and inferring one from a capped query would be a number that looks authoritative and isn't.

Gate: go vet ./..., go test ./... (full suite green including the new settings tests and the webcheck guard), go build ./cmd/cairnd, node --check on every script block. All new i18n keys populated in four languages.

Deferred-items cluster C (#311 items 1, 6, 13, 14, 15, 16). Item 2 deferred to #138 by decision. ## The headline finding: most of this was never a backend problem The deferred list described seven items blocked on missing endpoints. Reading the code, **four of them were already served and simply never consumed:** - **Item 14** — `auditQuery` has always parsed `from`/`to` as RFC3339 into `audit.Filter`. Only the two inputs were missing. Recorded as "endpoint support unconfirmed". - **Item 13** — `POST .../deactivate`, `POST .../reactivate` and `DELETE /api/v1/spaces/{name}` have existed since #286. Recorded as "admin API unconfirmed". - **Item 6** — `/admin/info` already returns `storageDriver` and `shareCount`. Overview never read them. - **Item 15** — `RetentionPeriods` is a `map[string]string`, not free text. The textarea is a *serialisation* of structured data, so there was no grammar to reverse-engineer — which was my stated blocker in slice D, and it was wrong. So the backend change here is one endpoint and one boolean, not seven endpoints. ## Retention rules are now editable rows (item 15) You picked the control panel over the read-only window, and building it justified the choice: **the mockup's typo case is a real bug in today's UI.** A mistyped prefix in the textarea is accepted in silence and protects nothing — no error, no warning, and you find out when a file you believed was held gets purged. Rows can validate the path against the instance; a textarea cannot. Two safeguards, as agreed: - **The textarea remains the single source of truth** and `saveSettings` is untouched — it still reads `$("set-retention").value` and parses it. The rows can only ever produce something that round-trips through the format the server already accepts. The textarea becomes read-only and moves into a **"Rules as stored"** disclosure, which is therefore literally the stored form rather than a rendering of it. - **Path validation warns, never blocks** — a rule may legitimately be written before the space it covers exists. Durations are a select of named windows plus **Custom…**, because storage is arbitrary Go durations and a fixed list would quietly make existing rules uneditable. ## One endpoint, with one algorithm (item 16) `GET /api/v1/admin/retention/resolve?path=` returns the server's own answer. `RetentionFor` and the new `RetentionRuleFor` now share one implementation, so the endpoint and the trash sweep can never disagree about a floor. Test-first, and the test is the interesting part: it pins the segment-boundary rule, so `/spaces/legal` can never silently cover `/spaces/legalese`, and it asserts `RetentionFor` and `RetentionRuleFor` always agree. The red step is enforced in CI, not just claimed — the workflow fails if the test passes before the implementation exists. ## Membership stops offering edits the IdP owns (item 1 / #129) `listMembers` already branched on `s.Mgr != nil` to decide whether membership comes from the directory; it just never told the client. It now returns `idpManaged`, and the dialog shows the notice that has been shipping hidden since the redesign, disables Add/role/Remove, and stops rendering remove buttons. Previously the dialog offered three actions the identity provider would overwrite. Controls are **disabled rather than hidden**, so the notice above them explains why — a row that vanishes teaches nothing. ## Space lifecycle (item 13) Deactivate / Reactivate / Delete on each row of the admin inventory. **Delete only appears once a space is deactivated**, because the server returns 409 otherwise — better to show the required order than to let someone discover it as an error. Deletion is confirmed by typing the space name, since it moves every file into the trash and reserves the name for the retention window. ## Not done, deliberately - **Item 2** (per-item notification read state) — deferred to #138. It needs per-user persistence, and every existing state file is what #138 is moving into the storage backend. Adding another `/data` JSON now means handing that milestone one more thing to migrate. - **Audit event count** on Overview — there is no count endpoint, and inferring one from a capped query would be a number that looks authoritative and isn't. Gate: `go vet ./...`, `go test ./...` (full suite green including the new settings tests and the webcheck guard), `go build ./cmd/cairnd`, `node --check` on every script block. All new i18n keys populated in four languages.
Cordy merged commit 6b3c939506 into main 2026-08-27 19:38:45 +00:00
Cordy deleted branch feat/cluster-c 2026-08-27 19:38:47 +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#333
No description provided.