async function loadBridges() { const el = document.getElementById('bridge-list'); if (!el) return; el.innerHTML = '
'; try { const r = await workerGet(`/api/itsm/active-bridges?project=${P()}`); const bridges = r.bridges || []; // KPI const active = bridges.length; const avgAge = active > 0 ? Math.round(bridges.reduce((s,b) => s + b.age_minutes, 0) / active) : 0; const comms = bridges.filter(b => b.comms_sent === 'Yes').length; const pir = bridges.filter(b => b.pir_date).length; document.getElementById('bri-kpi-active').textContent = active; document.getElementById('bri-kpi-age').textContent = avgAge || '–'; document.getElementById('bri-kpi-comms').textContent = comms; document.getElementById('bri-kpi-pir').textContent = pir; // Badge sidebar const badge = document.getElementById('bridge-badge'); if (badge) { badge.style.display = active > 0 ? '' : 'none'; badge.textContent = active; } if (bridges.length === 0) { el.innerHTML = '
Nessun Major Incident attivo
Tutti i sistemi operativi
'; return; } el.innerHTML = bridges.map(b => { const ageColor = b.age_minutes > 120 ? 'var(--red)' : b.age_minutes > 60 ? 'var(--orange)' : 'var(--green)'; const ageLabel = b.age_minutes >= 60 ? `${Math.floor(b.age_minutes/60)}h ${b.age_minutes%60}m` : `${b.age_minutes}min`; return `
${b.key} BRIDGE ATTIVO ⏱ ${ageLabel}
${b.summary}
Commander: ${b.commander} · Servizio: ${b.affected_service||'–'}
${b.bridge_url ? `🎙 Entra nel bridge` : ''}
${b.status_update ? `
Ultimo aggiornamento: ${b.status_update}
` : ''}
Comms clienti: ${b.comms_sent} ${b.pir_date ? `PIR: ${new Date(b.pir_date).toLocaleDateString('it-IT')}` : ''} Stato: ${b.status}
`; }).join(''); } catch(e) { el.innerHTML = '
⚠️
Errore caricamento bridge
'; } } async function openBridgeDetail(key) { try { const r = await workerGet(`/api/issue?key=${key}`); const f = r.issue?.fields || {}; document.getElementById('modal-bridge-key').textContent = `🌉 ${key} — Major Incident Bridge`; document.getElementById('modal-bridge-body').innerHTML = `
${f.priority?.name||'–'} ${f.status?.name||'–'}
${f.summary||'–'}
Commander:
${f.customfield_incident_commander?.displayName||f.assignee?.displayName||'–'}
Servizio impattato:
${f.customfield_affected_service||'–'}
Bridge URL:
${f.customfield_bridge_url?`Accedi al bridge →`:'Non configurato'}
Comms clienti inviate:
${f.customfield_customer_comms_sent?.value||'No'}
PIR pianificata:
${f.customfield_pir_date?new Date(f.customfield_pir_date).toLocaleDateString('it-IT'):'Da pianificare'}
RCA:
${f.customfield_rca||'In corso...'}
${f.customfield_bridge_status_update ? `
Ultimo status update
${f.customfield_bridge_status_update}
` : ''} ${f.customfield_bridge_participants ? `
Partecipanti bridge
${f.customfield_bridge_participants}
` : ''}
${f.customfield_bridge_url?`🎙 Entra nel bridge`:''} Apri in Jira →
`; openModal('modal-bridge'); } catch(e) { nhToast('Errore caricamento bridge', 'error'); } } function declareMajorIncident() { nhToast('Apri un ticket IT-MAJ in Jira per dichiarare un Major Incident — il bridge verrà creato automaticamente.', 'warning'); window.open(`${ITSMOPS_CONFIG.jira_base_url}/jira/software/projects/${P()}/boards`, '_blank'); } // ══════════════════════════════════════════════════════════ // ON-CALL SCHEDULE // ══════════════════════════════════════════════════════════ async function loadOnCall() { const nowEl = document.getElementById('oncall-now'); const calEl = document.getElementById('oncall-calendar-list'); if (!nowEl || !calEl) return; try { const today = new Date().toISOString().split('T')[0]; const [nowR, calR] = await Promise.all([ workerGet(`/api/itsm/oncall-schedule?project=${P()}&date=${today}`), workerGet(`/api/itsm/oncall-calendar?project=${P()}&weeks=4`), ]); // Turno attivo ora const active = nowR.schedules || []; if (active.length === 0) { nowEl.innerHTML = '
Nessun turno configurato per oggi.
'; } else { nowEl.innerHTML = active.map(s => `
${s.assignee.split(' ').map(w=>w[0]).slice(0,2).join('')}
${s.assignee}
${s.rotation} · Tier ${s.tier} · Escalation: ${s.escalation_policy}
${s.contact_phone ? `📞 ${s.contact_phone}` : ''} Turno attivo
`).join('
'); } // Calendario const calendar = calR.calendar || []; if (calendar.length === 0) { calEl.innerHTML = '
📅
Nessun turno pianificato
Aggiungi turni per le prossime settimane
'; return; } const tierColor = { L1:'var(--blue)', L2:'var(--purple)', L3:'var(--orange)', Manager:'var(--red)' }; calEl.innerHTML = ` ${calendar.map(i => { const f = i.fields; const tier = f.customfield_oncall_tier?.value || 'L1'; const start = f.customfield_oncall_schedule_date ? new Date(f.customfield_oncall_schedule_date).toLocaleString('it-IT',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}) : '–'; const end = f.customfield_oncall_end_date ? new Date(f.customfield_oncall_end_date).toLocaleString('it-IT',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}) : '–'; return ``; }).join('')}
OggettoResponsabileRotazioneTier InizioFineTelefono
${f.summary||i.key} ${f.assignee?.displayName||'Non assegnato'} ${f.customfield_oncall_rotation?.value||'–'} ${tier} ${start} ${end} ${f.customfield_oncall_contact_phone||'–'}
`; } catch(e) { if (nowEl) nowEl.innerHTML = '
Errore caricamento
'; if (calEl) calEl.innerHTML = '
⚠️
Errore caricamento
'; } } async function submitOnCallTurno() { const summary = document.getElementById('oc-summary')?.value?.trim(); const start = document.getElementById('oc-start')?.value; const end = document.getElementById('oc-end')?.value; const rotation = document.getElementById('oc-rotation')?.value; const tier = document.getElementById('oc-tier')?.value; const phone = document.getElementById('oc-phone')?.value?.trim(); const escalation = document.getElementById('oc-escalation')?.value; if (!summary || !start || !end) { nhToast('Compila oggetto, inizio e fine turno', 'warning'); return; } try { const fields = { project: { key: P() }, issuetype: { name: `${P()}-IT-ONCALL` }, summary, customfield_oncall_schedule_date: start, customfield_oncall_end_date: end, customfield_oncall_rotation: rotation ? { value: rotation } : undefined, customfield_oncall_tier: tier ? { value: tier } : undefined, customfield_oncall_contact_phone: phone || undefined, customfield_oncall_escalation_policy: escalation ? { value: escalation } : undefined, }; // Rimuovi campi undefined Object.keys(fields).forEach(k => fields[k] === undefined && delete fields[k]); const r = await workerPost('/api/issue', { body: { fields } }); if (r.key) { nhToast(`Turno ${r.key} creato!`, 'success'); closeModal('modal-oncall'); loadOnCall(); } } catch(e) { nhToast('Errore creazione turno: ' + (e.message||''), 'error'); } } function openNewOnCallModal() { // Pre-compila data/ora di default (ora corrente + 8 ore) const now = new Date(); const end = new Date(now.getTime() + 8 * 3600000); const fmt = d => d.toISOString().slice(0,16); const startEl = document.getElementById('oc-start'); const endEl = document.getElementById('oc-end'); if (startEl) startEl.value = fmt(now); if (endEl) endEl.value = fmt(end); openModal('modal-oncall'); } // ══ HOOK: Aggiorna showPage per Bridge e OnCall ════════════ const _showPageOrig2 = typeof showPage === 'function' ? showPage : null; if (_showPageOrig2) { const _prevShowPage = window.showPage || _showPageOrig2; window.showPage = function(name) { _prevShowPage(name); if (name === 'bridges') loadBridges(); if (name === 'oncall') loadOnCall(); if (name === 'sla') loadSLAFull(); const titles2 = { bridges: 'Major Incident Bridge', oncall: 'On-Call Schedule' }; if (titles2[name]) document.getElementById('topbar-title').textContent = titles2[name]; }; } // ══════════════════════════════════════════════════════════ // AIOPS — Correlation, Anomaly Detection, Capacity Forecast // ══════════════════════════════════════════════════════════ async function runAIOpsCorrelation() { const panel = document.getElementById('aiops-correlation-panel'); if (!panel) return; panel.innerHTML = '
Analisi correlazione in corso…
'; document.getElementById('aiops-kpi-clusters').textContent = '…'; document.getElementById('aiops-kpi-highconf').textContent = '…'; document.getElementById('aiops-kpi-problems').textContent = '…'; try { const r = await workerPost('/api/itsm/aiops-correlate', { project: P() }); // KPI document.getElementById('aiops-kpi-clusters').textContent = r.total_clusters ?? 0; document.getElementById('aiops-kpi-highconf').textContent = r.high_confidence_count ?? 0; document.getElementById('aiops-kpi-problems').textContent = r.auto_created_problem ? '1 ✓' : '0'; const clusters = r.clusters || []; if (!clusters.length) { panel.innerHTML = '
✅ Nessuna correlazione significativa rilevata — ' + (r.incidents_analyzed||0) + ' incident analizzati.
'; return; } const confColor = c => c >= 80 ? 'var(--red)' : c >= 60 ? 'var(--orange)' : 'var(--blue)'; const actionLabel = { create_problem:'🔴 Crea Problem', merge_tickets:'🔗 Mergia ticket', escalate:'⬆ Escalation', monitor:'👁 Monitora' }; panel.innerHTML = `
${r.incidents_analyzed||0} incident analizzati · ${clusters.length} cluster trovati · Timestamp: ${r.timestamp ? new Date(r.timestamp).toLocaleTimeString('it-IT') : '–'}
${r.auto_created_problem ? `
✅ Problem record creato automaticamente: ${r.auto_created_problem}
` : ''} ${clusters.map((c, i) => `
Cluster ${i+1} Score: ${c.correlation_score}% ${actionLabel[c.recommended_action]||c.recommended_action} Confidenza: ${c.confidence}%
🔧 Servizio: ${c.common_service||'–'} 📁 Categoria: ${c.common_category||'–'}
Root cause probabile: ${c.probable_root_cause}
${(c.incident_keys||[]).map(k => `${k}`).join('')}
`).join('')}`; } catch(e) { panel.innerHTML = `
Errore AIOps: ${e.message}
`; document.getElementById('aiops-kpi-clusters').textContent = '–'; } } async function runAIOpsAnomaly() { const el = document.getElementById('ai-tools-text'); const title = document.getElementById('ai-tools-result-title'); if (title) title.textContent = '🚨 Anomaly Detection'; if (el) el.textContent = 'Analisi anomalie in corso…'; try { const r = await workerGet(`/api/itsm/ai-anomaly?project=${P()}`); document.getElementById('aiops-kpi-anomalies').textContent = r.total_anomalies ?? 0; const anomalies = r.anomalies || []; if (!anomalies.length) { if (el) el.textContent = `✅ Nessuna anomalia rilevata nel periodo analizzato. Incident correnti: ${r.current_period}, baseline: ${r.baseline_period}.`; return; } const sevColor = { critical:'var(--red)', high:'var(--orange)', medium:'var(--blue)', low:'var(--green)' }; if (el) el.innerHTML = `
${r.current_period} incident (7gg) vs ${r.baseline_period} baseline · Generato: ${r.generated_at ? new Date(r.generated_at).toLocaleString('it-IT') : '–'}
${r.immediate_action_required ? '
⚠️ Azione immediata richiesta
' : ''} ${anomalies.map(a => `
${a.service} ${a.severity} ${a.type}
${a.description}
Baseline: ${a.baseline_count} → Corrente: ${a.current_count} (score: ${a.anomaly_score})
→ ${a.recommended_action}
`).join('')}`; } catch(e) { if (el) el.textContent = 'Errore anomaly detection: ' + e.message; } } async function runAIOpsCapacity() { const el = document.getElementById('ai-tools-text'); const title = document.getElementById('ai-tools-result-title'); if (title) title.textContent = '📊 Capacity Forecast'; if (el) el.textContent = 'Generazione forecast in corso…'; try { const r = await workerGet(`/api/itsm/ai-capacity-forecast?project=${P()}`); const forecast = r.forecast || []; if (!forecast.length) { if (el) el.textContent = r.error || 'Dati storici insufficienti per generare un forecast.'; return; } if (el) el.innerHTML = `
Forecast basato su ${r.historical_weeks} settimane storiche · Generato: ${r.generated_at ? new Date(r.generated_at).toLocaleString('it-IT') : '–'}
${forecast.map(f => ``).join('')}
SettimanaIncident previstiSR previsteConfidenza
${f.week} ${f.predicted_incidents} ${f.predicted_sr} ${f.confidence}%
${r.staff_recommendation ? `
Raccomandazione staff: ${r.staff_recommendation}
` : ''} ${r.risk_weeks?.length ? `
⚠️ Settimane a rischio: ${r.risk_weeks.join(', ')}
` : ''} ${r.key_drivers?.length ? `
Driver principali: ${r.key_drivers.join(' · ')}
` : ''}`; } catch(e) { if (el) el.textContent = 'Errore capacity forecast: ' + e.message; } } async function runSelfHeal() { const key = document.getElementById('selfheal-key')?.value?.trim(); if (!key) { nhToast('Inserisci la chiave ticket', 'warning'); return; } const el = document.getElementById('selfheal-result'); if (!el) return; el.style.display = 'block'; el.innerHTML = '
Generazione runbook AI in corso…
'; try { const r = await workerPost('/api/ai-analyze', { type: 'self-heal', project: P(), context: { ticket_key: key, message: `Genera runbook self-heal per ticket ${key}` } }); const result = r.result ? (typeof r.result === 'string' ? JSON.parse(r.result) : r.result) : {}; const analysis = result.incident_analysis || {}; const runbook = result.runbook || {}; const steps = runbook.steps || []; el.innerHTML = `
${runbook.title || 'Runbook AI — ' + key}
🎯 Root cause: ${analysis.root_cause_hypothesis||'–'} ⏱ ETA: ${runbook.estimated_resolution_min||'–'} min 🤖 Automazione: ${result.automation_coverage_pct||0}% 📊 Confidenza: ${analysis.confidence||0}%
${steps.map(s => `
${s.step}
${s.action}
${s.command_or_api ? `${s.command_or_api}` : ''}
✓ ${s.verification}
${s.requires_approval ? 'Richiede approvazione' : ''}
${s.automated?'AUTO':'MANUALE'}
`).join('')} ${result.escalation_required ? `
⬆ Escalation richiesta: ${result.escalation_reason}
` : ''}
`; } catch(e) { el.innerHTML = `
Errore: ${e.message}
`; } } // ══ HOOK showPage per AI-tools ═══════════════════════════ const _showPageOrig5 = window.showPage; if (_showPageOrig5) { window.showPage = function(name) { _showPageOrig5(name); if (name === 'ai-tools') { document.getElementById('topbar-title').textContent = 'AI Tools & AIOps'; } }; } // ══════════════════════════════════════════════════════════ // i18n Operator — lingua sincronizzata con Employee // ══════════════════════════════════════════════════════════ (function initOpsLang() { try { const lang = localStorage.getItem('nh_lang') || ITSMOPS_CONFIG.language || 'it'; if (lang !== 'it') { const langLabels = { en: 'EN', es: 'ES', pt: 'PT', it: 'IT' }; const topbarUser = document.getElementById('topbar-uname'); if (topbarUser) { const pill = document.createElement('span'); pill.style.cssText = 'font-size:10px;padding:1px 6px;border-radius:20px;background:var(--surface-3);color:var(--text-3);margin-left:4px;cursor:pointer'; pill.textContent = langLabels[lang] || 'IT'; pill.title = 'Language: ' + lang.toUpperCase(); topbarUser.parentElement?.appendChild(pill); } } } catch(e) {} })(); // ══════════════════════════════════════════════════════════ // PWA — Operator portal install prompt // ══════════════════════════════════════════════════════════ (function registerOpsPWA() { if (!document.querySelector('meta[name="theme-color"]')) { const meta = document.createElement('meta'); meta.name = 'theme-color'; meta.content = '#1a1a2e'; document.head.appendChild(meta); } if (!document.querySelector('link[rel="manifest"]')) { const link = document.createElement('link'); link.rel = 'manifest'; link.href = 'data:application/json,' + encodeURIComponent(JSON.stringify({ name: (ITSMOPS_CONFIG.project_name || 'NH') + ' IT Ops', short_name: 'IT Ops', start_url: './', display: 'standalone', background_color: '#1a1a2e', theme_color: '#156082', icons: [ { src: 'https://via.placeholder.com/192x192/156082/ffffff?text=OPS', sizes: '192x192', type: 'image/png' }, { src: 'https://via.placeholder.com/512x512/156082/ffffff?text=OPS', sizes: '512x512', type: 'image/png' }, ] })); document.head.appendChild(link); } })();