diff --git a/README.md b/README.md index 314e2bd..7f1d360 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ beyond PowerShell. > simulated 200-device fleet. Open the **Terminals** panel at the bottom of the page to watch > them work. -![USMT Status page](docs/screenshots/usmt-status.png) +![Overview page](docs/screenshots/overview.png) ## Why it exists @@ -28,6 +28,11 @@ a 1,300-device scope. ## Features +- **Overview**: the landing page, built to drive a refresh's open issues to zero. Counters for + offline sources, not started, *Bad Client* and *Broken WMI* tags, devices missing from AD / + SCCM / DNS, low disk, backup errors and undocumented apps; overall progress; devices per + Backup Status; a building x floor map of what's left; and a calendar of the deployment-date + tags showing how far each day's devices have got. Every number opens the devices behind it. - **Tracked Devices**: technicians register devices under tags (a name, a date batch, a department). The worker only sweeps tagged devices. Tags can be flagged *Skip USMT* for inventory-only tracking, and `SOURCE, DEST` lines create manual pairs for hardware exceptions. @@ -43,21 +48,35 @@ a 1,300-device scope. - **Missing software**: compares installed software on source and destination, filters out the baseline image using a pattern list, groups multi-part products into suites, and lists the specialty apps not yet on the new device. -- **Device details**: AD OU and groups, hardware, disk, RAM, client health, location, - department, users. Shown for both sides of a migration. -- **Custom View**: build your own table from any field, with drag-to-reorder, resizable and +- **Device details**: AD OU and groups, hardware, disk, RAM, monitors, client health, + location, department, users, for both sides of a migration. A blank says why: *Not in AD*, + *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 tracked device (completed pairs and destinations included) is pinged + hourly during business hours and at midnight, so each one shows when it was last online and + how many days it has been offline. +- **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. +- **Custom View**: build your own table from any field (including days offline, last online, + found in AD/SCCM/DNS and monitors), with drag-to-reorder, resizable and Excel-style filterable columns. Saved per browser. - **Workbench**: personal "stacks" of devices for the cart in front of you. -- **Knowledge base**: Markdown articles with an editor, image manager, PDF export, badges and - click-to-copy links. Renaming an image rewrites every article that links to it. Articles - named after an app are linked from that app in the software view. +- **Knowledge base**: Markdown articles with an editor, image manager, badges, alerts and + click-to-copy links. Each article has its own address (`/docs/Adobe Acrobat Pro`); the list + is searchable, counted and split into *Applications* and *Troubleshooting & Guides*; PDF + export and print. Renaming an image rewrites every article that links to it. Articles named + after an app are linked from that app in the software view. - **Business-hours scheduling**: the worker cycles every few minutes during working hours, sleeps overnight, and makes a midnight "pitstop" for the daily software refresh. - *Force Update* and *Force Full Refresh* skip the wait. + *Force Update* and *Force Full Refresh* skip the wait. Newly tracked devices are swept and + published first; the sync dialog shows how long each kind of device (SCCM association, + manual pair, completed snapshot...) took. | | | |---|---| -| ![Terminals](docs/screenshots/terminals.png) | ![Missing software](docs/screenshots/missing-software.png) | +| ![USMT Status](docs/screenshots/usmt-status.png) | ![Terminals](docs/screenshots/terminals.png) | +| ![Missing software](docs/screenshots/missing-software.png) | ![Custom View](docs/screenshots/custom-view.png) | | ![Device details](docs/screenshots/device-details.png) | ![Documentation](docs/screenshots/documentation.png) | ## Architecture @@ -78,12 +97,16 @@ refresh" buttons drop flag files that the worker watches for. | `Config\usmt-dash.psd1` | Every environment-specific setting | | `web\index.html` | The dashboard (single-file HTML/JS, about 4,500 lines) | | `web\demo\demo.js` | Terminal panel, plus the demo-mode browser sandbox | +| `web\overview.js` | Overview page (issue counters, progress, locations, deployment calendar) | +| `web\device-info.js` | Shared device helpers: missing-data reasons, presence, snapshot markers | +| `web\docs.js` | Knowledge base: permalinks, search, categories, PDF / print | ### API | Method & path | Purpose | |---|---| | `GET /` and static files | Dashboard, assets, `Data\` state files, `Documentation\` | +| `GET /docs/` | The dashboard, opened on that knowledge-base article | | `GET /get-config` | Mode flags for the front end | | `GET /get-logs?source=server\|worker&after=<id>` | Tail of the service console output (terminal panel) | | `POST /force-update` ยท `POST /force-full` | Wake the worker now (full = also refresh software/inventory caches) | @@ -172,8 +195,9 @@ different CMDB, PDQ, ...), add `Providers\<Name>.ps1`, implement these functions | `Get-DiskSpace <name>` | `@{ Total = '238 GB'; Free = '80 GB' }` | | `Get-LogInfo <name> <logName>` | Latest USMT progress log line (via the shared parser) | | `Get-DeviceAssociations <name>` | Objects with `SourceComputer`, `DestinationComputer`, `MigrationStatus` (0/1/2) | -| `Get-DeviceInventory <name> <session>` | Hashtable of detail fields (OU, Groups, Model, RAM, Location, ...) | +| `Get-DeviceInventory <name> <session>` | Hashtable of detail fields (OU, Groups, Model, RAM, MonitorCount, Monitors, Location, ...), plus `InAD` / `InSCCM` (`$true` / `$false`) | | `Get-InstalledSoftware <name>` | Rows of `Name`, `Version`, `Publisher`, `Source`, `InstallDate` | +| `Get-DeviceLastSeen <name> <session>` *(optional)* | When the device was last active, used to backfill "last online" the first time it's tracked | `Providers\Demo.ps1` is a complete working example that needs no infrastructure. diff --git a/docs/screenshots/custom-view.png b/docs/screenshots/custom-view.png new file mode 100644 index 0000000..b5a9358 Binary files /dev/null and b/docs/screenshots/custom-view.png differ diff --git a/docs/screenshots/overview.png b/docs/screenshots/overview.png new file mode 100644 index 0000000..ef16d8a Binary files /dev/null and b/docs/screenshots/overview.png differ diff --git a/web/device-info.js b/web/device-info.js index ce1337d..bdc9ef3 100644 --- a/web/device-info.js +++ b/web/device-info.js @@ -125,7 +125,7 @@ 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' }; + if (d === 0) return { text: presence(name).online ? 'Online' : '< 1 day', state: 'value' }; return { text: d + (d === 1 ? ' day' : ' days'), state: 'value' }; } if (key === 'LastOnline') { diff --git a/web/docs.js b/web/docs.js index f1c2646..fadab83 100644 --- a/web/docs.js +++ b/web/docs.js @@ -385,7 +385,14 @@ } } }); - wrap('openDoc', () => { renderSidebar(); decorate(); }, name => { setUrl(permalink(name), true); }); + wrap('openDoc', () => { + renderSidebar(); decorate(); + // Phones and tablets stack the list above the article: bring the article into view + if (window.matchMedia('(max-width: 1100px)').matches) { + const bar = document.getElementById('docToolbar'); + if (bar) window.scrollTo({ top: window.scrollY + bar.getBoundingClientRect().top - (parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--nav-h')) || 60) - 10, behavior: 'smooth' }); + } + }, name => { setUrl(permalink(name), true); }); wrap('formatCodeBlocks', decorate); wrap('setDocMode', mode => { const sel = document.getElementById('docCategory'); diff --git a/web/index.html b/web/index.html index b5d4832..3035691 100755 --- a/web/index.html +++ b/web/index.html @@ -3451,8 +3451,9 @@ 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 DeviceInfo.field(row["Source Device"], col.key).text; - if (col.kind === "advDst") return DeviceInfo.field(row["Destination Device"], col.key).text; + // multi-line inventory values (groups, monitors, disk) read as a comma list in a cell + if (col.kind === "adv") return DeviceInfo.field(row["Source Device"], col.key).text.replace(/<br\s*\/?>/gi, ", "); + if (col.kind === "advDst") return DeviceInfo.field(row["Destination Device"], col.key).text.replace(/<br\s*\/?>/gi, ", "); 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"; @@ -4315,7 +4316,7 @@ const forceBtn = document.getElementById('forceSyncBtn'); const forceFullBtn = document.getElementById('forceFullBtn'); - btn.innerText = `Data Updated At: ${globalDataLastUpdated}`; + btn.innerHTML = `<span class="st-pre">Data Updated At: </span>${globalDataLastUpdated}`; if (state.Status === "Cooldown") { document.getElementById('syncStatusText').innerText = "System is Idle (On Cooldown)"; @@ -4376,7 +4377,7 @@ if (payload.LastUpdated && payload.Devices) { apiData = payload.Devices; globalDataLastUpdated = payload.LastUpdated; - btn.innerText = `Data Updated At: ${globalDataLastUpdated}`; + btn.innerHTML = `<span class="st-pre">Data Updated At: </span>${globalDataLastUpdated}`; } else { apiData = payload; } diff --git a/web/layout.css b/web/layout.css index f48b772..b7c4cba 100644 --- a/web/layout.css +++ b/web/layout.css @@ -449,3 +449,24 @@ table.sync-bd th { color: var(--text-muted); font-weight: 600; } table.sync-bd tbody tr { cursor: help; } .sync-bd-phases { margin-top: 6px; color: var(--text-muted); line-height: 1.4; } #syncModal .modal-content { max-width: 460px !important; } + +/* Desktop nav between the hamburger breakpoint and wide screens: tighten so the links, + DEMO pill, theme toggle and sync status all fit on one line */ +@media (min-width: 1101px) and (max-width: 1600px) { + .top-nav .nav-btn { padding: 8px 7px; font-size: 0.9em; } + .top-nav h2 { margin-right: 8px; font-size: 1.25em; } + .status-btn { padding: 8px 10px; } +} +@media (min-width: 1101px) and (max-width: 1450px) { + .st-pre { display: none; } + .status-btn::before { content: 'Updated '; color: rgba(255,255,255,0.65); } +} +@media (min-width: 1101px) and (max-width: 1250px) { + .status-btn::before { content: none; } + .top-nav .nav-btn { padding: 8px 5px; font-size: 0.86em; } + .nav-logo { margin-right: 6px; } +} +@media (min-width: 1101px) and (max-width: 1180px) { + .nav-logo { display: none; } + .top-nav .nav-btn { padding: 8px 4px; } +}