From a5ee329c365397df7404ba3f01957f214c17ac16 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 9 Jul 2026 16:43:38 +0100 Subject: [PATCH] Further improvements to policies file tracking (#6941) # Description of Changes Fixes requested in review of #6903 --- .../policy/ledger/ProcessedFileEntity.java | 11 +- .../saas/V32__policy_processed_files.sql | 6 + .../public/locales/en-US/translation.toml | 6 + .../components/policies/PolicyDetailPanel.tsx | 380 ++++++++++-------- 4 files changed, 233 insertions(+), 170 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/ledger/ProcessedFileEntity.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/ledger/ProcessedFileEntity.java index b05c8b1eec..2e98025aff 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/ledger/ProcessedFileEntity.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/ledger/ProcessedFileEntity.java @@ -10,6 +10,7 @@ import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.IdClass; +import jakarta.persistence.Index; import jakarta.persistence.Table; import jakarta.persistence.Transient; @@ -25,7 +26,15 @@ import lombok.Setter; * violation rather than a silent merge. */ @Entity -@Table(name = "policy_processed_files") +@Table( + name = "policy_processed_files", + indexes = { + // presence cleanup: delete this policy's rows unseen since the sweep began + @Index(name = "idx_processed_files_policy_seen", columnList = "policy_id, last_seen"), + // cross-policy deletion consensus: existsByIdentityHashAndStatusNot filters + // identity_hash on its own, so it cannot ride the (policy_id, identity_hash) PK + @Index(name = "idx_processed_files_identity", columnList = "identity_hash") + }) @IdClass(ProcessedFileId.class) @NoArgsConstructor @Getter diff --git a/app/saas/src/main/resources/db/migration/saas/V32__policy_processed_files.sql b/app/saas/src/main/resources/db/migration/saas/V32__policy_processed_files.sql index f3a81de753..4be3fa07d8 100644 --- a/app/saas/src/main/resources/db/migration/saas/V32__policy_processed_files.sql +++ b/app/saas/src/main/resources/db/migration/saas/V32__policy_processed_files.sql @@ -28,3 +28,9 @@ CREATE TABLE IF NOT EXISTS policy_processed_files ( CREATE INDEX IF NOT EXISTS idx_processed_files_policy_seen ON policy_processed_files (policy_id, last_seen); + +-- The cross-policy deletion-consensus check (existsByIdentityHashAndStatusNot) filters identity_hash +-- alone, so it cannot use the (policy_id, identity_hash) primary key; it runs once per successfully +-- consumed file, so index it to avoid a full scan on the hot path. +CREATE INDEX IF NOT EXISTS idx_processed_files_identity + ON policy_processed_files (identity_hash); diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 6228914b66..1cc2ad8ff1 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -7511,6 +7511,12 @@ pause = "Pause" resume = "Resume" runNow = "Run now" +[portal.policies.detail.clearHistory] +body = "This policy will forget every file it has already processed and reprocess everything currently in its sources on the next run. The files themselves are not changed. This cannot be undone." +cancel = "Cancel" +confirm = "Clear history" +title = "Clear processed history?" + [portal.policies.detail.emptyActivity] description = "Documents will appear here once this policy runs." title = "No activity yet" diff --git a/frontend/editor/src/portal/components/policies/PolicyDetailPanel.tsx b/frontend/editor/src/portal/components/policies/PolicyDetailPanel.tsx index f8da1226b0..e198fd92c8 100644 --- a/frontend/editor/src/portal/components/policies/PolicyDetailPanel.tsx +++ b/frontend/editor/src/portal/components/policies/PolicyDetailPanel.tsx @@ -111,6 +111,7 @@ export function PolicyDetailPanel({ onRetry, }: PolicyDetailPanelProps) { const { t } = useTranslation(); + const [confirmingClear, setConfirmingClear] = useState(false); if (!policy) return null; const { category, config, state, steps, stats, activity } = policy; const isPaused = state.status === "paused"; @@ -136,200 +137,241 @@ export function PolicyDetailPanel({ } return ( - - {canDelete && ( - - )} - {onRun && ( + <> + + {canDelete && ( + + )} + {onRun && ( + + )} + {canClearHistory && ( + + )} - )} - {canClearHistory && ( - - )} - + + } + > + {/* Status + trigger strip */} +
+ {isPaused - ? t("portal.policies.detail.actions.resume") - : t("portal.policies.detail.actions.pause")} - - + ? t("portal.policies.status.paused") + : t("portal.policies.status.active")} + + {hasEditorSource && ( + <> + + · + + {trigger} + + · + + + {outputLabel} + + + )}
- } - > - {/* Status + trigger strip */} -
- - {isPaused - ? t("portal.policies.status.paused") - : t("portal.policies.status.active")} - - {hasEditorSource && ( - <> - - · - - {trigger} - - · - - {outputLabel} - - )} -
- {/* Enforces — plain text, no pills */} -
- - {t("portal.policies.detail.enforces")} - - - {enforceItems - ? enforceItems.map((op, i) => ( - - {i > 0 && ( - - {" "} - →{" "} - - )} - {humanizeEndpoint(op, t)} - - )) - : config.rules.map((r) => t(r)).join(" · ")} - -
- - {/* Sources */} - {state.sources.length > 0 && ( + {/* Enforces — plain text, no pills */}
- {t("portal.policies.detail.sources")} + {t("portal.policies.detail.enforces")} - {state.sources.map(sourceLabel).join(" · ")} + {enforceItems + ? enforceItems.map((op, i) => ( + + {i > 0 && ( + + {" "} + →{" "} + + )} + {humanizeEndpoint(op, t)} + + )) + : config.rules.map((r) => t(r)).join(" · ")}
- )} -

- {t("portal.policies.detail.recentActivity")} -

+ {/* Sources */} + {state.sources.length > 0 && ( +
+ + {t("portal.policies.detail.sources")} + + + {state.sources.map(sourceLabel).join(" · ")} + +
+ )} - {activity.length > 0 ? ( - - {activity.map((item, i) => ( -
- + {t("portal.policies.detail.recentActivity")} + + + {activity.length > 0 ? ( + + {activity.map((item, i) => ( +
- {item.status === "flagged" ? ( - - ) : item.status === "processing" ? ( - - ) : ( - - )} - - - - {item.doc} - - + {item.status === "flagged" ? ( - + + ) : item.status === "processing" ? ( + ) : ( - item.action + )} - - - {item.time} - - {item.status === "flagged" && onRetry && ( - + + + {item.doc} + + + {item.status === "flagged" ? ( + + ) : ( + item.action + )} + + + + {item.time} + + {item.status === "flagged" && onRetry && ( + + )} +
+ ))} +
+ ) : ( + + - ))} - - ) : ( - - + + )} + + + + + - )} - - - - - - - + + setConfirmingClear(false)} + width="sm" + title={t("portal.policies.detail.clearHistory.title")} + footer={ +
+ + +
+ } + > + {t("portal.policies.detail.clearHistory.body")} +
+ ); }