Presence, AD/SCCM/DNS reasons, monitors, snapshots, new-device priority
Worker: pings every tracked device on a presence sweep (hourly in business hours plus midnight) and records last online; inventory reports whether a device was found in AD and SCCM; monitors from WmiMonitorID; snapshots of the last reachable state replace "Cached"; newly tracked devices are swept and published first; totals count source devices only; per-type timing in Worker-State. Front end: details dialog shows presence, reasons instead of N/A, "Waiting for worker...", monitors and snapshot times, and refreshes in place; Custom View gains Days Offline, Last Online, Found In and monitor columns; manual pair changes show immediately; sync dialog shows timing by type. Custom View gets a large scrollbar; "Exactly ticked" filter.
This commit is contained in:
1 parent
50c2a18a68
commit
7868948bfc
10 files changed
+1230
-356
No files matched your search
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* 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 };
|
||||
}
|
||||
|
||||
// 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 (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' : '0 days', 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 `<span class="di-waiting">${f.text}</span>`;
|
||||
if (f.state === 'missing') return `<span class="di-missing">${f.text}</span>`;
|
||||
return f.text;
|
||||
}
|
||||
|
||||
function presenceHtml(name) {
|
||||
if (!isName(name)) return 'N/A';
|
||||
const p = presence(name);
|
||||
if (!p) return `<span class="di-waiting">${WAITING}</span>`;
|
||||
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)' : ''}<br>`;
|
||||
sub += `Last checked ${fmtWhen(p.checked)} (${ago(p.checked)})`;
|
||||
return `<b class="${cls}">${txt}</b><br><span class="di-sub">${sub}</span>`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Snapshot / staleness markers
|
||||
// ---------------------------------------------------------------------------------
|
||||
const CLOCK = '<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M8 4.5V8l2.5 1.6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
|
||||
|
||||
// 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 ` <span class="snap-mark" title="Last captured ${fmtWhen(at)} (${why})">${CLOCK}</span>`;
|
||||
}
|
||||
|
||||
// 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 ` <span class="snap-mark" title="Checked ${fmtWhen(at)}">${CLOCK}</span>`;
|
||||
}
|
||||
|
||||
function snapshotLine(row, side) {
|
||||
const at = row && row[side + ' Snapshot'];
|
||||
if (!at) return '';
|
||||
return `<br><span class="di-sub">${CLOCK.replace('<svg', '<svg class="di-inline-clock"')} Snapshot from ${fmtWhen(at)} (${ago(at)})</span>`;
|
||||
}
|
||||
|
||||
// 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 => `<tr title="${esc(HINT[x.Key] || '')}"><td>${esc(x.Label)}</td><td>${x.Count}</td><td>${secs(x.Seconds)}</td><td>${per(x.AvgMs)}</td></tr>`).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 `
|
||||
<div class="sync-bd-title">Last cycle by type <span>${[c.NewFirst ? `${c.NewFirst} new device(s) swept first` : '', c.FullRefresh ? 'full refresh' : ''].filter(Boolean).join(' · ')}</span></div>
|
||||
<table class="sync-bd"><thead><tr><th>Type</th><th>Devices</th><th>Time</th><th>Avg</th></tr></thead><tbody>${rows}</tbody></table>
|
||||
<div class="sync-bd-phases">Software ${secs(ph.Software || 0)} · Sweep ${secs(ph.Sweep || 0)} · Presence ${secs(ph.Presence || 0)} · Save ${secs(ph.Save || 0)}</div>
|
||||
${presence ? `<div class="sync-bd-phases">${presence}</div>` : ''}`;
|
||||
};
|
||||
})();
|
||||
Reference in new issue
Block a user