← back to Professional Directory

agents/dedupe-agent/server.js

62 lines

#!/usr/bin/env node
/**
 * Dedupe agent — HTTP control plane for entity-resolution passes.
 *
 *   GET /health    — { ok, agent, candidates }
 *   POST /run/link-locations — runs scripts/link_locations_to_orgs.js
 *
 * Future: NPI-based dedupe, fuzzy name dedupe, organization clustering.
 */
const fs = require('node:fs');
const path = require('path');
const { spawn } = require('node:child_process');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
const express = require('express');
const { pool, query } = require('../shared/db');

const PORT = Number(process.env.PORT_DEDUPE || process.env.PORT || 9873);
const ROOT = path.resolve(__dirname, '../..');
const SCRIPT_LINK = path.join(ROOT, 'scripts/link_locations_to_orgs.js');

const app = express();
app.use(express.json());

// LAN/tailnet auth gate (skip /health for watchdog probes)
const BASIC_AUTH = process.env.BASIC_AUTH || 'admin:DWSecure2024!';
const AUTH_HEADER = 'Basic ' + Buffer.from(BASIC_AUTH).toString('base64');
app.use((req, res, next) => {
  if (req.path === '/health' || req.path === '/healthz') return next();
  if (req.get('authorization') === AUTH_HEADER) return next();
  res.set('WWW-Authenticate', 'Basic realm="mac2-pm2"');
  res.status(401).send('auth required');
});

app.get('/health', async (_req, res) => {
  try {
    const r = await query(`
      SELECT
        (SELECT COUNT(*) FROM professional_locations) AS prof_locations_total,
        (SELECT COUNT(*) FROM professional_locations WHERE organization_id IS NOT NULL) AS linked,
        (SELECT COUNT(*) FROM professional_locations WHERE organization_id IS NULL) AS unlinked
    `, []);
    res.json({ ok: true, agent: 'pd-dedupe', ...r.rows[0] });
  } catch (e) {
    res.status(500).json({ ok: false, error: e.message });
  }
});

app.post('/run/link-locations', (_req, res) => {
  if (!fs.existsSync(SCRIPT_LINK)) {
    return res.status(500).json({ error: `missing script: ${SCRIPT_LINK}` });
  }
  const child = spawn('node', [SCRIPT_LINK], {
    cwd: ROOT, env: process.env, detached: true, stdio: 'ignore',
  });
  child.unref();
  res.json({ ok: true, started: 'link-locations', pid: child.pid });
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`[pd-dedupe] listening on http://127.0.0.1:${PORT}`);
});