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
+198
-100
@@ -196,11 +196,11 @@
|
||||
.sort-icon.active { color: white; font-weight: bold; }
|
||||
|
||||
/* ===== Custom View responsive table ===== */
|
||||
.custom-scroll-area { overflow: auto; }
|
||||
#custom { min-width: 0; max-width: 100%; width: 100%; overflow-x: hidden; box-sizing: border-box; }
|
||||
#custom * { min-width: 0; }
|
||||
#wrapper-custom { min-width: 0; max-width: 100%; width: 100%; overflow: hidden; box-sizing: border-box; }
|
||||
#scroll-custom { max-width: 100%; overflow: auto; max-height: calc(100vh - 60px - 50px - 210px); }
|
||||
.custom-scroll-area { overflow: auto; }
|
||||
#custom { min-width: 0; max-width: 100%; width: 100%; overflow-x: hidden; box-sizing: border-box; }
|
||||
#custom * { min-width: 0; }
|
||||
#wrapper-custom { min-width: 0; max-width: 100%; width: 100%; overflow: hidden; box-sizing: border-box; }
|
||||
#scroll-custom { max-width: 100%; overflow: auto; max-height: calc(100vh - 60px - 50px - 210px); }
|
||||
#custom-table-host { max-width: none; }
|
||||
#wrapper-custom.has-extra .custom-scroll-area thead th { position: sticky; top: 0; z-index: 20; box-shadow: inset 0 -2px 0 var(--border-light); }
|
||||
/* Header: bordered drag-grip strip on top; label left; icons grouped far right */
|
||||
@@ -226,10 +226,10 @@
|
||||
#table-custom .custom-th .col-remove { color: var(--danger, #e74c3c); font-weight: bold; cursor: pointer; opacity: 0; transition: opacity 0.15s; font-size: 1em; }
|
||||
#table-custom .custom-th:hover .col-remove { opacity: 1; }
|
||||
/* Full-height resize handles (overlay), active anywhere along the column border */
|
||||
#custom-resize-overlay { z-index: 40; }
|
||||
#custom-resize-overlay { z-index: 40; }
|
||||
#custom-resize-overlay .rz-handle { position: absolute; top: 0; width: 9px; height: 100%; cursor: col-resize; pointer-events: auto; transform: translateX(-50%); z-index: 40; }
|
||||
#custom-resize-overlay .rz-handle::after { content: ""; position: absolute; left: 50%; top: 0; width: 1px; height: 100%; background: transparent; transform: translateX(-50%); }
|
||||
#table-custom .custom-spacer-th, #table-custom .custom-spacer-td { border-right: none !important; border-left: none !important; background: var(--bg-card); }
|
||||
#table-custom .custom-spacer-th, #table-custom .custom-spacer-td { border-right: none !important; border-left: none !important; background: var(--bg-card); }
|
||||
#table-custom thead th.no-copy:not(.custom-th) { border-left: none; }
|
||||
#wrapper-custom.has-extra #table-custom tbody tr:hover { filter: brightness(1.08); }
|
||||
#wrapper-custom.has-extra #table-custom td, #wrapper-custom.has-extra #table-custom th { border-right: 1px solid var(--border-light); }
|
||||
@@ -353,6 +353,7 @@
|
||||
</style>
|
||||
<link rel="stylesheet" href="/layout.css">
|
||||
<script src="/layout.js"></script>
|
||||
<script src="/device-info.js"></script>
|
||||
<script src="/excel-filter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -662,6 +663,7 @@
|
||||
|
||||
<div class="sync-details" id="syncDetailsText">Waiting for server response...</div>
|
||||
<div class="sync-details" id="syncDurationText" style="color: var(--text-main); font-style: italic; margin-top: 5px;">Last Cycle Duration: N/A</div>
|
||||
<div id="syncBreakdown" class="sync-breakdown"></div>
|
||||
|
||||
<button class="force-btn" id="forceSyncBtn" onclick="forceUpdate()">Force Update Now</button>
|
||||
<button class="force-btn" id="forceFullBtn" onclick="forceFullRefresh()" style="margin-top: 10px;">Full Software/Advanced Refresh</button>
|
||||
@@ -2607,6 +2609,7 @@
|
||||
}
|
||||
|
||||
async function fetchSoftware(name) {
|
||||
window.currentSoftwareDevice = name;
|
||||
document.getElementById('modal').style.display = 'block';
|
||||
document.getElementById('modalTitle').innerHTML = `Software: ${name} ${inlineCopyBtn(name)}`;
|
||||
|
||||
@@ -2641,7 +2644,7 @@
|
||||
const newJsonString = JSON.stringify(data);
|
||||
|
||||
if (newJsonString !== cachedJsonString) {
|
||||
drawModalTables(data, false, name);
|
||||
if (window.currentSoftwareDevice === name) drawModalTables(data, false, name);
|
||||
softwareCache[name] = data;
|
||||
pullData();
|
||||
}
|
||||
@@ -2789,6 +2792,7 @@
|
||||
if (!match) return;
|
||||
|
||||
window.currentDetailsMatch = match;
|
||||
window.currentDetailsKey = match["Source Device"];
|
||||
document.getElementById('detailsModal').style.display = 'block';
|
||||
renderDetailsHTML();
|
||||
}
|
||||
@@ -2867,25 +2871,48 @@
|
||||
const tdStyle = `style="white-space: normal; word-wrap: break-word; overflow-wrap: break-word; padding: 10px;"`;
|
||||
const scrollBoxStyle = `style="flex: 1; overflow-y: auto; padding: 5px; border: 1px solid var(--border-light); border-radius: 4px;"`;
|
||||
|
||||
let advSrc = advancedDataCache[match["Source Device"]] || {};
|
||||
let advDst = advancedDataCache[match["Destination Device"]] || {};
|
||||
|
||||
const getAdv = (obj, key) => obj[key] ? obj[key] : 'N/A';
|
||||
const srcName = match["Source Device"], dstName = match["Destination Device"];
|
||||
const hasDst = DeviceInfo.isName(dstName);
|
||||
// Advanced fields explain a blank: "Not in AD", "Not in SCCM", "No DNS record", or
|
||||
// "Waiting for worker..." when the worker hasn't looked the device up yet
|
||||
const getAdv = (name, key) => DeviceInfo.html(name, key);
|
||||
const hwSpecs = name => {
|
||||
const m = DeviceInfo.field(name, "Model"), r = DeviceInfo.field(name, "RAM");
|
||||
if (m.state !== "value" && r.state !== "value") return getAdv(name, "Model");
|
||||
return `${getAdv(name, "Model")}<br>${r.state === "value" ? r.text + " RAM" : getAdv(name, "RAM")}`;
|
||||
};
|
||||
const monitorsCell = name => {
|
||||
const c = DeviceInfo.field(name, "MonitorCount");
|
||||
if (c.state !== "value") return getAdv(name, "MonitorCount");
|
||||
const list = DeviceInfo.field(name, "Monitors");
|
||||
return `<b>${c.text}</b>${list.state === "value" && list.text !== "None" ? "<br>" + list.text : ""}`;
|
||||
};
|
||||
const pairing = match["Pairing"] === "Manual" ? "Manual pair (no SCCM association)"
|
||||
: match["Pairing"] === "Skipped" ? "Skip USMT tag (inventory only)"
|
||||
: match["Pairing"] === "None" ? "No SCCM association"
|
||||
: match["Pairing"] === "Cached" ? "SCCM association (completed, network checks skipped)"
|
||||
: match["Pairing"] === "SCCM" ? "SCCM association" : null;
|
||||
const elapsed = e => (e && e !== "N/A" && e !== "Cached") ? '<br>Elapsed Time: ' + e : '';
|
||||
const space = match["Source Free Space"] && match["Source Free Space"] !== "N/A" && match["Source Free Space"] !== "Cached"
|
||||
? match["Source Free Space"] + ' / ' + match["Source Total Space"] : (match["Source Free Space"] || "N/A");
|
||||
|
||||
let mainRows = `
|
||||
<div ${window.isAdvView ? scrollBoxStyle : ''}>
|
||||
<table class="details-table" style="width:100%; border-collapse: collapse; table-layout: fixed;">
|
||||
${window.isAdvView ? `<tr><th colspan="2" style="text-align: center; background: var(--table-header-bg); color: var(--table-header-text);">Migration Overview</th></tr>` : ''}
|
||||
<tr><th width="40%">Source Device</th><td ${tdStyle}><b>${match["Source Device"]}</b> ${inlineCopyBtn(match["Source Device"])} ${srcAppBtn}</td></tr>
|
||||
<tr><th>Source Ping</th><td style="color: ${getPingColor(match["Source Online"])}; font-weight: bold; padding: 10px;">${match["Source Online"]}</td></tr>
|
||||
<tr><th>Space Available</th><td ${tdStyle}>${match["Source Free Space"] !== "N/A" ? match["Source Free Space"] + ' / ' + match["Source Total Space"] : "N/A"}</td></tr>
|
||||
<tr><th>Scanstate Line</th><td ${tdStyle}>${match["Source Scanstate Line"]}${(match["Source Scanstate Elapsed"] && match["Source Scanstate Elapsed"] !== "N/A" && match["Source Scanstate Elapsed"] !== "Cached") ? '<br>Elapsed Time: ' + match["Source Scanstate Elapsed"] : ''}</td></tr>
|
||||
<tr><th>Dest Device</th><td ${tdStyle}><b>${match["Destination Device"]}</b> ${inlineCopyBtn(match["Destination Device"])} ${dstAppBtn}</td></tr>
|
||||
<tr><th>Dest Ping</th><td style="color: ${getPingColor(match["Destination Online"])}; font-weight: bold; padding: 10px;">${match["Destination Online"]}</td></tr>
|
||||
<tr><th>Loadstate Line</th><td ${tdStyle}>${match["Destination Loadstate Line"]}${(match["Destination Loadstate Elapsed"] && match["Destination Loadstate Elapsed"] !== "N/A" && match["Destination Loadstate Elapsed"] !== "Cached") ? '<br>Elapsed Time: ' + match["Destination Loadstate Elapsed"] : ''}</td></tr>
|
||||
<tr><th width="40%">Source Device</th><td ${tdStyle}><b>${srcName}</b> ${inlineCopyBtn(srcName)} ${srcAppBtn}</td></tr>
|
||||
<tr><th>Source Ping</th><td style="color: ${getPingColor(match["Source Online"])}; font-weight: bold; padding: 10px;">${match["Source Online"]}${DeviceInfo.pingMark(match, "Source")}</td></tr>
|
||||
<tr><th>Source Presence</th><td ${tdStyle}>${DeviceInfo.presenceHtml(srcName)}</td></tr>
|
||||
<tr><th>Space Available</th><td ${tdStyle}>${space}${match["Source Space Checked"] && !match["Source Snapshot"] ? `<br><span class="di-sub">Checked ${DeviceInfo.fmtWhen(match["Source Space Checked"])}</span>` : ''}${DeviceInfo.snapshotLine(match, "Source")}</td></tr>
|
||||
<tr><th>Scanstate Line</th><td ${tdStyle}>${match["Source Scanstate Line"]}${elapsed(match["Source Scanstate Elapsed"])}${DeviceInfo.snapshotLine(match, "Source")}</td></tr>
|
||||
<tr><th>Dest Device</th><td ${tdStyle}><b>${dstName}</b> ${inlineCopyBtn(dstName)} ${dstAppBtn}</td></tr>
|
||||
<tr><th>Dest Ping</th><td style="color: ${getPingColor(match["Destination Online"])}; font-weight: bold; padding: 10px;">${match["Destination Online"]}${DeviceInfo.pingMark(match, "Destination")}</td></tr>
|
||||
${hasDst ? `<tr><th>Dest Presence</th><td ${tdStyle}>${DeviceInfo.presenceHtml(dstName)}</td></tr>` : ''}
|
||||
<tr><th>Loadstate Line</th><td ${tdStyle}>${match["Destination Loadstate Line"]}${elapsed(match["Destination Loadstate Elapsed"])}${DeviceInfo.snapshotLine(match, "Destination")}</td></tr>
|
||||
<tr><th>Backup Status</th><td style="background-color: ${statusBg}; color: ${statusText}; padding: 10px;"><b>${match["Backup Status"]}</b></td></tr>
|
||||
<tr><th>Specialty Software</th><td ${tdStyle}>${specialtySoftware}</td></tr>
|
||||
<tr><th>USMT Status</th><td ${tdStyle}>${match["USMT Status"]}</td></tr>
|
||||
<tr><th>USMT Status</th><td ${tdStyle}>${match["USMT Status"]}${pairing ? `<br><span class="di-sub">${pairing}</span>` : ''}</td></tr>
|
||||
${window.isAdvView ? '' : `<tr><th>Monitors</th><td ${tdStyle}>${monitorsCell(srcName)}</td></tr>`}
|
||||
<tr><th>Tags</th><td ${tdStyle}>${match["Tags"] || 'None'}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -2898,21 +2925,25 @@
|
||||
<div ${scrollBoxStyle}>
|
||||
<table class="details-table" style="width:100%; border-collapse: collapse; table-layout: fixed;">
|
||||
<tr><th colspan="2" style="text-align: center; background: #34495e; color: white;">Source Info</th></tr>
|
||||
<tr><th width="40%">Device Name</th><td ${tdStyle}><b>${match["Source Device"]}</b> ${inlineCopyBtn(match["Source Device"])}</td></tr>
|
||||
<tr><th>Source IP / MAC</th><td ${tdStyle}>${getAdv(advSrc, "IP")}<br>${getAdv(advSrc, "MAC")}</td></tr>
|
||||
<tr><th>Source OU</th><td ${tdStyle}>${getAdv(advSrc, "OU")}</td></tr>
|
||||
<tr><th>Source Groups</th><td ${tdStyle}>${getAdv(advSrc, "Groups")}</td></tr>
|
||||
<tr><th>Hardware Specs</th><td ${tdStyle}>${getAdv(advSrc, "Model")}<br>${(getAdv(advSrc, "RAM") !== 'N/A') ? getAdv(advSrc, "RAM") + ' RAM' : 'N/A'}</td></tr>
|
||||
<tr><th>Physical Disk</th><td ${tdStyle}>${getAdv(advSrc, "Disk")}</td></tr>
|
||||
<tr><th>SCCM Client</th><td ${tdStyle}>${getAdv(advSrc, "ClientStatus")}</td></tr>
|
||||
<tr><th>Dept / Bldg</th><td ${tdStyle}>${getAdv(advSrc, "Department")} / ${getAdv(advSrc, "Building")}</td></tr>
|
||||
<tr><th>Location / Vanity</th><td ${tdStyle}>${getAdv(advSrc, "Location")} / ${getAdv(advSrc, "Vanity")}</td></tr>
|
||||
<tr><th>Phone / Mount</th><td ${tdStyle}>Ext: ${getAdv(advSrc, "PhoneExt")}<br>${getAdv(advSrc, "Mount")}</td></tr>
|
||||
<tr><th>Room Type</th><td ${tdStyle}>${getAdv(advSrc, "RoomType")}</td></tr>
|
||||
<tr><th>Users (Pri / Last)</th><td ${tdStyle}>${getAdv(advSrc, "PrimaryUser")} / ${getAdv(advSrc, "LastUser")}</td></tr>
|
||||
<tr><th>Primary Users</th><td ${tdStyle}>${getAdv(advSrc, "SCCMPrimaryUsers")}</td></tr>
|
||||
<tr><th>Asset Info Entered</th><td ${tdStyle}>${getAdv(advSrc, "Updated")}<br>(By: ${getAdv(advSrc, "InfoBy")})</td></tr>
|
||||
<tr><th>Notes</th><td ${tdStyle}>${getAdv(advSrc, "Notes")}</td></tr>
|
||||
<tr><th width="40%">Device Name</th><td ${tdStyle}><b>${srcName}</b> ${inlineCopyBtn(srcName)}</td></tr>
|
||||
<tr><th>Found In</th><td ${tdStyle}>${getAdv(srcName, "FoundIn")}</td></tr>
|
||||
<tr><th>Presence</th><td ${tdStyle}>${DeviceInfo.presenceHtml(srcName)}</td></tr>
|
||||
${DeviceInfo.unknownEverywhere(srcName) ? `<tr><td colspan="2" ${tdStyle}><span class="di-missing">Not in AD, SCCM or DNS.</span><br><span class="di-sub">Nothing in the directory, the site database or DNS knows this name yet: a replacement that hasn't been imaged, or a typo in the tag.</span></td></tr>` : `
|
||||
<tr><th>Source IP / MAC</th><td ${tdStyle}>${getAdv(srcName, "IP")}<br>${getAdv(srcName, "MAC")}</td></tr>
|
||||
<tr><th>Source OU</th><td ${tdStyle}>${getAdv(srcName, "OU")}</td></tr>
|
||||
<tr><th>Source Groups</th><td ${tdStyle}>${getAdv(srcName, "Groups")}</td></tr>
|
||||
<tr><th>Hardware Specs</th><td ${tdStyle}>${hwSpecs(srcName)}</td></tr>
|
||||
<tr><th>Physical Disk</th><td ${tdStyle}>${getAdv(srcName, "Disk")}</td></tr>
|
||||
<tr><th>Monitors</th><td ${tdStyle}>${monitorsCell(srcName)}</td></tr>
|
||||
<tr><th>SCCM Client</th><td ${tdStyle}>${getAdv(srcName, "ClientStatus")}</td></tr>
|
||||
<tr><th>Dept / Bldg</th><td ${tdStyle}>${getAdv(srcName, "Department")} / ${getAdv(srcName, "Building")}</td></tr>
|
||||
<tr><th>Location / Vanity</th><td ${tdStyle}>${getAdv(srcName, "Location")} / ${getAdv(srcName, "Vanity")}</td></tr>
|
||||
<tr><th>Phone / Mount</th><td ${tdStyle}>Ext: ${getAdv(srcName, "PhoneExt")}<br>${getAdv(srcName, "Mount")}</td></tr>
|
||||
<tr><th>Room Type</th><td ${tdStyle}>${getAdv(srcName, "RoomType")}</td></tr>
|
||||
<tr><th>Users (Pri / Last)</th><td ${tdStyle}>${getAdv(srcName, "PrimaryUser")} / ${getAdv(srcName, "LastUser")}</td></tr>
|
||||
<tr><th>Primary Users</th><td ${tdStyle}>${getAdv(srcName, "SCCMPrimaryUsers")}</td></tr>
|
||||
<tr><th>Asset Info Entered</th><td ${tdStyle}>${getAdv(srcName, "Updated")}<br>(By: ${getAdv(srcName, "InfoBy")})</td></tr>
|
||||
<tr><th>Notes</th><td ${tdStyle}>${getAdv(srcName, "Notes")}</td></tr>`}
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
@@ -2921,21 +2952,25 @@
|
||||
<div ${scrollBoxStyle}>
|
||||
<table class="details-table" style="width:100%; border-collapse: collapse; table-layout: fixed;">
|
||||
<tr><th colspan="2" style="text-align: center; background: #34495e; color: white;">Destination Info</th></tr>
|
||||
<tr><th width="40%">Device Name</th><td ${tdStyle}><b>${match["Destination Device"]}</b> ${inlineCopyBtn(match["Destination Device"])}</td></tr>
|
||||
<tr><th>Dest IP / MAC</th><td ${tdStyle}>${getAdv(advDst, "IP")}<br>${getAdv(advDst, "MAC")}</td></tr>
|
||||
<tr><th>Dest OU</th><td ${tdStyle}>${getAdv(advDst, "OU")}</td></tr>
|
||||
<tr><th>Dest Groups</th><td ${tdStyle}>${getAdv(advDst, "Groups")}</td></tr>
|
||||
<tr><th>Hardware Specs</th><td ${tdStyle}>${getAdv(advDst, "Model")}<br>${(getAdv(advDst, "RAM") !== 'N/A') ? getAdv(advDst, "RAM") + ' RAM' : 'N/A'}</td></tr>
|
||||
<tr><th>Physical Disk</th><td ${tdStyle}>${getAdv(advDst, "Disk")}</td></tr>
|
||||
<tr><th>SCCM Client</th><td ${tdStyle}>${getAdv(advDst, "ClientStatus")}</td></tr>
|
||||
<tr><th>Dept / Bldg</th><td ${tdStyle}>${getAdv(advDst, "Department")} / ${getAdv(advDst, "Building")}</td></tr>
|
||||
<tr><th>Location / Vanity</th><td ${tdStyle}>${getAdv(advDst, "Location")} / ${getAdv(advDst, "Vanity")}</td></tr>
|
||||
<tr><th>Phone / Mount</th><td ${tdStyle}>Ext: ${getAdv(advDst, "PhoneExt")}<br>${getAdv(advDst, "Mount")}</td></tr>
|
||||
<tr><th>Room Type</th><td ${tdStyle}>${getAdv(advDst, "RoomType")}</td></tr>
|
||||
<tr><th>Users (Pri / Last)</th><td ${tdStyle}>${getAdv(advDst, "PrimaryUser")} / ${getAdv(advDst, "LastUser")}</td></tr>
|
||||
<tr><th>Primary Users</th><td ${tdStyle}>${getAdv(advDst, "SCCMPrimaryUsers")}</td></tr>
|
||||
<tr><th>Asset Info Entered</th><td ${tdStyle}>${getAdv(advDst, "Updated")}<br>(By: ${getAdv(advDst, "InfoBy")})</td></tr>
|
||||
<tr><th>Notes</th><td ${tdStyle}>${getAdv(advDst, "Notes")}</td></tr>
|
||||
<tr><th width="40%">Device Name</th><td ${tdStyle}><b>${dstName}</b> ${inlineCopyBtn(dstName)}</td></tr>
|
||||
<tr><th>Found In</th><td ${tdStyle}>${getAdv(dstName, "FoundIn")}</td></tr>
|
||||
<tr><th>Presence</th><td ${tdStyle}>${DeviceInfo.presenceHtml(dstName)}</td></tr>
|
||||
${DeviceInfo.unknownEverywhere(dstName) ? `<tr><td colspan="2" ${tdStyle}><span class="di-missing">Not in AD, SCCM or DNS.</span><br><span class="di-sub">Nothing in the directory, the site database or DNS knows this name yet: a replacement that hasn't been imaged, or a typo in the tag.</span></td></tr>` : `
|
||||
<tr><th>Dest IP / MAC</th><td ${tdStyle}>${getAdv(dstName, "IP")}<br>${getAdv(dstName, "MAC")}</td></tr>
|
||||
<tr><th>Dest OU</th><td ${tdStyle}>${getAdv(dstName, "OU")}</td></tr>
|
||||
<tr><th>Dest Groups</th><td ${tdStyle}>${getAdv(dstName, "Groups")}</td></tr>
|
||||
<tr><th>Hardware Specs</th><td ${tdStyle}>${hwSpecs(dstName)}</td></tr>
|
||||
<tr><th>Physical Disk</th><td ${tdStyle}>${getAdv(dstName, "Disk")}</td></tr>
|
||||
<tr><th>Monitors</th><td ${tdStyle}>${monitorsCell(dstName)}</td></tr>
|
||||
<tr><th>SCCM Client</th><td ${tdStyle}>${getAdv(dstName, "ClientStatus")}</td></tr>
|
||||
<tr><th>Dept / Bldg</th><td ${tdStyle}>${getAdv(dstName, "Department")} / ${getAdv(dstName, "Building")}</td></tr>
|
||||
<tr><th>Location / Vanity</th><td ${tdStyle}>${getAdv(dstName, "Location")} / ${getAdv(dstName, "Vanity")}</td></tr>
|
||||
<tr><th>Phone / Mount</th><td ${tdStyle}>Ext: ${getAdv(dstName, "PhoneExt")}<br>${getAdv(dstName, "Mount")}</td></tr>
|
||||
<tr><th>Room Type</th><td ${tdStyle}>${getAdv(dstName, "RoomType")}</td></tr>
|
||||
<tr><th>Users (Pri / Last)</th><td ${tdStyle}>${getAdv(dstName, "PrimaryUser")} / ${getAdv(dstName, "LastUser")}</td></tr>
|
||||
<tr><th>Primary Users</th><td ${tdStyle}>${getAdv(dstName, "SCCMPrimaryUsers")}</td></tr>
|
||||
<tr><th>Asset Info Entered</th><td ${tdStyle}>${getAdv(dstName, "Updated")}<br>(By: ${getAdv(dstName, "InfoBy")})</td></tr>
|
||||
<tr><th>Notes</th><td ${tdStyle}>${getAdv(dstName, "Notes")}</td></tr>`}
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
@@ -3225,13 +3260,15 @@
|
||||
Department: "Department", Building: "Building", Location: "Location", Vanity: "Vanity",
|
||||
PhoneExt: "Phone Ext", Mount: "Mount", RoomType: "Room Type",
|
||||
PrimaryUser: "Primary User", LastUser: "Last User", SCCMPrimaryUsers: "SCCM Primary Users",
|
||||
Updated: "Asset Info Updated", InfoBy: "Info Entered By", Notes: "Notes"
|
||||
Updated: "Asset Info Updated", InfoBy: "Info Entered By", Notes: "Notes",
|
||||
MonitorCount: "Monitor Count", Monitors: "Monitors",
|
||||
DaysOffline: "Days Offline", LastOnline: "Last Online", LastChecked: "Last Ping Check", FoundIn: "Found In (AD/SCCM/DNS)"
|
||||
};
|
||||
const ADV_FIELD_ORDER = ["IP","MAC","OU","Groups","Model","RAM","Disk","ClientStatus","Department","Building","Location","Vanity","PhoneExt","Mount","RoomType","PrimaryUser","LastUser","SCCMPrimaryUsers","Updated","InfoBy","Notes"];
|
||||
const ADV_FIELD_ORDER = ["DaysOffline","LastOnline","LastChecked","FoundIn","IP","MAC","OU","Groups","Model","RAM","Disk","MonitorCount","Monitors","ClientStatus","Department","Building","Location","Vanity","PhoneExt","Mount","RoomType","PrimaryUser","LastUser","SCCMPrimaryUsers","Updated","InfoBy","Notes"];
|
||||
|
||||
let customColumns = JSON.parse(localStorage.getItem('usmt_custom_columns')) || [...CUSTOM_DEFAULT_COLS]; // ordered list of column ids
|
||||
let customColWidths = JSON.parse(localStorage.getItem('usmt_custom_widths')) || {}; // id -> px width
|
||||
let customWidthsLocked = localStorage.getItem('usmt_custom_widths_locked') === '1'; // true once user manually resizes
|
||||
let customColWidths = JSON.parse(localStorage.getItem('usmt_custom_widths')) || {}; // id -> px width
|
||||
let customWidthsLocked = localStorage.getItem('usmt_custom_widths_locked') === '1'; // true once user manually resizes
|
||||
const MIN_COL_W = 110; // minimum column width (fits the header icon row)
|
||||
// Per-column filter: { text:"", excluded:[values] }. Excluded stored as arrays in localStorage.
|
||||
let customFilters = JSON.parse(localStorage.getItem('usmt_custom_filters')) || {}; // id -> {text, excluded:[]}
|
||||
@@ -3270,7 +3307,7 @@
|
||||
function getAdvKeys() {
|
||||
let keys = new Set(ADV_FIELD_ORDER);
|
||||
Object.values(advancedDataCache || {}).forEach(obj => {
|
||||
if (obj && typeof obj === 'object') Object.keys(obj).forEach(k => keys.add(k));
|
||||
if (obj && typeof obj === 'object') Object.keys(obj).forEach(k => { if (!DeviceInfo.HIDDEN.has(k)) keys.add(k); });
|
||||
});
|
||||
return Array.from(keys).sort((a, b) => {
|
||||
let ia = ADV_FIELD_ORDER.indexOf(a), ib = ADV_FIELD_ORDER.indexOf(b);
|
||||
@@ -3393,8 +3430,8 @@
|
||||
function getCustomCellValue(row, col) {
|
||||
const blank = v => (v === undefined || v === null || v === "") ? "N/A" : v;
|
||||
if (col.kind === "apiKey") return blank(row[col.key]);
|
||||
if (col.kind === "adv") return blank((advancedDataCache[row["Source Device"]] || {})[col.key]);
|
||||
if (col.kind === "advDst") return blank((advancedDataCache[row["Destination Device"]] || {})[col.key]);
|
||||
if (col.kind === "adv") return DeviceInfo.field(row["Source Device"], col.key).text;
|
||||
if (col.kind === "advDst") return DeviceInfo.field(row["Destination Device"], col.key).text;
|
||||
if (col.kind === "special") return getCustomSpecialty(row["Source Device"]);
|
||||
if (col.kind === "migration") {
|
||||
if (col.fn === "space") return (row["Source Free Space"] && row["Source Free Space"] !== "N/A") ? row["Source Free Space"] + " / " + row["Source Total Space"] : "N/A";
|
||||
@@ -3404,35 +3441,35 @@
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
// Returns {td-inner-html, cellStyle} for a column, replicating All Devices styling.
|
||||
function renderCustomCell(row, col, val) {
|
||||
const safe = String(val).replace(/"/g, '"');
|
||||
// Ping columns: colored + bold
|
||||
if (col.id === "__srcping" || col.id === "__destping") {
|
||||
return { html: `<b>${val}</b>`, style: `color:${getPingColor(val)}; font-weight:bold;` };
|
||||
}
|
||||
// Device names: bold + inline copy
|
||||
if (col.kind === "apiKey" && (col.key === "Source Device" || col.key === "Destination Device")) {
|
||||
return { html: `<b>${val}</b>${inlineCopyBtn(String(val))}`, style: "" };
|
||||
}
|
||||
// Backup / USMT status: bold
|
||||
if (col.key === "Backup Status" || col.id === "__usmt") {
|
||||
return { html: `<b>${val}</b>`, style: "" };
|
||||
}
|
||||
// Specialty Software: hyperlink any app that has documentation
|
||||
if (col.kind === "special") {
|
||||
if (!val || val === "None" || val === "Loading...") return { html: val, style: "" };
|
||||
const linked = val.split(", ").map(app => {
|
||||
const m = availableDocs.find(d => d.toLowerCase() === app.toLowerCase());
|
||||
return m
|
||||
? `<a href="?doc=${encodeURIComponent(m)}" target="_blank" onclick="event.stopPropagation();" style="color:#3498db; font-weight:bold;">${app}</a>`
|
||||
: app;
|
||||
});
|
||||
return { html: linked.join(", "), style: "" };
|
||||
}
|
||||
return { html: safe, style: "" };
|
||||
}
|
||||
|
||||
// Returns {td-inner-html, cellStyle} for a column, replicating All Devices styling.
|
||||
function renderCustomCell(row, col, val) {
|
||||
const safe = String(val).replace(/"/g, '"');
|
||||
// Ping columns: colored + bold
|
||||
if (col.id === "__srcping" || col.id === "__destping") {
|
||||
return { html: `<b>${val}</b>`, style: `color:${getPingColor(val)}; font-weight:bold;` };
|
||||
}
|
||||
// Device names: bold + inline copy
|
||||
if (col.kind === "apiKey" && (col.key === "Source Device" || col.key === "Destination Device")) {
|
||||
return { html: `<b>${val}</b>${inlineCopyBtn(String(val))}`, style: "" };
|
||||
}
|
||||
// Backup / USMT status: bold
|
||||
if (col.key === "Backup Status" || col.id === "__usmt") {
|
||||
return { html: `<b>${val}</b>`, style: "" };
|
||||
}
|
||||
// Specialty Software: hyperlink any app that has documentation
|
||||
if (col.kind === "special") {
|
||||
if (!val || val === "None" || val === "Loading...") return { html: val, style: "" };
|
||||
const linked = val.split(", ").map(app => {
|
||||
const m = availableDocs.find(d => d.toLowerCase() === app.toLowerCase());
|
||||
return m
|
||||
? `<a href="?doc=${encodeURIComponent(m)}" target="_blank" onclick="event.stopPropagation();" style="color:#3498db; font-weight:bold;">${app}</a>`
|
||||
: app;
|
||||
});
|
||||
return { html: linked.join(", "), style: "" };
|
||||
}
|
||||
return { html: safe, style: "" };
|
||||
}
|
||||
|
||||
function renderCustomTable() {
|
||||
const headerRow = document.getElementById('custom-header-row');
|
||||
const tbody = document.getElementById('tbody-custom');
|
||||
@@ -4095,22 +4132,22 @@
|
||||
|
||||
const dstPingStatus = row["Destination Online"] || "N/A";
|
||||
|
||||
const spaceStr = (row["Source Free Space"] !== "N/A" && row["Source Free Space"]) ?
|
||||
`${row["Source Free Space"]} / ${row["Source Total Space"]}` : "N/A";
|
||||
const spaceStr = (row["Source Free Space"] !== "N/A" && row["Source Free Space"] !== "Cached" && row["Source Free Space"]) ?
|
||||
`${row["Source Free Space"]} / ${row["Source Total Space"]}` : (row["Source Free Space"] || "N/A");
|
||||
|
||||
const destAppsBtn = (row["Destination Device"] && row["Destination Device"] !== "N/A") ?
|
||||
`<button class="soft-btn" onclick="event.stopPropagation(); fetchSoftware('${row["Destination Device"]}')">Apps</button>` : `<span style="color:var(--text-muted);">---</span>`;
|
||||
|
||||
rowsHtml += `<tr class="clickable-row" style='background-color:${themes[currentTheme].groupColors[g]}; cursor: pointer;' onclick="showDeviceDetails('${row["Source Device"]}')">
|
||||
<td title="${row["Source Device"]}"><b>${row["Source Device"]}</b>${inlineCopyBtn(row["Source Device"])}</td>
|
||||
<td style="color: ${getPingColor(srcPingStatus)}; font-weight: bold;">${srcPingStatus}</td>
|
||||
<td style="color: ${getPingColor(srcPingStatus)}; font-weight: bold;">${srcPingStatus}${DeviceInfo.pingMark(row, "Source")}</td>
|
||||
<td class="no-copy"><button class="soft-btn" onclick="event.stopPropagation(); fetchSoftware('${row["Source Device"]}')">Apps</button></td>
|
||||
<td title="${row["Source Scanstate Line"]}">${(g === "Backup In Progress" && row["Source Scanstate Elapsed"] && row["Source Scanstate Elapsed"] !== "N/A" && row["Source Scanstate Elapsed"] !== "Cached") ? row["Source Scanstate Elapsed"] + " " + String(row["Source Scanstate Line"]).trim().split(/\s+/).pop() : row["Source Scanstate Line"]}</td>
|
||||
<td title="${spaceStr}">${spaceStr}</td>
|
||||
<td title="${row["Source Scanstate Line"]}">${(g === "Backup In Progress" && row["Source Scanstate Elapsed"] && row["Source Scanstate Elapsed"] !== "N/A" && row["Source Scanstate Elapsed"] !== "Cached") ? row["Source Scanstate Elapsed"] + " " + String(row["Source Scanstate Line"]).trim().split(/\s+/).pop() : row["Source Scanstate Line"]}${DeviceInfo.snapshotMark(row, "Source")}</td>
|
||||
<td title="${spaceStr}">${spaceStr}${DeviceInfo.snapshotMark(row, "Source")}</td>
|
||||
<td title="${row["Destination Device"]}"><b>${row["Destination Device"]}</b>${inlineCopyBtn(row["Destination Device"])}</td>
|
||||
<td style="color: ${getPingColor(dstPingStatus)}; font-weight: bold;">${dstPingStatus}</td>
|
||||
<td style="color: ${getPingColor(dstPingStatus)}; font-weight: bold;">${dstPingStatus}${DeviceInfo.pingMark(row, "Destination")}</td>
|
||||
<td class="no-copy">${destAppsBtn}</td>
|
||||
<td title="${row["Destination Loadstate Line"]}">${(g === "Restore In Progress" && row["Destination Loadstate Elapsed"] && row["Destination Loadstate Elapsed"] !== "N/A" && row["Destination Loadstate Elapsed"] !== "Cached") ? row["Destination Loadstate Elapsed"] + " " + String(row["Destination Loadstate Line"]).trim().split(/\s+/).pop() : row["Destination Loadstate Line"]}</td>
|
||||
<td title="${row["Destination Loadstate Line"]}">${(g === "Restore In Progress" && row["Destination Loadstate Elapsed"] && row["Destination Loadstate Elapsed"] !== "N/A" && row["Destination Loadstate Elapsed"] !== "Cached") ? row["Destination Loadstate Elapsed"] + " " + String(row["Destination Loadstate Line"]).trim().split(/\s+/).pop() : row["Destination Loadstate Line"]}${DeviceInfo.snapshotMark(row, "Destination")}</td>
|
||||
<td title="${row["USMT Status"] || "N/A"}">${row["USMT Status"] || "N/A"}</td>
|
||||
<td title="${row.Tags || 'None'}">${row.Tags || 'None'}</td>
|
||||
<td style="text-align:center;" class="no-copy"><span class="copy-row-btn" title="Copy Row Data" onclick="copyRowData(this, event)">${COPY_SVG}</span></td>
|
||||
@@ -4267,7 +4304,7 @@
|
||||
document.getElementById('syncProgressFill').style.backgroundColor = "#27ae60";
|
||||
|
||||
if (state.LastDuration && state.LastDuration !== "N/A") {
|
||||
document.getElementById('syncDurationText').innerText = `Last cycle finished in ${state.LastDuration} with ${state.Total} devices`;
|
||||
document.getElementById('syncDurationText').innerText = `Last cycle finished in ${state.LastDuration} with ${state.Total} source devices`;
|
||||
}
|
||||
|
||||
forceBtn.disabled = false;
|
||||
@@ -4276,19 +4313,23 @@
|
||||
let percentage = 0;
|
||||
if (state.Total > 0) percentage = Math.round((state.Current / state.Total) * 100);
|
||||
document.getElementById('syncStatusText').innerText = "Live Sync in Progress...";
|
||||
document.getElementById('syncDetailsText').innerText = `${state.Status}: Device ${state.Current} of ${state.Total}`;
|
||||
document.getElementById('syncDetailsText').innerText = state.Status === "Presence Sweep..."
|
||||
? `Presence sweep: pinging device ${state.Current} of ${state.Total}`
|
||||
: `${state.Status}: Device ${state.Current} of ${state.Total}`;
|
||||
document.getElementById('syncProgressFill').style.width = `${percentage}%`;
|
||||
document.getElementById('syncProgressFill').innerText = `${percentage}%`;
|
||||
document.getElementById('syncProgressFill').style.backgroundColor = "#3498db";
|
||||
|
||||
if (state.LastDuration && state.LastDuration !== "N/A") {
|
||||
document.getElementById('syncDurationText').innerText = `Previous cycle took ${state.LastDuration} with ${state.Total} devices`;
|
||||
document.getElementById('syncDurationText').innerText = `Previous cycle took ${state.LastDuration}${state.LastCycle ? ` with ${state.LastCycle.Sources} source devices` : ''}`;
|
||||
}
|
||||
|
||||
forceBtn.disabled = true;
|
||||
forceFullBtn.disabled = true;
|
||||
}
|
||||
|
||||
document.getElementById('syncBreakdown').innerHTML = DeviceInfo.syncBreakdownHtml(state);
|
||||
|
||||
// Check if Tracked-Devices was updated by you or a teammate
|
||||
const trackRes = await fetch('Tracked-Devices.json?t=' + Date.now()).catch(() => null);
|
||||
let currentTrackedHash = "";
|
||||
@@ -4359,6 +4400,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
// A manual pair whose destination changed (or a device that became / stopped being a
|
||||
// manual pair) drops its stale row right away; a "Waiting for Worker..." row for
|
||||
// the new pairing is added below until the worker sweeps it
|
||||
apiData = apiData.filter(row => {
|
||||
let src = (row["Source Device"] || "").toUpperCase();
|
||||
let dest = (row["Destination Device"] || "").toUpperCase();
|
||||
let isManualRow = row["Pairing"] ? row["Pairing"] === "Manual" : (row["USMT Status"] === "N/A" && row["Backup Status"] === "N/A" && dest !== "N/A");
|
||||
if (manualPairs[src]) return isManualRow && dest === manualPairs[src];
|
||||
if (isManualRow && !manualPairs[src]) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Surgically strip old devices from apiData before the backend worker finishes purging them
|
||||
apiData = apiData.filter(row => {
|
||||
let src = (row["Source Device"] || "").toUpperCase();
|
||||
@@ -4397,13 +4450,14 @@
|
||||
apiData.push({
|
||||
"Source Device": dev,
|
||||
"Destination Device": destDev,
|
||||
"Pairing": isManual ? "Manual" : undefined,
|
||||
"Tags": tags.join(", "),
|
||||
"Source Total Space": placeholder,
|
||||
"Source Free Space": placeholder,
|
||||
"USMT Status": placeholder,
|
||||
"Backup Status": status,
|
||||
"Source Online": placeholder,
|
||||
"Destination Online": "Waiting for Worker...",
|
||||
"Source Online": "Waiting for Worker...",
|
||||
"Destination Online": isManual ? "Waiting for Worker..." : "-",
|
||||
"Source Scanstate Line": placeholder,
|
||||
"Destination Loadstate Line": placeholder
|
||||
});
|
||||
@@ -4415,6 +4469,7 @@
|
||||
renderMasterList();
|
||||
renderSoftwareTable(); // <-- Forces the Software tab to update instantly!
|
||||
renderCustomTable();
|
||||
refreshOpenDialogs();
|
||||
|
||||
// 3. LAZY LOAD SOFTWARE LAST
|
||||
fetch('Software-Cache.json?t=' + Date.now())
|
||||
@@ -4433,8 +4488,11 @@
|
||||
} else if (entry && entry.Data) {
|
||||
rawData = Array.isArray(entry.Data) ? entry.Data : [entry.Data];
|
||||
}
|
||||
if (rawData && !softwareCache[device]) {
|
||||
// Re-read an entry whenever the worker refreshed it (nightly sweep, new device)
|
||||
const stamp = (entry && entry.Timestamp) ? String(entry.Timestamp) : (rawData ? String(rawData.length) : '');
|
||||
if (rawData && (!softwareCache[device] || softwareStamps[device] !== stamp)) {
|
||||
softwareCache[device] = rawData.filter(s => s && s.Name && s.Name.trim() !== '' && s.Name.trim() !== '--');
|
||||
softwareStamps[device] = stamp;
|
||||
cacheUpdated = true;
|
||||
}
|
||||
}
|
||||
@@ -4442,7 +4500,8 @@
|
||||
myStacks.forEach(s => refreshStackTable(s.id));
|
||||
renderMasterList();
|
||||
renderSoftwareTable();
|
||||
renderDetailsHTML();
|
||||
renderCustomTable();
|
||||
refreshOpenDialogs(true);
|
||||
}
|
||||
}
|
||||
}).catch(() => null);
|
||||
@@ -4450,6 +4509,45 @@
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Open dialogs follow the data: when the worker finishes a cycle (or the software cache
|
||||
// changes) the device details and software dialogs re-render in place, keeping scroll
|
||||
// position, unless the user is selecting text in them.
|
||||
let softwareStamps = {};
|
||||
let lastDetailsSig = "", lastSoftwareSig = "";
|
||||
function refreshOpenDialogs(force) {
|
||||
const selecting = () => { const sel = window.getSelection(); return sel && sel.toString().length > 0; };
|
||||
const dm = document.getElementById('detailsModal');
|
||||
if (dm && dm.style.display === 'block' && window.currentDetailsKey && !selecting()) {
|
||||
const key = window.currentDetailsKey;
|
||||
const m = apiData.find(x => x["Source Device"] === key) || apiData.find(x => x["Destination Device"] === key);
|
||||
if (m) {
|
||||
const sig = JSON.stringify([m, advancedDataCache[m["Source Device"]], advancedDataCache[m["Destination Device"]],
|
||||
(softwareCache[m["Source Device"]] || []).length, (softwareCache[m["Destination Device"]] || []).length]);
|
||||
if (force || sig !== lastDetailsSig) {
|
||||
lastDetailsSig = sig;
|
||||
window.currentDetailsMatch = m;
|
||||
const mc = dm.querySelector('.modal-content');
|
||||
const boxes = Array.from(dm.querySelectorAll('#detailsBody div')).map(d => d.scrollTop);
|
||||
const top = mc.scrollTop;
|
||||
renderDetailsHTML();
|
||||
mc.scrollTop = top;
|
||||
Array.from(dm.querySelectorAll('#detailsBody div')).forEach((d, i) => { if (boxes[i]) d.scrollTop = boxes[i]; });
|
||||
}
|
||||
}
|
||||
}
|
||||
const sm = document.getElementById('modal');
|
||||
const dev = window.currentSoftwareDevice;
|
||||
if (sm && sm.style.display === 'block' && dev && Array.isArray(softwareCache[dev]) && !selecting()) {
|
||||
const sig = dev + JSON.stringify(softwareCache[dev]);
|
||||
if (sig !== lastSoftwareSig) {
|
||||
lastSoftwareSig = sig;
|
||||
drawModalTables(softwareCache[dev], true, dev);
|
||||
const search = document.getElementById('softSearch');
|
||||
if (search && search.value) filterSoftwareModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function startDashboard() {
|
||||
renderStacksOnly();
|
||||
await pullData();
|
||||
|
||||
Reference in new issue
Block a user