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
        New-HDTConsoleField -Label 'Deploy root' -Value $Workspace.DeployRoot -Property 'deployRoot'
        New-HDTConsoleField -Label 'Log level' -Value $Workspace.LogLevel -Property 'logLevel'
        New-HDTConsoleField -Label 'Credential' -Value (Get-HDTConsoleDisplayText -Text $Workspace.CredentialUser -Fallback '(none - the share is opened as the signed-in user)')
        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

    $bootCategory = New-HDTConsoleNode -Depth 2 -Kind 'Category' -Status 'Ok' -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

    [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)

    # -- 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)

    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

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

    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)

    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.
        [void] $node.Add($row)
        [void] $sequenceCategory.Children.Add($row)
    }

    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)
}