mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +02:00
Follow-up to #6598 (squash-merged into `SaaS`). These are the policy refinements made after that merge, against the current `SaaS` tip. ## Changes - **Simplify Security config + plain-language info buttons** — Redact config reduced to the PII field; Sanitise has no config (JavaScript-removal only) with a non-technical info button; per-tool info buttons reworded to match the tool-steps style. - **Hide 'Flatten PDF pages to images' from the watermark policy config** — new `PolicyWatermarkConfig` wrapping the watermark settings with the flatten checkbox gated off. - **Flatten-to-image on by default for redact + watermark** — both normalise `convertPDFToImage: true` on mount. - **Self-heal a stale backing folder** — `ensurePolicyFolder` recreates a backing folder whose `folderId` no longer resolves (preferring the backend's stored automation), instead of hanging Edit Settings on a permanent "Loading…". - **Version the input file on 'new version' output mode** — completed runs whose policy output mode is `new_version` replace the input file with a versioned child (origin tool `automate`) rather than adding a separate file; falls back to a new file if the input is gone. `outputMode` is plumbed through `PolicyState`, the local-cache default, and backend reconciliation. ## Verification - `typecheck:proprietary` + `typecheck:core` clean - policy + hooks vitest: 17 passing - eslint + prettier clean on all changed files
116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
/**
|
|
* Local cache + offline fallback for Policies — the backend (/api/v1/policies)
|
|
* is the source of truth. Holds per-category state (configured/active/paused,
|
|
* sources, scope, reviewer, field overrides) and broadcasts changes so hooks
|
|
* re-render. Mirrors the change-event pattern of the automation/folder stores.
|
|
*/
|
|
|
|
import { loadPolicyCatalog } from "@app/services/policyCatalog";
|
|
import type { PoliciesByCategory, PolicyState } from "@app/types/policies";
|
|
|
|
const STORAGE_KEY = "stirling-policies-state";
|
|
export const POLICIES_CHANGE_EVENT = "stirling:policies-changed";
|
|
|
|
function defaultState(): PolicyState {
|
|
// Unconfigured by default. The backend is the source of truth for what's
|
|
// actually configured + active; this is just the empty local-cache shape.
|
|
return {
|
|
configured: false,
|
|
status: "default",
|
|
sources: ["editor"],
|
|
scopeTypes: [],
|
|
// Empty by default; the wizard defaults the reviewer to the signed-in user.
|
|
reviewerEmail: "",
|
|
fieldValues: {},
|
|
// Default to versioning the input file rather than spawning a separate one.
|
|
outputMode: "new_version",
|
|
// No rename by default — the output keeps the input's filename.
|
|
outputName: "",
|
|
// Every catalog category is a shipped, built-in policy → default (not
|
|
// deletable). User-created policies (later) will set this false.
|
|
isDefault: true,
|
|
};
|
|
}
|
|
|
|
/** An obsolete reviewer email from earlier development, scrubbed from persisted
|
|
* state on read so it can re-default to the real signed-in user. */
|
|
const STALE_REVIEWER_EMAIL = "[email protected]";
|
|
|
|
/** Read the full policy state, seeding + healing any missing categories. */
|
|
export function loadPolicies(): PoliciesByCategory {
|
|
let parsed: Partial<PoliciesByCategory> = {};
|
|
try {
|
|
const raw =
|
|
typeof localStorage !== "undefined"
|
|
? localStorage.getItem(STORAGE_KEY)
|
|
: null;
|
|
if (raw) parsed = JSON.parse(raw) as Partial<PoliciesByCategory>;
|
|
} catch {
|
|
// Corrupt/unavailable storage — fall back to seed.
|
|
}
|
|
// Always reconcile against the current category list so a newly-added
|
|
// category gets a default rather than being undefined.
|
|
const out: PoliciesByCategory = {};
|
|
for (const cat of loadPolicyCatalog().categories) {
|
|
const merged = { ...defaultState(), ...(parsed[cat.id] ?? {}) };
|
|
// Migration: clear the obsolete persisted reviewer email so it re-defaults
|
|
// to the real signed-in user.
|
|
if (merged.reviewerEmail === STALE_REVIEWER_EMAIL)
|
|
merged.reviewerEmail = "";
|
|
out[cat.id] = merged;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function persist(state: PoliciesByCategory): void {
|
|
try {
|
|
if (typeof localStorage !== "undefined") {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
}
|
|
} catch {
|
|
// Best-effort; ignore quota/availability failures.
|
|
}
|
|
if (typeof window !== "undefined") {
|
|
window.dispatchEvent(new CustomEvent(POLICIES_CHANGE_EVENT));
|
|
}
|
|
}
|
|
|
|
/** Merge a partial update into one category's state and persist. */
|
|
export function updatePolicy(
|
|
categoryId: string,
|
|
patch: Partial<PolicyState>,
|
|
): PoliciesByCategory {
|
|
const current = loadPolicies();
|
|
const next: PoliciesByCategory = {
|
|
...current,
|
|
// Fall back to defaults so a not-yet-seeded category id still yields a
|
|
// complete PolicyState rather than a partial.
|
|
[categoryId]: {
|
|
...defaultState(),
|
|
...current[categoryId],
|
|
...patch,
|
|
},
|
|
};
|
|
persist(next);
|
|
return next;
|
|
}
|
|
|
|
/** Reset a category to its unconfigured default (the "Delete policy" action). */
|
|
export function resetPolicy(categoryId: string): PoliciesByCategory {
|
|
return updatePolicy(categoryId, {
|
|
...defaultState(),
|
|
configured: false,
|
|
status: "default",
|
|
// Drop the backing-folder + backend links (the caller deletes those).
|
|
folderId: undefined,
|
|
backendId: undefined,
|
|
});
|
|
}
|
|
|
|
/** Subscribe to policy-state changes (same-tab). Returns an unsubscribe fn. */
|
|
export function onPoliciesChange(cb: () => void): () => void {
|
|
if (typeof window === "undefined") return () => {};
|
|
window.addEventListener(POLICIES_CHANGE_EVENT, cb);
|
|
return () => window.removeEventListener(POLICIES_CHANGE_EVENT, cb);
|
|
}
|