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.
-
+
## 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.
| | |
|---|---|
-|  |  |
+|  |  |
+|  |  |
|  |  |
## 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=` | 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\.ps1`, implement these functions
| `Get-DiskSpace ` | `@{ Total = '238 GB'; Free = '80 GB' }` |
| `Get-LogInfo ` | Latest USMT progress log line (via the shared parser) |
| `Get-DeviceAssociations ` | Objects with `SourceComputer`, `DestinationComputer`, `MigrationStatus` (0/1/2) |
-| `Get-DeviceInventory ` | Hashtable of detail fields (OU, Groups, Model, RAM, Location, ...) |
+| `Get-DeviceInventory ` | Hashtable of detail fields (OU, Groups, Model, RAM, MonitorCount, Monitors, Location, ...), plus `InAD` / `InSCCM` (`$true` / `$false`) |
| `Get-InstalledSoftware ` | Rows of `Name`, `Version`, `Publisher`, `Source`, `InstallDate` |
+| `Get-DeviceLastSeen ` *(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(/
/gi, ", ");
+ if (col.kind === "advDst") return DeviceInfo.field(row["Destination Device"], col.key).text.replace(/
/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 = `Data Updated At: ${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 = `Data Updated At: ${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; }
+}