gigafibre-fsm/services/targo-hub/lib/provision.js
louispaulb 607ea54b5c refactor: reduce token count, DRY code, consolidate docs
Backend services:
- targo-hub: extract deepGetValue to helpers.js, DRY disconnect reasons
  lookup map, compact CAPABILITIES, consolidate vision.js prompts/schemas,
  extract dispatch scoring weights, trim section dividers across 9 files
- modem-bridge: extract getSession() helper (6 occurrences), resetIdleTimer(),
  consolidate DM query factory, fix duplicate username fill bug, trim headers
  (server.js -36%, tplink-session.js -47%, docker-compose.yml -57%)

Frontend:
- useWifiDiagnostic: extract THRESHOLDS const, split processDiagnostic into
  6 focused helpers (processOnlineStatus, processWanIPs, processRadios,
  processMeshNodes, processClients, checkRadioIssues)
- EquipmentDetail: merge duplicate ROLE_LABELS, remove verbose comments

Documentation (17 → 13 files, -1,400 lines):
- New consolidated README.md (architecture, services, dependencies, auth)
- Merge ECOSYSTEM-OVERVIEW into ARCHITECTURE.md
- Merge MIGRATION-PLAN + ARCHITECTURE-COMPARE + FIELD-GAP + CHANGELOG → MIGRATION.md
- Merge COMPETITIVE-ANALYSIS into PLATFORM-STRATEGY.md
- Update ROADMAP.md with current phase status
- Delete CONTEXT.md (absorbed into README)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 08:39:58 -04:00

176 lines
10 KiB
JavaScript

'use strict'
const { log, json, parseBody, erpRequest } = require('./helpers')
const { broadcast } = require('./sse')
let oktopus
try { oktopus = require('./oktopus') } catch (e) { log('Oktopus module not loaded:', e.message) }
async function handle (req, res, method, path) {
try {
const action = path.replace('/provision/', '').split('/').filter(Boolean)[0]
if (action === 'pre-authorize' && method === 'POST') {
const body = await parseBody(req)
const { gpon_serial, olt_ip, frame, slot, port, ont_id, equipment_name } = body
const vlans = body.vlans || {}
if (!gpon_serial || !olt_ip || frame === undefined || slot === undefined || port === undefined) {
return json(res, 400, { error: 'Missing required fields: gpon_serial, olt_ip, frame, slot, port' })
}
const vlanCmd = (vlan, gemport) => vlan
? `service-port auto vlan ${vlan} gemport ${gemport} gpon ${frame}/${slot}/${port} ont ${ont_id} transparent`
: `# no vlan for gemport ${gemport}`
const commands = [
`# OLT: ${olt_ip} — Pre-authorize ONT`,
`interface gpon ${frame}/${slot}`,
`ont add ${port} sn-auth "${gpon_serial}" omci ont-lineprofile-id ${body.line_profile || 10} ont-srvprofile-id ${body.service_profile || 10} desc "pre-auth-${equipment_name || 'unknown'}"`,
`ont confirm ${port} ont ${ont_id}`,
`quit`,
`# Service ports (VLANs)`,
vlanCmd(vlans.internet, 1), vlanCmd(vlans.manage, 2), vlanCmd(vlans.telephone, 3), vlanCmd(vlans.tv, 4),
]
if (equipment_name) {
try { await erpRequest('PUT', `/api/resource/Service Equipment/${encodeURIComponent(equipment_name)}`, { gpon_serial, status: 'Actif' }) }
catch (e) { log('ERPNext update error:', e.message) }
}
log(`[provision] Pre-authorize ${gpon_serial} on ${olt_ip} ${frame}/${slot}/${port} ONT:${ont_id}`)
return json(res, 200, { status: 'commands_generated', gpon_serial, olt_ip, port: `${frame}/${slot}/${port}`, ont_id, commands, note: 'Execute via n8n SSH node or manual SSH to OLT' })
}
if (action === 'on-scan' && method === 'POST') {
const body = await parseBody(req)
const { serial, equipment_name, service_location } = body
if (!serial) return json(res, 400, { error: 'Missing serial' })
const result = { serial, actions: [] }
if (equipment_name) {
try {
await erpRequest('PUT', `/api/resource/Service Equipment/${encodeURIComponent(equipment_name)}`, { serial_number: serial, mac_address: body.mac || null, status: 'Actif' })
result.actions.push({ action: 'equipment_updated', equipment_name, serial })
} catch (e) { result.actions.push({ action: 'equipment_update_failed', error: e.message }) }
}
if (service_location && (body.equipment_type === 'ONT' || serial.startsWith('TPLG') || serial.startsWith('RCMG'))) {
try {
const locRes = await erpRequest('GET', `/api/resource/Service Location/${encodeURIComponent(service_location)}?fields=["olt_ip","olt_port","ont_id","vlan_internet","vlan_manage","vlan_telephone","vlan_tv"]`)
const loc = locRes.data || {}
if (loc.olt_ip && loc.olt_port) {
const m = (loc.olt_port || '').match(/(\d+)\/(\d+)\/(\d+)/)
if (m) {
result.olt_pre_auth = { gpon_serial: serial, olt_ip: loc.olt_ip, frame: +m[1], slot: +m[2], port: +m[3], ont_id: loc.ont_id || 0, vlans: { internet: loc.vlan_internet, manage: loc.vlan_manage, telephone: loc.vlan_telephone, tv: loc.vlan_tv } }
result.actions.push({ action: 'olt_pre_auth_ready', message: 'OLT commands generated, send to n8n for SSH execution' })
}
}
} catch (e) { result.actions.push({ action: 'location_lookup_failed', error: e.message }) }
}
if (oktopus && body.mac && (body.equipment_type === 'ONT' || serial.startsWith('TPLG') || serial.startsWith('RCMG'))) {
try {
const oktRes = await oktopus.provisionDevice({
mac: body.mac,
serial,
service_location: body.service_location,
equipment_name: body.equipment_name,
customer: body.customer,
})
result.oktopus = oktRes
result.actions.push(...(oktRes.actions || []))
} catch (e) {
result.actions.push({ action: 'oktopus_provision_failed', error: e.message })
}
}
if (body.customer) broadcast('customer:' + body.customer, 'equipment-scanned', { serial, equipment_type: body.equipment_type, job: body.job_name })
log(`[provision] Scan: ${serial}${result.actions.length} actions`)
return json(res, 200, result)
}
if (action === 'swap' && method === 'POST') {
const body = await parseBody(req)
const { old_equipment_name, new_serial, equipment_type, customer, service_location } = body
const swapType = body.swap_type || 'permanent'
if (!old_equipment_name || !new_serial) return json(res, 400, { error: 'Missing old_equipment_name and new_serial' })
const result = { old_equipment: old_equipment_name, new_serial, swap_type: swapType, actions: [] }
const oldStatus = { permanent: 'Défectueux', diagnostic: 'En diagnostic', upgrade: 'Retourné' }[swapType] || 'Défectueux'
let oldEquip = {}
try {
const r = await erpRequest('GET', `/api/resource/Service Equipment/${encodeURIComponent(old_equipment_name)}`)
oldEquip = r.data?.data || r.data || {}
} catch { return json(res, 404, { error: 'Old equipment not found: ' + old_equipment_name }) }
try {
await erpRequest('PUT', `/api/resource/Service Equipment/${encodeURIComponent(old_equipment_name)}`, {
status: oldStatus,
notes: `${oldEquip.notes || ''}\n[${new Date().toISOString()}] ${swapType === 'diagnostic' ? 'Swap diagnostic' : 'Remplacé'} par ${new_serial}. Raison: ${body.reason || swapType}`.trim(),
})
result.actions.push({ action: 'old_status_updated', name: old_equipment_name, status: oldStatus })
} catch (e) { result.actions.push({ action: 'status_update_failed', error: e.message }) }
const newName = 'EQ-SWAP-' + new_serial.substring(0, 10).replace(/[^A-Za-z0-9]/g, '')
try {
await erpRequest('POST', '/api/resource/Service Equipment', {
name: newName, equipment_type: equipment_type || oldEquip.equipment_type,
serial_number: new_serial, mac_address: body.new_mac || null,
brand: oldEquip.brand, model: oldEquip.model,
customer: customer || oldEquip.customer, service_location: service_location || oldEquip.service_location,
subscription: oldEquip.subscription, status: 'Actif', ownership: 'Gigafibre',
wifi_ssid: oldEquip.wifi_ssid, wifi_password: oldEquip.wifi_password,
sip_username: oldEquip.sip_username, sip_password: oldEquip.sip_password,
fibre_line_profile: oldEquip.fibre_line_profile, fibre_service_profile: oldEquip.fibre_service_profile,
parent_device: oldEquip.parent_device,
notes: `Remplacement de ${old_equipment_name} (${oldEquip.serial_number || 'N/A'}). Raison: ${body.reason || 'défectueux'}`,
})
result.actions.push({ action: 'new_equipment_created', name: newName, serial: new_serial })
} catch (e) { result.actions.push({ action: 'create_new_failed', error: e.message }) }
if ((equipment_type || oldEquip.equipment_type) === 'ONT' && (service_location || oldEquip.service_location)) {
try {
const locRes = await erpRequest('GET', `/api/resource/Service Location/${encodeURIComponent(service_location || oldEquip.service_location)}?fields=["olt_ip","olt_port","ont_id","vlan_internet","vlan_manage","vlan_telephone","vlan_tv"]`)
const loc = locRes.data || {}
const m = (loc.olt_port || '').match(/(\d+)\/(\d+)\/(\d+)/)
if (loc.olt_ip && m) {
result.olt_swap_commands = [
`# OLT: ${loc.olt_ip} — Swap ONT on ${m[0]}`,
`interface gpon ${m[1]}/${m[2]}`,
`ont delete ${m[3]} ${loc.ont_id || 0}`,
`ont add ${m[3]} sn-auth "${new_serial}" omci ont-lineprofile-id ${oldEquip.fibre_line_profile || 10} ont-srvprofile-id ${oldEquip.fibre_service_profile || 10} desc "swap-${newName}"`,
`ont confirm ${m[3]} ont ${loc.ont_id || 0}`, `quit`,
loc.vlan_internet ? `service-port auto vlan ${loc.vlan_internet} gemport 1 gpon ${m[1]}/${m[2]}/${m[3]} ont ${loc.ont_id || 0} transparent` : '',
loc.vlan_manage ? `service-port auto vlan ${loc.vlan_manage} gemport 2 gpon ${m[1]}/${m[2]}/${m[3]} ont ${loc.ont_id || 0} transparent` : '',
].filter(Boolean)
result.actions.push({ action: 'olt_swap_commands_ready', message: 'Execute via n8n SSH node' })
}
} catch (e) { result.actions.push({ action: 'olt_swap_lookup_failed', error: e.message }) }
}
if (customer || oldEquip.customer) {
broadcast('customer:' + (customer || oldEquip.customer), 'equipment-swapped', { old: old_equipment_name, new_serial, reason: body.reason })
}
log(`[provision] Swap: ${old_equipment_name}${new_serial} (${result.actions.length} actions)`)
return json(res, 200, result)
}
if (action === 'equipment' && method === 'GET') {
const serial = decodeURIComponent(path.replace('/provision/', '').split('/').filter(Boolean)[1] || '')
if (!serial) return json(res, 400, { error: 'Missing serial' })
try {
const r = await erpRequest('GET', `/api/resource/Service Equipment?filters=[["serial_number","=","${serial}"]]&fields=["name","serial_number","mac_address","wifi_ssid","wifi_password","sip_username","sip_password","fibre_line_profile","fibre_service_profile","customer","service_location","equipment_type"]&limit_page_length=1`)
const equips = r.data || []
if (!equips.length) return json(res, 404, { error: 'Equipment not found for serial: ' + serial })
return json(res, 200, equips[0])
} catch (e) { return json(res, 502, { error: 'ERPNext lookup failed: ' + e.message }) }
}
return json(res, 400, { error: 'Unknown provision endpoint: ' + action })
} catch (e) {
log('Provisioning error:', e.message)
return json(res, 500, { error: 'Provisioning error: ' + e.message })
}
}
module.exports = { handle }