Bundles several in-progress efforts that were sitting uncommitted: - User queue-preferences (migration 087, API route, popover component) - QBO invoice soft-delete (migration 088) and AR diagnostics route - Dashboard/mobile engagement route and page adjustments - Docker Compose log-rotation config - One-off ticket/RMM investigation scripts (scripts/) - Planning docs: phase verification/pattern notes, mobile shell design spec - .gitignore: exclude local scratch financial/inventory data and Claude Code worktree/local-settings runtime state (never meant for version control) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
/**
|
|
* reanalyze-seubert-burst.ts
|
|
*
|
|
* One-shot: (1) reclassify ticket 699415's campaign now that the container
|
|
* has been rebuilt with the Phase 23 USER_AWARENESS classifier (the existing
|
|
* classification ran against the pre-Phase-23 code and is stale), and
|
|
* (2) run /analyze's parse+group steps for the 7 Seubert reports (699419,
|
|
* 699421, 699422, 699433, 699435, 699441, 699445) that landed before the
|
|
* company's automation gate was switched on and never got EML-parsed.
|
|
*
|
|
* Usage:
|
|
* POSTGRES_HOST=localhost npx tsx scripts/reanalyze-seubert-burst.ts
|
|
*/
|
|
|
|
import { config } from 'dotenv';
|
|
import { resolve } from 'path';
|
|
config({ path: resolve('/opt/stacks/pulse/.env.local') });
|
|
|
|
import postgresClient from '../lib/services/postgres-client';
|
|
import { detectPhishingTicket, type DetectableTicket } from '../lib/services/phishing-detector';
|
|
import { parseAndStoreMessage } from '../lib/services/phishing-eml-service';
|
|
import { groupReportIntoCampaign } from '../lib/services/campaign-grouping-service';
|
|
import { classifyCampaign } from '../lib/services/campaign-classifier';
|
|
|
|
const RECLASSIFY_CAMPAIGN_ID = '60a3613c-5b6c-45b1-8441-411a3fbf100c'; // ticket 699415
|
|
const UNPARSED_TICKET_IDS = [699419, 699421, 699422, 699433, 699435, 699441, 699445];
|
|
|
|
async function reanalyzeTicket(ticketId: number) {
|
|
const row = await postgresClient.query<{
|
|
id: string;
|
|
ticket_number: string | null;
|
|
title: string | null;
|
|
description: string | null;
|
|
company_id: number | null;
|
|
contact_id: number | null;
|
|
created_by_contact_id: number | null;
|
|
}>(
|
|
`SELECT id, ticket_number, title, description, company_id, contact_id, created_by_contact_id
|
|
FROM tickets WHERE id = $1`,
|
|
[ticketId]
|
|
);
|
|
const r = row.rows[0];
|
|
if (!r) {
|
|
console.log(` ticket ${ticketId}: NOT FOUND in Postgres`);
|
|
return;
|
|
}
|
|
|
|
const ticket: DetectableTicket = {
|
|
id: Number(r.id),
|
|
ticket_number: r.ticket_number,
|
|
title: r.title,
|
|
description: r.description,
|
|
company_id: r.company_id,
|
|
contact_id: r.contact_id,
|
|
created_by_contact_id: r.created_by_contact_id,
|
|
};
|
|
|
|
const detection = await detectPhishingTicket(ticket);
|
|
if (!detection.flagged || !detection.reportId) {
|
|
console.log(` ticket ${ticketId}: detector did not flag it (unexpected — it already has a reports row)`);
|
|
return;
|
|
}
|
|
|
|
const parseResult = await parseAndStoreMessage({ reportId: detection.reportId, ticketId });
|
|
const grouped = await groupReportIntoCampaign(detection.reportId);
|
|
|
|
console.log(
|
|
` ticket ${ticketId}: parse=${JSON.stringify(parseResult)} campaignId=${grouped?.campaignId ?? 'null'} groupMethod=${grouped?.groupMethod ?? 'null'}`
|
|
);
|
|
|
|
if (grouped?.campaignId) {
|
|
try {
|
|
const result = await classifyCampaign(grouped.campaignId);
|
|
console.log(` classified: verdict=${result.verdict}`);
|
|
} catch (err) {
|
|
console.log(` classify failed: ${err instanceof Error ? err.message : err}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log('=== Step 1: Reclassify 699415 campaign (stale pre-Phase-23 verdict) ===');
|
|
const before = await postgresClient.query<{ verdict: string | null }>(
|
|
`SELECT verdict FROM classifications WHERE campaign_id = $1 ORDER BY created_at DESC LIMIT 1`,
|
|
[RECLASSIFY_CAMPAIGN_ID]
|
|
);
|
|
console.log(` verdict before: ${before.rows[0]?.verdict ?? 'none'}`);
|
|
|
|
const result = await classifyCampaign(RECLASSIFY_CAMPAIGN_ID);
|
|
console.log(` verdict after: ${result.verdict}`);
|
|
console.log(` reasons: ${JSON.stringify(result.reasons ?? [], null, 2)}`);
|
|
|
|
console.log('\n=== Step 2: Analyze the 7 unparsed Seubert reports ===');
|
|
for (const ticketId of UNPARSED_TICKET_IDS) {
|
|
await reanalyzeTicket(ticketId);
|
|
}
|
|
|
|
console.log('\nDone.');
|
|
process.exit(0);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|