Backfill (scripts/backfill-wireless-equip.js): mirror F wireless CPEs (airos_ac/airosm/cambium) into OPS Service Equipment, resolving customer + service_location via device→service→delivery_id→Service Location(legacy_delivery_id), dedup by MAC-or-IP, serial=MAC when F sn is null, undo manifest. Park was already ~99% synced — created the 2 genuinely-missing (incl. James A. Bryson) + flagged devices without service/MAC. Verified: Bryson EQP-0000100001 → live LiteBeam 5AC signal -74/-72, airMAX 93/67. Signal interval coloring (F parity): wifiSigColor (airOS: >=-60 green … <-78 red) and fibreRxColor (Rx -8..-25 green, -25..-28 orange, else red) color the live signal tiles (wireless + fibre). Assistant analysis (check_signal advice) kept. Device + AP links in service details: CPE management GUI link (Interface → https://ip) in the wireless panel; AP link when its mgmt IP resolves (ops_airos.php resolves the AP by radio MAC in F's device table, best-effort → apIp/ap_ip), else shows the AP MAC. Hub normalizeAiros passes apIp through. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
5.3 KiB
JavaScript
81 lines
5.3 KiB
JavaScript
/* backfill-wireless-equip.js — crée les Service Equipment OPS manquants pour les CPE SANS-FIL de F
|
|
* (airos_ac / airosm / cambium). Résout customer+service_location via device→service→delivery_id→
|
|
* Service Location(legacy_delivery_id). NON DESTRUCTIF (crée seulement), dédup par MAC/IP, manifeste d'annulation.
|
|
* Usage: node backfill-wireless-equip.js (DRY-RUN — compte + échantillon, 0 écriture)
|
|
* node backfill-wireless-equip.js --apply (crée + écrit le manifeste)
|
|
*/
|
|
const erp = require('/app/lib/erp')
|
|
const mysql = require('mysql2/promise')
|
|
const fs = require('fs')
|
|
const APPLY = process.argv.includes('--apply')
|
|
const MANIFEST = '/app/data/backfill_wireless_equip.json'
|
|
const normMac = m => String(m || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
|
|
const brandOf = (cat, model) => /cambium/i.test(cat) ? 'Cambium' : (/ubiqu|nano|rocket|litebeam|powerbeam|bullet|airmax|airos|loco/i.test((model || '') + cat) ? 'Ubiquiti' : '')
|
|
|
|
;(async () => {
|
|
let cfg = {}; try { cfg = require('/app/lib/config') } catch (e) { /* fallback to env below */ }
|
|
const pool = mysql.createPool({
|
|
host: cfg.LEGACY_DB_HOST || process.env.LEGACY_DB_HOST || '10.100.80.100',
|
|
user: cfg.LEGACY_DB_USER || process.env.LEGACY_DB_USER || 'facturation',
|
|
password: cfg.LEGACY_DB_PASS || process.env.LEGACY_DB_PASS,
|
|
database: cfg.LEGACY_DB_NAME || process.env.LEGACY_DB_NAME || 'gestionclient',
|
|
connectionLimit: 2, connectTimeout: 8000,
|
|
})
|
|
// 1) CPE sans-fil F + delivery_id du service (préfère un service actif).
|
|
const [rows] = await pool.query(
|
|
"SELECT d.id device_id, d.category, d.manage, d.port, d.mac, d.sn, d.model, " +
|
|
" (SELECT s.delivery_id FROM service s WHERE s.device_id=d.id AND s.delivery_id>0 ORDER BY (s.status=1) DESC, s.id DESC LIMIT 1) delivery_id " +
|
|
" FROM device d WHERE d.category IN ('airos_ac','airosm','cambium') AND d.manage<>''")
|
|
await pool.end()
|
|
const wired = rows.filter(r => Number(r.delivery_id) > 0)
|
|
|
|
// 2) Index Service Location OPS par legacy_delivery_id → {name, customer}
|
|
const delIds = [...new Set(wired.map(r => Number(r.delivery_id)))]
|
|
const slIndex = {}
|
|
for (let i = 0; i < delIds.length; i += 100) {
|
|
const batch = delIds.slice(i, i + 100)
|
|
const sls = await erp.list('Service Location', { filters: [['legacy_delivery_id', 'in', batch]], fields: ['name', 'customer', 'legacy_delivery_id'], limit: 500 }).catch(() => [])
|
|
for (const s of sls) if (s.customer) slIndex[Number(s.legacy_delivery_id)] = { name: s.name, customer: s.customer }
|
|
}
|
|
|
|
// 3) Service Equipment existants (ceux avec ip_address = univers sans-fil) → dédup par MAC + IP
|
|
const existMac = new Set(), existIp = new Set()
|
|
const existing = await erp.list('Service Equipment', { filters: [['ip_address', '!=', '']], fields: ['name', 'mac_address', 'ip_address'], limit: 8000 }).catch(() => [])
|
|
for (const e of existing) { if (e.mac_address) existMac.add(normMac(e.mac_address)); if (e.ip_address) existIp.add(String(e.ip_address).trim()) }
|
|
|
|
// 4) calcule les créations
|
|
const toCreate = [], skips = { no_sl: 0, dup: 0, no_key: 0 }
|
|
for (const r of wired) {
|
|
const sl = slIndex[Number(r.delivery_id)]
|
|
if (!sl) { skips.no_sl++; continue }
|
|
const nm = normMac(r.mac), ip = String(r.manage).trim()
|
|
if (!nm && !ip) { skips.no_key++; continue } // ni MAC ni IP → indédupable, on saute
|
|
if ((nm && existMac.has(nm)) || (ip && existIp.has(ip))) { skips.dup++; continue }
|
|
// serial_number OBLIGATOIRE. F sn souvent NULL pour les CPE sans-fil → repli sur la MAC (identifiant réel, unique),
|
|
// sinon clé synthétique stable par device_id. mac_address garde le format d'origine (avec séparateurs).
|
|
const serial = (r.sn && String(r.sn).trim()) ? String(r.sn).trim() : (nm || ('AIROS-' + r.device_id))
|
|
const doc = {
|
|
equipment_type: 'Autre', brand: brandOf(r.category, r.model), model: r.model || '',
|
|
serial_number: serial, mac_address: r.mac || '', ip_address: r.manage, login_user: 'admin',
|
|
customer: sl.customer, service_location: sl.name, status: 'Actif', ownership: 'Gigafibre',
|
|
}
|
|
toCreate.push({ device_id: r.device_id, mac: r.mac, ip: r.manage, doc })
|
|
}
|
|
console.log(JSON.stringify({ f_wireless: rows.length, with_delivery: wired.length, existing_wireless_se: existing.length, to_create: toCreate.length, skipped: skips }, null, 1))
|
|
console.log('SAMPLE:', JSON.stringify(toCreate.slice(0, 4).map(x => x.doc), null, 1))
|
|
if (!APPLY) { console.log('DRY-RUN — passe --apply pour créer.'); return }
|
|
|
|
// 5) crée + manifeste
|
|
const created = []
|
|
let n = 0
|
|
for (const c of toCreate) {
|
|
const res = await erp.create('Service Equipment', c.doc).catch(e => ({ error: String(e.message || e) }))
|
|
const okName = (res && typeof res.name === 'string') ? res.name : null
|
|
if (okName) created.push({ name: okName, device_id: c.device_id, mac: c.mac, ip: c.ip })
|
|
else created.push({ error: String((res && res.error) || 'échec').slice(0, 160), status: res && res.status, device_id: c.device_id, mac: c.mac })
|
|
if (++n % 100 === 0) console.log('… ' + n + '/' + toCreate.length)
|
|
}
|
|
fs.writeFileSync(MANIFEST, JSON.stringify({ at: Date.now(), created }, null, 1))
|
|
console.log('CREATED ' + created.filter(c => c.name).length + ' / ' + toCreate.length + ' · errors ' + created.filter(c => c.error).length + ' · manifest ' + MANIFEST)
|
|
})().catch(e => { console.log('FATAL', e.message) })
|