Commit graph

574 commits

Author SHA1 Message Date
13208b3a92 docs(15): create phase plan 2026-07-15 07:31:25 -04:00
3f8e8dc826 docs(phase-15): create phase plan
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wWroM6FXkQJiH3JgYcony
2026-07-15 07:17:10 -04:00
7fa94cb6a5 docs(state): record phase 15 context session 2026-07-15 06:56:47 -04:00
d3ff9d088b docs(15): capture phase context 2026-07-15 06:56:37 -04:00
0228639f24 docs: create milestone v3.0 roadmap (7 phases) 2026-07-14 23:52:00 -04:00
cefa963565 docs: define milestone v3.0 requirements 2026-07-14 23:38:23 -04:00
149bd08bd6 chore: clear v2.0 phase directories for v3.0 milestone start
Phase artifacts remain in git history (v2.0 PAX8 Integration); .planning/phases/
is cleared for the new v3.0 Phishing Triage Automation phase numbering.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 23:34:37 -04:00
93d2715fe7 docs: start milestone v3.0 Phishing Triage Automation 2026-07-14 23:34:19 -04:00
2e5eab68e6 docs(phase-14): evolve PROJECT.md after phase completion 2026-07-12 18:30:09 -04:00
37f81017f5 docs(phase-14): complete phase execution 2026-07-12 18:29:11 -04:00
1dcceecfd9 docs(14): re-verify phase 14 as passed after permission-split fix 2026-07-12 18:28:58 -04:00
00f196c115 fix(auth): stop hasPermission crashing for non-admin ("user") roles
hasPermission()'s parameter was named userRole: string, shadowing the
module-level userRole role object exported earlier in the same file.
The internal roles map's `user: userRole` entry therefore bound to the
shadowed string parameter (e.g. "user") instead of the actual role
object — so any permission check for a "user"-role session (the only
non-admin role in the app) hit `"user".statements[resource]`, which is
undefined, and threw instead of returning false.

Net effect: every requirePermission()-gated route in the app returned
a 500 instead of a 403 for non-admin users. This predates phase 14 —
surfaced now because phase 14's PAX8 resolve route is admin-gated and
got exercised by a non-admin account during verification.

Renamed the parameter to roleName to remove the collision. Added
lib/permissions.test.ts (previously zero coverage on this file) to
lock in the "user"/admin/super-admin behavior and prevent regression.
2026-07-12 18:20:36 -04:00
f490c16a40 docs(260712-ash): record quick task in STATE.md 2026-07-12 07:54:00 -04:00
8df563c533 chore: merge executor worktree (worktree-agent-ae30b6495d2fffaf0) 2026-07-12 07:53:13 -04:00
9627b8b73c docs(260712-ash): add execution summary
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 07:52:55 -04:00
6ed6c66810 feat(260712-ash): add PAX8 card to sync overview page
- Append PAX8 entry to INTEGRATIONS with logo, description, and detail link
- Fetch /api/pax8/sync in fetchAll, wire pax8 branches in getSummary/getStatusIcon
- Add PAX8 stats block (last sync, companies, subscriptions) to card render
- Add public/logos/pax8.ico as placeholder (copied from itglue.ico — no network
  access available to fetch the real PAX8 favicon; replace with the real logo
  when convenient)
2026-07-12 07:52:21 -04:00
3ac111e021 feat(260712-ash): add PAX8 sync detail page
- New app/admin/sync/pax8/page.tsx mirroring sentinelone/duo pattern
- Polls GET /api/pax8/sync every 10s, shows companies/subscriptions/products stats
- Sync Now button POSTs with triggeredBy, handles 403/409 via sonner toast, polls until complete
2026-07-12 07:51:23 -04:00
a8e5afe73a docs(260712-ash): pre-dispatch plan for PAX8 admin sync UI 2026-07-12 07:49:50 -04:00
770cc836ef docs(14): add code review report and human verification checkpoint 2026-07-11 15:25:39 -04:00
635141080b docs(14): add code review report 2026-07-11 15:16:45 -04:00
d56db023be fix(14): cast companies-list id to Number to match resolve route's z.number() schema
companies.id is BIGINT and node-postgres serializes it as a string. The
manual-search fallback (D-05) fetches from this route and sends the id
straight through to POST /company-matches/[id]/resolve, whose Zod schema
requires a JS number with no coercion — every manual-search resolution was
rejected with 400. Every other PAX8 route in this phase already casts
bigint columns via Number(); this route was the one omission.

Found by code review (14-REVIEW.md, CR-01).
2026-07-11 15:16:38 -04:00
1f5a65ecc1 docs(phase-14): update tracking after wave 4 2026-07-11 15:09:40 -04:00
a6f5ca39a3 docs(14-06): record human verification checkpoint approval 2026-07-11 15:09:40 -04:00
3bf0244648 docs(phase-14): update tracking after wave 3 2026-07-11 14:46:09 -04:00
3951298da2 chore: merge executor worktree (worktree-agent-a08db5af4833dc6c9) 2026-07-11 14:45:17 -04:00
e80a816ec6 docs(14-05): record plan 05 execution summary
Needs Review tab (review cards, candidate resolve, manual-search
combobox, count badge) + companies-list requireAuth hardening.
2026-07-11 14:44:57 -04:00
13272f9a6f feat(14-05): manual company-search combobox fallback (D-05/D-09)
- Every review card gets a Command/Popover combobox fed by
  /api/data/companies-list, fetched once via ensureCompaniesLoaded()
  and shared across cards
- Selecting a company enables a "Link to selected company" button that
  calls the same resolve() handler as the candidate buttons
- Zero-candidate reviews (D-09) show only the manual picker; reviews
  with candidates show both candidate buttons and the manual picker
2026-07-11 14:43:56 -04:00
40efa1d3b6 feat(14-05): Needs Review tab review cards, candidate resolve, count badge
- Amber-bordered cards (border-amber-200) list unresolved PAX8 company
  match reviews, fetched from /api/pax8/company-matches on first tab
  activation
- Each candidate row offers a "Link company" button that POSTs to
  .../[id]/resolve; on success the card is optimistically removed,
  toast.success fires, and reviewTotal decrements
- Needs Review TabsTrigger shows a count badge when reviewTotal > 0
- Error/loading/empty states mirror device-link-conflicts' Alert/
  Skeleton/empty-state trio per UI-SPEC copy
- Zero-candidate reviews render the D-09 "No suggested matches" empty
  state; manual-search combobox insertion point left for Task 3
2026-07-11 14:43:05 -04:00
564be52b97 feat(14-05): harden companies-list route with requireAuth
- companies-list now feeds the manual-search fallback for authenticated
  UI (Needs Review tab), closes previously-unauthenticated gap
- response shape unchanged: [{ id, company_name }]
2026-07-11 14:42:10 -04:00
e9d5846cc8 docs(phase-14): update tracking after wave 2 2026-07-11 14:39:01 -04:00
30d8c9d46f chore: merge executor worktree (worktree-agent-a91f4d8cb6bd9f301) 2026-07-11 14:38:08 -04:00
a6ad8d504d docs(14-04): append self-check results to summary
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 14:37:50 -04:00
9b6faa95a1 docs(14-04): record plan completion summary
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 14:37:37 -04:00
543ac39d83 feat(14-04): add top-level PAX8 nav entry
- New top-level navigationItems entry (href: /pax8, icon: ShoppingCart)
- Not added to the Engagement/Admin visibleItems super-admin gate — visible to all authenticated users on both desktop NavigationMenu and mobile Sheet (D-07)
2026-07-11 14:36:52 -04:00
51470f8892 feat(14-04): /pax8 page shell + Companies tab (DataTable + DetailModal drill-down)
- New app/pax8/page.tsx client page with PageHeader + Companies/Needs Review Tabs shell
- Companies tab: DataTable of PAX8 companies (name, matched Autotask company or Unmatched badge, active subscription count, city/state/country) with sort/search/pagination against /api/pax8/companies
- Row click fetch-then-opens the extended DetailModal (kind="pax8_company") with subscriptions + cost breakdown from /api/pax8/companies/[id]
- Needs Review tab left as a marked placeholder for Plan 14-05
2026-07-11 14:36:31 -04:00
8e1114aeec docs(phase-14): update tracking after wave 1 2026-07-11 14:33:19 -04:00
8ce2d3fc9b chore: merge executor worktree (worktree-agent-a77f9b3a365cc0b6d) 2026-07-11 14:31:20 -04:00
580e7ac508 chore: merge executor worktree (worktree-agent-a11c767c2a8721d5b) 2026-07-11 14:31:20 -04:00
57db03b9ca chore: merge executor worktree (worktree-agent-a7f5ed0d03f07bb8a) 2026-07-11 14:31:20 -04:00
65acbe1adf docs(14-02): append self-check result to summary
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
2026-07-11 14:30:31 -04:00
8306caf04d docs(14-02): add plan summary
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
2026-07-11 14:30:18 -04:00
4d7a58b46c docs(14-02): drop literal candidate_company_ids mention from resolver docstring
Satisfies the plan's grep-based acceptance check for "no reference to
candidate_company_ids in the resolver source" — code already had no
membership check, this only reworded the explanatory comment.
2026-07-11 14:29:43 -04:00
a6dcc9e29e docs(14-03): record DetailModal PAX8 extension summary
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
2026-07-11 14:29:26 -04:00
d61acf9ca5 docs(14-01): append self-check results to SUMMARY.md 2026-07-11 14:29:13 -04:00
a81e358be7 feat(14-02): add POST /api/pax8/company-matches/[id]/resolve route
- requirePermission('admin','access')-gated (D-08) — the write side of the
  asymmetric read/write auth split
- zod-validated body (companyId positive int, note <=500 chars)
- Delegates the two-table write to resolvePax8CompanyMatch inside
  postgresClient.transaction(); maps ResolveResult codes to HTTP status
  (ok->200, not_found->404, already_resolved->409, company_not_found->400)
2026-07-11 14:29:07 -04:00
501c7e4106 docs(14-01): complete PAX8 companies API routes plan
- Add SUMMARY.md documenting the two routes, deviations, and next-phase readiness
2026-07-11 14:28:58 -04:00
0ce51a0167 feat(14-02): implement resolvePax8CompanyMatch resolver
- Two-table transactional write: pax8_companies.match_method='manual' AND
  pax8_company_match_review.resolved_* in one call, so the matcher's
  re-scoring guard (pax8-company-matcher.ts ~216-231) never re-flags a
  manually resolved company
- Guards not_found / already_resolved via FOR UPDATE select
- Validates target company existence + active state (substitute for
  candidate-membership check — D-05/D-09 allow non-candidate ids)
- All five vitest behavior cases green; tsc clean
2026-07-11 14:28:48 -04:00
04c75bedf8 feat(14-03): render subscriptions cost-breakdown table in Formatted tab
- New section guarded by Array.isArray(data.subscriptions), rendered as
  the first child so it appears above field groups (UI-SPEC focal point)
- Product label falls back productName -> sku -> 'Unknown item'
- Amounts use latestBilledAmount directly (never unit_price * quantity),
  rendered font-mono tabular-nums, plus a summed total row
- Empty array shows a muted "No subscriptions" state instead of crashing
- Raw tab and existing ticket Description block unchanged
2026-07-11 14:28:45 -04:00
afdcf1412d test(14-02): add failing test for resolvePax8CompanyMatch
- Five behavior cases: success (both writes), not_found, already_resolved,
  company_not_found, and non-candidate companyId still resolves (D-05/D-09)
- Hand-rolled mock tx asserts SQL + bound params per query call
2026-07-11 14:28:44 -04:00
3492a16176 feat(14-03): add kind prop + PAX8_COMPANY_GROUPS to DetailModal
- Add optional kind?: 'ticket' | 'company' | 'pax8_company' prop to DetailModalProps
- Add PAX8_COMPANY_GROUPS field-group set (Identity + System, camelCase keys)
- detectGroups() checks kind === 'pax8_company' first, before existing sniff branches
- TICKET_GROUPS/COMPANY_GROUPS and their detection branches unchanged
2026-07-11 14:28:15 -04:00