Private/Get-HDTConsoleShareNode.ps1

function Get-HDTConsoleShareNode {
    <#
        .SYNOPSIS
            Builds the rows for one deployment share - the share itself and the
            three categories beneath it.
 
        .DESCRIPTION
            One share's subtree, so Get-HDTConsoleTreeNode can hold several of
            them without repeating any of this. The rows start at Depth 1
            because Depth 0 is the Deployment Shares root every share hangs off,
            the way Deployment Workbench roots them.
 
            A SHARE THAT WOULD NOT OPEN IS STILL A ROW. With one share, a bad
            path could reasonably throw; with four, throwing means three shares
            an administrator can see nothing of because of a fourth. So a
            failure is a row that says which path failed and why, and the other
            shares are unaffected.
 
        .PARAMETER Workspace
            A share from Get-HDTConsoleWorkspace, or a failure from
            New-HDTConsoleShareFailure.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject[] - console nodes in
            display order.
 
        .EXAMPLE
            Get-HDTConsoleShareNode -Workspace (Get-HDTConsoleWorkspace -Path 'C:\HDTLab\Share')
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject[]])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNull()]
        [object] $Workspace
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $node = New-Object -TypeName System.Collections.ArrayList
    $header = Get-HDTConsoleHeader -Workspace $Workspace

    # -- a share that would not open ---------------------------------------

    if ($Workspace.Status -ne 'Ok') {
        $field = @(
            New-HDTConsoleField -Label 'Path' -Value $Workspace.Root
            New-HDTConsoleField -Label 'Could not be opened' -Value $Workspace.Error
        )

        [void] $node.Add((New-HDTConsoleNode -Depth 1 -Kind 'Share' -Status 'Error' `
                    -Text ('{0} - (could not be opened)' -f $Workspace.Root) `
                    -Field $field `
                    -Command ("Get-HDTConsoleWorkspace -Path '{0}'" -f $Workspace.Root) `
                    -Header $header))

        return [pscustomobject[]] @($node)
    }

    # -- the share ---------------------------------------------------------
    #
    # TWO SHAPES, ONE PASS. $node is the flat reading, in display order, and
    # every row is also added to its parent's Children so the window has a tree
    # to expand. Building them separately is how the two would come to disagree.

    # THE THREE THIS ROW SETS, and they arrived here from the Windows PE
    # window's Bootstrap tab: a share's name, where clients reach it and how
    # much it logs are the SHARE's settings, and that tab's subject is the rules
    # file. The id and the schema version are fixed at creation; 'Opened from'
    # is where this console found it.
    #
    # THE CREDENTIAL IS NOT AMONG THEM. Setting it writes two things - a line in
    # workspace.yaml and a protected file beside it - so a box writing one would
    # leave a share declaring an account no secret exists for, which is a build
    # that refuses. Set-HDTShareCredential does both.
    $shareField = @(
        New-HDTConsoleField -Label 'Share' -Value $Workspace.Name -Property 'name'
        New-HDTConsoleField -Label 'Id' -Value $Workspace.Id
        New-HDTConsoleField -Label 'Schema version' -Value $Workspace.SchemaVersion
        New-HDTConsoleField -Label 'Opened from' -Value $Workspace.Root
        # THE TWO PATHS ARE NOT THE SAME PATH, and they sit next to each other:
        # 'Opened from' is the disk this console reached the share on, and this
        # is what a machine in WinPE dials. A local path here is a share that
        # deploys nothing.
        #
        # AND IT SHOULD BE A NAME, NOT AN ADDRESS. deployRoot is baked into the
        # boot image at build time, and this lab's host address is a DHCP lease
        # that moves when the Wi-Fi changes - a name survives what an octet does
        # not. That is a sentence too many for a tooltip, so the hint carries the
        # trap a technician hits and this comment carries the reason.
        New-HDTConsoleField -Label 'Deploy root' -Value $Workspace.DeployRoot -Property 'deployRoot' `
            -Hint ('What a machine in WinPE connects to - not the path above, which is only where this ' +
            'console opened the share. A local path here is a share that deploys nothing.')

        # INFO IS THE ORDINARY SETTING and Debug is not a louder version of it:
        # it logs every variable resolution, which is what makes it the level to
        # turn on for a deployment that is failing and off again afterwards.
        New-HDTConsoleField -Label 'Log level' -Value $Workspace.LogLevel -Property 'logLevel' `
            -Choice (Get-HDTWorkspaceLogLevel) `
            -Hint ('How much every deployment from this share writes. Info is the ordinary setting; ' +
            'Debug logs every variable resolution and is worth turning back afterwards.')

        # READ-ONLY ON PURPOSE, and the hint says so rather than leaving a
        # technician to discover it by clicking. Setting it writes two things -
        # this line and a protected file beside it - so a box that wrote one
        # would leave the share naming an account no secret exists for, which is
        # a build that refuses.
        New-HDTConsoleField -Label 'Credential' -Value (Get-HDTConsoleDisplayText -Text $Workspace.CredentialUser -Fallback '(none - the share is opened as the signed-in user)') `
            -Hint ('Read-only here: setting it writes this line and a protected file beside it. ' +
            'Set-HDTShareCredential writes both.')
        New-HDTConsoleField -Label 'Document' -Value $Workspace.WorkspacePath
    )

    $shareNode = New-HDTConsoleNode -Depth 1 -Kind 'Share' -Status 'Ok' `
        -Text ('{0} ({1})' -f $Workspace.Name, $Workspace.Id) `
        -Field $shareField `
        -Command ("Get-HDTConsoleWorkspace -Path '{0}'" -f $Workspace.Root) `
        -Header $header -Subject $Workspace

    [void] $node.Add($shareNode)

    # THE CATEGORY ORDER IS THE ORDER A SHARE IS BUILT IN, and it is not
    # Workbench's alphabetical-ish one: boot image, operating systems, drivers,
    # then task sequences. A share is useless until it has an image that boots
    # and an OS to lay down; drivers make that OS work on the hardware in front
    # of you; the task sequence is what finally ties them together, and it is
    # the thing that can only be written once the other three exist. Reading top
    # to bottom is reading the order the work happens in.
    #
    # MONITORING COMES LAST, AFTER ALL OF IT, for the same reason: it is what
    # happens once the share has been built, not another thing to build. The
    # four categories above are a share's contents; this one is the share in
    # use.

    # -- the boot image ----------------------------------------------------

    $bootFolder = Get-HDTWorkspacePath -Root $Workspace.Root -Kind Boot

    # NAMED, LIKE THE OTHER THREE CATEGORIES, because a window matches on Name
    # and reads Text. And it carries workspace.yaml: the Windows PE window hangs
    # off this row as well as off the image under it, and that window edits the
    # document rather than the share root.
    $bootCategory = New-HDTConsoleNode -Depth 2 -Kind 'Category' -Status 'Ok' `
        -Name 'BootImage' -Text 'Boot Image' `
        -Field @(
        New-HDTConsoleField -Label 'Folder' -Value $bootFolder
        New-HDTConsoleField -Label 'Image name' -Value $Workspace.BootImage.Name
    ) `
        -Command ("Get-HDTWorkspacePath -Root '{0}' -Kind Boot" -f $Workspace.Root) `
        -Header $header -Subject $Workspace.WorkspacePath

    [void] $node.Add($bootCategory)
    [void] $shareNode.Children.Add($bootCategory)

    $bootNode = Get-HDTConsoleBootImageNode -BootImage $Workspace.BootImage -Workspace $Workspace -Header $header

    [void] $node.Add($bootNode)
    [void] $bootCategory.Children.Add($bootNode)

    # -- applications ------------------------------------------------------
    #
    # WORKBENCH'S THIRD CATEGORY, and the one an administrator changes weekly:
    # media is imported once, sequences are written once, and the application
    # list moves every time somebody ships a new version. The engine has had the
    # whole catalog since M7 - Import, Get, Set, detection, dependency order -
    # and none of it was on screen, so the part that changes most was the part
    # that could only be reached from a prompt.

    $appFolder = Get-HDTWorkspacePath -Root $Workspace.Root -Kind Applications
    $appCommand = "Get-HDTApplication -WorkspaceRoot '{0}' -FileSystem (New-HDTFileSystem)" -f $Workspace.Root

    # NAMED, as the other two are: the window hangs New Application off this row
    # and must tell it from the rest without parsing a label somebody may
    # reword.
    $appCategory = New-HDTConsoleNode -Depth 2 -Kind 'Category' -Status 'Ok' `
        -Name 'Applications' `
        -Text ('Applications ({0})' -f @($Workspace.Application).Count) `
        -Field @(
        New-HDTConsoleField -Label 'Folder' -Value $appFolder
        New-HDTConsoleField -Label 'Applications' -Value @($Workspace.Application).Count
    ) `
        -Command $appCommand -Header $header

    [void] $node.Add($appCategory)
    [void] $shareNode.Children.Add($appCategory)

    $appRow = New-Object -TypeName System.Collections.ArrayList

    foreach ($application in @($Workspace.Application)) {

        # THE FOUR THAT CAN BE TYPED INTO, as on the other two rows: Workbench
        # edits name, comments and both command lines on an application's
        # Properties sheet. The id is not among them - it is the folder name and
        # what a sequence names to select it, so changing it is a move.
        $field = @(
            New-HDTConsoleField -Label 'Id' -Value $application.Id
            New-HDTConsoleField -Label 'Name' -Value $application.Name -Property 'name'
            # NO FALLBACK IN A BOX THAT WRITES, which is the rule the sequence
            # rows already follow: '(not recorded)' is for reading, and leaving
            # it in a box that writes app.yaml means an administrator adding a
            # publisher has to delete the words first - or, worse, tabs out and
            # writes the phrase into the document as the publisher.
            New-HDTConsoleField -Label 'Publisher' -Value ([string] $application.Publisher) -Property 'publisher'
            New-HDTConsoleField -Label 'Version' -Value ([string] $application.Version) -Property 'version'
            New-HDTConsoleField -Label 'Description' -Value ([string] $application.Description) -Property 'description'
            # WHERE THE COMMAND LINE RUNS, WHICH THE ROW CANNOT SHOW.
            # Invoke-HDTInstallApplicationsStep hands the line to cmd.exe with
            # the application's own source folder as the working directory, so a
            # bare 'setup.msi' resolves and %CD% is that folder.
            #
            # %~dp0 IS THE ONE EVERY ADMINISTRATOR REACHES FOR, and it is the
            # one that does not work: it expands only inside a .cmd file, and
            # there is no batch file here - the string IS the command. Typed
            # into this box it reaches msiexec as six literal characters and
            # comes back as 1619, "the installation package could not be
            # opened", naming a path that does not exist rather than the
            # mistake. That is a defect a technician cannot diagnose from the
            # error, which is exactly what a hint is for.
            New-HDTConsoleField -Label 'Install' -Value $application.Install -Property 'install' `
                -Hint 'Runs through cmd.exe from the application''s own folder, so %CD% is that folder and a bare file name resolves. %~dp0 expands only inside a .cmd file, not here.'
            New-HDTConsoleField -Label 'Uninstall' -Value ([string] $application.Uninstall) -Property 'uninstall' `
                -Hint 'Optional, and run exactly as Install is: cmd.exe, the application''s own folder, %CD% for it, no %~dp0. For an .msi: msiexec.exe /x {ProductCode} /qn /norestart.'
            New-HDTConsoleField -Label 'Runs in' -Value $application.RunIn

            # THE THREE THAT WERE A REPORT AND ARE NOW A FORM. Workbench edits
            # every one of them on an application's Properties sheet;
            # Set-HDTApplication has written all three since M7, and the pane
            # showed them as text nobody could correct - so the only way to fix
            # an exit code list was a prompt.
            #
            # NO FALLBACK TEXT IN A BOX THAT WRITES. '(nothing)' and '(none)'
            # are for reading, and tabbing out of a box holding one of them
            # would put the word into app.yaml as a dependency id. The empty
            # box IS the answer, and the ? beside it says what empty means.
            New-HDTConsoleField -Label 'Success codes' -Value ((@($application.SuccessCode) | ForEach-Object { [string] $_ }) -join ', ') -Property 'successCodes' `
                -Hint 'Exit codes that mean it worked, comma separated. Empty inherits 0 and 3010 - and 3010 is "installed, reboot required", not a failure.'
            New-HDTConsoleField -Label 'Reboot codes' -Value ((@($application.RebootCode) | ForEach-Object { [string] $_ }) -join ', ') -Property 'rebootCodes' `
                -Hint 'Exit codes that mean the machine owes a restart, comma separated. Empty inherits 3010; the sequence reboots and resumes at the next application.'
            New-HDTConsoleField -Label 'Detection' -Value $application.DetectText -Property 'detect' `
                -Hint 'The rule that decides it is already installed, written as app.yaml writes it: type: msiProduct, then the keys that type takes. Empty means it installs every time.'
            # SHOWN HERE, PICKED FROM THE MENU. This is the one of the four that
            # can be checked against something: an exit code and a detection
            # rule are knowledge about the package, and typing is the only way
            # to state them, but a dependency is an id that either is or is not
            # on this share. A misspelled one is not caught until a deployment
            # runs, when Resolve-HDTApplicationOrder refuses the WHOLE plan
            # rather than that one application - so the tree's Depends On item
            # offers what is there and refuses what would close a loop.
            New-HDTConsoleField -Label 'Depends on' -Value (@($application.Dependency) -join ', ') `
                -Hint 'Ids that must install first. Right-click this application and choose Depends On to change them: they are picked from what is on the share, so an id that does not exist cannot be written here.'
            New-HDTConsoleField -Label 'Source' -Value $application.SourcePath
            New-HDTConsoleField -Label 'Document' -Value $application.Path
        )

        # THE NAME, AND THE VERSION IF THE NAME DOES NOT ALREADY CARRY IT -
        # never 'id - name', which the other two categories use. See
        # Get-HDTConsoleApplicationLabel for why.
        #
        # THE ID IS ON THE ROW'S OWN PROPERTIES PANE, where it is needed by
        # whoever is writing a sequence that names it.
        #
        # ONE RULE, SHARED WITH THE EDITOR'S APPLICATION PAGE. A row somebody
        # ticks there has to read the same as the row they found here.
        $text = Get-HDTConsoleApplicationLabel -Name ([string] $application.Name) `
            -Version ([string] $application.Version) -Id ([string] $application.Id)

        if ($application.Status -eq 'Error') {
            $text = '{0} - (unreadable)' -f $application.Id
            $field = @(
                New-HDTConsoleField -Label 'Id' -Value $application.Id
                New-HDTConsoleField -Label 'Document' -Value $application.Path
                New-HDTConsoleField -Label 'Could not be read' -Value $application.Error
            )
        }

        $row = New-HDTConsoleNode -Depth 3 -Kind 'Application' -Status $application.Status `
            -Text $text -Field $field `
            -Name $application.Id `
            -Command ("Get-HDTApplication -WorkspaceRoot '{0}' -Id '{1}' -FileSystem (New-HDTFileSystem)" -f
                $Workspace.Root, $application.Id) `
            -Header $header -Subject $application

        $row | Add-Member -MemberType NoteProperty -Name 'Folder' `
            -Value ([string] $application.Folder) -Force

        [void] $appRow.Add($row)
    }

    $appGrouped = Group-HDTConsoleFolderRow -Row ([object[]] @($appRow)) -Depth 3 `
        -Declared ([string[]] @($Workspace.Folder.Application)) -Category Application -Header $header

    foreach ($current in @($appGrouped.Node)) { [void] $node.Add($current) }
    foreach ($current in @($appGrouped.TopLevel)) { [void] $appCategory.Children.Add($current) }

    if (@($Workspace.Application).Count -eq 0) {
        $row = New-HDTConsoleNode -Depth 3 -Kind 'Empty' -Status 'Ok' -Text '(none)' `
            -Field @(
            New-HDTConsoleField -Label 'Folder' -Value $appFolder
            New-HDTConsoleField -Label '' -Value 'There is no application on this share yet. Add one with Import-HDTApplication; it lands as a folder under the folder above with an app.yaml and a source\ payload in it.'
        ) `
            -Command $appCommand -Header $header

        [void] $node.Add($row)
        [void] $appCategory.Children.Add($row)
    }

    # -- operating systems -------------------------------------------------

    $osFolder = Get-HDTWorkspacePath -Root $Workspace.Root -Kind OperatingSystems
    $osCommand = "Get-HDTWorkspacePath -Root '{0}' -Kind OperatingSystems" -f $Workspace.Root

    # NAMED, NOT JUST LABELLED, for the same reason TaskSequences is: the window
    # hangs Import Operating System off this row and must be able to tell it
    # from the other three categories without parsing a label somebody may
    # reword.
    $osCategory = New-HDTConsoleNode -Depth 2 -Kind 'Category' -Status 'Ok' `
        -Name 'OperatingSystems' `
        -Text ('Operating Systems ({0})' -f @($Workspace.OperatingSystem).Count) `
        -Field @(
        New-HDTConsoleField -Label 'Folder' -Value $osFolder
        New-HDTConsoleField -Label 'Operating Systems' -Value @($Workspace.OperatingSystem).Count
    ) `
        -Command $osCommand -Header $header

    [void] $node.Add($osCategory)
    [void] $shareNode.Children.Add($osCategory)

    $osRow = New-Object -TypeName System.Collections.ArrayList

    foreach ($operatingSystem in @($Workspace.OperatingSystem)) {
        $image = foreach ($current in @($operatingSystem.Image)) {
            '{0,-3} {1} [{2}] {3}' -f $current.Index, $current.Name, $current.Edition, $current.Version
        }

        # THE TWO THAT CAN BE TYPED INTO, as on a task sequence: Workbench edits
        # both on an imported OS's Properties sheet, and everything below them
        # is a reading of the media rather than a decision anybody made. The id
        # is not among them - it is the folder name and what a sequence names to
        # select the image, so changing it is a move.
        $field = @(
            New-HDTConsoleField -Label 'Id' -Value $operatingSystem.Id
            New-HDTConsoleField -Label 'Name' -Value $operatingSystem.Name -Property 'name'
            New-HDTConsoleField -Label 'Description' -Value ([string] $operatingSystem.Description) -Property 'description'
            New-HDTConsoleField -Label 'Type' -Value $operatingSystem.Type
            New-HDTConsoleField -Label 'Architecture' -Value (Get-HDTConsoleDisplayText -Text $operatingSystem.Architecture -Fallback '(not recorded)')
            New-HDTConsoleField -Label 'Default index' -Value $operatingSystem.DefaultIndex
            New-HDTConsoleField -Label ('Images ({0})' -f $operatingSystem.ImageCount) -Value (@($image) -join [System.Environment]::NewLine)
            New-HDTConsoleField -Label 'Source path' -Value $operatingSystem.SourcePath
            New-HDTConsoleField -Label 'Image path' -Value $operatingSystem.ImagePath
            New-HDTConsoleField -Label 'Document' -Value $operatingSystem.Path
        )

        $text = '{0} - {1}' -f $operatingSystem.Id, $operatingSystem.Name
        if ($operatingSystem.Status -eq 'Error') {
            $text = '{0} - (unreadable)' -f $operatingSystem.Id
            $field = @(
                New-HDTConsoleField -Label 'Id' -Value $operatingSystem.Id
                New-HDTConsoleField -Label 'Document' -Value $operatingSystem.Path
                New-HDTConsoleField -Label 'Could not be read' -Value $operatingSystem.Error
            )
        }

        $row = New-HDTConsoleNode -Depth 3 -Kind 'OperatingSystem' -Status $operatingSystem.Status `
            -Text $text -Field $field `
            -Command ("Get-HDTOperatingSystem -WorkspaceRoot '{0}' -Id '{1}' -FileSystem (New-HDTFileSystem)" -f
                $Workspace.Root, $operatingSystem.Id) `
            -Header $header -Subject $operatingSystem

        # THE FOLDER THE DOCUMENT NAMED, carried on the row so the grouping
        # below can read it without going back to the document.
        $row | Add-Member -MemberType NoteProperty -Name 'Folder' `
            -Value ([string] $operatingSystem.Folder) -Force

        [void] $osRow.Add($row)
    }

    $osGrouped = Group-HDTConsoleFolderRow -Row ([object[]] @($osRow)) -Depth 3 `
        -Declared ([string[]] @($Workspace.Folder.OperatingSystem)) -Category OperatingSystem -Header $header

    foreach ($current in @($osGrouped.Node)) { [void] $node.Add($current) }
    foreach ($current in @($osGrouped.TopLevel)) { [void] $osCategory.Children.Add($current) }

    if (@($Workspace.OperatingSystem).Count -eq 0) {
        $row = New-HDTConsoleNode -Depth 3 -Kind 'Empty' -Status 'Ok' -Text '(none)' `
            -Field @(
            New-HDTConsoleField -Label 'Folder' -Value $osFolder
            New-HDTConsoleField -Label '' -Value 'There is no operating system on this share yet. Import one with Import-HDTOperatingSystem; it lands as a folder under the folder above with an os.yaml in it.'
        ) `
            -Command $osCommand -Header $header

        [void] $node.Add($row)
        [void] $osCategory.Children.Add($row)
    }

    # -- drivers -----------------------------------------------------------
    #
    # THE FOLDER, AND AN HONEST SENTENCE ABOUT THE REST. DESIGN 7 describes a
    # driver store and the engine has not built one - there is no Get-HDTDriver
    # and no driver schema. The category is here because this is where it
    # belongs in the order, and because an administrator looking for drivers
    # should find out they are not supported yet from the console rather than
    # from a deployment that silently installs none.

    $driverCommand = "Get-HDTWorkspacePath -Root '{0}' -Kind Drivers" -f $Workspace.Root

    $driverCategory = New-HDTConsoleNode -Depth 2 -Kind 'Category' -Status 'Ok' -Text 'Drivers' `
        -Icon (Get-HDTConsoleIcon -Kind 'DriverStore' -Status 'Ok') `
        -Field @(
        New-HDTConsoleField -Label 'Folder' -Value $Workspace.Driver.Folder
        New-HDTConsoleField -Label 'Folder exists' -Value (Get-HDTConsoleFlagText -Value $Workspace.Driver.Present)
    ) `
        -Command $driverCommand -Header $header

    [void] $node.Add($driverCategory)
    [void] $shareNode.Children.Add($driverCategory)

    $driverRow = New-HDTConsoleNode -Depth 3 -Kind 'Empty' -Status 'Ok' -Text '(not supported yet)' `
        -Field @(
        New-HDTConsoleField -Label 'Folder' -Value $Workspace.Driver.Folder
        New-HDTConsoleField -Label 'Folder exists' -Value (Get-HDTConsoleFlagText -Value $Workspace.Driver.Present)
        New-HDTConsoleField -Label '' -Value ('The engine has no driver catalog yet: there is no command that reads this folder and no step that injects from it, so nothing here would reach a deployed machine. The console will list it as soon as the engine can read it.')
    ) `
        -Command $driverCommand -Header $header

    [void] $node.Add($driverRow)
    [void] $driverCategory.Children.Add($driverRow)

    # -- task sequences ----------------------------------------------------

    $sequenceFolder = Get-HDTWorkspacePath -Root $Workspace.Root -Kind TaskSequences
    $sequenceCommand = "Get-HDTWorkspacePath -Root '{0}' -Kind TaskSequences" -f $Workspace.Root

    # NAMED, NOT JUST LABELLED. The text carries a count - 'Task Sequences (5)' -
    # and the window has to be able to tell this row from the other three
    # categories to hang New Task Sequence off it. Matching on a label would put
    # a parser in the window and break the day somebody rewords it.
    $sequenceCategory = New-HDTConsoleNode -Depth 2 -Kind 'Category' -Status 'Ok' `
        -Name 'TaskSequences' `
        -Text ('Task Sequences ({0})' -f @($Workspace.TaskSequence).Count) `
        -Field @(
        New-HDTConsoleField -Label 'Folder' -Value $sequenceFolder
        New-HDTConsoleField -Label 'Task Sequences' -Value @($Workspace.TaskSequence).Count
    ) `
        -Command $sequenceCommand -Header $header

    [void] $node.Add($sequenceCategory)
    [void] $shareNode.Children.Add($sequenceCategory)

    $sequenceRow = New-Object -TypeName System.Collections.ArrayList

    foreach ($sequence in @($Workspace.TaskSequence)) {
        # DESIGN 12'S "VALIDATION, SURFACED INLINE". Test-HDTTaskSequence answers
        # what a schema cannot - would this sequence actually work on the machine
        # you are about to deploy - and Get-HDTConsoleWorkspace has already asked
        # it. This puts the answer where somebody will see it before they boot a
        # machine rather than after.
        $lint = Get-HDTConsoleLintText -Finding $sequence.Finding

        # THE TWO THAT CAN BE TYPED INTO, and they are typed into HERE rather
        # than in a window that has to be opened first: renaming a sequence is
        # the commonest edit there is, and Workbench asks for a properties
        # dialog to do it. The id is not among them - it is the folder name, so
        # changing it is a move rather than an edit.
        #
        # NO '(none)' ON THE DESCRIPTION: the fallback is for reading, and a box
        # somebody tabs out of writes what is in it. The empty box says the same
        # thing and writes nothing.
        $field = @(
            New-HDTConsoleField -Label 'Id' -Value $sequence.Id
            New-HDTConsoleField -Label 'Name' -Value $sequence.Name -Property 'name'
            New-HDTConsoleField -Label 'Description' -Value ([string] $sequence.Description) -Property 'description'
            New-HDTConsoleField -Label 'Steps' -Value $sequence.StepCount
            New-HDTConsoleField -Label 'Groups' -Value $sequence.GroupCount
            New-HDTConsoleField -Label 'Validation' -Value $lint.Detail
            New-HDTConsoleField -Label 'Document' -Value $sequence.Path
        )

        $text = '{0} - {1}' -f $sequence.Id, $sequence.Name

        # THE COUNT GOES ON THE ROW. A tree of thirty sequences is scanned, not
        # read: the row has to say "this one" without being opened, and the pane
        # is for somebody who has already decided to look.
        $sequenceStatus = $sequence.Status

        if ($sequence.Status -eq 'Ok' -and -not [string]::IsNullOrEmpty($lint.Caption)) {
            $text = '{0} - {1} ({2})' -f $sequence.Id, $sequence.Name, $lint.Caption
            $sequenceStatus = $lint.Status
        }

        if ($sequence.Status -eq 'Error') {
            $text = '{0} - (unreadable)' -f $sequence.Id
            $field = @(
                New-HDTConsoleField -Label 'Id' -Value $sequence.Id
                New-HDTConsoleField -Label 'Document' -Value $sequence.Path
                New-HDTConsoleField -Label 'Could not be read' -Value $sequence.Error
            )
        }

        # THE ID IS THE NAME, and Text is only the label. Name falls back to Text
        # when it is not given, so the tree's Remove was handed
        # 'DEMO-M4 - Deploy Windows 11 LTSC' as an id and refused it for having
        # spaces - a row that would not go, for a reason that appeared in the
        # command box rather than anywhere near the row. It is also what survives
        # somebody rewording the label.
        $row = New-HDTConsoleNode -Depth 3 -Kind 'TaskSequence' -Status $sequenceStatus `
            -Name ([string] $sequence.Id) `
            -Text $text -Field $field `
            -Command ("Import-HDTSequenceDocument -Path '{0}' -FileSystem (New-HDTFileSystem)" -f $sequence.Path) `
            -Header $header -Subject $sequence

        # THE BROWSER STOPS HERE, and the steps are Get-HDTConsoleSequenceEditor's.
        # Deployment Workbench lists task sequences in the tree and edits their
        # steps in a properties window opened from one; CLAUDE.md asks for a
        # console close enough to it that muscle memory transfers. Expanding
        # every step of every sequence of every share would also be unusable at
        # the size an administrator runs - the lab's own sample share is four
        # sequences and over thirty step rows before a single operating system.
        $row | Add-Member -MemberType NoteProperty -Name 'Folder' `
            -Value ([string] $sequence.Folder) -Force

        [void] $sequenceRow.Add($row)
    }

    $sequenceGrouped = Group-HDTConsoleFolderRow -Row ([object[]] @($sequenceRow)) -Depth 3 `
        -Declared ([string[]] @($Workspace.Folder.TaskSequence)) -Category TaskSequence -Header $header

    foreach ($current in @($sequenceGrouped.Node)) { [void] $node.Add($current) }
    foreach ($current in @($sequenceGrouped.TopLevel)) { [void] $sequenceCategory.Children.Add($current) }

    if (@($Workspace.TaskSequence).Count -eq 0) {
        $row = New-HDTConsoleNode -Depth 3 -Kind 'Empty' -Status 'Ok' -Text '(none)' `
            -Field @(
            New-HDTConsoleField -Label 'Folder' -Value $sequenceFolder
            New-HDTConsoleField -Label '' -Value 'There is no task sequence on this share yet. A task sequence is a folder under the folder above with a sequence.yaml in it.'
        ) `
            -Command $sequenceCommand -Header $header

        [void] $node.Add($row)
        [void] $sequenceCategory.Children.Add($row)
    }

    # -- monitoring --------------------------------------------------------
    #
    # DESIGN 12 lists it among the tree's categories, so an administrator looking
    # for what is running finds it where they find everything else about the
    # share rather than having to know a command exists. It comes LAST because it
    # is the share in use rather than another thing to build.
    #
    # THE WHOLE SUBTREE IS Get-HDTConsoleMonitorNode'S, and that is not tidiness:
    # the console rebuilds this branch on a timer while the window is open
    # (ROADMAP M8, "tailing"), and the tree as first drawn has to be the same
    # shape as the tree as refreshed. One builder, called from both, is the only
    # arrangement in which they cannot drift.

    $monitorCategory = Get-HDTConsoleMonitorNode -Path $Workspace.Root -Header $header `
        -Monitor $Workspace.Monitor

    [void] $node.Add($monitorCategory)
    [void] $shareNode.Children.Add($monitorCategory)

    # The flat reading gets the run rows too - it is every row in display order,
    # and a caller counting them must not find the branch missing from it.
    foreach ($current in @($monitorCategory.Children)) {
        [void] $node.Add($current)
    }

    return [pscustomobject[]] @($node)
}