Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
69 changes: 69 additions & 0 deletions web/src/components/system-safety-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, unknown>): string {
const parts: string[] = [];
for (const [k, v] of Object.entries(changes)) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -276,6 +321,30 @@ function SafetyForm({ settings }: { settings: SystemSafetySettings }) {
))}
</div>
</div>
{arming && (
<div className="space-y-1 rounded-md border border-dashed p-3">
<p className="text-sm">
Saving this starts enforcing the categories ticked above. Actions in
them will {arming.effects.join(" and ")}.
</p>
{/* 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) && (
<p className="text-xs text-muted-foreground">
Covered: {arming.labels.join(", ")}.
</p>
)}
{arming.visibilityRidesAlong && (
<p className="text-xs text-amber-600 dark:text-amber-500">
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.
</p>
)}
</div>
)}
<div className="space-y-2 border-t pt-3">
<Label className="text-sm">Revision pinning</Label>
<label className="flex items-start gap-2 text-sm cursor-pointer">
Expand Down
Loading