wulf-pulse/scripts/reconcile-device-links.ts

58 lines
1.9 KiB
TypeScript
Raw Normal View History

/**
* reconcile-device-links.ts
*
* Walks `device_external_ids` rows where `configuration_item_id IS NULL` and
* tries to link each to a configuration_item via cascading match strategies
* (serial MAC hostname-in-company). Conflicts (multiple CIs match) are
* logged and skipped no auto-merge.
*
* Usage:
* npx tsx scripts/reconcile-device-links.ts --dry-run
* npx tsx scripts/reconcile-device-links.ts --dry-run --limit=10000
* npx tsx scripts/reconcile-device-links.ts --limit=5000
*/
import { config } from 'dotenv';
config({ path: '.env.local' });
import { reconcileUnlinkedDevices } from '@/lib/services/device-link-reconciler';
function parseArgs() {
const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const limitArg = args.find((a) => a.startsWith('--limit='));
const limit = limitArg ? parseInt(limitArg.split('=')[1], 10) : 5000;
return { dryRun, limit };
}
async function main() {
const { dryRun, limit } = parseArgs();
console.log(
`[reconcile-device-links] starting${dryRun ? ' (DRY RUN — no writes)' : ''}; limit=${limit}`
);
const start = Date.now();
const result = await reconcileUnlinkedDevices({ limit, dryRun });
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
console.log('');
console.log(`scanned: ${result.scanned}`);
console.log(`linked: ${result.linked}${dryRun ? ' (would link)' : ''}`);
console.log(`conflicts: ${result.conflicts} (multiple CIs matched — skipped)`);
console.log(`unmatched: ${result.unmatched}`);
console.log('');
console.log('by confidence:');
for (const [k, v] of Object.entries(result.byConfidence)) {
if (v > 0) console.log(` ${k.padEnd(22)} ${v}`);
}
console.log('');
console.log(`done in ${elapsed}s`);
// pg pool keeps the process alive otherwise.
process.exit(0);
}
main().catch((err) => {
console.error('[reconcile-device-links] fatal:', err);
process.exit(1);
});