/* * USMT Dash - device info helpers * * Shared by the details dialog, Custom View, the tables and the Overview page: * - Advanced fields say WHY a value is missing ("Not in AD", "Not in SCCM", "No DNS record") * instead of a bare N/A, and "Waiting for worker..." when the worker hasn't looked yet. * - Presence: last time a device answered a ping and how long it has been offline. * - Snapshot markers: values the worker saved while a device was reachable are shown with * a clock icon and the time they were captured. */ (function () { 'use strict'; const WAITING = 'Waiting for worker...'; const AD_FIELDS = new Set(['OU', 'Groups']); // Fields computed here rather than stored by the worker (also offered as Custom View columns) const COMPUTED = { DaysOffline: 'Days Offline', LastOnline: 'Last Online', LastChecked: 'Last Ping Check', FoundIn: 'Found In (AD / SCCM / DNS)' }; // Internal bookkeeping fields that aren't useful as columns const HIDDEN = new Set(['InAD', 'InSCCM', 'InDNS', 'Fetched', 'LastPing', 'FirstChecked', 'LastOnlineSource']); const cache = () => (typeof advancedDataCache !== 'undefined' && advancedDataCache) ? advancedDataCache : {}; const isName = n => !!n && n !== 'N/A' && n !== '---'; const empty = v => v === undefined || v === null || v === '' || v === 'N/A'; function entry(name) { if (!isName(name)) return null; const c = cache(); return c[name] || c[String(name).toUpperCase()] || null; } function toDate(iso) { if (!iso) return null; const d = new Date(iso); return isNaN(d) ? null : d; } function fmtWhen(iso) { const d = toDate(iso); if (!d) return ''; const sameYear = d.getFullYear() === new Date().getFullYear(); return d.toLocaleString(undefined, { month: 'short', day: 'numeric', year: sameYear ? undefined : 'numeric', hour: 'numeric', minute: '2-digit' }); } function ago(iso) { const d = toDate(iso); if (!d) return ''; const s = Math.max(0, (Date.now() - d.getTime()) / 1000); if (s < 60) return 'just now'; if (s < 3600) return Math.floor(s / 60) + ' min ago'; if (s < 86400) return Math.floor(s / 3600) + ' h ago'; const days = Math.floor(s / 86400); return days + (days === 1 ? ' day ago' : ' days ago'); } // Why a field is empty, from the worker's lookup flags (null = no particular reason) function missingReason(a, key) { const noAD = a.InAD === false, noSCCM = a.InSCCM === false, noDNS = a.InDNS === false; if (noAD && noSCCM && noDNS) return 'Not in AD, SCCM or DNS'; if (AD_FIELDS.has(key)) return noAD ? 'Not in AD' : null; if (key === 'IP') { if (noDNS && noSCCM) return 'No DNS record, not in SCCM'; if (noDNS) return 'No DNS record'; return null; } return noSCCM ? 'Not in SCCM' : null; } // { text, state } where state is value | waiting | missing | na function field(name, key) { if (!isName(name)) return { text: 'N/A', state: 'na' }; if (COMPUTED[key]) return computed(name, key); const a = entry(name); if (!a) return { text: WAITING, state: 'waiting' }; const v = a[key]; // the worker's default room type isn't real data when SCCM has no record if (key === 'RoomType' && a.InSCCM === false && v === 'Standard') return { text: missingReason(a, key), state: 'missing' }; if (!empty(v)) return { text: String(v), state: 'value' }; const why = missingReason(a, key); return why ? { text: why, state: 'missing' } : { text: 'N/A', state: 'na' }; } // --------------------------------------------------------------------------------- // Presence // --------------------------------------------------------------------------------- function presence(name) { const a = entry(name); if (!a || !a.LastChecked) return null; const last = toDate(a.LastOnline); const online = a.LastPing === 'Online'; const days = online ? 0 : (last ? (Date.now() - last.getTime()) / 86400000 : null); return { online, last, lastIso: a.LastOnline, checked: a.LastChecked, ping: a.LastPing, first: a.FirstChecked, days, source: a.LastOnlineSource }; } // Skip USMT tags are inventory only: the worker never pings them function notPinged(name) { try { const rows = typeof apiData !== 'undefined' && Array.isArray(apiData) ? apiData : []; return rows.some(r => r['Source Device'] === name && r.Pairing === 'Skipped'); } catch (e) { return false; } } const NOT_PINGED = 'Not pinged (Skip USMT tag)'; // Whole days offline (0 when online or offline less than a day), null if never seen function daysOffline(name) { const p = presence(name); if (!p) return undefined; if (p.online) return 0; if (p.days === null) return null; return Math.floor(p.days); } function offlineText(name) { if (!isName(name)) return 'N/A'; const p = presence(name); if (!p) return WAITING; if (p.online) return 'Online now'; if (!p.last) return 'Never seen online (tracked since ' + fmtWhen(p.first) + ')'; const hours = (Date.now() - p.last.getTime()) / 3600000; if (hours < 1) return 'Offline less than an hour'; if (hours < 24) return 'Offline ' + Math.floor(hours) + ' h'; const d = Math.floor(hours / 24); return d + (d === 1 ? ' day' : ' days') + ' offline'; } function computed(name, key) { const a = entry(name); if (!a) return { text: WAITING, state: 'waiting' }; if (['DaysOffline', 'LastOnline', 'LastChecked'].includes(key) && !a.LastChecked && notPinged(name)) return { text: NOT_PINGED, state: 'na' }; if (key === 'DaysOffline') { const d = daysOffline(name); if (d === undefined) return { text: WAITING, state: 'waiting' }; if (d === null) return { text: 'Never seen', state: 'value' }; if (d === 0) return { text: presence(name).online ? 'Online' : '< 1 day', state: 'value' }; return { text: d + (d === 1 ? ' day' : ' days'), state: 'value' }; } if (key === 'LastOnline') { const p = presence(name); if (!p) return { text: WAITING, state: 'waiting' }; if (p.online) return { text: 'Online now', state: 'value' }; return p.last ? { text: fmtWhen(p.lastIso), state: 'value' } : { text: 'Never seen', state: 'value' }; } if (key === 'LastChecked') { return a.LastChecked ? { text: fmtWhen(a.LastChecked) + ' (' + (a.LastPing || '?') + ')', state: 'value' } : { text: WAITING, state: 'waiting' }; } if (key === 'FoundIn') { if (a.InAD === undefined && a.InSCCM === undefined && a.InDNS === undefined) return { text: WAITING, state: 'waiting' }; const yes = [], no = []; [['AD', a.InAD], ['SCCM', a.InSCCM], ['DNS', a.InDNS]].forEach(([n, v]) => { if (v === true) yes.push(n); else if (v === false) no.push(n); }); if (!no.length) return { text: 'AD, SCCM, DNS', state: 'value' }; return { text: 'Missing from ' + no.join(', '), state: 'missing' }; } return { text: 'N/A', state: 'na' }; } // True when AD, SCCM and DNS all said "never heard of it" (not imaged yet, or a typo) function unknownEverywhere(name) { const a = entry(name); return !!a && a.InAD === false && a.InSCCM === false && a.InDNS === false; } // HTML for a details-dialog cell function html(name, key) { const f = field(name, key); if (f.state === 'waiting') return `${f.text}`; if (f.state === 'missing') return `${f.text}`; return f.text; } function presenceHtml(name) { if (!isName(name)) return 'N/A'; const p = presence(name); if (!p) return notPinged(name) ? `${NOT_PINGED}` : `${WAITING}`; const txt = offlineText(name); const cls = p.online ? 'di-online' : (p.days !== null && p.days >= 3 ? 'di-offline-long' : 'di-offline'); let sub = ''; if (!p.online && p.last) sub = `Last online ${fmtWhen(p.lastIso)}${p.source === 'Inventory' ? ' (from client activity)' : ''}`; sub += `Last checked ${fmtWhen(p.checked)} (${ago(p.checked)})`; return `${txt}${sub}`; } // --------------------------------------------------------------------------------- // Snapshot / staleness markers // --------------------------------------------------------------------------------- const CLOCK = ''; // side: 'Source' | 'Destination' function snapshotMark(row, side) { const at = row && row[side + ' Snapshot']; if (!at) return ''; const why = row['Backup Status'] === 'Completed' ? 'migration completed' : 'device unreachable'; return ` ${CLOCK}`; } // Ping results older than 15 minutes (completed pairs are only re-pinged by the presence sweep) function pingMark(row, side) { const at = row && row[side + ' Checked']; const d = toDate(at); if (!d || Date.now() - d.getTime() < 15 * 60000) return ''; return ` ${CLOCK}`; } function snapshotLine(row, side) { const at = row && row[side + ' Snapshot']; if (!at) return ''; return `${CLOCK.replace('`; } // Software inventory timestamp for a device (Software-Cache.json entries carry one) window.DeviceInfo = { WAITING, COMPUTED, HIDDEN, entry, field, html, unknownEverywhere, presence, presenceHtml, daysOffline, offlineText, snapshotMark, snapshotLine, pingMark, fmtWhen, ago, isName }; })(); // Data Synchronization dialog: per-type timing of the last cycle (SCCM associations vs // manual pairs vs completed snapshots...), the phases, and the presence sweep schedule. (function () { 'use strict'; const esc = v => String(v).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const secs = s => (s >= 60 ? (s / 60).toFixed(1) + ' min' : (+s).toFixed(1) + ' s'); const per = ms => (ms >= 1000 ? (ms / 1000).toFixed(1) + ' s' : ms + ' ms'); const HINT = { SCCM: 'Association lookup, ping + disk + log on both devices', Manual: 'No SCCM lookup: ping both devices only', Cached: 'Completed: shown from its snapshot, no network checks', NoAssoc: 'No association: source checked on its own', Skipped: 'Inventory only, no network checks' }; window.DeviceInfo.syncBreakdownHtml = function (state) { const c = state && state.LastCycle; if (!c || !Array.isArray(c.Categories)) return ''; const rows = c.Categories.map(x => `${esc(x.Label)}${x.Count}${secs(x.Seconds)}${per(x.AvgMs)}`).join(''); const ph = c.Phases || {}; const ps = c.PresenceSweep || {}; let presence = ''; if (ps.Last) { const next = new Date(new Date(ps.Last).getTime() + (ps.EveryMinutes || 60) * 60000); presence = `Presence sweep: last ${DeviceInfo.fmtWhen(ps.Last)}${ps.Ran ? ` (pinged ${ps.Pinged} more)` : ''}, next after ${next.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}`; } return ` Last cycle by type ${[c.NewFirst ? `${c.NewFirst} new device(s) swept first` : '', c.FullRefresh ? 'full refresh' : ''].filter(Boolean).join(' · ')} TypeDevicesTimeAvg${rows} Software ${secs(ph.Software || 0)} · Sweep ${secs(ph.Sweep || 0)} · Presence ${secs(ph.Presence || 0)} · Save ${secs(ph.Save || 0)} ${presence ? `${presence}` : ''}`; }; })();