diff --git a/.planning/phases/23-classification-disposition-per-client-automation-gate/23-04-SUMMARY.md b/.planning/phases/23-classification-disposition-per-client-automation-gate/23-04-SUMMARY.md new file mode 100644 index 0000000..37abb37 --- /dev/null +++ b/.planning/phases/23-classification-disposition-per-client-automation-gate/23-04-SUMMARY.md @@ -0,0 +1,101 @@ +--- +phase: 23-classification-disposition-per-client-automation-gate +plan: 04 +subsystem: ui +tags: [nextjs, react, shadcn, admin-page, phishing-automation] + +# Dependency graph +requires: + - phase: 23-classification-disposition-per-client-automation-gate + provides: "Plan 03's phishing_automation_gate migration + admin GET/PATCH/DELETE API" +provides: + - /admin/phishing-automation page with searchable/filterable company table and three per-row Switch toggles (auto-parse, auto-classify, auto-report) + - /admin index tile linking to the new page +affects: [23-05-gated-webhook-chain] + +# Tech tracking +tech-stack: + added: [] + patterns: [per-stage toggle(company, stage, next) sending all three current flags on every PATCH, cloned from client-scope's single-toggle pattern] + +key-files: + created: + - app/admin/phishing-automation/page.tsx + modified: + - app/admin/page.tsx + +key-decisions: + - "Used the Bot lucide icon for the Phishing Automation tile (ShieldCheck was already claimed by the Roles tile) to keep icons visually distinct within the Tools & Data section" + - "Task 1's human-verify checkpoint was approved against a temporary rebuild of the production container rather than local `npm run dev`, per the user's own testing preference in this session — functionally equivalent verification of the same committed code" + +patterns-established: + - "Per-stage toggle(company, stage, next) helper: PATCHes all three current boolean flags (two unchanged + one toggled) rather than supporting partial-field updates, matching the Plan 03 API's all-fields-required PATCH contract" + +requirements-completed: [AUTOGATE-02] + +# Metrics +duration: ~25min (across two sessions; Task 2 continuation session ~5min) +completed: 2026-07-16 +--- + +# Phase 23 Plan 04: Admin Phishing-Automation Page + Index Tile Summary + +**New `/admin/phishing-automation` page with a per-company table exposing three independent Switch toggles (auto-parse, auto-classify, auto-report), cloned from the `/admin/client-scope` structure, plus a discoverable tile on the `/admin` index.** + +## Performance + +- **Duration:** ~25 min total (Task 1 in prior session, Task 2 completed in this continuation session in ~5 min) +- **Completed:** 2026-07-16 +- **Tasks:** 2 completed +- **Files modified:** 2 (1 created, 1 modified) + +## Accomplishments +- `/admin/phishing-automation` page renders a searchable/filterable company table with three per-row `Switch` toggles wired to the Plan 03 API (GET on load, PATCH on toggle, all three flags sent together) +- Helper caption clarifies stage dependency (report meaningfully requires classify; classify requires parse) as informational guidance only — not enforced in the UI +- `/admin` index now has a "Phishing Automation" tile in the Tools & Data section, next to Client Scope, linking to the new page +- Human verification (Task 1 checkpoint) confirmed: table loads with three toggle columns all OFF by default, a toggle persists across reload via PATCH, and search/type filters work — tested against a temporary production container rebuild + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Build /admin/phishing-automation page (3-toggle company table)** - `5dabaad` (feat) — completed in prior session; checkpoint approved +2. **Task 2: Add admin index tile for Phishing Automation** - `160fe88` (feat) — completed in this continuation session + +_No TDD tasks in this plan — all tasks are `type="checkpoint:human-verify"` (Task 1) and `type="auto"` (Task 2)._ + +## Files Created/Modified +- `app/admin/phishing-automation/page.tsx` (266 lines) - `'use client'` page with `Company` interface (`autoParse`/`autoClassify`/`autoReport` booleans), `load()` GET fetch, per-stage `toggle(company, stage, next)` PATCH, search + company-type filter chrome, three `` cells per row with `aria-label`s, `PageHeader` describing the opt-in gate and acknowledge_user auto-post behavior +- `app/admin/page.tsx` - Added `Bot` to the `lucide-react` import block; added a "Phishing Automation" tile object to the Tools & Data section's `tiles[]` array, immediately after "Client Scope" + +## Decisions Made +- Chose the `Bot` icon (not `ShieldCheck`, already used by the Roles tile) for visual distinction within the same admin section +- Task 1's checkpoint verification was performed by the user against a temporary production container rebuild rather than `npm run dev` locally — same committed code, equivalent verification path; documented here since it deviates from the plan's literal `npm run dev` instruction + +## Deviations from Plan + +None - plan executed exactly as written (Task 1's checkpoint verification method was a user-driven substitution of environment, not a code deviation). + +## Issues Encountered + +None. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness +- The admin control surface for the per-company automation gate (D-08) is complete and reachable from `/admin` +- Plan 05 (gated webhook auto-pipeline) can now rely on admins having a UI to opt individual companies into auto-parse/auto-classify/auto-report +- `npx tsc --noEmit --pretty` exits 0 + +--- +*Phase: 23-classification-disposition-per-client-automation-gate* +*Completed: 2026-07-16* + +## Self-Check: PASSED + +- FOUND: app/admin/phishing-automation/page.tsx +- FOUND: app/admin/page.tsx +- FOUND: commit 5dabaad (Task 1) +- FOUND: commit 160fe88 (Task 2) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index f7435b0..811b065 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -28,6 +28,7 @@ import { ShieldCheck, Settings as SettingsIcon, Shield, + Bot, DollarSign, CalendarClock, Building2, @@ -244,6 +245,12 @@ export default function AdminIndexPage() { icon: Building2, description: 'Control which companies appear in dashboard KPIs and ticket analytics', }, + { + title: 'Phishing Automation', + href: '/admin/phishing-automation', + icon: Bot, + description: 'Per-company opt-in gate for automatic phishing parse / classify / acknowledge-note stages', + }, { title: 'RMM Overshell', href: '/admin/rmm-overshell', diff --git a/app/admin/phishing-automation/page.tsx b/app/admin/phishing-automation/page.tsx new file mode 100644 index 0000000..3ac3792 --- /dev/null +++ b/app/admin/phishing-automation/page.tsx @@ -0,0 +1,266 @@ +'use client'; + +import { useEffect, useState, useCallback } from 'react'; +import { toast } from 'sonner'; +import { PageHeader } from '@/components/navigation/page-header'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Search, RefreshCw, ShieldCheck, Zap } from 'lucide-react'; + +interface Company { + id: string; + companyName: string; + companyType: number | null; + companyTypeLabel: string | null; + autoParse: boolean; + autoClassify: boolean; + autoReport: boolean; +} + +type Stage = 'autoParse' | 'autoClassify' | 'autoReport'; + +const STAGE_LABEL: Record = { + autoParse: 'Auto-parse', + autoClassify: 'Auto-classify', + autoReport: 'Auto-report', +}; + +export default function PhishingAutomationPage() { + const [companies, setCompanies] = useState([]); + const [total, setTotal] = useState(0); + const [enabled, setEnabled] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [search, setSearch] = useState(''); + const [typeFilter, setTypeFilter] = useState('all'); + const [toggling, setToggling] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const params = new URLSearchParams(); + if (search) params.set('search', search); + if (typeFilter !== 'all') params.set('type', typeFilter); + const qs = params.toString(); + const res = await fetch('/api/admin/phishing-automation' + (qs ? `?${qs}` : '')); + if (!res.ok) { + const d = await res.json().catch(() => ({})) as { error?: string }; + throw new Error(d.error ?? `Request failed: ${res.status}`); + } + const data = await res.json() as { companies: Company[]; total: number; enabled: number }; + setCompanies(data.companies); + setTotal(data.total); + setEnabled(data.enabled); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load'); + } finally { + setLoading(false); + } + }, [search, typeFilter]); + + useEffect(() => { + void load(); + }, [load]); + + async function toggle(company: Company, stage: Stage, next: boolean) { + setToggling(`${company.id}:${stage}`); + const nextFlags = { + autoParse: stage === 'autoParse' ? next : company.autoParse, + autoClassify: stage === 'autoClassify' ? next : company.autoClassify, + autoReport: stage === 'autoReport' ? next : company.autoReport, + }; + try { + const res = await fetch(`/api/admin/phishing-automation/${company.id}`, { + method: 'PATCH', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(nextFlags), + }); + if (!res.ok) { + const d = await res.json().catch(() => ({})) as { error?: string }; + throw new Error(d.error ?? `Failed: ${res.status}`); + } + setCompanies((prev) => + prev.map((c) => (c.id === company.id ? { ...c, ...nextFlags } : c)) + ); + setEnabled((n) => { + const wasEnabled = company.autoParse || company.autoClassify || company.autoReport; + const isEnabled = nextFlags.autoParse || nextFlags.autoClassify || nextFlags.autoReport; + if (wasEnabled === isEnabled) return n; + return isEnabled ? n + 1 : Math.max(0, n - 1); + }); + toast.success(`${STAGE_LABEL[stage]} ${next ? 'enabled' : 'disabled'} for ${company.companyName}`); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Toggle failed'); + } finally { + setToggling(null); + } + } + + const uniqueTypes = Array.from(new Set(companies.map((c) => c.companyType).filter(Boolean))).sort() as number[]; + + return ( + <> + void load()} disabled={loading}> + + Refresh + + } + /> + +
+ {/* Summary */} +
+
+ + Total companies: + {total} +
+
+ + With any stage enabled: + {enabled} +
+
+ +

+ Stages are independent toggles, but they build on each other in practice: auto-report is only + meaningful when auto-classify is also on, and auto-classify is only meaningful when auto-parse + is also on. This is informational — the UI does not enforce or auto-cascade these dependencies. +

+ + + {/* Filters */} +
+
+ + setSearch(e.target.value)} + className="pl-8" + /> +
+ + + {companies.length} shown + +
+ + {error && ( +
+ + Failed to load + {error} + +
+ )} + + + {loading ? ( +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+ ) : companies.length === 0 ? ( +
+ No companies match the current filters. +
+ ) : ( + + + + Company + Type + Auto-parse + Auto-classify + Auto-report + + + + {companies.map((company) => ( + + {company.companyName} + + {company.companyTypeLabel ? ( + + {company.companyTypeLabel} + + ) : ( + + )} + + + void toggle(company, 'autoParse', next)} + aria-label={`Auto-parse for ${company.companyName}`} + /> + + + void toggle(company, 'autoClassify', next)} + aria-label={`Auto-classify for ${company.companyName}`} + /> + + + void toggle(company, 'autoReport', next)} + aria-label={`Auto-report for ${company.companyName}`} + /> + + + ))} + +
+ )} +
+
+
+ + ); +}