Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,64 @@
The format is based on and uses the types of changes according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.2.0] - 2026-06-30

### Added

- Near real-time patching dashboard. New public functions `Set-SPSUpdateStatus` /
`Get-SPSUpdateStatus` persist and read a shared, per-scope JSON status store
(atomic writes, each writer owns its file), `Get-SPSStatusCampaignPath` resolves
the campaign folder, and `Export-SPSUpdateProgressReport` renders a self-contained,
auto-refreshing HTML dashboard (overall state, per-phase sections, colored state
badges, per-item exit codes and per-sequence percentage).
- New optional `StatusStorePath` config key (UNC share) for the status store, with a
local `Results\status` fallback.
- New `-Action ResetStatus` to clear a campaign before a fresh patching round.
- `SPSUpdate.ps1` is instrumented to feed the store and regenerate the dashboard:
ProductUpdate per server (per-setup-file items), the four mount/upgrade sequences
(per-database items and a running percentage), the Configuration Wizard per server
(local and remote), and the side-by-side step. The master regenerates the dashboard
on every wait-loop iteration and writes a final completed dashboard.
- New `Test-SPSUpdateReadiness.ps1` pre-flight check (module, config, DPAPI secret,
elevation, status store write access, per-server CredSSP reachability). The status store
check probes write access **both as the current user and as the InstallAccount** (the
account that runs the sequence tasks), since the upgrade sequences only publish their
status — and therefore only appear on the dashboard — when the service account can write
to the share.
- `-Action ResetStatus` now creates the campaign folder and an empty "waiting" dashboard so
it can be opened in a browser before patching begins.
- The master keeps the dashboard live during the staggered sequence start (regenerated after
each task start and every ~10s during the 60-90s OWSTimer pause), not only once the wait
loop is reached.

### Changed

- Bumped the module manifest to `4.2.0` and exported the four new functions.
- Extended the shared HTML head helper with an optional meta-refresh and state-badge styles.
- Documented that the InstallAccount needs **Modify** on the status store share (SMB + NTFS)
for the upgrade phase to appear on the dashboard (Getting-Started, Configuration, Usage and
the offline guide).
- The dashboard now collapses finished scopes (Done/Skipped) by default and keeps active ones
(Running/Pending/Failed/Warning) expanded, using native `<details>`/`<summary>` (progressive
disclosure, accessible, no JS). Each collapsed scope still shows its badge, percentage and a
compact "N/M done" count on the summary line.
- The real process exit codes now flow to the dashboard: `Start-SPSProductUpdate` returns the
setup.exe exit code (shown in the ProductUpdate item's Exit column, e.g. `0`, `17022` = reboot
required), and the Configuration Wizard records the psconfig.exe exit code in its detail
(`PSConfig completed (exit 0)`), for the local and remote servers.

### Fixed

- The dashboard roll-up counted each unit of work once instead of summing both a scope and
its items (e.g. two servers each installing one binary now read "2 Done", not "4").
- `Start-SPSProductUpdate` no longer leaks the `iisreset -start` Process object into its output
(it is piped to `Out-Null`), so its return value is a clean exit code.

### Tests

- Added cross-platform Pester suites for the status store round-trip and resilience, and
for the dashboard (running / failed / completed / empty / HTML-encoding).

## [4.1.0] - 2026-06-29

### Added
Expand Down
52 changes: 42 additions & 10 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,56 @@
# SPSUpdate - Release Notes

## [4.1.0] - 2026-06-29
## [4.2.0] - 2026-06-30

This release adds a self-contained HTML report for the ContentDatabase inventory, in the
same spirit as the SPSUserSync reports. It builds on the v4.0.0 modernization and is fully
backward compatible.
This release adds a near real-time patching dashboard: while a cumulative update is rolled
out across the farm, SPSUpdate records the progress of every phase into a shared status
store and the master assembles a self-contained, auto-refreshing HTML dashboard.

### Added

- New public function `Export-SPSUpdateDbReport` that renders the ContentDatabase inventory (`<App>-<Env>-<Farm>-ContentDBs.json`) as a self-contained, offline HTML report: summary cards (total databases, total size, balance spread), the per-sequence LPT distribution (count / size / percentage bars), and a sortable, filterable table of every database. Inventories generated before v4.1.0 (no size) still render and fall back to distributing by database count.
- `SPSUpdate.ps1` now writes the HTML report under a new `Results\` folder whenever the inventory is (re)generated (the `InitContentDB` action and the Default-mode prime). Report failures warn but never block the run.
- Private report helpers: `ConvertTo-SPSHtmlEncoded`, `Get-SPSReportCardHtml`, `Get-SPSReportHtmlHead`, `Get-SPSReportDistributionHtml`, `Get-SPSReportHtmlScript`.
- Status store and dashboard functions: `Set-SPSUpdateStatus` / `Get-SPSUpdateStatus`
(atomic per-scope JSON store, each writer owns its file), `Get-SPSStatusCampaignPath`
(resolves `<root>\<App>-<Env>-<Farm>`), and `Export-SPSUpdateProgressReport` (renders a
self-contained HTML dashboard with overall state, per-phase sections, colored state
badges, per-item exit codes and per-sequence percentage; meta-refresh while running).
- New optional `StatusStorePath` config key (UNC share) with a local `Results\status`
fallback.
- New `-Action ResetStatus` to clear a campaign before a fresh patching round.
- `SPSUpdate.ps1` is instrumented end-to-end: ProductUpdate per server (per-setup-file
items), the four mount/upgrade sequences (per-database items and a running percentage),
the Configuration Wizard per server (local and remote), and side-by-side. The master
regenerates the dashboard on every wait-loop iteration and writes a final completed
dashboard with auto-refresh off.
- New `Test-SPSUpdateReadiness.ps1` pre-flight check (module, config, DPAPI secret,
elevation, status store write access, per-server CredSSP reachability). The status store
check probes write access **both as the current user and as the InstallAccount**, since the
upgrade sequences run as the service account and only appear on the dashboard when it can
write to the share.
- The dashboard collapses finished scopes (Done/Skipped) by default and keeps active ones
expanded (native `<details>`/`<summary>`, accessible, no JS); each collapsed line still
shows its badge, percentage and an `N/M done` count.
- Real process exit codes are surfaced: the ProductUpdate item's Exit column shows the
setup.exe code (`0`, `17022` = reboot required, ...), and the Configuration Wizard records
the psconfig.exe code in its detail (`PSConfig completed (exit 0)`).

### How to use

1. Set `StatusStorePath` to a UNC share writable by the InstallAccount from every server
(grant it **Modify** on the SMB share and NTFS — the sequence tasks run as that account).
2. Run `Test-SPSUpdateReadiness.ps1` to confirm the environment (both write probes green).
3. `SPSUpdate.ps1 -ConfigFile '<farm>.psd1' -Action ResetStatus` to start a clean campaign.
4. Open `<StatusStorePath>\<App>-<Env>-<Farm>\_dashboard.html` in a browser.
5. Run `-Action ProductUpdate` on each server, then the default master run; watch the
dashboard update itself.

### Changed

- `Initialize-SPSContentDbJsonFile` now persists `SizeInBytes` and `SizeInMB` for each database in the inventory JSON. This is backward compatible: the mount/upgrade flow still reads only `Name`, `WebAppUrl` and `Server`.
- Bumped the module manifest to `4.1.0` and exported `Export-SPSUpdateDbReport`.
- Bumped the module manifest to `4.2.0` and exported the new functions.

### Notes

- An inventory produced by v4.0.0 (without size) renders correctly; regenerate it with `-Action InitContentDB` to get the size columns and size-based balancing in the report.
- Validated end-to-end on a real three-server Subscription Edition farm with an actual
cumulative update (binary install, parallel content-database upgrade, and the post-setup
Configuration Wizard, all reflected live on the dashboard).

A full list of changes in each version can be found in the [change log](CHANGELOG.md)
10 changes: 9 additions & 1 deletion src/Config/CONTOSO-PROD.example.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@
# Possible values : any key present in secrets.psd1, e.g. 'PROD-ADM'.
CredentialKey = 'PROD-ADM'

# StatusStorePath : OPTIONAL UNC share where every server writes its patching
# progress so the live HTML dashboard can be assembled (near real-time tracking,
# v4.2.0+). It must be writable by the InstallAccount from every farm server.
# Leave empty/omit to fall back to the local Results\status folder (in that case
# ProductUpdate runs on other servers are not captured in the master dashboard).
# Possible values : '' or a UNC path, e.g. '\\fileserver\spsupdate-status'.
StatusStorePath = '\\fileserver\spsupdate-status'

# --- Binaries (REQUIRED block; used by -Action ProductUpdate) ---------------------
Binaries = @{
# ProductUpdate : allow the binary installation step.
Expand All @@ -61,7 +69,7 @@
# ShutdownServices : stop Search/Timer/IIS services during install to speed it up
# (they are restored to their prior state afterwards).
# Possible values : $true | $false. Default if omitted: $true
ShutdownServices = $false
ShutdownServices = $true
}

# --- Content database handling (OPTIONAL) ----------------------------------------
Expand Down
29 changes: 27 additions & 2 deletions src/Modules/SPSUpdate.Common/Private/Get-SPSReportHtmlHead.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
(
[Parameter(Mandatory = $true)]
[System.String]
$Title
$Title,

[Parameter()]
[System.Int32]
$RefreshSeconds = 0
)

$css = @'
Expand Down Expand Up @@ -43,8 +47,29 @@ tbody tr:nth-child(even){background:var(--zebra)}
.pager{display:flex;gap:8px;align-items:center;font-size:12px}
.pager button{padding:4px 10px;border:1px solid var(--line);background:#fff;border-radius:4px;cursor:pointer}
.pager button:disabled{opacity:.4;cursor:default}
.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;color:#fff}
.badge.Pending{background:#9aa4ad}
.badge.Running{background:#1f6fb2}
.badge.Done{background:#2e9b57}
.badge.Failed{background:#c0392b}
.badge.Warning{background:#c19c00;color:#222}
.badge.Skipped{background:#cfd6dc;color:#333}
.phase{margin-top:18px}
.scope{border:1px solid var(--line);border-radius:6px;padding:10px 12px;margin:8px 0;background:#fff}
details.scope>summary{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--brand-dark);cursor:pointer;list-style:none;outline:none}
details.scope>summary::-webkit-details-marker{display:none}
details.scope>summary::before{content:'';flex:0 0 auto;width:0;height:0;border-left:5px solid var(--muted);border-top:4px solid transparent;border-bottom:4px solid transparent;transition:transform .15s ease;margin-right:2px}
details.scope[open]>summary::before{transform:rotate(90deg)}
details.scope>summary:hover{color:var(--brand)}
details.scope>summary:focus-visible{box-shadow:0 0 0 2px #cfe0ef;border-radius:4px}
.scope-head{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--brand-dark)}
.scope-detail{color:var(--muted);font-size:11px;margin-top:6px}
.count{color:var(--muted);font-weight:600}
.items{margin-top:6px}
.live{color:#2e9b57;font-size:11px}
.footer{color:var(--muted);font-size:11px;margin-top:24px;border-top:1px solid var(--line);padding-top:8px}
'@

return "<!DOCTYPE html><html lang=`"en`"><head><meta charset=`"utf-8`"><meta name=`"viewport`" content=`"width=device-width, initial-scale=1`"><title>$Title</title><style>$css</style></head><body>"
$refreshTag = if ($RefreshSeconds -gt 0) { "<meta http-equiv=`"refresh`" content=`"$RefreshSeconds`">" } else { '' }
return "<!DOCTYPE html><html lang=`"en`"><head><meta charset=`"utf-8`"><meta name=`"viewport`" content=`"width=device-width, initial-scale=1`">$refreshTag<title>$Title</title><style>$css</style></head><body>"
}
Loading
Loading