Skip to content
Open
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
12 changes: 12 additions & 0 deletions .changeset/restore-policy-backfill-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"dashboard": minor
"server": minor
---

Restore and rework the run controls in the risk-policy progress sheet so they communicate **mode** (extend vs re-analyze) separately from **scope** (recent 1,000 / custom / all).

- **Scope** picker: radio with "Recent 1,000 messages" (default), "Custom amount" (numeric input), "All messages".
- **Primary action** "Analyze new messages" — only scans messages with no analysis at the current policy version. Safe / cheap / repeatable; pressing it twice does not re-do work already done.
- **Secondary action** "Re-analyze" dropdown — bumps the policy version and re-scans the same scope. Explicit, expensive, hidden behind a click.

Backend: `triggerRiskAnalysis` now accepts `reanalyze` (default `false`). With `reanalyze=false` the workflow is signalled without bumping `risk_policy_version`, so the drain picks up only unanalyzed messages. With `reanalyze=true` the existing bump-and-redo behavior is preserved for explicit re-runs.
8 changes: 6 additions & 2 deletions .speakeasy/out.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18778,7 +18778,7 @@ paths:
x-speakeasy-name-override: status
/rpc/risk.policies.trigger:
post:
description: Manually trigger risk analysis for a policy, starting or signaling the drain workflow. Defaults to the most recent 100 unanalyzed messages; pass `limit=0` to backfill every unanalyzed message.
description: Manually trigger risk analysis for a policy. Defaults to extending analysis to the most recent 100 messages not yet analyzed at the current policy version; pass `limit=0` to cover every message in scope. Pass `reanalyze=true` to bump the policy version and re-scan messages already analyzed at the current version (e.g. after a rule change).
operationId: triggerRiskAnalysis
parameters:
- allowEmptyValue: true
Expand Down Expand Up @@ -36220,10 +36220,14 @@ components:
format: uuid
limit:
type: integer
description: Cap the backfill at the most recent N unanalyzed messages. Defaults to 100 (the recent-N drain budget). Pass 0 to request a full backfill of every unanalyzed message.
description: Cap the run at the most recent N messages. Defaults to 100 (the recent-N drain budget). Pass 0 to request every message in scope.
default: 100
format: int32
minimum: 0
reanalyze:
type: boolean
description: When true, bump the policy version so messages already analyzed at the current version are re-scanned. When false (default), only messages with no analysis at the current version are scanned.
default: false
required:
- id
UpdateAssistantForm:
Expand Down
135 changes: 99 additions & 36 deletions client/dashboard/src/pages/security/PolicyCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
Shield,
Ellipsis,
Loader2,
ChevronDown,
ChevronRight,
RefreshCw,
} from "lucide-react";
Expand Down Expand Up @@ -273,9 +274,9 @@ function PolicyCenterContent() {
deleteMutation.mutate({ request: { id } });
};

const handleTrigger = (id: string) => {
const handleTrigger = (id: string, limit?: number, reanalyze?: boolean) => {
triggerMutation.mutate({
request: { triggerRiskAnalysisRequestBody: { id } },
request: { triggerRiskAnalysisRequestBody: { id, limit, reanalyze } },
});
};

Expand Down Expand Up @@ -385,7 +386,6 @@ function PolicyCenterContent() {
<TableHead>Name</TableHead>
<TableHead>Action</TableHead>
<TableHead>Categories</TableHead>
<TableHead>Progress</TableHead>
<TableHead>Status</TableHead>
<TableHead className="w-[60px]" />
</TableRow>
Expand Down Expand Up @@ -417,16 +417,6 @@ function PolicyCenterContent() {
))}
</div>
</TableCell>
<TableCell>
{policy.pendingMessages > 0 ? (
<span className="text-muted-foreground text-xs">
{policy.totalMessages - policy.pendingMessages}/
{policy.totalMessages} analyzed
</span>
) : (
<Badge variant="secondary">Complete</Badge>
)}
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
<Switch
checked={policy.enabled}
Expand Down Expand Up @@ -534,7 +524,9 @@ function PolicyCenterContent() {
{runPanelPolicy && (
<RunPanel
policy={runPanelPolicy}
onTrigger={() => handleTrigger(runPanelPolicy.id)}
onTrigger={(limit, reanalyze) =>
handleTrigger(runPanelPolicy.id, limit, reanalyze)
}
isTriggerPending={triggerMutation.isPending}
/>
)}
Expand Down Expand Up @@ -794,17 +786,6 @@ function PolicySheetBody({
/>
</div>
)}

{/* Enabled toggle */}
<div className="flex items-center justify-between">
<div>
<Label className="text-sm font-medium">Enabled</Label>
<p className="text-muted-foreground text-xs">
Enable this policy to begin scanning messages.
</p>
</div>
<Switch checked={formEnabled} onCheckedChange={setFormEnabled} />
</div>
</div>
);
}
Expand All @@ -813,15 +794,32 @@ function PolicySheetBody({
/* RunPanel */
/* -------------------------------------------------------------------------- */

type BackfillMode = "recent" | "custom" | "all";

function scopeToLimit(mode: BackfillMode, customLimit: string): number | null {
if (mode === "recent") return 1000;
if (mode === "all") return 0;
const n = parseInt(customLimit, 10);
return Number.isFinite(n) && n > 0 ? n : null;
}

function scopeLabel(mode: BackfillMode, customLimit: string): string {
if (mode === "all") return "all messages";
const n = mode === "recent" ? 1000 : parseInt(customLimit, 10);
return `${(Number.isFinite(n) ? n : 0).toLocaleString()} recent messages`;
}

function RunPanel({
policy,
onTrigger,
isTriggerPending,
}: {
policy: RiskPolicy;
onTrigger: () => void;
onTrigger: (limit?: number, reanalyze?: boolean) => void;
isTriggerPending: boolean;
}) {
const [backfillMode, setBackfillMode] = useState<BackfillMode>("recent");
const [customLimit, setCustomLimit] = useState<string>("1000");
const {
data: status,
isLoading,
Expand Down Expand Up @@ -940,17 +938,82 @@ function RunPanel({
) : null}
</div>

<SheetFooter className="border-border border-t px-6 py-4">
<Button
onClick={onTrigger}
disabled={isTriggerPending}
className="w-full"
<SheetFooter className="border-border flex-col gap-3 border-t px-6 py-4">
<RadioGroup
value={backfillMode}
onValueChange={(v) => setBackfillMode(v as BackfillMode)}
className="gap-2"
>
{isTriggerPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Trigger Analysis
</Button>
<label className="flex cursor-pointer items-center gap-2 text-sm">
<RadioGroupItem value="recent" id="backfill-recent" />
Recent 1,000 messages
</label>
<label className="flex cursor-pointer items-center gap-2 text-sm">
<RadioGroupItem value="custom" id="backfill-custom" />
Custom amount
<Input
type="number"
min={1}
value={customLimit}
onChange={(value) => setCustomLimit(value)}
onFocus={() => setBackfillMode("custom")}
disabled={isTriggerPending}
className="ml-2 w-28"
aria-label="Custom backfill limit"
/>
</label>
<label className="flex cursor-pointer items-center gap-2 text-sm">
<RadioGroupItem value="all" id="backfill-all" />
All messages
</label>
</RadioGroup>
<p className="text-muted-foreground text-xs">
Skips messages already analyzed at this policy version. Use{" "}
<em>Re-analyze</em> to re-scan them.
</p>
<div className="flex gap-2">
<Button
onClick={() => {
const n = scopeToLimit(backfillMode, customLimit);
if (n !== null) onTrigger(n, false);
}}
disabled={
isTriggerPending ||
scopeToLimit(backfillMode, customLimit) === null
}
className="flex-1"
>
{isTriggerPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Analyze new messages
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
disabled={
isTriggerPending ||
scopeToLimit(backfillMode, customLimit) === null
}
>
Re-analyze
<ChevronDown className="ml-1 h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="cursor-pointer"
onSelect={() => {
const n = scopeToLimit(backfillMode, customLimit);
if (n !== null) onTrigger(n, true);
}}
>
Re-analyze {scopeLabel(backfillMode, customLimit)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</SheetFooter>
</>
);
Expand Down
16 changes: 8 additions & 8 deletions client/sdk/.speakeasy/gen.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/sdk/src/funcs/riskPoliciesTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { Result } from "../types/fp.js";
* triggerRiskAnalysis risk
*
* @remarks
* Manually trigger risk analysis for a policy, starting or signaling the drain workflow. Defaults to the most recent 100 unanalyzed messages; pass `limit=0` to backfill every unanalyzed message.
* Manually trigger risk analysis for a policy. Defaults to extending analysis to the most recent 100 messages not yet analyzed at the current policy version; pass `limit=0` to cover every message in scope. Pass `reanalyze=true` to bump the policy version and re-scan messages already analyzed at the current version (e.g. after a rule change).
*/
export function riskPoliciesTrigger(
client: GramCore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@ export type TriggerRiskAnalysisRequestBody = {
*/
id: string;
/**
* Cap the backfill at the most recent N unanalyzed messages. Defaults to 100 (the recent-N drain budget). Pass 0 to request a full backfill of every unanalyzed message.
* Cap the run at the most recent N messages. Defaults to 100 (the recent-N drain budget). Pass 0 to request every message in scope.
*/
limit?: number | undefined;
/**
* When true, bump the policy version so messages already analyzed at the current version are re-scanned. When false (default), only messages with no analysis at the current version are scanned.
*/
reanalyze?: boolean | undefined;
};

/** @internal */
export type TriggerRiskAnalysisRequestBody$Outbound = {
id: string;
limit: number;
reanalyze: boolean;
};

/** @internal */
Expand All @@ -28,6 +33,7 @@ export const TriggerRiskAnalysisRequestBody$outboundSchema: z.ZodMiniType<
> = z.object({
id: z.string(),
limit: z._default(z.int(), 100),
reanalyze: z._default(z.boolean(), false),
});

export function triggerRiskAnalysisRequestBodyToJSON(
Expand Down
2 changes: 1 addition & 1 deletion client/sdk/src/react-query/riskPoliciesTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export type RiskPoliciesTriggerMutationError =
* triggerRiskAnalysis risk
*
* @remarks
* Manually trigger risk analysis for a policy, starting or signaling the drain workflow. Defaults to the most recent 100 unanalyzed messages; pass `limit=0` to backfill every unanalyzed message.
* Manually trigger risk analysis for a policy. Defaults to extending analysis to the most recent 100 messages not yet analyzed at the current policy version; pass `limit=0` to cover every message in scope. Pass `reanalyze=true` to bump the policy version and re-scan messages already analyzed at the current version (e.g. after a rule change).
*/
export function useRiskPoliciesTriggerMutation(
options?: MutationHookOptions<
Expand Down
2 changes: 1 addition & 1 deletion client/sdk/src/sdk/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export class Policies extends ClientSDK {
* triggerRiskAnalysis risk
*
* @remarks
* Manually trigger risk analysis for a policy, starting or signaling the drain workflow. Defaults to the most recent 100 unanalyzed messages; pass `limit=0` to backfill every unanalyzed message.
* Manually trigger risk analysis for a policy. Defaults to extending analysis to the most recent 100 messages not yet analyzed at the current policy version; pass `limit=0` to cover every message in scope. Pass `reanalyze=true` to bump the policy version and re-scan messages already analyzed at the current version (e.g. after a rule change).
*/
async trigger(
request: operations.TriggerRiskAnalysisRequest,
Expand Down
7 changes: 5 additions & 2 deletions server/design/risk/design.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ var _ = Service("risk", func() {
})

Method("triggerRiskAnalysis", func() {
Description("Manually trigger risk analysis for a policy, starting or signaling the drain workflow. Defaults to the most recent 100 unanalyzed messages; pass `limit=0` to backfill every unanalyzed message.")
Description("Manually trigger risk analysis for a policy. Defaults to extending analysis to the most recent 100 messages not yet analyzed at the current policy version; pass `limit=0` to cover every message in scope. Pass `reanalyze=true` to bump the policy version and re-scan messages already analyzed at the current version (e.g. after a rule change).")

Payload(func() {
security.ByKeyPayload()
Expand All @@ -395,10 +395,13 @@ var _ = Service("risk", func() {
Attribute("id", String, "The policy ID.", func() {
Format(FormatUUID)
})
Attribute("limit", Int32, "Cap the backfill at the most recent N unanalyzed messages. Defaults to 100 (the recent-N drain budget). Pass 0 to request a full backfill of every unanalyzed message.", func() {
Attribute("limit", Int32, "Cap the run at the most recent N messages. Defaults to 100 (the recent-N drain budget). Pass 0 to request every message in scope.", func() {
Minimum(0)
Default(100)
})
Attribute("reanalyze", Boolean, "When true, bump the policy version so messages already analyzed at the current version are re-scanned. When false (default), only messages with no analysis at the current version are scanned.", func() {
Default(false)
})
Required("id")
})

Expand Down
Loading
Loading