From d7b55c64436032f344813433a37c720e59e775a6 Mon Sep 17 00:00:00 2001 From: SiteRelEnby <125829806+SiteRelEnby@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:54:51 -0400 Subject: [PATCH] Say what turning on System Safety will cover Setting an auth tier, or a grace period above zero, is the moment the safety settings start doing anything: at the `none` tier with no grace, a ticked category enforces nothing. So somebody who sets a tier to get a password prompt before deletions has, in the same action, put that prompt in front of every other ticked category - including Profile visibility, which is the one category that ships on and therefore the one they never switched on themselves. That is the reported surprise: a safeguard doing its job, experienced as a malfunction. The Safety page now says so before the save, lists what will be covered, and names Profile visibility explicitly as on-by-default rather than something the user chose, with a pointer to untick it if they meant this for deletions only. The wording adapts to what is actually being armed, so a grace window says publishing will now wait rather than implying only a re-auth. Silent on an ordinary save: it appears only when a change is arming something, which is what keeps it worth reading. The one-item case is deliberately not listed twice - when profile visibility is the only category covered, the callout below already names it and a restating list reads as a stutter. --- CHANGELOG.md | 2 + web/src/components/system-safety-card.tsx | 69 +++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8752a56..fd82320a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ All notable changes to Sheaf are documented here. The format is based on [Keep a - **The OpenPlural interchange format is now called PluralPort**, following the standard's own rename upstream (a name conflict; the spec version stays 0.1). Exports now stamp `pluralport_version` and download as `.pluralport.zip`, and the API's format/source values are `pluralport` / `pluralport_file`. Nothing you already have stops working: files stamped with the old `openplural_version` (or bundles carrying `openplural.json`) still import, the old API values are still accepted as deprecated aliases, and self-hosters can keep the `OPENPLURAL_MAX_PRESERVED_MB` env var (the new `PLURALPORT_MAX_PRESERVED_MB` name wins if both are set). +- **Turning on System Safety now tells you what it will cover.** Setting an auth tier, or a grace period above zero, is the moment those settings start doing anything: until then a ticked category enforces nothing. The Safety settings page now says so before you save, lists what will be covered, and specifically points out that Profile visibility is on by default rather than something you turned on, so a password prompt you added for deleting things does not quietly appear in front of publishing too. It also tells you when a grace period means publishing will now wait, rather than leaving you to find out days later. + ### Fixed - **"Public" is no longer offered on instances that have sharing turned off.** When a server has public profiles and share links disabled, nothing can be published there, and the server has always refused any attempt to set something to Public - but the app went on offering Public in every privacy menu, so choosing it just produced a permissions error with no hint as to why. Public is now shown greyed out on those instances, with one line explaining that sharing is off, on members, groups, custom fields, relationships and the system-wide setting alike. Anything already public keeps its setting and can still be made less visible: nothing ever stands between you and reducing what you share. On those instances, creating something new as Public is now refused the same way changing an existing thing to Public already was, which closes a gap where "make a new one" got around a limit that "change this one" enforced. diff --git a/web/src/components/system-safety-card.tsx b/web/src/components/system-safety-card.tsx index 1c67d2b5..ba44988b 100644 --- a/web/src/components/system-safety-card.tsx +++ b/web/src/components/system-safety-card.tsx @@ -82,6 +82,50 @@ const categoryLabels: { }, ]; +/** + * What a tier or grace change is about to start enforcing, or null when it is + * not arming anything. + * + * Arming the tier, or opening a grace window, does not change WHICH categories + * are ticked. It changes whether being ticked does anything at all: at the + * `none` tier with no grace, a ticked category enforces nothing. So somebody + * who sets a tier to get a password prompt before deletions has, in the same + * action, put that prompt in front of every other ticked category - including + * Profile visibility, the one category that ships on and therefore the one they + * never switched on themselves. + * + * That is the reported surprise, and it is far cheaper to say here than to let + * someone discover it later when a publish is refused. Silent on an ordinary + * save, so it stays worth reading when it does appear. + */ +function newlyEnforced( + current: SystemSafetySettings, + draft: SystemSafetySettings, +): { labels: string[]; effects: string[]; visibilityRidesAlong: boolean } | null { + const tierArming = current.auth_tier === "none" && draft.auth_tier !== "none"; + const graceArming = + current.grace_period_days === 0 && draft.grace_period_days > 0; + if (!tierArming && !graceArming) return null; + + const on = categoryLabels.filter((c) => draft[c.key] as boolean); + if (on.length === 0) return null; + + const effects: string[] = []; + if (tierArming) effects.push("ask you to re-authenticate"); + if (graceArming) { + effects.push(`wait out the ${draft.grace_period_days}-day grace period`); + } + return { + labels: on.map((c) => c.label), + effects, + // Only called out when the user is not the one who just ticked it. If they + // turned it on in this same edit they already know it is there. + visibilityRidesAlong: + (draft.applies_to_profile_visibility as boolean) && + (current.applies_to_profile_visibility as boolean), + }; +} + function changeSummary(changes: Record): string { const parts: string[] = []; for (const [k, v] of Object.entries(changes)) { @@ -187,6 +231,7 @@ function SafetyForm({ settings }: { settings: SystemSafetySettings }) { }); const dirty = hasDiff(settings, draft); + const arming = newlyEnforced(settings, draft); const loosening = detectLoosening(settings, draft); const needsReauth = loosening && (settings.grace_period_days > 0 || draft.grace_period_days > 0); @@ -276,6 +321,30 @@ function SafetyForm({ settings }: { settings: SystemSafetySettings }) { ))} + {arming && ( +
+

+ Saving this starts enforcing the categories ticked above. Actions in + them will {arming.effects.join(" and ")}. +

+ {/* Skipped when profile visibility is the only thing covered: the + callout below already names it, and a one-item list restating it + reads as a stutter. */} + {(!arming.visibilityRidesAlong || arming.labels.length > 1) && ( +

+ Covered: {arming.labels.join(", ")}. +

+ )} + {arming.visibilityRidesAlong && ( +

+ That includes Profile visibility, which is on by default rather + than something you turned on. Publishing a share view, or raising + a member, group or field to public, goes through the same gate. + Untick it above if you meant this for deleting things only. +

+ )} +
+ )}