diff --git a/Module/README.md b/Module/README.md index 63c8463..328437e 100644 --- a/Module/README.md +++ b/Module/README.md @@ -520,11 +520,14 @@ UltraTree uses internal configuration that can be customized by modifying `Priva |---------|---------|-------------| | `MaxDuplicateGroups` | 20 | Max duplicate groups in HTML report | | `MaxPathsPerGroup` | 5 | Max file paths shown per duplicate group | -| `MaxTopFolders` | 8 | Top folders displayed in bar chart | +| `MaxTopFolders` | 25 | Top folders in ranked table per drive | +| `MaxTopFiles` | 50 | Top files in ranked table per drive | | `MaxFileTypes` | 10 | Top file types to display | | `MaxResults` | 40 | Max items in results table | | `MaxPathLength` | 50 | Truncate paths longer than this | +`Get-FolderSizes` returns `Items` as one mixed file/folder list capped by `-Top` (default 40). For HTML reports that need many Top Files rows, pass a larger `-Top` (for example `-Top 200`) to `Get-FolderSizes` before `ConvertTo-NinjaOneHtml`. + ### Disk Health Thresholds | Setting | Default | Description | diff --git a/Module/docs/CHANGELOG.md b/Module/docs/CHANGELOG.md index 8f4967f..a1db065 100644 --- a/Module/docs/CHANGELOG.md +++ b/Module/docs/CHANGELOG.md @@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- TBD +## [1.0.2] + +### Added + +- `ConvertTo-NinjaOneHtml`: `-MaxTopFiles`, `-MaxTopFolders`, `-ShowAllResults`, `-FooterSuffix` parameters +- Per-drive ranked **Top Files** and **Top Folders** tables (full width) +- Two-column **Cleanup** + **File Types** layout per drive +- `New-HtmlRankedStack` helper for stacked ranked tables in NinjaOne WYSIWYG + +### Changed + +- NinjaOne dark mode: `stat-desc` for muted text; explicit dark text on info-card titles/descriptions +- Status badges use inline background and foreground colors (WYSIWYG-safe contrast) +- Footer shows UltraTree version from the module manifest; optional `-FooterSuffix` for caller branding +- Removed duplicate `$script:Config.Version`; version comes from `UltraTree.psd1` only ## [0.2.0] ### Added - Initial release. - diff --git a/Module/docs/configuration.md b/Module/docs/configuration.md index 1f61b51..6470564 100644 --- a/Module/docs/configuration.md +++ b/Module/docs/configuration.md @@ -23,12 +23,17 @@ Control how much data appears in HTML reports: |---------|---------|-------------| | `MaxDuplicateGroups` | 20 | Maximum duplicate groups shown in report | | `MaxPathsPerGroup` | 5 | Maximum file paths shown per duplicate group | -| `MaxTopFolders` | 8 | Number of folders in the bar chart | +| `MaxTopFolders` | 25 | Top folders in ranked table per drive | +| `MaxTopFiles` | 50 | Top files in ranked table per drive | | `MaxFileTypes` | 10 | Number of file types in the table | | `MaxResults` | 40 | Maximum items in the full results table | | `MaxPathLength` | 50 | Truncate paths longer than this in display | | `MaxLabelLength` | 12 | Truncate chart labels longer than this | +### Top Files / Top Folders and `-Top` + +`ConvertTo-NinjaOneHtml` builds per-drive Top Files and Top Folders tables from `Get-FolderSizes` output. `Items` is a **single mixed list** of files and folders, sorted by size and truncated by `-Top` (default 40) across all drives. Folders usually fill most of that list, so raise `-Top` when you need more file rows (for example `-Top 200`). + ## Disk Health Thresholds Control health status indicators: diff --git a/Module/docs/functions/convertto-ninjaonehtml.md b/Module/docs/functions/convertto-ninjaonehtml.md index 2206da3..ed519a1 100644 --- a/Module/docs/functions/convertto-ninjaonehtml.md +++ b/Module/docs/functions/convertto-ninjaonehtml.md @@ -6,6 +6,10 @@ Converts scan results from `Get-FolderSizes` into an HTML report optimized for N ```powershell ConvertTo-NinjaOneHtml [-ScanResults] + [[-MaxTopFiles] ] + [[-MaxTopFolders] ] + [[-ShowAllResults] ] + [[-FooterSuffix] ] ``` ## Parameters @@ -13,32 +17,43 @@ ConvertTo-NinjaOneHtml [-ScanResults] | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | **ScanResults** | PSCustomObject | Yes | Output from `Get-FolderSizes`. Supports pipeline input. | +| **MaxTopFiles** | int | No | Max file rows per drive in Top Files table. Default: 50 (`Display.MaxTopFiles`). | +| **MaxTopFolders** | int | No | Max folder rows per drive in Top Folders table. Default: 25 (`Display.MaxTopFolders`). | +| **ShowAllResults** | bool | No | Include the full "All Results by Size" table. Default: `$true`. | +| **FooterSuffix** | string | No | Text appended after the UltraTree version in the footer (e.g. `", Script v1.4.2"`). | ## Examples ### Basic Usage ```powershell -$results = Get-FolderSizes -DriveLetter C +$results = Get-FolderSizes -DriveLetter C -Top 200 $html = ConvertTo-NinjaOneHtml -ScanResults $results ``` ### Pipeline Usage ```powershell -$html = Get-FolderSizes -AllDrives | ConvertTo-NinjaOneHtml +$html = Get-FolderSizes -AllDrives -Top 200 | ConvertTo-NinjaOneHtml ``` ### Full Scan with Duplicates ```powershell -$html = Get-FolderSizes -AllDrives -FindDuplicates | ConvertTo-NinjaOneHtml +$html = Get-FolderSizes -AllDrives -FindDuplicates -Top 200 | ConvertTo-NinjaOneHtml +``` + +### Compact report (no full results table) + +```powershell +$html = Get-FolderSizes -DriveLetter C -Top 200 | + ConvertTo-NinjaOneHtml -ShowAllResults:$false -FooterSuffix ", Script v1.4.2" ``` ### Save to File ```powershell -Get-FolderSizes -AllDrives -FindDuplicates | +Get-FolderSizes -AllDrives -FindDuplicates -Top 200 | ConvertTo-NinjaOneHtml | Out-File "DiskReport.html" -Encoding UTF8 ``` @@ -46,7 +61,7 @@ Get-FolderSizes -AllDrives -FindDuplicates | ### Set NinjaOne Custom Field ```powershell -$results = Get-FolderSizes -AllDrives -FindDuplicates +$results = Get-FolderSizes -AllDrives -FindDuplicates -Top 200 $html = $results | ConvertTo-NinjaOneHtml $html | Ninja-Property-Set-Piped treesize ``` @@ -68,8 +83,9 @@ Each scanned drive gets its own section with: - **Drive Stats** - Used space, free space, health status (Healthy/Warning/Critical) - **Disk Usage Chart** - Visual bar showing used vs free space -- **Top Folders Chart** - Bar chart of largest folders -- **File Types Table** - Breakdown by extension +- **Top Files** - Ranked table of largest files (path, size, modified) +- **Top Folders** - Ranked table of largest folders +- **Cleanup** and **File Types** - Side-by-side in a two-column row - **Cleanup Suggestions** - Categorized cleanup opportunities ### Duplicates Section @@ -82,6 +98,8 @@ If `-FindDuplicates` was used: ### Results Table +When `-ShowAllResults` is true (default): + - Full sortable table of all items - Color-coded by size severity - Shows path, size, type, and last modified date @@ -89,7 +107,8 @@ If `-FindDuplicates` was used: ### Footer - Scan timestamp -- UltraTree version +- UltraTree module version (from the loaded module manifest) +- Optional suffix from `-FooterSuffix` ## HTML Features @@ -97,21 +116,26 @@ The generated HTML includes: - **Bootstrap 5** - Responsive grid and components - **Font Awesome 6** - Icons for status and categories -- **Charts.css** - Lightweight CSS-based charts -- **Dark/Light support** - Respects system preference +- **Inline badge colors** - Readable in NinjaOne WYSIWYG (wrapper CSS is stripped) +- **Dark/Light support** - Uses `stat-desc` for muted text; explicit dark text on info cards - **Mobile-friendly** - Responsive design ## Customization The HTML output is controlled by the module's configuration. See [Configuration](../configuration.md) for options like: -- `MaxTopFolders` - Number of folders in bar chart +- `MaxTopFiles` - Top files in ranked table (default 50) +- `MaxTopFolders` - Top folders in ranked table (default 25) - `MaxFileTypes` - Number of file types shown - `MaxResults` - Items in results table - `MaxDuplicateGroups` - Duplicate groups displayed +### `-Top` interaction + +`Get-FolderSizes` returns `Items` as one mixed file/folder list truncated by `-Top` (default 40). Folders usually dominate that list. Use a larger `-Top` (for example `-Top 200`) when you need the Top Files table to fill out — especially with `-AllDrives`. + ## Notes -- HTML includes external CDN references for Bootstrap, Font Awesome, and Charts.css +- HTML assumes Bootstrap 5 and Font Awesome 6 are available in the NinjaOne WYSIWYG host page +- For offline viewing, wrap with `New-HtmlWrapper` (loads CDN assets) - Best viewed in modern browsers or NinjaOne WYSIWYG fields -- For offline viewing, external resources need internet connectivity diff --git a/Module/src/Tests/Unit/Private/HtmlGeneration.Tests.ps1 b/Module/src/Tests/Unit/Private/HtmlGeneration.Tests.ps1 index 5cdff87..4dfd9d7 100644 --- a/Module/src/Tests/Unit/Private/HtmlGeneration.Tests.ps1 +++ b/Module/src/Tests/Unit/Private/HtmlGeneration.Tests.ps1 @@ -1,4 +1,4 @@ -BeforeAll { +BeforeAll { Set-Location -Path $PSScriptRoot $ModuleName = 'UltraTree' $PathToManifest = [System.IO.Path]::Combine('..', '..', '..', $ModuleName, "$ModuleName.psd1") @@ -11,7 +11,7 @@ Describe 'HTML Generation Functions' -Tag Unit { Context 'New-HtmlStatCard' { It 'Generates valid stat card HTML' { InModuleScope UltraTree { - $html = New-HtmlStatCard -Value "100 GB" -Description "Test Desc" -Color "#ff0000" + $html = New-HtmlStatCard -Value '100 GB' -Description 'Test Desc' -Color '#ff0000' $html | Should -Match 'class="stat-card"' $html | Should -Match 'class="stat-value"' $html | Should -Match 'class="stat-desc"' @@ -23,14 +23,14 @@ Describe 'HTML Generation Functions' -Tag Unit { It 'Includes icon when provided' { InModuleScope UltraTree { - $html = New-HtmlStatCard -Value "5" -Description "Items" -Icon "fas fa-folder" + $html = New-HtmlStatCard -Value '5' -Description 'Items' -Icon 'fas fa-folder' $html | Should -Match 'fas fa-folder' } } It 'Uses default color when not provided' { InModuleScope UltraTree { - $html = New-HtmlStatCard -Value "5" -Description "Items" + $html = New-HtmlStatCard -Value '5' -Description 'Items' $html | Should -Match '#337ab7' } } @@ -39,22 +39,24 @@ Describe 'HTML Generation Functions' -Tag Unit { Context 'New-HtmlInfoCard' { It 'Generates info card with Warning type' { InModuleScope UltraTree { - $html = New-HtmlInfoCard -Title "Test" -Description "Desc" -Type "Warning" + $html = New-HtmlInfoCard -Title 'Test' -Description 'Desc' -Type 'Warning' $html | Should -Match 'class="info-card warning"' $html | Should -Match 'fa-solid fa-triangle-exclamation' + $html | Should -Match 'info-title" style="color: #333;"' + $html | Should -Match 'info-description" style="color: #666;"' } } It 'Generates info card with Danger type' { InModuleScope UltraTree { - $html = New-HtmlInfoCard -Title "Test" -Description "Desc" -Type "Danger" + $html = New-HtmlInfoCard -Title 'Test' -Description 'Desc' -Type 'Danger' $html | Should -Match 'class="info-card danger"' } } It 'Info type has no extra class' { InModuleScope UltraTree { - $html = New-HtmlInfoCard -Title "Test" -Description "Desc" -Type "Info" + $html = New-HtmlInfoCard -Title 'Test' -Description 'Desc' -Type 'Info' $html | Should -Match 'class="info-card"' $html | Should -Not -Match 'class="info-card info"' } @@ -64,23 +66,28 @@ Describe 'HTML Generation Functions' -Tag Unit { Context 'New-HtmlTag' { It 'Generates basic tag' { InModuleScope UltraTree { - $html = New-HtmlTag -Text "Healthy" + $html = New-HtmlTag -Text 'Healthy' $html | Should -Match 'class="tag"' $html | Should -Match 'Healthy' + $html | Should -Match 'color: #333' } } It 'Adds expired type class' { InModuleScope UltraTree { - $html = New-HtmlTag -Text "Critical" -Type "expired" + $html = New-HtmlTag -Text 'Critical' -Type 'expired' $html | Should -Match 'class="tag expired"' + $html | Should -Match 'background-color: #d9534f' + $html | Should -Match 'color: #fff' } } It 'Adds disabled type class' { InModuleScope UltraTree { - $html = New-HtmlTag -Text "Warning" -Type "disabled" + $html = New-HtmlTag -Text 'Warning' -Type 'disabled' $html | Should -Match 'class="tag disabled"' + $html | Should -Match 'background-color: #f0ad4e' + $html | Should -Match 'color: #333' } } } @@ -88,7 +95,7 @@ Describe 'HTML Generation Functions' -Tag Unit { Context 'New-HtmlCard' { It 'Generates card with title and body' { InModuleScope UltraTree { - $html = New-HtmlCard -Title "Test Card" -Body "

Content

" + $html = New-HtmlCard -Title 'Test Card' -Body '

Content

' $html | Should -Match 'class="card flex-grow-1"' $html | Should -Match 'class="card-title-box"' $html | Should -Match 'Test Card' @@ -98,14 +105,14 @@ Describe 'HTML Generation Functions' -Tag Unit { It 'Includes icon when provided' { InModuleScope UltraTree { - $html = New-HtmlCard -Title "Test" -Icon "fas fa-folder" -Body "Content" + $html = New-HtmlCard -Title 'Test' -Icon 'fas fa-folder' -Body 'Content' $html | Should -Match 'fas fa-folder' } } It 'Applies body style' { InModuleScope UltraTree { - $html = New-HtmlCard -Title "Test" -Body "Content" -BodyStyle "padding: 0;" + $html = New-HtmlCard -Title 'Test' -Body 'Content' -BodyStyle 'padding: 0;' $html | Should -Match 'style="padding: 0;"' } } @@ -113,20 +120,20 @@ Describe 'HTML Generation Functions' -Tag Unit { Context 'New-HtmlBarChart' { It 'Returns empty string for null items' { - InModuleScope UltraTree { New-HtmlBarChart -Items $null | Should -Be "" } + InModuleScope UltraTree { New-HtmlBarChart -Items $null | Should -Be '' } } It 'Returns empty string for empty array' { - InModuleScope UltraTree { New-HtmlBarChart -Items @() | Should -Be "" } + InModuleScope UltraTree { New-HtmlBarChart -Items @() | Should -Be '' } } It 'Generates chart with items' { InModuleScope UltraTree { $items = @( - @{ Label = "Folder1"; Value = 1GB } - @{ Label = "Folder2"; Value = 500MB } + @{ Label = 'Folder1'; Value = 1GB } + @{ Label = 'Folder2'; Value = 500MB } ) - $html = New-HtmlBarChart -Items $items -Title "Test Chart" + $html = New-HtmlBarChart -Items $items -Title 'Test Chart' $html | Should -Match 'charts-css bar' $html | Should -Match 'Test Chart' $html | Should -Match 'Folder1' @@ -137,11 +144,11 @@ Describe 'HTML Generation Functions' -Tag Unit { Context 'New-HtmlDuplicatesTable' { It 'Returns empty string for null groups' { - InModuleScope UltraTree { New-HtmlDuplicatesTable -DuplicateGroups $null -TotalWasted 0 | Should -Be "" } + InModuleScope UltraTree { New-HtmlDuplicatesTable -DuplicateGroups $null -TotalWasted 0 | Should -Be '' } } It 'Returns empty string for empty groups' { - InModuleScope UltraTree { New-HtmlDuplicatesTable -DuplicateGroups @() -TotalWasted 0 | Should -Be "" } + InModuleScope UltraTree { New-HtmlDuplicatesTable -DuplicateGroups @() -TotalWasted 0 | Should -Be '' } } It 'Generates table with duplicate groups' { @@ -150,7 +157,7 @@ Describe 'HTML Generation Functions' -Tag Unit { [PSCustomObject]@{ FileSize = 100MB WastedSpace = 100MB - Files = @("C:\path1\file.exe", "C:\path2\file.exe") + Files = @('C:\path1\file.exe', 'C:\path2\file.exe') } ) $html = New-HtmlDuplicatesTable -DuplicateGroups $groups -TotalWasted 100MB diff --git a/Module/src/Tests/Unit/Public/ConvertTo-NinjaOneHtml.Tests.ps1 b/Module/src/Tests/Unit/Public/ConvertTo-NinjaOneHtml.Tests.ps1 index 8a53d8d..ffcd9eb 100644 --- a/Module/src/Tests/Unit/Public/ConvertTo-NinjaOneHtml.Tests.ps1 +++ b/Module/src/Tests/Unit/Public/ConvertTo-NinjaOneHtml.Tests.ps1 @@ -1,7 +1,8 @@ -BeforeAll { +BeforeAll { Set-Location -Path $PSScriptRoot $ModuleName = 'UltraTree' $PathToManifest = [System.IO.Path]::Combine('..', '..', '..', $ModuleName, "$ModuleName.psd1") + $script:ModuleVersion = (Import-PowerShellDataFile $PathToManifest).ModuleVersion Get-Module $ModuleName -ErrorAction SilentlyContinue | Remove-Module -Force Import-Module $PathToManifest -Force } @@ -18,6 +19,14 @@ Describe 'ConvertTo-NinjaOneHtml' -Tag Unit { $command = Get-Command ConvertTo-NinjaOneHtml $command.Parameters['ScanResults'].Attributes.ValueFromPipeline | Should -Contain $true } + + It 'Has optional MaxTopFiles, MaxTopFolders, ShowAllResults, and FooterSuffix parameters' { + $command = Get-Command ConvertTo-NinjaOneHtml + $command.Parameters.ContainsKey('MaxTopFiles') | Should -Be $true + $command.Parameters.ContainsKey('MaxTopFolders') | Should -Be $true + $command.Parameters.ContainsKey('ShowAllResults') | Should -Be $true + $command.Parameters.ContainsKey('FooterSuffix') | Should -Be $true + } } Context 'Help documentation' { @@ -59,22 +68,31 @@ Describe 'ConvertTo-NinjaOneHtml' -Tag Unit { # Add mock drive info $mockScanResults.DriveInfo.Add([PSCustomObject]@{ - Drive = "C:" - TotalSize = 500GB - UsedSpace = 250GB - FreeSpace = 250GB - UsedPercent = 50.0 - }) + Drive = 'C:' + TotalSize = 500GB + UsedSpace = 250GB + FreeSpace = 250GB + UsedPercent = 50.0 + }) # Add mock items $mockScanResults.Items.Add([PSCustomObject]@{ - Drive = "C:" - Path = "C:\Windows" - Size = "25.00 GB" - SizeBytes = 25GB - IsDirectory = $true - LastModified = "2024-01-01" - }) + Drive = 'C:' + Path = 'C:\Windows' + Size = '25.00 GB' + SizeBytes = 25GB + IsDirectory = $true + LastModified = '2024-01-01' + }) + + $mockScanResults.Items.Add([PSCustomObject]@{ + Drive = 'C:' + Path = 'C:\pagefile.sys' + Size = '8.00 GB' + SizeBytes = 8GB + IsDirectory = $false + LastModified = '2024-01-01' + }) } It 'Returns string output' { @@ -102,9 +120,71 @@ Describe 'ConvertTo-NinjaOneHtml' -Tag Unit { $html | Should -Match 'Cleanup Potential' } - It 'Contains version footer' { + It 'Contains UltraTree version footer' { + $html = ConvertTo-NinjaOneHtml -ScanResults $mockScanResults + $html | Should -Match "UltraTree v$([regex]::Escape($script:ModuleVersion))" + $html | Should -Not -Match 'TreeSize v' + } + + It 'Contains Top Files ranked table when file items exist' { + $html = ConvertTo-NinjaOneHtml -ScanResults $mockScanResults + $html | Should -Match 'Top Files' + $html | Should -Match '' + } + + It 'Contains Top Folders ranked table when folder items exist' { + $html = ConvertTo-NinjaOneHtml -ScanResults $mockScanResults + $html | Should -Match 'Top Folders' + } + + It 'Uses two-column Cleanup and File Types layout' { + $html = ConvertTo-NinjaOneHtml -ScanResults $mockScanResults + $html | Should -Match 'col-xl-6 col-lg-6 col-md-12 d-flex flex-column' + $html | Should -Not -Match 'col-xl-4 col-lg-4 col-md-12 d-flex' + } + + It 'Does not use hardcoded muted text outside info cards' { + $html = ConvertTo-NinjaOneHtml -ScanResults $mockScanResults + $htmlWithoutInfoCards = ($html -split '
+ [CmdletBinding()] param ( [array]$Items, [string]$Title = "Top Items" diff --git a/Module/src/UltraTree/Private/New-HtmlCard.ps1 b/Module/src/UltraTree/Private/New-HtmlCard.ps1 index f209a23..056d36a 100644 --- a/Module/src/UltraTree/Private/New-HtmlCard.ps1 +++ b/Module/src/UltraTree/Private/New-HtmlCard.ps1 @@ -1,4 +1,4 @@ -function New-HtmlCard { +function New-HtmlCard { <# .SYNOPSIS Base card template - other card functions should use this. @@ -15,6 +15,7 @@ function New-HtmlCard { .PARAMETER CardStyle Optional inline CSS for the card element. #> + [CmdletBinding()] param ( [string]$Title, [string]$Icon = "", diff --git a/Module/src/UltraTree/Private/New-HtmlCleanupSuggestions.ps1 b/Module/src/UltraTree/Private/New-HtmlCleanupSuggestions.ps1 index 4b3bb6a..9c8cc22 100644 --- a/Module/src/UltraTree/Private/New-HtmlCleanupSuggestions.ps1 +++ b/Module/src/UltraTree/Private/New-HtmlCleanupSuggestions.ps1 @@ -1,4 +1,4 @@ -function New-HtmlCleanupSuggestions { +function New-HtmlCleanupSuggestions { <# .SYNOPSIS Creates cleanup suggestions display with categorization. @@ -9,14 +9,15 @@ function New-HtmlCleanupSuggestions { .PARAMETER Compact Switch to use compact list format instead of full cards. #> + [CmdletBinding()] param ( [array]$Suggestions, [switch]$Compact ) - if ($null -eq $Suggestions -or $Suggestions.Count -eq 0) { return "" } + if ($null -eq $Suggestions -or $Suggestions.Count -eq 0) { return '' } - $broomIcon = Get-ThemeIcon -IconName "Broom" + $broomIcon = Get-ThemeIcon -IconName 'Broom' if ($Compact) { $items = foreach ($sug in $Suggestions) { @@ -25,7 +26,7 @@ function New-HtmlCleanupSuggestions { $category = $script:CleanupCategories | Where-Object { $_.Name -eq $sug.Category } $displayName = if ($category) { $category.DisplayName } else { $sug.Path } - "
  • $displayName
    $sizeText
  • " + "
  • $displayName
    $sizeText
  • " } $body = @" @@ -33,7 +34,7 @@ function New-HtmlCleanupSuggestions { $($items -join "`n ") "@ - New-HtmlCard -Title "Cleanup" -Icon $broomIcon -Body $body -BodyStyle "padding: 12px;" + New-HtmlCard -Title 'Cleanup' -Icon $broomIcon -Body $body -BodyStyle 'padding: 12px;' } else { $cards = foreach ($sug in $Suggestions) { @@ -41,12 +42,12 @@ function New-HtmlCleanupSuggestions { $category = $script:CleanupCategories | Where-Object { $_.Name -eq $sug.Category } $displayName = if ($category) { $category.DisplayName } else { $sug.Path } $description = if ($category) { $category.Description } else { $sug.Description } - $severity = if ($category) { $category.Severity } else { "Info" } + $severity = if ($category) { $category.Severity } else { 'Info' } New-HtmlInfoCard -Title "$displayName`: $sizeText" -Description $description -Type $severity } $body = $cards -join "`n " - New-HtmlCard -Title "Cleanup Suggestions" -Icon $broomIcon -Body $body -CardStyle "margin-bottom: 16px;" + New-HtmlCard -Title 'Cleanup Suggestions' -Icon $broomIcon -Body $body -CardStyle 'margin-bottom: 16px;' } } diff --git a/Module/src/UltraTree/Private/New-HtmlDuplicatesTable.ps1 b/Module/src/UltraTree/Private/New-HtmlDuplicatesTable.ps1 index 238fd47..cf807d1 100644 --- a/Module/src/UltraTree/Private/New-HtmlDuplicatesTable.ps1 +++ b/Module/src/UltraTree/Private/New-HtmlDuplicatesTable.ps1 @@ -1,4 +1,4 @@ -function New-HtmlDuplicatesTable { +function New-HtmlDuplicatesTable { <# .SYNOPSIS Creates a table displaying duplicate file groups. @@ -9,6 +9,7 @@ function New-HtmlDuplicatesTable { .PARAMETER TotalWasted Total wasted space across all duplicate groups. #> + [CmdletBinding()] param ( [array]$DuplicateGroups, [long]$TotalWasted @@ -38,8 +39,7 @@ function New-HtmlDuplicatesTable { }) -join "
    " if ($remaining -gt 0) { - $mutedColor = Get-ThemeColor -Severity "Muted" - $pathList += "
    +$remaining more" + $pathList += "
    +$remaining more" } $severity = Get-WastedSpaceSeverity -WastedBytes $group.WastedSpace @@ -48,15 +48,15 @@ function New-HtmlDuplicatesTable { @"
    - - + + "@ } @" -

    Duplicate Files (Wasted: $totalWastedText)

    +

    Duplicate Files (Wasted: $totalWastedText)

    $fileName
    $fileCount × $sizeText
    $pathList$fileName
    $fileCount × $sizeText
    $pathList $wastedText
    diff --git a/Module/src/UltraTree/Private/New-HtmlFileTypeTable.ps1 b/Module/src/UltraTree/Private/New-HtmlFileTypeTable.ps1 index 7c9b4e5..d117def 100644 --- a/Module/src/UltraTree/Private/New-HtmlFileTypeTable.ps1 +++ b/Module/src/UltraTree/Private/New-HtmlFileTypeTable.ps1 @@ -1,4 +1,4 @@ -function New-HtmlFileTypeTable { +function New-HtmlFileTypeTable { <# .SYNOPSIS Creates a table showing file types by total size. @@ -7,6 +7,7 @@ function New-HtmlFileTypeTable { .PARAMETER FileTypes Array of file type objects with Extension, FileCount, and TotalSize properties. #> + [CmdletBinding()] param ([array]$FileTypes) $colors = @("#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD", "#98D8C8", "#F7DC6F", "#BB8FCE", "#85C1E9", "#F8B500", "#00CED1", "#FF7F50", "#9370DB", "#20B2AA") diff --git a/Module/src/UltraTree/Private/New-HtmlInfoCard.ps1 b/Module/src/UltraTree/Private/New-HtmlInfoCard.ps1 index 868df28..ea11a1b 100644 --- a/Module/src/UltraTree/Private/New-HtmlInfoCard.ps1 +++ b/Module/src/UltraTree/Private/New-HtmlInfoCard.ps1 @@ -1,4 +1,4 @@ -function New-HtmlInfoCard { +function New-HtmlInfoCard { <# .SYNOPSIS Creates an info/alert card with icon and description. @@ -11,6 +11,7 @@ function New-HtmlInfoCard { .PARAMETER Type The card type: Info, Warning, Danger, or Success. #> + [CmdletBinding()] param ( [string]$Title, [string]$Description, @@ -21,12 +22,17 @@ function New-HtmlInfoCard { $style = Get-SeverityStyle -Severity $Type $classExtra = if ($Type -eq "Info") { "" } else { " $($Type.ToLower())" } + # NinjaOne dark mode inherits light page text, but info-card.* keeps light-theme + # tinted backgrounds in WYSIWYG fields — force readable text on those cards. + $titleStyle = ' style="color: #333;"' + $descStyle = ' style="color: #666;"' + @"
    -
    $Title
    -
    $Description
    +
    $Title
    +
    $Description
    "@ diff --git a/Module/src/UltraTree/Private/New-HtmlLineChart.ps1 b/Module/src/UltraTree/Private/New-HtmlLineChart.ps1 index c1bdf6b..c401eda 100644 --- a/Module/src/UltraTree/Private/New-HtmlLineChart.ps1 +++ b/Module/src/UltraTree/Private/New-HtmlLineChart.ps1 @@ -1,4 +1,4 @@ -function New-HtmlLineChart { +function New-HtmlLineChart { <# .SYNOPSIS Creates a line/segment chart showing disk usage. @@ -9,6 +9,7 @@ function New-HtmlLineChart { .PARAMETER Total The total value for calculating percentages. #> + [CmdletBinding()] param ( [array]$Segments, [long]$Total diff --git a/Module/src/UltraTree/Private/New-HtmlRankedStack.ps1 b/Module/src/UltraTree/Private/New-HtmlRankedStack.ps1 new file mode 100644 index 0000000..2ea4fd7 --- /dev/null +++ b/Module/src/UltraTree/Private/New-HtmlRankedStack.ps1 @@ -0,0 +1,36 @@ +function New-HtmlRankedStack { + <# + .SYNOPSIS + Stacks ranked table cards full-width for NinjaOne WYSIWYG layout. + .DESCRIPTION + Wraps one or more HTML card fragments in a full-width Bootstrap row so + Top Files and Top Folders tables stack vertically in NinjaOne WYSIWYG fields. + .PARAMETER Sections + HTML card fragments (e.g. from New-HtmlTable). + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [string[]]$Sections + ) + + $parts = @($Sections | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + if ($parts.Count -eq 0) { return '' } + + $cards = for ($i = 0; $i -lt $parts.Count; $i++) { + $card = $parts[$i] -replace '
    +
    +$($cards -join "`n") +
    +
    +"@ +} diff --git a/Module/src/UltraTree/Private/New-HtmlStatCard.ps1 b/Module/src/UltraTree/Private/New-HtmlStatCard.ps1 index a8de849..6954f65 100644 --- a/Module/src/UltraTree/Private/New-HtmlStatCard.ps1 +++ b/Module/src/UltraTree/Private/New-HtmlStatCard.ps1 @@ -1,4 +1,4 @@ -function New-HtmlStatCard { +function New-HtmlStatCard { <# .SYNOPSIS Creates a stat card with value, description, and optional icon. @@ -13,6 +13,7 @@ function New-HtmlStatCard { .PARAMETER Icon Optional FontAwesome icon class. #> + [CmdletBinding()] param ( [string]$Value, [string]$Description, diff --git a/Module/src/UltraTree/Private/New-HtmlTable.ps1 b/Module/src/UltraTree/Private/New-HtmlTable.ps1 index a352412..59493bb 100644 --- a/Module/src/UltraTree/Private/New-HtmlTable.ps1 +++ b/Module/src/UltraTree/Private/New-HtmlTable.ps1 @@ -1,4 +1,4 @@ -function New-HtmlTable { +function New-HtmlTable { <# .SYNOPSIS Creates a table of files/folders with size and type information. @@ -11,6 +11,7 @@ function New-HtmlTable { .PARAMETER Icon Optional FontAwesome icon class for the title. #> + [CmdletBinding()] param ( [array]$Items, [string]$Title = "Results", @@ -31,7 +32,7 @@ function New-HtmlTable { - + "@ } diff --git a/Module/src/UltraTree/Private/New-HtmlTag.ps1 b/Module/src/UltraTree/Private/New-HtmlTag.ps1 index ed5071d..743d7b4 100644 --- a/Module/src/UltraTree/Private/New-HtmlTag.ps1 +++ b/Module/src/UltraTree/Private/New-HtmlTag.ps1 @@ -1,19 +1,40 @@ -function New-HtmlTag { +function New-HtmlTag { <# .SYNOPSIS Creates an inline tag/badge element. .DESCRIPTION Generates HTML for a small tag or badge with optional type styling. + Uses inline colors so NinjaOne WYSIWYG fields render badges without wrapper CSS. .PARAMETER Text The tag text content. .PARAMETER Type Optional type: empty string (default), "disabled", or "expired". #> + [CmdletBinding()] param ( [string]$Text, [ValidateSet("", "disabled", "expired")] [string]$Type = "" ) + $classExtra = if ($Type) { " $Type" } else { "" } - "
    $Text
    " + $baseStyle = 'display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 600;' + + switch ($Type) { + 'expired' { + $bgColor = Get-ThemeColor -Severity 'Danger' + $fgColor = '#fff' + } + 'disabled' { + $bgColor = Get-ThemeColor -Severity 'Warning' + $fgColor = '#333' + } + default { + $bgColor = Get-ThemeColor -Severity 'Success' + $fgColor = '#333' + } + } + + $style = "$baseStyle background-color: $bgColor; color: $fgColor;" + "
    $Text
    " } diff --git a/Module/src/UltraTree/Public/ConvertTo-NinjaOneHtml.ps1 b/Module/src/UltraTree/Public/ConvertTo-NinjaOneHtml.ps1 index 8dcc127..d0151e1 100644 --- a/Module/src/UltraTree/Public/ConvertTo-NinjaOneHtml.ps1 +++ b/Module/src/UltraTree/Public/ConvertTo-NinjaOneHtml.ps1 @@ -1,14 +1,26 @@ -function ConvertTo-NinjaOneHtml { +function ConvertTo-NinjaOneHtml { <# .SYNOPSIS Converts scan results to NinjaOne-compatible HTML report. .DESCRIPTION Transforms the output from Get-FolderSizes into a formatted HTML report suitable for display in NinjaOne WYSIWYG custom fields. Includes statistics - cards, charts, tables, and cleanup recommendations. + cards, ranked Top Files / Top Folders tables, cleanup suggestions, and + file type breakdown per drive. .PARAMETER ScanResults The PSCustomObject output from Get-FolderSizes containing Items, FileTypes, CleanupSuggestions, Duplicates, DriveInfo, and statistics. + .PARAMETER MaxTopFiles + Maximum file rows in per-drive Top Files table. Defaults to Display.MaxTopFiles (50). + Get-FolderSizes returns Items as one mixed file/folder list truncated by -Top (default 40). + Raise -Top on Get-FolderSizes when you need more file rows (for example -Top 200). + .PARAMETER MaxTopFolders + Maximum folder rows in per-drive Top Folders table. Defaults to Display.MaxTopFolders (25). + Subject to the same Get-FolderSizes -Top limit as MaxTopFiles. + .PARAMETER ShowAllResults + When true, includes the full "All Results by Size" table. Default true for backward compatibility. + .PARAMETER FooterSuffix + Optional text appended after the UltraTree version in the footer (e.g. caller script version). .OUTPUTS String containing HTML markup for the report. .EXAMPLE @@ -17,23 +29,37 @@ function ConvertTo-NinjaOneHtml { $html | Ninja-Property-Set-Piped treesize .EXAMPLE $results = Get-FolderSizes -DriveLetter C - $html = ConvertTo-NinjaOneHtml -ScanResults $results + $html = ConvertTo-NinjaOneHtml -ScanResults $results -ShowAllResults:$false -FooterSuffix ", Script v1.4.2" $wrappedHtml = New-HtmlWrapper -Content $html -Title "Disk Report" $wrappedHtml | Out-File "report.html" .NOTES The output HTML assumes Bootstrap 5, Font Awesome 6, and Charts.css are available. For standalone viewing, wrap with New-HtmlWrapper. + + Top Files and Top Folders tables read from Get-FolderSizes Items, which is one + mixed list capped by -Top. Use a larger -Top when generating reports that need + many file rows (MaxTopFiles defaults to 50). #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [PSCustomObject]$ScanResults + [PSCustomObject]$ScanResults, + + [int]$MaxTopFiles = 0, + + [int]$MaxTopFolders = 0, + + [bool]$ShowAllResults = $true, + + [string]$FooterSuffix = '' ) process { $html = [System.Text.StringBuilder]::new() $cfg = $script:Config + $maxFiles = if ($MaxTopFiles -gt 0) { $MaxTopFiles } else { $cfg.Display.MaxTopFiles } + $maxFolders = if ($MaxTopFolders -gt 0) { $MaxTopFolders } else { $cfg.Display.MaxTopFolders } # === TOTAL SUMMARY STAT CARDS === $driveCount = $ScanResults.DriveInfo.Count @@ -49,6 +75,8 @@ function ConvertTo-NinjaOneHtml { $searchIcon = Get-ThemeIcon -IconName "Search" $copyIcon = Get-ThemeIcon -IconName "Copy" $broomIcon = Get-ThemeIcon -IconName "Broom" + $fileIcon = Get-ThemeIcon -IconName "File" + $folderIcon = Get-ThemeIcon -IconName "Folder" [void]$html.AppendLine('
    ') [void]$html.AppendLine('
    ') @@ -80,7 +108,18 @@ function ConvertTo-NinjaOneHtml { $freeColor = Get-ThemeColor -Severity "Free" $driveCleanup = @($ScanResults.CleanupSuggestions | Where-Object { $_.Drive -eq $drive.Drive }) - $driveFolders = @($ScanResults.Items | Where-Object { $_.Drive -eq $drive.Drive -and $_.IsDirectory } | Select-Object -First $cfg.Display.MaxTopFolders) + $driveFiles = @( + $ScanResults.Items | + Where-Object { $_.Drive -eq $drive.Drive -and -not $_.IsDirectory } | + Sort-Object -Property SizeBytes -Descending | + Select-Object -First $maxFiles + ) + $driveFolders = @( + $ScanResults.Items | + Where-Object { $_.Drive -eq $drive.Drive -and $_.IsDirectory } | + Sort-Object -Property SizeBytes -Descending | + Select-Object -First $maxFolders + ) $driveFileTypes = @($ScanResults.FileTypes | Where-Object { $_.Drive -eq $drive.Drive } | Select-Object -First $cfg.Display.MaxFileTypes) # Row 1: Stat cards @@ -113,33 +152,35 @@ function ConvertTo-NinjaOneHtml { [void]$html.AppendLine('
    ') - # Row 3: Three-column layout + # Row 3: Top Files + Top Folders ranked tables (full width) + $rankedSections = @() + if ($driveFiles.Count -gt 0) { + $rankedSections += New-HtmlTable -Items $driveFiles -Title "Top Files" -Icon $fileIcon + } + if ($driveFolders.Count -gt 0) { + $rankedSections += New-HtmlTable -Items $driveFolders -Title "Top Folders" -Icon $folderIcon + } + if ($rankedSections.Count -gt 0) { + [void]$html.AppendLine((New-HtmlRankedStack -Sections $rankedSections)) + } + + # Row 4: Cleanup + File Types (two columns) [void]$html.AppendLine('
    ') - [void]$html.AppendLine('
    ') + [void]$html.AppendLine('
    ') if ($driveCleanup.Count -gt 0) { [void]$html.AppendLine((New-HtmlCleanupSuggestions -Suggestions $driveCleanup -Compact)) } else { $checkIcon = Get-ThemeIcon -IconName "CheckCircle" - [void]$html.AppendLine("
      No Cleanup Needed

    No significant cleanup opportunities found.

    ") + [void]$html.AppendLine("
      No Cleanup Needed

    No significant cleanup opportunities found.

    ") } [void]$html.AppendLine('
    ') - [void]$html.AppendLine('
    ') - if ($driveFolders.Count -gt 0) { - $chartItems = $driveFolders | ForEach-Object { - $label = Split-Path $_.Path -Leaf - if ([string]::IsNullOrEmpty($label)) { $label = $_.Path } - @{ Label = $label; Value = $_.SizeBytes } - } - [void]$html.AppendLine((New-HtmlBarChart -Items $chartItems -Title "Top Folders")) - } - [void]$html.AppendLine('
    ') - - [void]$html.AppendLine('
    ') + [void]$html.AppendLine('
    ') if ($driveFileTypes.Count -gt 0) { - [void]$html.AppendLine((New-HtmlFileTypeTable -FileTypes $driveFileTypes)) + $fileTypesHtml = (New-HtmlFileTypeTable -FileTypes $driveFileTypes) -replace '
    ', '
    ' + [void]$html.AppendLine($fileTypesHtml) } [void]$html.AppendLine('
    ') @@ -152,13 +193,20 @@ function ConvertTo-NinjaOneHtml { } # === FULL RESULTS TABLE === - $listIcon = Get-ThemeIcon -IconName "List" - [void]$html.AppendLine((New-HtmlTable -Items $ScanResults.Items -Title "All Results by Size" -Icon $listIcon)) + if ($ShowAllResults) { + $listIcon = Get-ThemeIcon -IconName "List" + [void]$html.AppendLine((New-HtmlTable -Items $ScanResults.Items -Title "All Results by Size" -Icon $listIcon)) + } # === FOOTER === $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm" - $mutedColor = Get-ThemeColor -Severity "Muted" - [void]$html.AppendLine("

    TreeSize v$($cfg.Version) | Scanned: $scanTime

    ") + $moduleVersion = $MyInvocation.MyCommand.Module.Version + $footerText = "UltraTree v$moduleVersion" + if ($FooterSuffix) { + $footerText += $FooterSuffix + } + $footerText += " | Scanned: $scanTime" + [void]$html.AppendLine("

    $footerText

    ") return $html.ToString() } diff --git a/Module/src/UltraTree/Public/Get-FolderSizes.ps1 b/Module/src/UltraTree/Public/Get-FolderSizes.ps1 index e6022ea..8d73277 100644 --- a/Module/src/UltraTree/Public/Get-FolderSizes.ps1 +++ b/Module/src/UltraTree/Public/Get-FolderSizes.ps1 @@ -11,7 +11,11 @@ function Get-FolderSizes { .PARAMETER MaxDepth Maximum folder depth to include in results (default: 5, range: 1-20). .PARAMETER Top - Maximum number of results to return (default: 40, range: 1-1000). + Maximum number of items returned in ScanResults.Items (default: 40, range: 1-1000). + Items is a single list of files and folders mixed together, sorted by size globally + (across all drives when using -AllDrives). Folders usually dominate this list. + For ranked Top Files tables in ConvertTo-NinjaOneHtml, use a larger -Top value + (for example -Top 200) so enough file rows remain after truncation. .PARAMETER FolderSize Show only folders in results (excludes individual files). .PARAMETER FileSize diff --git a/Module/src/UltraTree/Public/New-HtmlWrapper.ps1 b/Module/src/UltraTree/Public/New-HtmlWrapper.ps1 index 08ea92f..b56727e 100644 --- a/Module/src/UltraTree/Public/New-HtmlWrapper.ps1 +++ b/Module/src/UltraTree/Public/New-HtmlWrapper.ps1 @@ -1,4 +1,4 @@ -function New-HtmlWrapper { +function New-HtmlWrapper { <# .SYNOPSIS Wraps HTML fragment with full document including CSS/JS dependencies. @@ -16,6 +16,7 @@ function New-HtmlWrapper { Get-FolderSizes -AllDrives | ConvertTo-NinjaOneHtml | New-HtmlWrapper | Out-File "report.html" #> [CmdletBinding()] + [OutputType([string])] param ( [Parameter(ValueFromPipeline)] [string]$Content, diff --git a/Module/src/UltraTree/UltraTree.psd1 b/Module/src/UltraTree/UltraTree.psd1 index f5f3c1e..6d0bdfa 100644 --- a/Module/src/UltraTree/UltraTree.psd1 +++ b/Module/src/UltraTree/UltraTree.psd1 @@ -1,4 +1,4 @@ -# +# # Module manifest for module 'UltraTree' # # Generated by: Jan Scholte @@ -12,7 +12,7 @@ RootModule = 'UltraTree.psm1' # Version number of this module. -ModuleVersion = '1.0.1' +ModuleVersion = '1.0.2' # Supported PSEditions # CompatiblePSEditions = @()
    $typeIcon $($item.Path) $($item.Size)$lastMod$lastMod