commit 1c531bc2ab5722b2188cf0dcf3aeb2053900e73f Author: Johan Sandoval Date: Tue Oct 6 15:20:43 2026 +0000 Generalize USMT Dash and add a containerized demo mode Remove all environment-specific data and make the dashboard adoptable anywhere, then add a demo that runs the real PowerShell services. - Strip cached device data, internal knowledge-base articles and screenshots, server/site names and the old project notes; replace the knowledge base with generic sample articles and diagrams - Move every environment value into Config/usmt-dash.psd1 - Route all infrastructure access in the worker and Get-Software.ps1 through a provider (Providers/SCCM.ps1), with a config-driven asset inventory view instead of a hard-coded custom class - Add Providers/Demo.ps1: a deterministic simulated fleet whose migrations progress in real time, and Demo/New-DemoData.ps1 to seed it - Server: -Demo makes the API read-only; add get-config and get-logs; serve static files only from web/, Documentation/ and an allowlist of Data/ files with a path-traversal guard; gzip large responses - Mirror both services' console output to Data/Logs via a Write-Host proxy, and add a terminal panel to the dashboard that streams it - Demo sandbox (web/demo/demo.js): visitor changes to tags, articles and images are kept in the visitor's browser and echoed in the terminals; nothing a visitor does reaches the server - Vendor marked, highlight.js and html2pdf.js instead of loading CDNs - Dockerfile and compose file for hosting the demo (Coolify-ready) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8b0c3c2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +Data/* +!Data/.gitkeep +*.md.bak +docker-compose.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa4bf9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Runtime state written by the worker and server +Data/* +!Data/.gitkeep diff --git a/Config/usmt-dash.psd1 b/Config/usmt-dash.psd1 new file mode 100644 index 0000000..a86a4d5 --- /dev/null +++ b/Config/usmt-dash.psd1 @@ -0,0 +1,121 @@ +@{ + # ===================================================================================== + # USMT Dash configuration + # + # Every environment-specific value lives in this file. To adopt the dashboard in your + # own environment, copy this file, fill in the SCCM section, and run the two services: + # + # .\USMT-Worker.ps1 # data collector (keep running) + # .\USMT-Server.ps1 # web server + JSON API (keep running) + # + # Pass -Demo to either script (or set Mode = 'Demo', or env USMT_MODE=Demo) to run + # against the built-in simulated fleet instead of real infrastructure. + # Pass -ConfigPath to point at a different config file. + # ===================================================================================== + + # 'Live' = query real infrastructure through the provider below. + # 'Demo' = simulated fleet, read-only API, visitor changes stay in the browser. + Mode = 'Live' + + # Data source adapter used in Live mode. Loaded from .\Providers\.ps1 + Provider = 'SCCM' + + Server = @{ + Port = 8080 + # HttpListener prefix. '*' binds every interface (may need a URL ACL on Windows: + # netsh http add urlacl url=http://*:8080/ user=DOMAIN\svc-account) + Prefix = 'http://*:{0}/' + } + + Worker = @{ + # Seconds between cycles during business hours + CycleSeconds = 180 + + # Outside business hours the worker sleeps until the next morning, with a + # midnight "pitstop" that runs the daily software/advanced-info refresh. + BusinessHours = @{ + Enabled = $true + Start = '07:30' + End = '17:30' + Weekdays = @(1, 2, 3, 4, 5) # 0 = Sunday ... 6 = Saturday + } + + # Flush the local DNS client cache at the start of each cycle (Windows only) + FlushDnsEachCycle = $true + + # Devices whose admin share hangs; disk and log checks are skipped for these + DiskSkipList = @() + + # How long a cached software inventory is considered fresh + SoftwareCacheHours = 24 + + # A task-sequence log modified within this window counts as "active" + ActiveLogHours = 24 + } + + # ------------------------------------------------------------------------------------- + # SCCM / ConfigMgr provider (.\Providers\SCCM.ps1) + # ------------------------------------------------------------------------------------- + SCCM = @{ + SiteCode = 'ABC' + ProviderMachineName = 'sccm01.corp.example.com' # SMS Provider for the CMSite PSDrive + SqlServer = 'sccm01.corp.example.com' # Site database server + Database = 'CM_ABC' + + # Where the worker looks for USMT progress logs on each device (relative to the + # admin share below). First folder containing a matching log wins. + AdminShare = 'C$' + LogFolders = @('Windows\CCM\Logs\SMSTSLog', 'Windows\CCM\Logs') + ScanStateLog = 'scanstateprogress' + LoadStateLog = 'loadstateprogress' + + # Many organizations extend hardware inventory with a custom class that holds + # asset/location details (room, department, mount type, ...). Map that class here. + # Set Enabled = $false if you don't have one - those fields will show "N/A". + AssetInventory = @{ + Enabled = $true + View = 'v_GS_ASSET_INFO0' # primary inventory view + FallbackView = '' # optional older/secondary view, queried only if the primary has no row + # Dashboard field = column in the view + Columns = @{ + Updated = 'Date_Updated0' + Location = 'Location0' + Vanity = 'VanityLocation0' + Department = 'Department0' + Building = 'Building0' + Mount = 'Mount_Type0' + PrimaryUser = 'Primary_User0' + PhoneExt = 'Phone_Extension0' + InfoBy = 'Info_Entered_By0' + Notes = 'Notes0' + } + # Boolean (1/0) columns that describe what kind of space the device is in. + # Any column set to 1 adds its label to the "Room Type" field. + RoomTypes = @( + @{ Label = 'Conference Room'; Column = 'ConferenceRoom0' } + @{ Label = 'Hoteling Desk'; Column = 'HotelingDesk0' } + @{ Label = 'Training Room'; Column = 'TrainingRoom0' } + @{ Label = 'Reception'; Column = 'Reception0' } + @{ Label = 'Shared Space'; Column = 'SharedSpace0' } + ) + } + } + + # ------------------------------------------------------------------------------------- + # Demo provider (.\Providers\Demo.ps1) - simulated fleet used by the public demo + # ------------------------------------------------------------------------------------- + Demo = @{ + DeviceCount = 200 # tracked source devices seeded at startup + CycleSeconds = 45 # cooldown between cycles (business hours are ignored) + DelayMsPerDevice = 35 # artificial per-device latency so the progress bar moves + LifecycleHours = 24 # how long one simulated migration takes, start to finish + Verbose = $true # run the worker with -v style tracing + } + + # Console output from both services is mirrored to Data\Logs\*.log as JSON lines so the + # dashboard's terminal panel can stream it. + Logging = @{ + Enabled = $true + MaxLines = 3000 + } +} diff --git a/Data/.gitkeep b/Data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Demo/Blacklisted-Apps.txt b/Demo/Blacklisted-Apps.txt new file mode 100644 index 0000000..414ba39 --- /dev/null +++ b/Demo/Blacklisted-Apps.txt @@ -0,0 +1,38 @@ +Microsoft 365 Apps for enterprise * +Microsoft Edge +Microsoft Edge WebView2 Runtime +Microsoft OneDrive +Microsoft Teams* +Microsoft Visual C++ * +Microsoft Intune Management Extension +Microsoft Policy Platform +Microsoft .NET * +Microsoft Windows Desktop Runtime * +Configuration Manager Client +Google Chrome +Adobe Acrobat Reader* +7-Zip * +Endpoint Protection Agent +Intel(R) Management Engine Components +Realtek Audio Driver +Windows Driver Package * +HP Support Assistant +Dell Command | Update +Lenovo Vantage Service +Cisco Secure Client* +Adobe Acrobat Pro = Adobe Acrobat (64-bit), Adobe Acrobat DC* +QuickBooks = QuickBooks Desktop*, QuickBooks Database Server Manager*, Intuit Data Protect* +Salesforce = Salesforce for Outlook*, Salesforce Data Loader* +Adobe Creative Cloud = Adobe Creative Cloud, Adobe Photoshop*, Adobe Illustrator*, Adobe InDesign* +Microsoft SQL Server Management Studio = SQL Server Management Studio*, SSMS Post Install Tasks* +Microsoft Power BI = Microsoft Power BI Desktop*, Power Pivot for Excel* +Microsoft Visio = Microsoft Visio* +Microsoft Project = Microsoft Project* +Python = Python 3.*, Python Launcher* +Wireshark = Wireshark*, Npcap* +Zoom = Zoom Workplace*, Zoom* +Slack = Slack* +Tableau = Tableau 20* +Notepad++ = Notepad++* +Rule: PUB* = Kiosk Launcher*, Shared PC Cleanup Utility* +Combo: Finance Reporting Bundle = QuickBooks Desktop* && Microsoft Power BI Desktop* diff --git a/Demo/New-DemoData.ps1 b/Demo/New-DemoData.ps1 new file mode 100644 index 0000000..d6c44f8 --- /dev/null +++ b/Demo/New-DemoData.ps1 @@ -0,0 +1,122 @@ +# Seeds Data\ with a simulated fleet for demo mode: tracking tags (Tracked-Devices.json), +# the app blacklist, and a pre-warmed software cache so the first worker cycle is quick. +# Run before starting the services. -Force wipes any existing state first. +param( + [switch]$Force, + [string]$ConfigPath +) + +$root = Split-Path $PSScriptRoot -Parent +. (Join-Path $root "Lib/Common.ps1") +$config = Import-DashConfig -Path $ConfigPath -Demo +$v = $false +. (Join-Path $root "Providers/Demo.ps1") +. (Join-Path $root "Lib/Common.ps1") # re-import so nothing in the provider shadows the helpers + +$dataDir = Join-Path $root "Data" +if ($Force -and (Test-Path $dataDir)) { + Get-ChildItem $dataDir -Recurse -File | Remove-Item -Force +} +if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir | Out-Null } + +$trackedPath = Join-Path $dataDir "Tracked-Devices.json" +if ((Test-Path $trackedPath) -and -not $Force) { + Write-Host "Demo data already present (use -Force to reseed)." -ForegroundColor DarkGray + return +} + +$count = [int]$config.Demo.DeviceCount +Write-Host "Seeding demo fleet ($count tracked devices)..." -ForegroundColor Cyan + +# --- 1. Generate source devices --- +function New-FleetName([int]$i, [string]$prefix) { return "$prefix-$(New-DemoSerial "fleet$i")" } + +$skipPublic = [System.Collections.Generic.List[string]]::new() +$skipLaptops = [System.Collections.Generic.List[string]]::new() +$pool = [System.Collections.Generic.List[string]]::new() +$i = 0 +while ($skipPublic.Count -lt 14) { $skipPublic.Add((New-FleetName ($i++) "PUBDT")) } +while ($skipLaptops.Count -lt 16) { $skipLaptops.Add((New-FleetName ($i++) (Get-DemoPick "lt$i" 'p' @("PRVLT", "PRVLT", "PRVLT", "PUBLT")))) } +while ($pool.Count -lt ($count - $skipPublic.Count - $skipLaptops.Count - 3)) { + $r = Get-DemoRand "mix$i" 'prefix' + $prefix = if ($r -lt 0.55) { "PRVDT" } elseif ($r -lt 0.80) { "PUBDT" } elseif ($r -lt 0.95) { "PRVLT" } else { "PUBLT" } + $pool.Add((New-FleetName ($i++) $prefix)) +} + +# Manual "Source, Destination" pairs - hardware exceptions with no SCCM association +$manual = @() +for ($m = 0; $m -lt 3; $m++) { + $src = New-FleetName ($i++) "PRVDT" + $dst = New-FleetName ($i++) "PRVDT" + $manual += "$src, $dst" +} + +# --- 2. Date-batch tags (recent and upcoming working days) --- +function Get-Workday([datetime]$from, [int]$offset) { + $d = $from.Date; $step = [math]::Sign($offset); $left = [math]::Abs($offset) + while ($left -gt 0) { $d = $d.AddDays($step); if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $left-- } } + return $d +} +$today = Get-Date +$dateTags = @(-4, -3, -2, -1, 0, 1) | ForEach-Object { (Get-Workday $today $_).ToString("M/d") } + +$tags = [ordered]@{} +foreach ($t in $dateTags) { $tags[$t] = [System.Collections.Generic.List[string]]::new() } +for ($n = 0; $n -lt $pool.Count; $n++) { $tags[$dateTags[$n % $dateTags.Count]].Add($pool[$n]) } +$tags["Manual Pairs"] = [System.Collections.Generic.List[string]]::new([string[]]$manual) + +# Department tags overlap the date batches, like a tech grouping their own work +$byDept = @{} +foreach ($d in $pool) { + $dep = (Get-DemoProfile $d).Department.Name + if (-not $byDept.ContainsKey($dep)) { $byDept[$dep] = [System.Collections.Generic.List[string]]::new() } + $byDept[$dep].Add($d) +} +foreach ($pair in @(@("Accounting", "Accounting"), @("Sales Floor", "Sales"), @("Exec Suite", "Executive"))) { + if ($byDept.ContainsKey($pair[1])) { $tags[$pair[0]] = [System.Collections.Generic.List[string]]::new([string[]]@($byDept[$pair[1]] | Select-Object -First 12)) } +} +$tags["Broken WMI"] = [System.Collections.Generic.List[string]]::new([string[]]@($pool | Where-Object { (Get-DemoRand $_ 'wmi') -lt 0.03 } | Select-Object -First 5)) +$tags["Public PCs"] = $skipPublic +$tags["Laptops"] = $skipLaptops + +$skipTags = @("Public PCs", "Laptops") +$tracked = [ordered]@{} +foreach ($t in $tags.Keys) { + $tracked[$t] = [ordered]@{ devices = @($tags[$t]); skipUSMT = ($skipTags -contains $t) } +} +($tracked | ConvertTo-Json -Depth 5) | Set-Content -Path $trackedPath -Encoding UTF8 + +# --- 3. Blacklist / suite rules --- +Copy-Item (Join-Path $PSScriptRoot "Blacklisted-Apps.txt") (Join-Path $dataDir "Blacklisted-Apps.txt") -Force +$blacklist = @(Get-Content (Join-Path $dataDir "Blacklisted-Apps.txt") | Where-Object { $_.Trim() -ne "" } | ForEach-Object { $_.Trim() }) + +# --- 4. Pre-warmed software cache (the worker would otherwise build it one device at a time) --- +$allDevices = [System.Collections.Generic.List[string]]::new() +foreach ($d in ($pool + $skipPublic + $skipLaptops)) { + $allDevices.Add($d) + $p = Get-DemoProfile $d + if (-not $p.NoAssoc -and -not ($skipPublic -contains $d) -and -not ($skipLaptops -contains $d)) { $allDevices.Add((Get-DemoDestName $d)) } +} +foreach ($pair in $manual) { $allDevices.AddRange([string[]]@($pair -split '\s*,\s*')) } + +$stamp = (Get-Date).ToString("o") +$cache = [ordered]@{} +foreach ($d in $allDevices) { + $rows = foreach ($row in (Get-InstalledSoftware -ComputerName $d)) { + $isSpecialty = $true + foreach ($bad in $blacklist) { if ($row.Name -like $bad) { $isSpecialty = $false; break } } + [PSCustomObject]@{ Name = $row.Name; Version = $row.Version; Publisher = $row.Publisher; Source = $row.Source; InstallDate = $row.InstallDate; IsSpecialty = $isSpecialty } + } + $cache[$d] = [PSCustomObject]@{ Timestamp = $stamp; Data = @($rows) } +} +[System.IO.File]::WriteAllText((Join-Path $dataDir "Software-Cache.json"), ($cache | ConvertTo-Json -Depth 6 -Compress), [System.Text.Encoding]::UTF8) + +# --- 5. Empty state for the rest --- +Set-Content -Path (Join-Path $dataDir "Completed.json") -Value "[]" -Encoding UTF8 +Set-Content -Path (Join-Path $dataDir "Data.json") -Value "[]" -Encoding UTF8 +Set-Content -Path (Join-Path $dataDir "Advanced-Cache.json") -Value "{}" -Encoding UTF8 +Set-Content -Path (Join-Path $dataDir "Worker-State.json") -Value '{"Status":"Preparing...","Current":0,"Total":0,"LastUpdated":"Never","NextCycle":"Calculating...","LastDuration":"N/A"}' -Encoding UTF8 +Set-Content -Path (Join-Path $dataDir "Whitelisted-IPs.txt") -Value "" -Encoding UTF8 + +$unique = @($tracked.Values | ForEach-Object { $_.devices } | ForEach-Object { $_ }) | Select-Object -Unique +Write-Host "Seeded $($tracked.Count) tags, $(@($unique).Count) roster entries, $($cache.Count) software inventories." -ForegroundColor Green diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6187388 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# USMT Dash - demo container +# +# Runs the real USMT-Server.ps1 and USMT-Worker.ps1 under PowerShell 7 with -Demo, so the +# worker sweeps a simulated fleet instead of SCCM/AD/SMB. The web API is read-only in demo +# mode; visitor changes are simulated in each visitor's browser. +# +# docker build -t usmt-dash . +# docker run -p 8080:8080 usmt-dash +FROM mcr.microsoft.com/powershell:lts-ubuntu-22.04 + +ENV USMT_MODE=Demo \ + POWERSHELL_TELEMETRY_OPTOUT=1 \ + POWERSHELL_UPDATECHECK=Off \ + DOTNET_CLI_TELEMETRY_OPTOUT=1 \ + TZ=UTC + +WORKDIR /app +COPY . /app + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata \ + && rm -rf /var/lib/apt/lists/* \ + && useradd --system --home /app --shell /usr/sbin/nologin usmt \ + && mkdir -p /app/Data \ + && chown -R usmt:usmt /app/Data \ + && chmod +x /app/docker/start.sh + +USER usmt +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD pwsh -NoProfile -Command "try { if ((Invoke-WebRequest -UseBasicParsing http://127.0.0.1:8080/get-config -TimeoutSec 5).StatusCode -eq 200) { exit 0 } } catch {}; exit 1" + +ENTRYPOINT ["/app/docker/start.sh"] diff --git a/Documentation/Adobe Acrobat Pro.md b/Documentation/Adobe Acrobat Pro.md new file mode 100644 index 0000000..a353d2c --- /dev/null +++ b/Documentation/Adobe Acrobat Pro.md @@ -0,0 +1,26 @@ +[[Blue:Licensed]] [[Gray:Named User]] + +# Adobe Acrobat Pro + +Acrobat Pro is licensed per user through the Adobe Admin Console. The installer is the same unified package as Acrobat Reader; signing in with a licensed account unlocks Pro features. + +## Install + +1. Confirm the user is assigned an Acrobat Pro license in the Adobe Admin Console (ask the license owner if not). +2. On the new device open **Software Center** and install **Adobe Acrobat (64-bit)**. + - Manual installer if Software Center is unavailable: [[Copy:\\fileserver\software\AcrobatPro\Setup.exe]] +3. Launch Acrobat and have the user sign in with their company account. +4. Verify **Help > About Adobe Acrobat** shows *Acrobat Pro*. + +## Common Issues + +**Still shows "Acrobat Reader" after sign-in** +1. Sign out (**Help > Sign Out**), close Acrobat, sign back in. +2. If it persists, the license assignment hasn't synced yet. Wait 15 minutes and repeat. + +**Default PDF handler reverted to Edge** +- Settings > Apps > Default apps > search `.pdf` > choose **Adobe Acrobat**. + +## Migration Notes +- USMT carries over Acrobat preferences, but **digital signature certificates** stored outside the user profile must be re-imported. +- Custom stamps live in `%AppData%\Adobe\Acrobat\DC\Stamps`. Confirm they came across. diff --git a/Documentation/Adobe Creative Cloud.md b/Documentation/Adobe Creative Cloud.md new file mode 100644 index 0000000..83cc30d --- /dev/null +++ b/Documentation/Adobe Creative Cloud.md @@ -0,0 +1,21 @@ +[[Blue:Licensed]] [[Yellow:Large Install]] + +# Adobe Creative Cloud + +Creative Cloud apps (Photoshop, Illustrator, InDesign, ...) are installed through the **Creative Cloud Desktop** app after the user signs in. + +## Install + +1. Install **Adobe Creative Cloud** from Software Center. +2. Have the user sign in with their company account. +3. From the **Apps** tab install only what was on the old device. The **All Devices** page in USMT Dash lists them under *Missing Software*. + +> Expect 5-20 GB per app. Check free space on the destination before starting. + +## Fonts and Presets +- Synced fonts return automatically after sign-in. +- Local presets, brushes and swatches are in `%AppData%\Adobe\`. USMT migrates these, but verify on first launch. + +## Troubleshooting +- **"You don't have access to manage apps"**: the user's license isn't assigned. Contact the license owner. +- **Install stuck at 0%**: sign out of Creative Cloud, delete `%LocalAppData%\Adobe\OOBE`, reboot and retry. diff --git a/Documentation/Archive/Legacy VPN Client.md b/Documentation/Archive/Legacy VPN Client.md new file mode 100644 index 0000000..25b8095 --- /dev/null +++ b/Documentation/Archive/Legacy VPN Client.md @@ -0,0 +1,7 @@ +[[Gray:Archived]] + +# Legacy VPN Client + +*Archived: replaced by Cisco Secure Client on all laptops.* + +The old VPN client must be **uninstalled** before installing Cisco Secure Client, or the two network filter drivers will conflict. diff --git a/Documentation/Create Bootable USB.md b/Documentation/Create Bootable USB.md new file mode 100644 index 0000000..21635b6 --- /dev/null +++ b/Documentation/Create Bootable USB.md @@ -0,0 +1,22 @@ +[[Gray:Reference]] + +# Create a Bootable Imaging USB + +Use this when PXE isn't available (remote site, isolated network, or a device that won't PXE boot). + +## You'll Need +- A USB drive of **32 GB or more** (it will be erased) +- Access to the ConfigMgr console + +## Steps +1. In the ConfigMgr console go to **Software Library > Operating Systems > Task Sequences**. +2. Right-click the deployment task sequence > **Create Task Sequence Media**. +3. Choose **Bootable media** > **Dynamic media**. +4. Select **Removable USB drive** and pick the drive. +5. Leave the media unprotected or set a password per team policy. +6. Select the current production **boot image** and distribution point. +7. Finish the wizard. Writing the media takes about 5-10 minutes. + +## Booting From It +- Press the vendor boot-menu key (**F9** HP, **F12** Dell/Lenovo) and choose the USB device (UEFI). +- If Secure Boot blocks the USB, confirm it was built from a signed boot image. diff --git a/Documentation/Device Storage Remediation.md b/Documentation/Device Storage Remediation.md new file mode 100644 index 0000000..8c440f0 --- /dev/null +++ b/Documentation/Device Storage Remediation.md @@ -0,0 +1,31 @@ +[[Yellow:Remediation]] + +# Device Storage Remediation + +USMT needs free space on the source device for its working files. Devices under about **15 GB free** often fail the backup. USMT Dash shows free space in the **USMT Status** table. + +## 1. Remove stale profiles +1. Press **Win**, type *About your PC*, open **Advanced system settings**. +2. Under **User Profiles** click **Settings**. +3. Delete profiles named *Account Unknown* and profiles of users who no longer use the device. **Never** delete the current user's profile. + +## 2. Clear temp folders +```powershell +Remove-Item "$env:WinDir\Temp\*" -Recurse -Force -ErrorAction SilentlyContinue +Remove-Item "C:\Temp\*" -Recurse -Force -ErrorAction SilentlyContinue +``` + +## 3. Clear the ConfigMgr cache +Control Panel > **Configuration Manager** > *Cache* tab > **Configure Settings** > **Delete Files** > *Delete persisted content*. + +## 4. Rebuild the search index (if Windows.edb is huge) +```powershell +Stop-Service WSearch -Force +Remove-Item "C:\ProgramData\Microsoft\Search\Data\Applications\Windows\Windows.edb" -Force +Start-Service WSearch +``` + +## 5. Disk Cleanup +Run `cleanmgr /sageset:1` once to tick every category, then `cleanmgr /sagerun:1`. + +After cleanup, use **Force Update** in USMT Dash (click the status button in the top bar) to refresh the free-space reading. diff --git a/Documentation/Diskpart Steps.md b/Documentation/Diskpart Steps.md new file mode 100644 index 0000000..d1f24f1 --- /dev/null +++ b/Documentation/Diskpart Steps.md @@ -0,0 +1,26 @@ +[[Red:Destructive]] + +# Diskpart: Wipe and Re-partition a Disk (UEFI/GPT) + +> **This erases the disk.** Only use it on a device whose backup is complete, or on new hardware. + +1. Boot into WinPE or the task sequence wizard and press **F8** for a command prompt. +2. Type `diskpart`. +3. Enter each line and press Enter after each: + +``` +list disk +select disk 0 +clean +convert gpt +create partition efi size=300 +format quick fs=fat32 label="System" +assign letter=S +create partition msr size=128 +create partition primary +format quick fs=ntfs label="Windows" +assign letter=C +exit +``` + +4. Close the prompt and restart the task sequence. diff --git a/Documentation/Manually Start USMT Backup.md b/Documentation/Manually Start USMT Backup.md new file mode 100644 index 0000000..5fd40a0 --- /dev/null +++ b/Documentation/Manually Start USMT Backup.md @@ -0,0 +1,16 @@ +[[Yellow:Remediation]] + +# Manually Start a USMT Backup + +Use this when a device in USMT Dash sits at **Not Started** while its replacement is ready. + +1. Find the device (the **Tracked Devices** or **USMT Status** page shows its last ping state). +2. Restart it. The USMT backup won't start while another user is signed in. Then sign in. +3. Open **Software Center**. + - Blank page or "failed to load"? Run [[Copy:C:\Windows\CCM\ccmrepair.exe]], wait for it to finish, then reopen Software Center. + - Still broken? Tag the device in USMT Dash (e.g. `Broken WMI`) and move on. See [WMI Namespace Fix](?doc=WMI%20Namespace%20Fix). +4. Run **USMT Backup** from the *Operating Systems* tab. + - Fails immediately? Run `gpupdate /force`, then Software Center > Options > **Sync Policy**. Wait a few minutes and retry. +5. When the button turns grey and shows *Installing...* the backup is running. You can sign out and move to the next device. + +In USMT Dash the row changes to **Backup In Progress** within one worker cycle, and the scanstate line shows progress. diff --git a/Documentation/Microsoft Office.md b/Documentation/Microsoft Office.md new file mode 100644 index 0000000..8366ec3 --- /dev/null +++ b/Documentation/Microsoft Office.md @@ -0,0 +1,25 @@ +[[Green:Baseline]] + +# Microsoft Office (Microsoft 365 Apps) + +Microsoft 365 Apps for enterprise is part of the standard image and is on every device. It's in the app blacklist, so it never shows up as missing software. + +## After a Migration +1. Open **Outlook** and confirm the mailbox loads. A new profile is created on first launch. +2. Open **Word** > **File > Account** and confirm *Product Activated* with the user's account. +3. Check **OneDrive** is signed in and syncing (blue cloud in the system tray). + +## Common Fixes + +**Activation errors / "Unlicensed Product"** +1. Close all Office apps. +2. Run [[Copy:Microsoft Support and Recovery Assistant|https://aka.ms/SaRA-officeUninstallFromPC]] or sign out of every account under **File > Account** and sign back in. + +**Outlook signatures missing** +- Signatures are stored in `%AppData%\Microsoft\Signatures`. USMT migrates the folder; if they're missing, copy it from the migration store. + +**Quick repair** +- Settings > Apps > Installed apps > Microsoft 365 Apps > Modify > **Quick Repair**. + +## Visio and Project +Visio and Project are licensed separately. See [Microsoft Visio](?doc=Microsoft%20Visio) and [Microsoft Project](?doc=Microsoft%20Project). diff --git a/Documentation/Microsoft Power BI.md b/Documentation/Microsoft Power BI.md new file mode 100644 index 0000000..a5fefd3 --- /dev/null +++ b/Documentation/Microsoft Power BI.md @@ -0,0 +1,12 @@ +[[Gray:Optional]] + +# Microsoft Power BI + +## Install +- Software Center > **Microsoft Power BI Desktop (x64)**. +- Sign in with the user's company account to publish to the Power BI service. + +## Notes +- `.pbix` reports are usually in OneDrive or SharePoint, so nothing to migrate locally. +- **Power Pivot for Excel** is grouped with Power BI in the software view. It's enabled from Excel > File > Options > Add-ins > COM Add-ins. +- Gateways (on-premises data gateway) are installed on servers, **never** on user workstations. diff --git a/Documentation/Microsoft Project.md b/Documentation/Microsoft Project.md new file mode 100644 index 0000000..3a43fb7 --- /dev/null +++ b/Documentation/Microsoft Project.md @@ -0,0 +1,12 @@ +[[Blue:Licensed]] + +# Microsoft Project + +## Install +1. Confirm the user has a Project license (Project Plan 3 or 5). +2. Software Center > **Microsoft Project - en-us**. +3. Launch and verify activation under **File > Account**. + +## Notes +- Like Visio, Project must match the installed Microsoft 365 Apps channel. +- Global template customizations live in `%AppData%\Microsoft\MS Project\` (Global.MPT). Confirm it came across if the user relies on custom views. diff --git a/Documentation/Microsoft Teams.md b/Documentation/Microsoft Teams.md new file mode 100644 index 0000000..3163968 --- /dev/null +++ b/Documentation/Microsoft Teams.md @@ -0,0 +1,21 @@ +[[Green:Baseline]] + +# Microsoft Teams + +The new Teams client is provisioned per user. It installs automatically the first time a user signs in to a device built from the standard image. + +## Checks After Migration +1. Teams launches and signs in without prompting for a password (SSO). +2. **Settings > Devices**: the correct headset/camera is selected. +3. The **Teams Meeting Add-in** appears in Outlook's ribbon. + +## Meeting Add-in Missing in Outlook +1. Close Outlook and Teams. +2. Start Teams, wait for it to fully load, then start Outlook. +3. If still missing: Outlook > File > Options > Add-ins > Manage **COM Add-ins** > Go > enable *Microsoft Teams Meeting Add-in*. + +## Clear the Teams Cache +```powershell +Get-Process ms-teams -ErrorAction SilentlyContinue | Stop-Process -Force +Remove-Item "$env:LocalAppData\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams" -Recurse -Force +``` diff --git a/Documentation/Microsoft Visio.md b/Documentation/Microsoft Visio.md new file mode 100644 index 0000000..03ef84b --- /dev/null +++ b/Documentation/Microsoft Visio.md @@ -0,0 +1,14 @@ +[[Blue:Licensed]] + +# Microsoft Visio + +Visio Plan 2 is licensed per user in Microsoft 365. + +## Install +1. Confirm the user has a Visio license assigned (Microsoft 365 admin center). +2. Software Center > **Microsoft Visio - en-us**. +3. Open Visio > **File > Account** and confirm the product is activated. + +## Notes +- Visio installs alongside Microsoft 365 Apps and must use the same channel (Current/Monthly Enterprise) or the install will fail. +- Custom stencils: `Documents\My Shapes`. USMT migrates these with the Documents folder. diff --git a/Documentation/Notepad++.md b/Documentation/Notepad++.md new file mode 100644 index 0000000..b8a773d --- /dev/null +++ b/Documentation/Notepad++.md @@ -0,0 +1,9 @@ +[[Gray:Optional]] + +# Notepad++ + +Free text and code editor, mostly requested by IT and Engineering. + +- Software Center > **Notepad++ (64-bit x64)** +- Plugins live in `C:\Program Files\Notepad++\plugins` and are **not** migrated by USMT. Reinstall them from **Plugins > Plugins Admin**. +- Settings and sessions are in `%AppData%\Notepad++`, which USMT migrates. diff --git a/Documentation/QuickBooks.md b/Documentation/QuickBooks.md new file mode 100644 index 0000000..bab0413 --- /dev/null +++ b/Documentation/QuickBooks.md @@ -0,0 +1,23 @@ +[[Blue:Licensed]] [[Red:Finance Approval]] + +# QuickBooks + +QuickBooks Desktop is installed for Accounting and Finance users only. Each install needs a **license and product number** from the Finance team. Never reuse one from another device without approval. + +The software scan shows three entries that USMT Dash groups as one *QuickBooks* suite: +- QuickBooks Desktop Pro +- QuickBooks Database Server Manager +- Intuit Data Protect + +## Install +1. Get the license and product number from the Finance team's request ticket. +2. Run the installer: [[Copy:\\fileserver\software\QuickBooks\Setup_QuickBooks.exe]] +3. Choose **Custom and Network Options** > *I'll be using QuickBooks on this computer*. +4. Launch, register, and apply all updates (**Help > Update QuickBooks Desktop**). + +## Company Files +- Company files (`.QBW`) live on the finance share, **not** on the local device: [[Copy:\\fileserver\finance\QuickBooks]] +- If the user had a local company file, move it to the share. Don't open it from a USB drive. + +## Multi-user Mode +The file server runs **Database Server Manager**. Workstations don't need it unless the user hosts files locally. diff --git a/Documentation/Salesforce.md b/Documentation/Salesforce.md new file mode 100644 index 0000000..7e00461 --- /dev/null +++ b/Documentation/Salesforce.md @@ -0,0 +1,23 @@ +[[Blue:Licensed]] + +# Salesforce + +Salesforce itself is web based. Desktop components are only needed by some Sales and Support users. + +| Component | Who needs it | +|---|---| +| Salesforce for Outlook / Outlook integration | Sales reps who log email to Salesforce | +| Salesforce Data Loader | Sales Ops / admins doing bulk imports | + +## Install +1. Install the components that were on the old device (check *Missing Software* on the **All Devices** page). +2. Installer share: [[Copy:\\fileserver\software\Salesforce\]] +3. **Data Loader** needs a Java runtime. The installer bundles one; accept the defaults. + +## Outlook Integration +The modern integration is an Outlook add-in deployed from the Microsoft 365 admin center. After migration: +1. Open Outlook and look for the **Salesforce** button on the ribbon. +2. If it's missing, wait for add-in sync (up to 24 hours) or ask the M365 admins to confirm the assignment. + +## Single Sign-On +Users sign in with **Use Custom Domain** > `company` > SSO. They shouldn't need a separate password. diff --git a/Documentation/Slack.md b/Documentation/Slack.md new file mode 100644 index 0000000..75072f6 --- /dev/null +++ b/Documentation/Slack.md @@ -0,0 +1,11 @@ +[[Gray:Optional]] + +# Slack + +## Install +1. Software Center > **Slack (Machine - MSI)**. The machine-wide installer keeps Slack updated for all users on shared devices. +2. Sign in to the company workspace with SSO. + +## Notes +- Slack data is cloud based. Nothing needs to be migrated. +- If the user had the **per-user** Slack installed (from slack.com), uninstall it first so two copies don't fight over the protocol handler. diff --git a/Documentation/Tableau.md b/Documentation/Tableau.md new file mode 100644 index 0000000..952f3a5 --- /dev/null +++ b/Documentation/Tableau.md @@ -0,0 +1,13 @@ +[[Blue:Licensed]] + +# Tableau Desktop + +## Install +1. Installer: [[Copy:\\fileserver\software\Tableau\]] +2. Activate with the user's license key (from the Analytics team) or sign in to Tableau Cloud if using login-based licensing. + +## Before Wiping the Old Device +Licenses activated with a product key must be **deactivated** first (Help > Manage Product Keys > Deactivate), or the key stays consumed. + +## Drivers +Database drivers (SQL Server, Oracle, ...) aren't included. Reinstall the ones the user had. Check *Missing Software* on the **All Devices** page. diff --git a/Documentation/Using USMT Dash.md b/Documentation/Using USMT Dash.md new file mode 100644 index 0000000..426babc --- /dev/null +++ b/Documentation/Using USMT Dash.md @@ -0,0 +1,36 @@ +[[Blue:Start Here]] + +# Using USMT Dash + +USMT Dash tracks a Windows device-refresh project: which old (**source**) device is being replaced by which new (**destination**) device, and how far each migration has got. + +![How USMT Dash works](images/USMT%20Dash%20Architecture.png) + +## The Pages +| Page | What it's for | +|---|---| +| **Tracked Devices** | Register devices under a *tag* (your name, a date batch, a department...). The worker only watches tagged devices. | +| **My Workbench** | Personal stacks: paste the devices on your cart and see just those. Saved in your browser. | +| **USMT Status** | Every tracked migration, grouped by status, with ping, free space and USMT log lines. | +| **All Devices** | Compares source and destination software and lists *specialty* apps missing from the new device. | +| **Custom View** | Build your own table: add, remove, resize, drag and filter any column. | +| **Documentation** | This knowledge base. Articles named after an app are linked from that app in the software view. | + +## Migration Status +![Migration lifecycle](images/Migration%20Lifecycle.png) + +| Status | Meaning | +|---|---| +| Not Started | Association exists, no backup yet | +| Backup In Progress | Scanstate log on the source is actively being written | +| Backup Completed | Backup finished, waiting for the new device | +| Restore In Progress | Loadstate log on the destination is active | +| Completed | Restore finished successfully | +| Not Associated | No computer association in ConfigMgr | +| Skipped | The device's tag has **Skip USMT** ticked (inventory only) | + +## Tips +- Pair devices manually with `SOURCE, DESTINATION` on one line when there's no ConfigMgr association. +- Click any device name for full details: location, users, hardware, AD groups. +- **Copy Location Info** builds a ready-to-paste table for facilities or asset tickets. +- In articles, `[[Copy:text]]` creates a click-to-copy link, and `[[Yellow:Label]]` adds a badge. diff --git a/Documentation/WMI Namespace Fix.md b/Documentation/WMI Namespace Fix.md new file mode 100644 index 0000000..9c0eb6d --- /dev/null +++ b/Documentation/WMI Namespace Fix.md @@ -0,0 +1,31 @@ +[[Yellow:Remediation]] [[Red:Admin Required]] + +# WMI Namespace Fix + +Symptoms: Software Center is blank, the ConfigMgr client shows *Inactive*, or the backup task sequence fails with WMI errors (`0x80041003`, `0x8004100e`). + +## 1. Verify the repository +Open an **elevated** PowerShell: +```powershell +winmgmt /verifyrepository +``` +- `WMI repository is consistent`: skip to step 3. +- `inconsistent`: continue. + +## 2. Salvage the repository +```powershell +winmgmt /salvagerepository +``` +Run `/verifyrepository` again. If it's still inconsistent, as a **last resort**: +```powershell +winmgmt /resetrepository +``` + +## 3. Repair the ConfigMgr client +```powershell +& "$env:WinDir\CCM\ccmrepair.exe" +``` +Wait 10-15 minutes, then confirm **Configuration Manager** in Control Panel lists its actions. + +## 4. Re-check in USMT Dash +The client status in the device details should return to *Installed / Active* after the next hardware inventory cycle. diff --git a/Documentation/Zoom.md b/Documentation/Zoom.md new file mode 100644 index 0000000..dd395c5 --- /dev/null +++ b/Documentation/Zoom.md @@ -0,0 +1,12 @@ +[[Gray:Optional]] + +# Zoom + +## Install +- Software Center > **Zoom Workplace (64-bit)**. +- Users sign in with **SSO** using the company domain `company`. + +## Common Issues +- **Camera or microphone not detected**: Windows Settings > Privacy & security > Camera / Microphone > allow desktop apps. +- **Outlook plugin**: the Zoom add-in for Outlook is deployed centrally. No local install needed. +- **Old version prompts to update**: Software Center installs are updated automatically. Have the user dismiss the in-app prompt. diff --git a/Documentation/images/Migration Lifecycle.png b/Documentation/images/Migration Lifecycle.png new file mode 100644 index 0000000..7418ca2 Binary files /dev/null and b/Documentation/images/Migration Lifecycle.png differ diff --git a/Documentation/images/USMT Dash Architecture.png b/Documentation/images/USMT Dash Architecture.png new file mode 100644 index 0000000..40406f1 Binary files /dev/null and b/Documentation/images/USMT Dash Architecture.png differ diff --git a/Get-Software.ps1 b/Get-Software.ps1 new file mode 100755 index 0000000..4990526 --- /dev/null +++ b/Get-Software.ps1 @@ -0,0 +1,140 @@ +param( + [string]$ComputerName, + [string]$ConfigPath, + [switch]$Demo +) + +if ([string]::IsNullOrWhiteSpace($ComputerName)) { return "[]" } + +$scriptDir = $PSScriptRoot +$dataDir = Join-Path $scriptDir "Data" + +if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir | Out-Null } +$cacheFile = Join-Path $dataDir "Software-Cache.json" +$blacklistFile = Join-Path $dataDir "Blacklisted-Apps.txt" + +if (-not (Test-Path $cacheFile)) { Set-Content -Path $cacheFile -Value "{}" -Encoding UTF8 } +if (-not (Test-Path $blacklistFile)) { Set-Content -Path $blacklistFile -Value "" -Encoding UTF8 } + +. (Join-Path $scriptDir "Lib/Common.ps1") +$config = Import-DashConfig -Path $ConfigPath -Demo:$Demo +$cacheHours = $config.Worker.SoftwareCacheHours + +# --- 1. CHECK THE CACHE (24-Hour Timer Only) --- +$needsUpdate = $true +if (Test-Path $cacheFile) { + try { + $rawCache = Read-SharedText $cacheFile + + if (-not [string]::IsNullOrWhiteSpace($rawCache)) { + $cacheData = $rawCache | ConvertFrom-Json + + $propExists = $false + foreach ($p in $cacheData.psobject.properties) { + if ($p.Name -eq $ComputerName) { $propExists = $true; break } + } + + if ($propExists) { + $deviceCache = $cacheData.$ComputerName + if ($null -ne $deviceCache.Timestamp -and $null -ne $deviceCache.Data) { + $cacheTime = [datetime]$deviceCache.Timestamp + + # Logic Check: Is cache still fresh? + if ($cacheTime -gt (Get-Date).AddHours(-1 * $cacheHours)) { + $needsUpdate = $false + $outData = @($deviceCache.Data) + return (ConvertTo-Json -InputObject $outData -Depth 5 -Compress) + } + } + } + } + } catch { + # If cache is corrupt, proceed to the provider + } +} + +if (-not $needsUpdate) { return } + +# --- 2. LOAD BLACKLIST --- +$blacklist = @() +if (Test-Path $blacklistFile) { + $blacklist = Get-Content $blacklistFile | Where-Object { $_.Trim() -ne "" } | ForEach-Object { $_.Trim() } +} + +# --- 3. PROVIDER QUERY (SCCM SQL in Live mode, simulated fleet in Demo mode) --- +$providerName = if ($config.IsDemo) { "Demo" } else { $config.Provider } +$v = $false +. (Join-Path $scriptDir "Providers/$providerName.ps1") + +$results = [System.Collections.Generic.List[PSCustomObject]]::new() + +try { + foreach ($row in (Get-InstalledSoftware -ComputerName $ComputerName)) { + $appName = "$($row.Name)".Trim() + + if ([string]::IsNullOrWhiteSpace($appName) -or $appName -eq "--") { continue } + + $isSpecialty = $true + foreach ($badApp in $blacklist) { + if ($appName -like $badApp) { + $isSpecialty = $false + break + } + } + + $results.Add([PSCustomObject]@{ + Name = $appName + Version = $row.Version + Publisher = $row.Publisher + Source = $row.Source + InstallDate = $row.InstallDate + IsSpecialty = $isSpecialty + }) + } +} catch {} + +# --- 4. ATOMIC CACHE SAVE --- +try { + $cacheEntry = [PSCustomObject]@{ + Timestamp = (Get-Date).ToString("o") + Data = @($results) + } + + for ($i = 0; $i -lt 10; $i++) { + try { + $fs = [System.IO.File]::Open($cacheFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) + $sr = New-Object System.IO.StreamReader($fs) + $rawCache = $sr.ReadToEnd() + + $latestCache = [PSCustomObject]@{} + if (-not [string]::IsNullOrWhiteSpace($rawCache)) { + try { $latestCache = $rawCache | ConvertFrom-Json } catch {} + } + + $propExists = $false + foreach ($p in $latestCache.psobject.properties) { + if ($p.Name -eq $ComputerName) { $propExists = $true; break } + } + + if ($propExists) { + $latestCache.$ComputerName = $cacheEntry + } else { + $latestCache | Add-Member -MemberType NoteProperty -Name $ComputerName -Value $cacheEntry -Force + } + + $fs.SetLength(0) + $sw = New-Object System.IO.StreamWriter($fs, [System.Text.Encoding]::UTF8) + $sw.Write(($latestCache | ConvertTo-Json -Depth 6 -Compress)) + $sw.Flush() + $sw.Close() + $sr.Close() + $fs.Close() + break + } catch { + Start-Sleep -Milliseconds 300 + } + } +} catch {} + +$outData = @($results) +return (ConvertTo-Json -InputObject $outData -Depth 5 -Compress) diff --git a/Lib/Common.ps1 b/Lib/Common.ps1 new file mode 100644 index 0000000..a248b0d --- /dev/null +++ b/Lib/Common.ps1 @@ -0,0 +1,129 @@ +# Shared helpers for USMT-Server.ps1, USMT-Worker.ps1 and Get-Software.ps1. +# Dot-source this file; it must stay compatible with Windows PowerShell 5.1 and PowerShell 7. + +function Import-DashConfig { + param([string]$Path, [switch]$Demo) + + if (-not $Path) { $Path = Join-Path (Split-Path $PSScriptRoot -Parent) "Config\usmt-dash.psd1" } + $Path = $Path -replace '[\\/]', [System.IO.Path]::DirectorySeparatorChar + if (-not (Test-Path $Path)) { throw "Config file not found: $Path" } + + $cfg = Import-PowerShellDataFile -Path $Path + if ($Demo -or $env:USMT_MODE -eq 'Demo') { $cfg.Mode = 'Demo' } + $cfg.IsDemo = ($cfg.Mode -eq 'Demo') + return $cfg +} + +function Join-DashPath { + # Join-Path that tolerates the backslash-separated relative paths used in config on Linux + param([string]$Base, [string]$Child) + return Join-Path $Base ($Child -replace '[\\/]', [System.IO.Path]::DirectorySeparatorChar) +} + +# ----------------------------------------------------------------------------------------- +# Console log mirror +# +# Write-Host is proxied so every line printed to the console is also appended to a JSON +# lines file: {"i":,"t":"