docs(22): capture phase context
This commit is contained in:
parent
ec7c2114bf
commit
47c3510667
2 changed files with 423 additions and 0 deletions
|
|
@ -0,0 +1,310 @@
|
|||
# Phase 22: Approval UI (LiveLink) - Context
|
||||
|
||||
**Gathered:** 2026-07-16
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
A security operator opens an Autotask ticket, clicks a LiveLink button, and
|
||||
lands on a stable, ticket-ID-addressable Pulse page (`/phishing/tickets/
|
||||
{ticketId}`) that resolves the ticket to its campaign and shows that
|
||||
campaign's timeline, gathered evidence, and current classification — with
|
||||
approve/remediate/mark-false-positive actions right there, calling the
|
||||
existing Phase 20 APIs directly. Authenticated via the existing Better Auth
|
||||
session only, no separate token/query-param auth scheme.
|
||||
|
||||
By explicit user decision during this discussion (see Scope Expansion below),
|
||||
this phase ALSO includes a minimal `/phishing` campaigns list page + a nav
|
||||
entry, so the review page has a discoverable entry point beyond LiveLink.
|
||||
This is additive scope beyond the literal REVIEW-01..06 wording — planner
|
||||
should add it as its own plan/success-criterion, not silently fold it into
|
||||
the ticket-scoped page's work.
|
||||
|
||||
Does NOT cover: actually wiring a real remediation provider (that's
|
||||
REMEDEXEC-01..05, v2 — Phase 20's simulated-effect model stays as-is), any
|
||||
change to the classification engine (Phase 19) or remediation/audit APIs
|
||||
(Phase 20) themselves, or the Autotask triage note (Phase 21, independent and
|
||||
already complete).
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Scope Expansion — Campaigns List Page
|
||||
- **D-00:** The user explicitly chose to expand this phase's scope beyond
|
||||
REVIEW-01..06: in addition to the ticket-ID-scoped review page, this phase
|
||||
also builds a minimal `/phishing` campaigns list page and adds a nav entry
|
||||
pointing to it. Rationale surfaced during discussion: a nav entry needs
|
||||
something to point to, and the ticket-scoped page alone isn't
|
||||
browsable/discoverable without already knowing a ticket ID. This was
|
||||
flagged as scope creep during discussion and the user confirmed they want
|
||||
it included now rather than deferred. Planner should treat this as an
|
||||
additional deliverable with its own success criterion, distinct from
|
||||
REVIEW-01..06's ticket-scoped page. The list page can be genuinely minimal —
|
||||
no new requirement beyond "operator can see and click into recent
|
||||
campaigns" — it should reuse `GET /api/phishing/campaigns` (Phase 18,
|
||||
already exists, no new endpoint needed).
|
||||
|
||||
### Page Chrome & Navigation
|
||||
- **D-01:** The review page (and the new list page) use Pulse's full
|
||||
navigation shell — top `AppNavigation`, `PageHeader`, standard
|
||||
`container mx-auto px-6 py-6` layout — same chrome as `/admin`, `/analyzer`.
|
||||
Rejected the standalone/`/mobile`-`/kiosk`-style no-nav-chrome pattern:
|
||||
this is a page staff may also navigate to directly (via the new list page),
|
||||
not exclusively a LiveLink drop-in.
|
||||
- **D-02:** A nav entry is added (exact placement — top-level nav item vs.
|
||||
a sub-item under an existing menu — is planner's call per DESIGN.md's
|
||||
existing nav IA conventions).
|
||||
|
||||
### Approve/Remediate Action UX
|
||||
- **D-03:** The operator selects which recommended action(s) to approve via
|
||||
a checkbox list (one checkbox per `recommendedActions` entry from the
|
||||
latest classification), with each action's default params visible and
|
||||
editable inline. A single "Approve selected" button submits all checked
|
||||
actions in one `POST /approve` call with the (possibly overridden) params
|
||||
per action — matching `ApproveActionInput { actionType, params? }`
|
||||
(`remediation-service.ts`) exactly. No blanket "approve everything" button.
|
||||
- **D-04:** After any of approve/remediate/mark-false-positive succeeds, the
|
||||
page refetches the campaign detail endpoint and re-renders from the fresh
|
||||
response — no optimistic local state mutation. Matches the project's
|
||||
no-SWR/react-query local-`fetch()` convention (CLAUDE.md).
|
||||
- **D-05:** Once a campaign has reached a resolved state (any
|
||||
`remediation_actions` row `completed`, or marked false positive), the
|
||||
approve/remediate/mark-false-positive buttons remain visible in the layout
|
||||
but are disabled with a tooltip/inline explanation (e.g. "Already
|
||||
remediated on {date} by {approver}") — not removed from the DOM. Keeps the
|
||||
page layout stable across all campaign states rather than reflowing.
|
||||
|
||||
### Permission Gating (carried forward, not re-discussed)
|
||||
- **D-06:** Button disabled/hidden state for approve/remediate/
|
||||
mark-false-positive uses the EXACT same check the API enforces —
|
||||
`hasPermission(session.user.role, 'phishing', 'approve' | 'remediate')`
|
||||
from `lib/permissions.ts`, called client-side via `useSession()`
|
||||
(`lib/auth-client.ts`). This is a plain, DB-free function already safe to
|
||||
import client-side — no new permission surface, no relaxed/separate model,
|
||||
directly satisfying REVIEW-06's "never uses a relaxed or separate
|
||||
permission check" requirement.
|
||||
|
||||
### Not-Yet-Triaged / Ungrouped Handling
|
||||
- **D-07:** If a ticket ID has no `reports` row yet (Phase 15's scanner/
|
||||
on-demand analyze hasn't processed it — a real possibility since LiveLink
|
||||
can be clicked immediately after ticket creation), the page renders a
|
||||
friendly empty state ("Not yet triaged") with a button that calls the
|
||||
existing `POST /api/phishing/tickets/{ticket_id}/analyze` (Phase 18) right
|
||||
there — turning what would otherwise be a dead end into a one-click fix.
|
||||
Not a 404.
|
||||
- **D-08:** If a `reports` row exists but `campaign_id` is still null (the
|
||||
narrow race window before Phase 18's grouping runs), the page renders that
|
||||
report's own evidence standalone, with a note that campaign
|
||||
grouping/classification hasn't completed yet — it does not crash or fall
|
||||
back to the same empty state as D-07 (there IS evidence to show, just no
|
||||
campaign wrapper yet).
|
||||
|
||||
### Evidence Rendering Safety
|
||||
- **D-09:** Extracted URLs (from Phase 16's `indicators`/`messages.urls`) are
|
||||
rendered as **inert, non-clickable text** — plain monospace with a
|
||||
copy-to-clipboard affordance, never an `<a href>`. This is a stricter
|
||||
posture than the Phase 21 triage note's URL sanitization (which strips
|
||||
query strings but still describes URLs in prose); here, on an interactive
|
||||
page an operator might actually click, the decision is to remove all click
|
||||
risk entirely rather than rely on sanitization alone.
|
||||
- **D-10:** Attachment metadata (filename, content-type, size, hash — per
|
||||
EVID-01/EVID-04, no file content is ever stored) is displayed as plain
|
||||
evidence, as-is, no extra redaction — consistent with Phase 21's precedent
|
||||
of treating hashes/filenames as evidence, not secrets requiring redaction.
|
||||
|
||||
### Claude's Discretion (explicitly deferred to research + planner)
|
||||
- Exact nav placement for the new list page's entry (top-level item vs.
|
||||
nested under an existing menu) — per DESIGN.md's nav IA table, planner's
|
||||
call.
|
||||
- Whether the campaign-detail data comes from extending the existing
|
||||
`GET /api/phishing/campaigns/[id]` (Phase 18) in place, or a new
|
||||
page-specific endpoint — the existing endpoint currently omits
|
||||
`remediation_actions`, `audit_events`, full message evidence (headers/
|
||||
URLs/attachments/body_preview), and full classification fields (`reasons`/
|
||||
`recommended_actions`/`requires_approval`); research/planner's call which
|
||||
approach fits better, as long as no other consumer of the existing
|
||||
minimal shape breaks.
|
||||
- A new ticket→campaign resolution path is needed for
|
||||
`/phishing/tickets/{ticketId}` (no such lookup exists today) — whether
|
||||
this is a new API route (e.g. `GET /api/phishing/tickets/{ticket_id}/
|
||||
campaign`) that the page calls, or a direct server-side lookup — planner's
|
||||
call, but must go through the API-route pattern per CLAUDE.md ("no
|
||||
`'use server'` actions... everything is API routes called via fetch").
|
||||
- Blast-radius freshness: whether the review page calls `getBlastRadius()`
|
||||
fresh on each page load (matches the Phase 21 triage-note precedent,
|
||||
costs an extra Mimecast API round-trip per view) or reads whatever the
|
||||
most recent `classifications` row already persisted in `reasons` (cheaper,
|
||||
possibly stale) — no strong preference surfaced, planner's call.
|
||||
- Exact timeline merge/sort implementation for REVIEW-02 (reports +
|
||||
classification history + audit events, chronological) — a UI/data-shape
|
||||
detail, not a user-vision question.
|
||||
- Exact visual treatment of the timeline (vertical timeline component vs.
|
||||
simple ordered list) — no reference example given, planner's call as long
|
||||
as chronological order and all three event sources are represented.
|
||||
- Exact wording/layout of the minimal campaigns list page (columns shown,
|
||||
filters if any) — no requirement drives this beyond "operator can see and
|
||||
click into recent campaigns"; planner's call, should stay minimal per D-00.
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Requirements & roadmap
|
||||
- `.planning/REQUIREMENTS.md` — REVIEW-01..06 (full text), traceability table
|
||||
showing all six mapped to Phase 22, Pending.
|
||||
- `.planning/ROADMAP.md` §"Phase 22: Approval UI (LiveLink)" — goal, depends-on
|
||||
(Phase 19 + Phase 20 only), 6 success criteria, `UI hint: yes`.
|
||||
|
||||
### Existing API surface (this phase wires the UI to these — reuse, don't duplicate)
|
||||
- `app/api/phishing/campaigns/[id]/route.ts` — current `GET` shape (Phase 18);
|
||||
does NOT yet include `remediation_actions`, `audit_events`, full message/
|
||||
indicator evidence, or full classification fields — likely needs extending
|
||||
(see Claude's Discretion).
|
||||
- `app/api/phishing/campaigns/[id]/approve/route.ts` — `POST` body
|
||||
`{ actions: ApproveActionInput[] }`, gated `requirePermission('phishing',
|
||||
'approve')`, returns `ApprovedRemediationAction[]`.
|
||||
- `app/api/phishing/campaigns/[id]/remediate/route.ts` — `POST`, no body,
|
||||
gated `requirePermission('phishing', 'remediate')`, idempotent, returns
|
||||
`RemediateResult`.
|
||||
- `app/api/phishing/campaigns/[id]/mark-false-positive/route.ts` — `POST`,
|
||||
optional `{ reason? }` body, gated `requirePermission('phishing',
|
||||
'approve')`, blocked (409) if approved/completed remediation exists
|
||||
(Phase 20 D-04).
|
||||
- `app/api/phishing/campaigns/[id]/triage-note/route.ts` — Phase 21, not
|
||||
directly used by this page per ROADMAP (separate concern) but same
|
||||
campaign-scoped route-shape precedent.
|
||||
- `app/api/phishing/tickets/[ticket_id]/analyze/route.ts` — Phase 18
|
||||
on-demand analyze; this is what D-07's empty-state "analyze now" button
|
||||
calls.
|
||||
- `app/api/phishing/campaigns/route.ts` — Phase 18 list endpoint; the new
|
||||
D-00 campaigns list page should reuse this, not build a new one.
|
||||
|
||||
### Schema (this phase reads; writes only via existing service functions)
|
||||
- `migrations/097_phishing_triage_schema.sql` — full 7-table shape
|
||||
(`campaigns`, `reports`, `messages`, `indicators`, `classifications`,
|
||||
`remediation_actions`, `audit_events`) — the page's evidence/timeline
|
||||
rendering must account for every column shape here.
|
||||
|
||||
### Service layer (read for exact request/response shapes)
|
||||
- `lib/services/remediation-service.ts` — `ApproveActionInput { actionType,
|
||||
params? }`, `ApprovedRemediationAction`, `RemediateResult`,
|
||||
`MarkFalsePositiveResult`, `RemediationValidationError`/
|
||||
`RemediationConflictError` — the UI's approve form must produce exactly
|
||||
this input shape.
|
||||
- `lib/services/mimecast-blast-radius.ts` — `getBlastRadius()`,
|
||||
`BlastRadiusResult` (`status: 'ok' | 'unavailable'`) — REVIEW-03's explicit
|
||||
`unavailable` state requirement.
|
||||
- `lib/services/triage-note-service.ts` — Phase 21 precedent for gathering a
|
||||
campaign's full current evidence (reports/classification/remediation/
|
||||
indicators join pattern) — useful reference for whatever endpoint this
|
||||
phase builds/extends.
|
||||
|
||||
### Auth/permissions (reused, not re-decided)
|
||||
- `lib/permissions.ts` — `phishing: ["read","analyze","approve","remediate"]`
|
||||
statement; `hasPermission(roleName, resource, action)` — plain function,
|
||||
safe to call client-side for D-06's button-gating.
|
||||
- `lib/auth-client.ts` — `useSession()` hook exposes `session.user.role`
|
||||
client-side.
|
||||
- `lib/auth-utils.ts` — `requirePermission(resource, action)` — the
|
||||
server-side check every action route already uses; the UI's client-side
|
||||
check (D-06) must mirror this, not diverge from it.
|
||||
|
||||
### UI conventions
|
||||
- `DESIGN.md` — nav IA table (role-gating hides items entirely, no
|
||||
greyed-out nav variants — precedent, though D-05 explicitly chooses the
|
||||
opposite for in-page action buttons, which is a different UI element than
|
||||
top nav); `AppNavigation` returns `null` under `/mobile` — precedent for a
|
||||
standalone-shell pattern that this phase explicitly did NOT choose (D-01).
|
||||
- `.planning/codebase/CONVENTIONS.md`, `STRUCTURE.md`, `STACK.md` — general
|
||||
component/styling/naming conventions (shadcn primitives, kebab-case files,
|
||||
camelCase API responses, `components/admin/DetailModal.tsx` card+tabs
|
||||
pattern as a possible reference for evidence display).
|
||||
|
||||
### Prior phase decisions (for consistency)
|
||||
- `.planning/phases/19-classification-engine/19-CONTEXT.md` — D-08 action
|
||||
vocabulary (7 types, destructive vs non-destructive), classification shape
|
||||
this page must render.
|
||||
- `.planning/phases/20-remediation-approval-audit-safety/20-CONTEXT.md` —
|
||||
D-01 simulated-effect model, D-02 permission grants, D-03 approval
|
||||
granularity (this page's approve UX directly implements D-03), D-04
|
||||
mark-false-positive state guard.
|
||||
- `.planning/phases/21-autotask-triage-note/21-CONTEXT.md` — evidence-
|
||||
gathering join pattern (`triage-note-service.ts`), sanitization precedent
|
||||
this phase's URL-safety decision (D-09) deliberately goes further than.
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `lib/permissions.ts`'s `hasPermission()` — directly reusable client-side
|
||||
for D-06's button gating, no new permission-check code needed.
|
||||
- `lib/services/triage-note-service.ts`'s evidence-gathering SQL joins
|
||||
(reports → messages → indicators, classifications, remediation_actions) —
|
||||
the query pattern this phase's data-fetching should mirror.
|
||||
- `components/admin/DetailModal.tsx` — card + tabs (formatted/raw) pattern;
|
||||
potential reference for organizing evidence/timeline/classification into
|
||||
sections on the review page, though this is a full page not a modal.
|
||||
- `app/api/phishing/campaigns/route.ts` — existing list endpoint, reusable
|
||||
as-is for the new D-00 list page (no new endpoint needed for that surface).
|
||||
|
||||
### Established Patterns
|
||||
- `requirePermission(resource, action)` early-return pattern — every new
|
||||
route this phase might add (e.g. a ticket→campaign resolver) should follow
|
||||
this Phase 18 D-06 convention.
|
||||
- Client pages are `'use client'` + `useState`/`useEffect`/`fetch()` — no
|
||||
SWR/react-query, matches D-04's refetch-after-action approach.
|
||||
- `sonner` toasts for action feedback (`toast.success()`/`toast.error()`) —
|
||||
should wrap approve/remediate/mark-false-positive calls.
|
||||
|
||||
### Integration Points
|
||||
- New review page reads: campaign detail (extended or new endpoint per
|
||||
Claude's Discretion), and needs a ticket→campaign resolution step before
|
||||
that.
|
||||
- New review page writes: calls existing `/approve`, `/remediate`,
|
||||
`/mark-false-positive` endpoints directly — no new write logic, this
|
||||
phase is UI + read-endpoint-shaping only.
|
||||
- New list page reads: existing `GET /api/phishing/campaigns` (Phase 18),
|
||||
no new endpoint.
|
||||
- Nav: `components/navigation/app-navigation.tsx` gets a new entry pointing
|
||||
to the list page (D-02).
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
No literal visual reference/mockup was provided. The user's clearest,
|
||||
specific preference from this discussion is the URL-safety posture (D-09,
|
||||
inert copy-only text — explicitly stricter than "just sanitize the query
|
||||
string") and the mid-discussion scope decision to add a campaigns list page
|
||||
+ nav entry (D-00) rather than defer it.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- **Campaigns list page filtering/sorting beyond "recent"** — no requirement
|
||||
drives anything beyond a minimal browsable list (D-00); richer
|
||||
filtering/search is a future-phase concern if it ever comes up.
|
||||
- **Real remediation provider wiring** — unchanged from Phase 20, still
|
||||
`REMEDEXEC-01..05`, v2.
|
||||
- **False-positive reversibility** — unchanged from Phase 20's deferred item;
|
||||
not revisited here.
|
||||
|
||||
None beyond the above — discussion otherwise stayed within phase scope.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 22-approval-ui-livelink-addressable-campaign-review-and-approve*
|
||||
*Context gathered: 2026-07-16*
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# Phase 22: Approval UI (LiveLink) - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-07-16
|
||||
**Phase:** 22-approval-ui-livelink-addressable-campaign-review-and-approve
|
||||
**Areas discussed:** Page chrome & feel, Approve/remediate action UX, Not-yet-triaged empty state, URL evidence rendering safety
|
||||
|
||||
---
|
||||
|
||||
## Page chrome & feel
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Full Pulse shell | Top nav, PageHeader, standard container padding — same chrome as /admin, /analyzer | ✓ |
|
||||
| Standalone/focused (no top nav) | Distraction-free landing page, like /kiosk, /mobile | |
|
||||
|
||||
**User's choice:** Full Pulse shell.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| LiveLink only for now | No new nav link anywhere in Pulse | |
|
||||
| Also worth a nav entry | Add a lightweight link/section for staff to find recent campaigns | ✓ |
|
||||
|
||||
**User's choice:** Also worth a nav entry.
|
||||
**Notes:** This required a scope-boundary check — a nav entry needs a target page, and REVIEW-01..06 only scope the ticket-ID page.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Defer the list page (recommended) | Phase 22 stays scoped to ticket-ID page only; note campaigns-list+nav as a deferred idea | |
|
||||
| Add a minimal list page now | Expands Phase 22 scope to include a simple /phishing list page + nav link | ✓ |
|
||||
|
||||
**User's choice:** Add a minimal list page now — explicit scope expansion, confirmed after the tradeoff was surfaced.
|
||||
**Notes:** Captured as D-00 in CONTEXT.md with an explicit note to planner that this is additive scope, not part of REVIEW-01..06.
|
||||
|
||||
---
|
||||
|
||||
## Approve/remediate action UX
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Checkbox list + param editor | Each recommended action as a checkbox, editable default params, one "Approve selected" button | ✓ |
|
||||
| One-click buttons, no override | Each action gets its own "Approve" button using classifier defaults, no override UI | |
|
||||
|
||||
**User's choice:** Checkbox list + param editor.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Refetch the campaign data | Re-call the campaign detail endpoint after a successful action | ✓ |
|
||||
| Optimistic local update | Update local state immediately without a round-trip | |
|
||||
|
||||
**User's choice:** Refetch the campaign data.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Disappear, replaced by status | Buttons gone once resolved, replaced by a status line | |
|
||||
| Stay visible but disabled | Buttons remain, greyed out with a tooltip explaining why | ✓ |
|
||||
|
||||
**User's choice:** Stay visible but disabled.
|
||||
|
||||
---
|
||||
|
||||
## Not-yet-triaged empty state
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Friendly empty state + manual trigger | Explains "not yet triaged", offers a button to call the existing on-demand analyze endpoint | ✓ |
|
||||
| Plain 404 / not found | Simplest — ticket has no report, page says not found | |
|
||||
|
||||
**User's choice:** Friendly empty state + manual trigger.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Show the report standalone | Render the report's own evidence with a note that grouping/classification hasn't completed | ✓ |
|
||||
| Treat same as not-yet-triaged | Show the same empty/pending state, one code path | |
|
||||
|
||||
**User's choice:** Show the report standalone.
|
||||
|
||||
---
|
||||
|
||||
## URL evidence rendering safety
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Inert copy-only text | Plain monospace text with copy-to-clipboard, never an `<a href>` | ✓ |
|
||||
| Clickable but sanitized | Real link with query strings/fragments stripped | |
|
||||
|
||||
**User's choice:** Inert copy-only text.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Plain evidence display | Filename/type/size/hash shown as-is | ✓ |
|
||||
| Filename hidden, hash-only | Only show type/size/hash, hide filename as extra caution | |
|
||||
|
||||
**User's choice:** Plain evidence display.
|
||||
|
||||
---
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
- Exact nav placement for the new list page's entry (top-level vs. nested under an existing menu)
|
||||
- Whether the campaign-detail data comes from extending the existing `GET /api/phishing/campaigns/[id]` in place, or a new page-specific endpoint
|
||||
- Whether a new ticket→campaign resolution route is a dedicated endpoint or resolved another way (must still go through the API-route pattern)
|
||||
- Blast-radius freshness — fresh `getBlastRadius()` call per page view vs. reading what the classification already persisted
|
||||
- Exact timeline merge/sort implementation and visual treatment (vertical timeline vs. simple ordered list)
|
||||
- Exact columns/layout of the minimal campaigns list page
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- Campaigns list page filtering/sorting beyond a minimal "recent" view
|
||||
- Real remediation provider wiring (unchanged from Phase 20 — REMEDEXEC-01..05, v2)
|
||||
- False-positive reversibility (unchanged from Phase 20's deferred item)
|
||||
Loading…
Add table
Add a link
Reference in a new issue