From 9c21567f6d1eea930ff3a7a07c0f69d35da149fd Mon Sep 17 00:00:00 2001 From: Johan Sandoval Date: Tue, 6 Oct 2026 20:49:58 +0000 Subject: [PATCH] Undocumented apps: per-device filter and wider device column; presence for Skip USMT - Source Devices in the undocumented-apps list filters by device name, like the software columns; Devices and the action column take less room. - Drop the one-click documented pick (Documented + Only ticked does it). - Stage legend underline spans label and number. - Device details title stays on one line; the name truncates instead. - Presence sweep also pings devices under Skip USMT tags. --- Config/usmt-dash.psd1 | 5 +++-- README.md | 4 ++-- USMT-Worker.ps1 | 15 ++++++++------- web/excel-filter.js | 16 +--------------- web/layout.css | 13 +++++++------ web/overview.css | 6 +++++- web/overview.js | 7 +++++-- 7 files changed, 31 insertions(+), 35 deletions(-) diff --git a/Config/usmt-dash.psd1 b/Config/usmt-dash.psd1 index 89b0461..1f0dfee 100644 --- a/Config/usmt-dash.psd1 +++ b/Config/usmt-dash.psd1 @@ -52,8 +52,9 @@ # A task-sequence log modified within this window counts as "active" ActiveLogHours = 24 - # Completed pairs skip network checks in the sweep, so they're pinged this often to keep - # "last online" / "days offline" current (every other device is pinged every cycle). + # Completed pairs and Skip USMT devices skip network checks in the sweep, so they're + # pinged this often to keep "last online" / "days offline" current (every other device + # is pinged every cycle). # Cycles only run in business hours plus the midnight pitstop: 60 = hourly, plus midnight. PresenceSweepMinutes = 60 } diff --git a/README.md b/README.md index 624ed57..23c3991 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ a 1,300-device scope. *Not in SCCM*, *No DNS record*, or *Waiting for worker...* if it hasn't been looked up yet. The dialog refreshes in place when the worker finishes a cycle. - **Presence**: every ping is recorded, so each device shows when it was last online and how - many days it has been offline. Completed pairs, which skip network checks in the sweep, are - pinged hourly during business hours and at midnight to keep theirs current. + many days it has been offline. Completed pairs and Skip USMT devices, which skip network checks + in the sweep, are pinged hourly during business hours and at midnight to keep theirs current. - **Snapshots**: the last free space and scanstate / loadstate lines seen while a device was reachable are kept with their timestamps, and shown for completed migrations and offline devices instead of blanks. diff --git a/USMT-Worker.ps1 b/USMT-Worker.ps1 index b42990e..128ea6e 100755 --- a/USMT-Worker.ps1 +++ b/USMT-Worker.ps1 @@ -735,27 +735,28 @@ while ($true) { $sweepSeconds = $phaseWatch.Elapsed.TotalSeconds $phaseWatch.Restart() - # --- PRESENCE SWEEP: completed pairs --- - # Every other device is pinged by the sweep itself on every cycle. Completed pairs skip - # their network checks, so they're pinged here: hourly during business hours (cycles only - # run then) and on the midnight pitstop. + # --- PRESENCE SWEEP: completed pairs and Skip USMT devices --- + # Every other device is pinged by the sweep itself on every cycle. Completed pairs and + # devices under a Skip USMT tag skip network checks, so they're pinged here: hourly during + # business hours (cycles only run then) and on the midnight pitstop. $presenceCount = 0 $presenceDue = ((Get-Date) - $global:LastPresenceSweep).TotalMinutes -ge ($presenceMinutes - 0.5) -or $runFullSoftwareSweep if ($presenceDue) { $completedDevs = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($r in $deviceTable) { - if ($r.Pairing -ne "Cached") { continue } + if ($r.Pairing -ne "Cached" -and $r.Pairing -ne "Skipped") { continue } foreach ($n in @($r."Source Device", $r."Destination Device")) { if ($n -and $n -ne "N/A") { $null = $completedDevs.Add($n) } } } $toPing = @($completedDevs | Where-Object { -not $global:PingedThisCycle.Contains($_) } | Sort-Object) - Write-Host "`n[Presence Sweep: pinging $($toPing.Count) device(s) from completed migrations]" -ForegroundColor Cyan + Write-Host "`n[Presence Sweep: pinging $($toPing.Count) completed / Skip USMT device(s)]" -ForegroundColor Cyan $i = 0 foreach ($n in $toPing) { $i++ Update-WorkerState "Presence Sweep..." $i $toPing.Count $globalLastDuration "Calculating..." $null = Test-Device $n -Session $session } - # Cached / skipped rows pick up the fresh result + # Completed rows pick up the fresh result (Skip USMT rows keep "Skipped" in the ping + # column; their presence shows in the device details and Custom View) foreach ($r in $deviceTable) { if ($r.Pairing -eq "Cached") { Set-RowPresence $r -Source -Destination } } diff --git a/web/excel-filter.js b/web/excel-filter.js index e0d34e6..c39878f 100644 --- a/web/excel-filter.js +++ b/web/excel-filter.js @@ -22,7 +22,7 @@ const cardsQuery = window.matchMedia('(max-width: 1100px)'); const sheetQuery = window.matchMedia('(max-width: 600px)'); - const LIST_COLUMNS = /^(tags|missing software|specialty software)$/i; + const LIST_COLUMNS = /^(tags|missing software|specialty software|source devices)$/i; const SKIP_COLUMNS = /(^|\s)apps$/i; // "Apps" button columns have nothing to filter const BLANK = '(Blanks)'; const NO_READING = '(No reading)'; // space column: offline / cached / not checked @@ -596,20 +596,6 @@ }; pick('Documented', isDocumented); pick('Undocumented', v => !isDocumented(v)); - // One click: devices whose apps ALL have an article ("easy" devices) - if (setMode) { - const b = el('button', 'xf-pick xf-pick-main'); b.type = 'button'; - b.textContent = 'Devices with only documented apps'; - b.title = 'Show devices where every app in this column has a documentation article (and devices with none)'; - b.onclick = () => { - search.value = ''; - opts.forEach(o => { o.row.hidden = false; o.cb.checked = o.v === 'None' || (o.v !== BLANK && isDocumented(o.v)); }); - setMode('subset'); - sync(); - ok.click(); - }; - pickBar.appendChild(b); - } } search.oninput = () => { const q = search.value.trim().toLowerCase(); diff --git a/web/layout.css b/web/layout.css index 519a19b..912bc6a 100644 --- a/web/layout.css +++ b/web/layout.css @@ -476,16 +476,17 @@ a.nav-brand:hover h2 { color: #d6eaf8; } a.nav-brand:focus-visible { outline: 2px solid #3498db; outline-offset: 3px; } /* Device details title: name on the left, Simple / Advanced View toggle on the right, one line */ -#detailsTitle { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding-right: 10px; min-width: 0; } -#detailsTitle .dt-name { min-width: 0; overflow-wrap: anywhere; } +#detailsTitle { display: flex; align-items: center; gap: 12px; flex-wrap: nowrap; padding-right: 10px; min-width: 0; } +/* the name gives way (ellipsis) so the toggle never drops to a second line */ +#detailsTitle .dt-name { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +#detailsTitle .dt-name .copy-row-btn { vertical-align: middle; } +#detailsTitle .dt-toggle { flex: 0 0 auto; } #detailsTitle .dt-toggle { margin-left: auto; cursor: pointer; padding: 6px 12px; border-radius: 4px; background: #34495e; color: #fff; border: none; font-size: 0.55em; font-weight: bold; white-space: nowrap; transition: background 0.2s; } #detailsTitle .dt-toggle:hover { background: #3498db; } -@media (max-width: 700px) { #detailsTitle { padding-right: 32px; } #detailsTitle .dt-toggle { margin-left: 0; } } +@media (max-width: 700px) { #detailsTitle { font-size: 1.15em; gap: 8px; } } -/* Excel filter: three list modes, and the one-click "only documented" pick */ +/* Excel filter: three list modes */ .xf-seg3 .xf-seg-btn { font-size: 0.78em; padding-left: 4px; padding-right: 4px; } -.xf-pick.xf-pick-main { flex-basis: 100%; background: #27ae60; border-color: #27ae60; color: #fff; font-weight: 600; } -.xf-pick.xf-pick-main:hover { background: #219150; border-color: #219150; } diff --git a/web/overview.css b/web/overview.css index fe44279..4830d1b 100644 --- a/web/overview.css +++ b/web/overview.css @@ -24,7 +24,8 @@ .ov-tile.ov-selected { border: 2px solid #3498db !important; background: rgba(52,152,219,0.12); box-shadow: 0 0 0 3px rgba(52,152,219,0.25); transform: none; position: relative; } .ov-showing { display: inline-block; margin-top: 6px; font-size: 0.72em; font-weight: 700; color: #fff; background: #3498db; border-radius: 999px; padding: 2px 8px; } .ov-bar-row.ov-selected { background: rgba(52,152,219,0.16); box-shadow: inset 3px 0 0 #3498db; } -.ov-legend-item.ov-selected { color: var(--text-main); text-decoration: underline; text-decoration-color: #3498db; text-decoration-thickness: 2px; text-underline-offset: 4px; } +.ov-legend-item.ov-selected { color: var(--text-main); box-shadow: inset 0 -2px 0 #3498db; } +.ov-legend-item.ov-selected .ov-key { box-shadow: none; } .ov-heat-cell.ov-selected, .ov-heat-sum.ov-selected { box-shadow: 0 0 0 2px var(--bg-card), 0 0 0 5px #3498db !important; } .ov-heat-sum.ov-selected { background: rgba(52,152,219,0.16); } @@ -160,3 +161,6 @@ table.ov-heat td { padding: 0; text-align: center; border: 0; background: none; .ov-bar-row { grid-template-columns: 128px 1fr 32px; } .ov-bar-label { font-size: 0.82em; } } + +/* Undocumented apps list: room goes to the device names */ +#ov-table td.ov-src-list { white-space: normal; overflow-wrap: anywhere; } diff --git a/web/overview.js b/web/overview.js index 5c738bf..d4c6708 100644 --- a/web/overview.js +++ b/web/overview.js @@ -604,14 +604,17 @@ const table = el('table', 'master-table ov-table'); table.id = 'ov-table'; const thead = el('thead'), tr = el('tr'); - ['App', 'Devices', 'Sources with it', ''].forEach(h => tr.appendChild(el('th', null, h))); + // Source Devices is a list column: its filter lists every device name, like the software columns + [['App', '26%'], ['Devices', '90px'], ['Source Devices', ''], ['', '130px']].forEach(([h, w]) => { const th = el('th', null, h); if (w) th.style.width = w; tr.appendChild(th); }); thead.appendChild(tr); table.appendChild(thead); const tb = el('tbody'); items.forEach(({ app, devs }) => { const row = el('tr'); const a = el('td'); a.appendChild(el('b', null, app)); row.appendChild(a); row.appendChild(el('td', null, String(devs.length))); - const d = el('td', null, devs.slice(0, 6).join(', ') + (devs.length > 6 ? ` +${devs.length - 6} more` : '')); d.title = devs.join(', '); + const d = el('td', 'ov-src-list', devs.slice(0, 8).join(', ') + (devs.length > 8 ? ` +${devs.length - 8} more` : '')); + d.title = devs.join(', '); + d.setAttribute('data-val', devs.join(', ')); // the filter and copy see every name row.appendChild(d); const act = el('td', 'no-copy'); const btn = el('button', 'ov-btn', 'Write article'); btn.type = 'button';