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:
Johan Sandoval committed 2026-10-06 19:09:49 +00:00
1 parent 50c2a18a68
commit 7868948bfc
10 files changed
+1230 -356

No files matched your search

+198 -100
View File
@@ -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, '&quot;');
// 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, '&quot;');
// 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();