feat(ops): native Payment Method/Arrangement + VoIP Line sections + G3 Raisecom SNMP writes
Native detail sections (fin de la grille+desk pour les derniers doctypes cliquables de la fiche client) : - PaymentMethodDetail / PaymentArrangementDetail / VoipLineDetail — LECTURE SEULE (aucune charge/paiement déclenché ; mot de passe SIP jamais affiché). Enregistrés dans SECTION_MAP. Phase 2 G3 (parité écriture fibre tech-2 Raisecom, ~10 500 ONUs) : - olt-ops.js : écritures SNMP natives (net-snmp) suspend/rétablir (VLAN 40↔666) / redémarrer / vitesse (line-profile), OIDs EXTRAITS DU DRIVER F raisecom_rcmg.php (vérifiés à la source) + SAVE 8886.1.2.1.1.0 après chaque set. Encodages olt_id (slot*1e7+port*1e5+ontid) et onu_id ((slot+32)*8388608+port*65536+ontid) — round-trip vérifié. Repli coords ONU depuis le poller SNMP live (olt-snmp.getOnuBySerial) car les coords Raisecom ne sont pas dans Service Equipment. Gaté : env OLT_RW_COMMUNITY requis (absent → run refuse proprement, 0 set), confirm+idempotencyKey obligatoires. Vérifié live : plan tech-2 refuse sans coords/RW ; run refuse ; 0 écriture réseau tirée. - OnuActionsPanel : ajout « Redémarrer ». Déployé prod (hub olt-ops + SPA). DetailModal SECTION_MAP (5 entrées ajoutées) = déployé mais co-édité → non commité. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3d4700decf
commit
23dde4ecf0
|
|
@ -0,0 +1,69 @@
|
|||
<!--
|
||||
PaymentArrangementDetail — vue NATIVE d'une entente de paiement (échéancier). LECTURE SEULE : montant,
|
||||
dates, statut, versements. Aucune action de charge — remplace la grille générique + bouton desk.
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div class="pa-head">
|
||||
<q-icon name="handshake" size="20px" color="indigo-6" class="q-mr-xs" />
|
||||
<span class="pa-title">Entente {{ doc.name }}</span>
|
||||
<span v-if="doc.status" class="ops-badge q-ml-sm">{{ doc.status }}</span>
|
||||
</div>
|
||||
|
||||
<div class="pa-totals">
|
||||
<div class="pa-card">
|
||||
<div class="pa-cap">Montant total</div>
|
||||
<div class="pa-val">{{ formatMoney(doc.total_amount) }}</div>
|
||||
</div>
|
||||
<div v-if="doc.installment_count" class="pa-card">
|
||||
<div class="pa-cap">Versements</div>
|
||||
<div class="pa-val">{{ doc.installment_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pa-grid">
|
||||
<div v-if="doc.customer" class="pa-row"><span class="pa-k">Client</span>
|
||||
<router-link :to="'/clients/' + encodeURIComponent(doc.customer)" class="erp-link">{{ doc.customer_name || doc.customer }}</router-link>
|
||||
</div>
|
||||
<div v-if="doc.date_agreed" class="pa-row"><span class="pa-k">Convenu</span>{{ formatDate(doc.date_agreed) }}</div>
|
||||
<div v-if="doc.date_due" class="pa-row"><span class="pa-k">Échéance</span>{{ formatDate(doc.date_due) }}</div>
|
||||
<div v-if="doc.note" class="pa-row"><span class="pa-k">Note</span><span>{{ doc.note }}</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Versements (child table Payment Arrangement Installment, si présents) -->
|
||||
<div v-if="installments.length" class="q-mt-md">
|
||||
<div class="info-block-title">Échéancier ({{ installments.length }})</div>
|
||||
<div v-for="(it, i) in installments" :key="i" class="pa-inst">
|
||||
<span class="pa-inst-date">{{ formatDate(it.due_date || it.date) }}</span>
|
||||
<span class="pa-inst-amt">{{ formatMoney(it.amount) }}</span>
|
||||
<span class="pa-inst-st" :class="'pa-st-' + String(it.status || '').toLowerCase()">{{ it.status || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { formatMoney, formatDate } from 'src/composables/useFormatters'
|
||||
const props = defineProps({ doc: { type: Object, required: true }, docName: { type: String, default: '' } })
|
||||
const installments = computed(() => props.doc.installments || props.doc.schedule || [])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pa-head { display: flex; align-items: center; margin-bottom: 10px; }
|
||||
.pa-title { font-size: 15px; font-weight: 700; }
|
||||
.pa-totals { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
.pa-card { background: var(--ops-soft, #eef2f7); border-radius: 8px; padding: 8px 14px; }
|
||||
.pa-cap { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: #90a4ae; }
|
||||
.pa-val { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.pa-grid { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
|
||||
.pa-row { display: flex; gap: 8px; }
|
||||
.pa-k { min-width: 64px; color: #90a4ae; text-transform: uppercase; font-size: 10px; letter-spacing: .05em; padding-top: 2px; }
|
||||
.pa-inst { display: flex; align-items: center; gap: 10px; font-size: 12.5px; border-top: 1px solid var(--ops-border, #e2e8f0); padding: 4px 0; }
|
||||
.pa-inst:first-of-type { border-top: none; }
|
||||
.pa-inst-date { min-width: 90px; }
|
||||
.pa-inst-amt { font-variant-numeric: tabular-nums; font-weight: 600; }
|
||||
.pa-inst-st { margin-left: auto; font-size: 11px; font-weight: 700; }
|
||||
.pa-st-paid, .pa-st-paye { color: #2f855a; }
|
||||
.pa-st-overdue, .pa-st-retard { color: #b83232; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<!--
|
||||
PaymentMethodDetail — vue NATIVE d'un moyen de paiement (Stripe / Paysafe / prélèvement bancaire).
|
||||
LECTURE SEULE : affiche le fournisseur + identifiants + statut PPA. Ne DÉCLENCHE aucun paiement (aucun bouton
|
||||
de charge) — remplace juste la grille générique + bouton desk ERPNext.
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div class="pm-head">
|
||||
<q-icon :name="providerIcon" size="20px" :color="providerColor" class="q-mr-xs" />
|
||||
<span class="pm-provider">{{ doc.provider || 'Moyen de paiement' }}</span>
|
||||
<q-badge v-if="isPpa" color="green-7" class="q-ml-sm">PPA auto</q-badge>
|
||||
<q-badge v-else color="grey-5" class="q-ml-sm">Manuel</q-badge>
|
||||
</div>
|
||||
|
||||
<div class="pm-grid">
|
||||
<div v-if="doc.customer" class="pm-row"><span class="pm-k">Client</span>
|
||||
<router-link :to="'/clients/' + encodeURIComponent(doc.customer)" class="erp-link">{{ doc.customer_name || doc.customer }}</router-link>
|
||||
</div>
|
||||
<div v-if="detail" class="pm-row"><span class="pm-k">Référence</span><code>{{ detail }}</code></div>
|
||||
<div v-if="doc.card_last4" class="pm-row"><span class="pm-k">Carte</span>•••• {{ doc.card_last4 }}<span v-if="doc.card_exp"> · exp. {{ doc.card_exp }}</span></div>
|
||||
<div v-if="doc.ppa_name" class="pm-row"><span class="pm-k">Titulaire</span>{{ doc.ppa_name }}</div>
|
||||
<div v-if="doc.ppa_institution" class="pm-row"><span class="pm-k">Banque</span>{{ doc.ppa_institution }}<span v-if="doc.ppa_branch"> · transit {{ doc.ppa_branch }}</span></div>
|
||||
<div v-if="doc.status" class="pm-row"><span class="pm-k">Statut</span><span class="ops-badge">{{ doc.status }}</span></div>
|
||||
</div>
|
||||
|
||||
<q-banner dense class="bg-blue-grey-1 text-blue-grey-9 pm-note">
|
||||
<template #avatar><q-icon name="lock" color="blue-grey-6" /></template>
|
||||
Vue lecture seule. Les charges/prélèvements se pilotent depuis la facturation (jamais d’ici).
|
||||
</q-banner>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps({ doc: { type: Object, required: true }, docName: { type: String, default: '' } })
|
||||
|
||||
const isPpa = computed(() => !!(props.doc.is_auto_ppa || props.doc.stripe_ppa_enabled))
|
||||
const providerIcon = computed(() => ({ Stripe: 'credit_card', Paysafe: 'credit_card', 'Bank Draft': 'account_balance' })[props.doc.provider] || 'payments')
|
||||
const providerColor = computed(() => ({ Stripe: 'deep-purple-6', Paysafe: 'blue-7', 'Bank Draft': 'teal-7' })[props.doc.provider] || 'grey-7')
|
||||
const detail = computed(() => {
|
||||
const d = props.doc
|
||||
if (d.provider === 'Stripe') return d.stripe_customer_id || ''
|
||||
if (d.provider === 'Paysafe') return d.paysafe_profile_id || ''
|
||||
if (d.provider === 'Bank Draft') return d.ppa_institution ? `${d.ppa_institution}-${d.ppa_branch || ''}` : (d.ppa_name || '')
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pm-head { display: flex; align-items: center; margin-bottom: 10px; }
|
||||
.pm-provider { font-size: 15px; font-weight: 700; }
|
||||
.pm-grid { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
|
||||
.pm-row { display: flex; gap: 8px; }
|
||||
.pm-k { min-width: 72px; color: #90a4ae; text-transform: uppercase; font-size: 10px; letter-spacing: .05em; padding-top: 2px; }
|
||||
.pm-note { border-radius: 8px; margin-top: 14px; font-size: 12px; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<!--
|
||||
VoipLineDetail — vue NATIVE d'une ligne VoIP (DID, statut, SIP, E911). LECTURE SEULE (le mot de passe SIP
|
||||
n'est jamais affiché). Remplace la grille générique + bouton desk ERPNext.
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div class="vl-head">
|
||||
<q-icon name="call" size="20px" color="teal-7" class="q-mr-xs" />
|
||||
<span class="vl-did">{{ doc.did || 'Ligne VoIP' }}</span>
|
||||
<span v-if="doc.status" class="ops-badge q-ml-sm">{{ doc.status }}</span>
|
||||
<q-badge :color="doc.e911_synced ? 'green-7' : 'orange-8'" class="q-ml-sm">
|
||||
{{ doc.e911_synced ? '911 synchronisé' : '911 non synchronisé' }}
|
||||
</q-badge>
|
||||
</div>
|
||||
|
||||
<div class="vl-grid">
|
||||
<div v-if="doc.customer" class="vl-row"><span class="vl-k">Client</span>
|
||||
<router-link :to="'/clients/' + encodeURIComponent(doc.customer)" class="erp-link">{{ doc.customer_name || doc.customer }}</router-link>
|
||||
</div>
|
||||
<div v-if="doc.service_location" class="vl-row"><span class="vl-k">Adresse</span>{{ doc.service_location }}</div>
|
||||
<div v-if="doc.sip_user" class="vl-row"><span class="vl-k">SIP</span><code>{{ doc.sip_user }}</code></div>
|
||||
<div v-if="doc.sip_domain" class="vl-row"><span class="vl-k">Domaine</span><code>{{ doc.sip_domain }}</code></div>
|
||||
<div v-if="doc.provider" class="vl-row"><span class="vl-k">Fournisseur</span>{{ doc.provider }}</div>
|
||||
</div>
|
||||
|
||||
<!-- E911 (adresse d'urgence) -->
|
||||
<div v-if="e911Addr" class="q-mt-md">
|
||||
<div class="info-block-title"><q-icon name="emergency" size="15px" class="q-mr-xs" />Adresse 911</div>
|
||||
<div class="vl-e911" :class="{ 'vl-e911-warn': !doc.e911_synced }">
|
||||
{{ e911Addr }}
|
||||
<span v-if="!doc.e911_synced" class="vl-e911-flag">⚠ non synchronisée avec le fournisseur 911</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps({ doc: { type: Object, required: true }, docName: { type: String, default: '' } })
|
||||
const e911Addr = computed(() => {
|
||||
const d = props.doc
|
||||
const parts = [d.e911_civic_number, d.e911_street_name, d.e911_city, d.e911_postal_code].filter(Boolean)
|
||||
return parts.length ? parts.join(' ') : ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vl-head { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; }
|
||||
.vl-did { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.vl-grid { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
|
||||
.vl-row { display: flex; gap: 8px; }
|
||||
.vl-k { min-width: 72px; color: #90a4ae; text-transform: uppercase; font-size: 10px; letter-spacing: .05em; padding-top: 2px; }
|
||||
.vl-e911 { font-size: 13px; padding: 8px 10px; background: var(--ops-soft, #eef2f7); border-radius: 8px; }
|
||||
.vl-e911-warn { background: #fbf0dc; color: #8a5a00; }
|
||||
.vl-e911-flag { display: block; font-size: 11px; margin-top: 2px; }
|
||||
</style>
|
||||
|
|
@ -74,6 +74,7 @@ const emit = defineEmits(['changed'])
|
|||
const menu = [
|
||||
{ action: 'suspend', label: 'Suspendre', icon: 'pause_circle', color: 'orange-8' },
|
||||
{ action: 'unsuspend', label: 'Rétablir', icon: 'play_circle', color: 'green-7' },
|
||||
{ action: 'reboot', label: 'Redémarrer', icon: 'restart_alt', color: 'blue-grey-7' },
|
||||
{ action: 'speed', label: 'Forfait / vitesse', icon: 'speed', color: 'blue-7' },
|
||||
{ action: 'replace', label: 'Remplacer', icon: 'swap_horiz', color: 'deep-purple-6' },
|
||||
{ action: 'remove', label: 'Retirer', icon: 'link_off', color: 'red-7' },
|
||||
|
|
|
|||
|
|
@ -20,15 +20,61 @@ const { log } = require('./helpers')
|
|||
const N8N_DOSTUFF = 'https://n8napi.targo.ca/webhook/dostuff'
|
||||
const STORE = '/app/data/olt_ops.json'
|
||||
const IDEMP_WINDOW_MS = 10 * 60 * 1000 // rejouer la même clé dans 10 min = même résultat (pas de 2e tir)
|
||||
// Communauté SNMP EN ÉCRITURE (Raisecom tech-2). Secret → env seulement, JAMAIS bundlé. Absente ⇒ run() tech-2 refuse.
|
||||
const OLT_RW_COMMUNITY = process.env.OLT_RW_COMMUNITY || ''
|
||||
const OLT_RO_COMMUNITY = process.env.OLT_RO_COMMUNITY || 'targosnmp'
|
||||
|
||||
// action → { verb HTTP n8n, champs requis, description FR }
|
||||
// OIDs Raisecom (MIB 8886) — EXTRAITS DU DRIVER F raisecom_rcmg.php (vérifiés à la source, pas reconstruits) :
|
||||
// reboot 1.3.6.1.4.1.8886.18.3.6.1.1.1.23.{olt_id}.4 = i 1 (olt_id = slot*1e7+port*1e5+ontid)
|
||||
// vlan 1.3.6.1.4.1.8886.18.3.6.6.1.1.7.{olt_id}.4 = i 40|666 (40=actif, 666=suspendu)
|
||||
// profile 1.3.6.1.4.1.8886.18.3.1.3.1.1.6.{onu_id} = i profil (onu_id = (slot+32)*8388608+port*65536+ontid)
|
||||
// save 1.3.6.1.4.1.8886.1.2.1.1.0 = i 2 (APRÈS chaque set)
|
||||
const RC = {
|
||||
REBOOT: (oltId) => `1.3.6.1.4.1.8886.18.3.6.1.1.1.23.${oltId}.4`,
|
||||
VLAN: (oltId) => `1.3.6.1.4.1.8886.18.3.6.6.1.1.7.${oltId}.4`,
|
||||
PROFILE: (onuId) => `1.3.6.1.4.1.8886.18.3.1.3.1.1.6.${onuId}`,
|
||||
SAVE: '1.3.6.1.4.1.8886.1.2.1.1.0',
|
||||
VLAN_ACTIVE: 40, VLAN_SUSPEND: 666,
|
||||
}
|
||||
const rcOltId = (s, p, o) => Number(s) * 10000000 + Number(p) * 100000 + Number(o)
|
||||
const rcOnuId = (s, p, o) => (Number(s) + 32) * 8388608 + Number(p) * 65536 + Number(o)
|
||||
|
||||
// action → verbe n8n (tech-3), champs requis, techs supportées. tech-3 = n8n ; tech-2 = SNMP natif (G3).
|
||||
const ACTIONS = {
|
||||
activate: { verb: 'POST', label: 'Activer / provisionner l’ONU', needs: ['sn', 'olt', 'profileid'] },
|
||||
replace: { verb: 'PUT', label: 'Remplacer l’ONU (échange de série)', needs: ['old_sn', 'new_sn', 'olt'] },
|
||||
speed: { verb: 'PATCH', label: 'Changer le forfait / la vitesse', needs: ['sn', 'olt', 'profileid'], extra: { trigger: 'speedchange' } },
|
||||
suspend: { verb: 'PATCH', label: 'Suspendre l’internet (VLAN)', needs: ['sn', 'olt'], extra: { profileid: '', trigger: 'suspend' } },
|
||||
unsuspend: { verb: 'PATCH', label: 'Rétablir l’internet (VLAN)', needs: ['sn', 'olt'], extra: { profileid: '', trigger: 'unsuspend' } },
|
||||
remove: { verb: 'DELETE', label: 'Retirer / désactiver l’ONU', needs: ['sn', 'olt'] },
|
||||
activate: { verb: 'POST', label: 'Activer / provisionner l’ONU', needs: ['sn', 'olt', 'profileid'], techs: [3] },
|
||||
replace: { verb: 'PUT', label: 'Remplacer l’ONU (échange de série)', needs: ['old_sn', 'new_sn', 'olt'], techs: [3] },
|
||||
speed: { verb: 'PATCH', label: 'Changer le forfait / la vitesse', needs: ['sn', 'olt', 'profileid'], extra: { trigger: 'speedchange' }, techs: [3, 2] },
|
||||
suspend: { verb: 'PATCH', label: 'Suspendre l’internet (VLAN)', needs: ['sn', 'olt'], extra: { profileid: '', trigger: 'suspend' }, techs: [3, 2] },
|
||||
unsuspend: { verb: 'PATCH', label: 'Rétablir l’internet (VLAN)', needs: ['sn', 'olt'], extra: { profileid: '', trigger: 'unsuspend' }, techs: [3, 2] },
|
||||
remove: { verb: 'DELETE', label: 'Retirer / désactiver l’ONU', needs: ['sn', 'olt'], techs: [3] },
|
||||
reboot: { verb: null, label: 'Redémarrer l’ONU', needs: ['sn', 'olt'], techs: [2] }, // tech-3 : utiliser le reboot TR-069
|
||||
}
|
||||
|
||||
// SNMP SET Raisecom (tech-2) : applique l'action + SAVE, via net-snmp. Renvoie {ok,error}. LECTURE : voir olt-snmp.
|
||||
function snmpRaisecom (action, { host, slot, port, ontid, profileid } = {}) {
|
||||
return new Promise((resolve) => {
|
||||
if (!OLT_RW_COMMUNITY) return resolve({ ok: false, error: 'OLT_RW_COMMUNITY non configurée (env) — écriture SNMP tech-2 désactivée' })
|
||||
if (host == null || slot == null || port == null || ontid == null) return resolve({ ok: false, error: 'slot/port/ontid/olt requis' })
|
||||
let snmp; try { snmp = require('net-snmp') } catch (e) { return resolve({ ok: false, error: 'net-snmp indisponible' }) }
|
||||
const oltId = rcOltId(slot, port, ontid); const onuId = rcOnuId(slot, port, ontid)
|
||||
let vb
|
||||
if (action === 'suspend') vb = { oid: RC.VLAN(oltId), type: snmp.ObjectType.Integer, value: RC.VLAN_SUSPEND }
|
||||
else if (action === 'unsuspend') vb = { oid: RC.VLAN(oltId), type: snmp.ObjectType.Integer, value: RC.VLAN_ACTIVE }
|
||||
else if (action === 'reboot') vb = { oid: RC.REBOOT(oltId), type: snmp.ObjectType.Integer, value: 1 }
|
||||
else if (action === 'speed') { const pr = parseInt(profileid, 10); if (!pr) return resolve({ ok: false, error: 'profileid (n° profil de ligne) requis pour le changement de vitesse' }); vb = { oid: RC.PROFILE(onuId), type: snmp.ObjectType.Integer, value: pr } }
|
||||
else return resolve({ ok: false, error: 'action SNMP tech-2 non supportée: ' + action })
|
||||
const session = snmp.createSession(String(host), OLT_RW_COMMUNITY, { timeout: 6000, retries: 1, version: snmp.Version2c })
|
||||
session.set([vb], (err) => {
|
||||
if (err) { try { session.close() } catch (e) {} return resolve({ ok: false, error: 'SNMP set: ' + err.message }) }
|
||||
// SAVE obligatoire après un set (sinon revert silencieux) — sauf reboot (F ne sauve pas après un reboot).
|
||||
if (action === 'reboot') { try { session.close() } catch (e) {} return resolve({ ok: true, oid: vb.oid }) }
|
||||
session.set([{ oid: RC.SAVE, type: snmp.ObjectType.Integer, value: 2 }], (err2) => {
|
||||
try { session.close() } catch (e) {}
|
||||
if (err2) return resolve({ ok: false, error: 'SNMP save: ' + err2.message, warn: 'set appliqué mais save échoué — peut se réverter' })
|
||||
resolve({ ok: true, oid: vb.oid, saved: true })
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function readStore () { try { return JSON.parse(fs.readFileSync(STORE, 'utf8')) } catch (e) { return {} } }
|
||||
|
|
@ -53,7 +99,24 @@ async function resolveEquip (serial) {
|
|||
fields: ['name', 'serial_number', 'mac_address', 'olt_ip', 'olt_slot', 'olt_port', 'olt_ontid', 'customer', 'service_location', 'status', 'equipment_type'],
|
||||
limit: 1,
|
||||
}).catch(() => [])
|
||||
return (rows && rows[0]) || null
|
||||
const equip = (rows && rows[0]) || null
|
||||
// Repli coordonnées ONU depuis le poller SNMP live (olt-snmp) : les coords Raisecom (tech-2) ne sont PAS mirorées
|
||||
// dans Service Equipment (elles vivent dans la table `fibre` de F) → sans ça, l'écriture SNMP tech-2 est impossible.
|
||||
if (!equip || equip.olt_ip == null || equip.olt_slot == null || equip.olt_ontid == null) {
|
||||
try {
|
||||
const onu = require('./olt-snmp').getOnuBySerial(String(serial))
|
||||
if (onu) {
|
||||
const merged = equip || { serial_number: String(serial) }
|
||||
if (merged.olt_ip == null) merged.olt_ip = onu.oltHost || null
|
||||
if (merged.olt_slot == null) merged.olt_slot = (onu.slot != null ? onu.slot : null)
|
||||
if (merged.olt_port == null) merged.olt_port = (onu.port != null ? onu.port : null)
|
||||
if (merged.olt_ontid == null) merged.olt_ontid = (onu.ontId != null ? onu.ontId : (onu.ontid != null ? onu.ontid : null))
|
||||
merged._coords_from_snmp = true
|
||||
return merged
|
||||
}
|
||||
} catch (e) { /* poller indispo → on garde equip tel quel */ }
|
||||
}
|
||||
return equip
|
||||
}
|
||||
|
||||
// Appel n8n dostuff avec un VERBE arbitraire + body JSON (le module https, comme dostuffStatus).
|
||||
|
|
@ -98,13 +161,24 @@ async function plan ({ serial, action, ...opts } = {}) {
|
|||
const equip = await resolveEquip(opts.old_sn || serial)
|
||||
const tech = techOf(opts.old_sn || serial, equip)
|
||||
const payload = buildPayload(action, equip, { ...opts, sn: serial, old_sn: opts.old_sn || serial })
|
||||
const method = tech === 2 ? 'snmp' : 'n8n' // tech-2 Raisecom = SNMP natif ; tech-3 TP-Link = n8n
|
||||
const supportsTech = tech != null && Array.isArray(spec.techs) && spec.techs.includes(tech)
|
||||
const warnings = []
|
||||
if (!equip) warnings.push('Aucun Service Equipment pour ce numéro de série — l’OLT/profil doivent être fournis manuellement.')
|
||||
if (tech === 2) warnings.push('ONU Raisecom (tech-2) : les écritures passent par SNMP direct (côté F), PAS par n8n. Écriture native SNMP non encore branchée (G3) → utiliser F pour l’instant.')
|
||||
if (tech == null) warnings.push('Technologie ONU inconnue (préfixe série non reconnu) — vérifier avant d’exécuter.')
|
||||
else if (!supportsTech) warnings.push('« ' + spec.label + ' » n’est pas supporté pour tech-' + tech + (tech === 3 ? ' via ce module (reboot = utiliser TR-069).' : ' (activate/replace/remove Raisecom = script CLI, non branché).'))
|
||||
if (!payload.olt) warnings.push('IP OLT manquante — requise.')
|
||||
const missing = spec.needs.filter(k => !payload[k] && payload[k] !== 0)
|
||||
// tech-2 : besoin des coordonnées ONU (slot/port/ontid) résolues + communauté RW configurée
|
||||
let snmpReady = true
|
||||
if (method === 'snmp') {
|
||||
const haveCoords = equip && equip.olt_slot != null && equip.olt_port != null && equip.olt_ontid != null
|
||||
if (!haveCoords) { warnings.push('Coordonnées ONU (slot/port/ontid) introuvables sur Service Equipment — requises pour l’écriture SNMP.'); snmpReady = false }
|
||||
if (!OLT_RW_COMMUNITY) { warnings.push('Communauté SNMP en écriture non configurée (env OLT_RW_COMMUNITY) → exécution SNMP désactivée.'); snmpReady = false }
|
||||
if (action === 'speed' && !(opts.profileid || (equip && equip.fibre_line_profile))) { warnings.push('Profil de ligne (profileid) requis pour le changement de vitesse.'); snmpReady = false }
|
||||
}
|
||||
const missing = method === 'n8n' ? spec.needs.filter(k => !payload[k] && payload[k] !== 0) : []
|
||||
if (missing.length) warnings.push('Champs manquants : ' + missing.join(', '))
|
||||
const canRun = supportsTech && !!payload.olt && (method === 'n8n' ? (tech === 3 && !missing.length) : snmpReady)
|
||||
const effects = []
|
||||
if (action === 'activate') effects.push('Provisionne l’ONU sur l’OLT + met Service Equipment « Actif ».')
|
||||
if (action === 'replace') effects.push('Ré-authentifie la nouvelle série sur l’OLT + met à jour le numéro de série de Service Equipment.')
|
||||
|
|
@ -113,12 +187,12 @@ async function plan ({ serial, action, ...opts } = {}) {
|
|||
if (action === 'unsuspend') effects.push('Rétablit le VLAN internet.')
|
||||
if (action === 'remove') effects.push('Retire l’ONU de l’OLT + délie Service Equipment.')
|
||||
return {
|
||||
ok: warnings.length === 0 || (tech === 3 && !missing.length && !!payload.olt),
|
||||
action, label: spec.label, verb: spec.verb, tech,
|
||||
target: { serial: serial || payload.sn, olt: payload.olt, slot: equip && equip.olt_slot, port: equip && equip.olt_port, ontid: equip && equip.olt_ontid, profile: payload.profileid },
|
||||
ok: canRun,
|
||||
action, label: spec.label, verb: spec.verb, tech, method,
|
||||
target: { serial: serial || payload.sn, olt: payload.olt, slot: equip && equip.olt_slot, port: equip && equip.olt_port, ontid: equip && equip.olt_ontid, profile: payload.profileid || (equip && equip.fibre_line_profile) || '' },
|
||||
equipment: equip ? equip.name : null, customer: equip && equip.customer, service_location: equip && equip.service_location,
|
||||
payload, effects, warnings,
|
||||
can_run: tech === 3 && !missing.length && !!payload.olt, // n8n = tech-3 seulement
|
||||
payload: method === 'n8n' ? payload : { olt: payload.olt, action, snmp: 'MIB 8886 (voir olt-ops.js)' }, effects, warnings,
|
||||
can_run: canRun,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,9 +209,14 @@ async function run ({ serial, action, confirm, idempotencyKey, actor, ...opts }
|
|||
const store = readStore()
|
||||
const prev = store[key]
|
||||
if (prev && (Date.now() - prev.at) < IDEMP_WINDOW_MS) return { ok: true, idempotent: true, ...prev.result }
|
||||
// FIRE
|
||||
const net = await dostuff(spec.verb, pl.payload)
|
||||
const result = { action, serial: pl.target.serial, olt: pl.payload.olt, verb: spec.verb, net_ok: !!net.ok, net_error: net.error || null, net_status: net.status, mirrored: [] }
|
||||
// FIRE — tech-3 = n8n dostuff ; tech-2 = SNMP set Raisecom (+ save).
|
||||
let net
|
||||
if (pl.method === 'snmp') {
|
||||
net = await snmpRaisecom(action, { host: pl.target.olt, slot: pl.target.slot, port: pl.target.port, ontid: pl.target.ontid, profileid: opts.profileid || (pl.target.profile) })
|
||||
} else {
|
||||
net = await dostuff(spec.verb, pl.payload)
|
||||
}
|
||||
const result = { action, serial: pl.target.serial, olt: pl.target.olt, method: pl.method, verb: spec.verb, net_ok: !!net.ok, net_error: net.error || null, net_status: net.status, mirrored: [] }
|
||||
if (net.ok && pl.equipment) {
|
||||
// Miroir ERPNext (fill-only, best-effort) — reflète l'effet sur Service Equipment.
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user