wulf-pulse/scripts/reconcile-device-links.ts
lorentz 1112a06afe feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
  resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
  receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
  xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
  link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
  panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
  admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00

57 lines
1.9 KiB
TypeScript

/**
* 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);
});