- 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>
57 lines
1.9 KiB
TypeScript
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);
|
|
});
|