Skip to content
ConsoleMonster

Dashboard frames

Build a dashboard frame with facts, meters, attention items and sections in PowerShell or C#, laid out for the terminal's width.

A dashboard frame is a summary that prints once and says how something is doing: the title line with the scope's facts and its state, a row of meters, a "Needs attention" list with the worst problems first, summary sections packed beside it, detail sections below, a legend and a footer. You describe the parts; the frame decides where they go for the width it gets, so every summary built on it reads the same way. New-ConsoleDashboardFrame takes the parts as records, DashboardFrame takes them as objects in C#, and Ui.DashboardFrame() chains them in C# fluent.

Try it

The estate and its numbers are fictional and nothing is queried. The three programs print the same cells.

Install-Module ConsoleMonster -Scope CurrentUser
PowerShellserver-estate.ps1
# Summarize a server estate in one dashboard frame: facts, meters, problems first, then the details.

Import-Module -Name ConsoleMonster
# All data is synthetic and inline; no services are contacted or changed.

# Data: fictional inputs kept in memory.
$sites = @(
    [pscustomobject] @{ Site = 'London'; Servers = '22'; Healthy = '21' }
    [pscustomobject] @{ Site = 'Amsterdam'; Servers = '16'; Healthy = '15' }
    [pscustomobject] @{ Site = 'New York'; Servers = '10'; Healthy = '8' }
)
$backups = @(
    [pscustomobject] @{ Job = 'Nightly SQL'; 'Last run' = '02:10'; Size = '84 GB' }
    [pscustomobject] @{ Job = 'Weekly files'; 'Last run' = 'Sun 01:00'; Size = '312 GB' }
)

# Layout: the frame places each part for the terminal's width.
$frame = @{
    Title = 'Server estate'
    Fact = 'contoso.example', '48 servers', '3 sites'
    State = @(
        [pscustomobject] @{ State = 'Failing'; Count = 1; Word = 'failing' }
        [pscustomobject] @{ State = 'Warning'; Count = 3; Word = 'warning' }
    )
    Meter = @(
        [pscustomobject] @{ Label = 'Servers healthy'; Healthy = 44; Warning = 3; Failing = 1 }
        [pscustomobject] @{ Label = 'Backups current'; Healthy = 46; Whole = 48 }
        [pscustomobject] @{ Label = 'Oldest patch'; Value = '41 days'; Fact = 'FS-02'
            State = 'Warning' }
        [pscustomobject] @{ Label = 'Open alerts'; Value = 5 }
    )
    Attention = @(
        [pscustomobject] @{ State = 'Failing'
            Identity = 'SQL-03'
            Reason = 'Disk D: 97% full'
            Order = 1
                Values = '97%' }
        [pscustomobject] @{ State = 'Warning'
            Identity = 'FS-02'
            Reason = 'Patches 41 days old'
            Order = 2
                Values = '41 d' }
        [pscustomobject] @{ State = 'Warning'
            Identity = 'WEB-07'
            Reason = 'Certificate expires in 9 days'
                Order = 3
            Values = '9 d' }
    )
    Noun = 'server'
    OutOf = 48
    Section = @(
        [pscustomobject] @{ Title = 'Sites'; Facts = 'by location'
            Content = { $sites | New-ConsoleTable } }
        [pscustomobject] @{ Title = 'Backups'; Placement = 'Detail'
            Content = { $backups | New-ConsoleTable } }
    )
    Legend = [ordered] @{ Healthy = 'healthy'; Warning = 'warning'; Failing = 'failing' }
    FooterCount = '48 servers'
}

# Behaviour: print the frame once and return.
New-ConsoleDashboardFrame @frame | Show-Console
Full program
# Summarize a server estate in one dashboard frame: facts, meters, problems first, then the details.

Import-Module -Name ConsoleMonster
# All data is synthetic and inline; no services are contacted or changed.

# Data: fictional inputs kept in memory.
$sites = @(
    [pscustomobject] @{ Site = 'London'; Servers = '22'; Healthy = '21' }
    [pscustomobject] @{ Site = 'Amsterdam'; Servers = '16'; Healthy = '15' }
    [pscustomobject] @{ Site = 'New York'; Servers = '10'; Healthy = '8' }
)
$backups = @(
    [pscustomobject] @{ Job = 'Nightly SQL'; 'Last run' = '02:10'; Size = '84 GB' }
    [pscustomobject] @{ Job = 'Weekly files'; 'Last run' = 'Sun 01:00'; Size = '312 GB' }
)

# Layout: the frame places each part for the terminal's width.
$frame = @{
    Title = 'Server estate'
    Fact = 'contoso.example', '48 servers', '3 sites'
    State = @(
        [pscustomobject] @{ State = 'Failing'; Count = 1; Word = 'failing' }
        [pscustomobject] @{ State = 'Warning'; Count = 3; Word = 'warning' }
    )
    Meter = @(
        [pscustomobject] @{ Label = 'Servers healthy'; Healthy = 44; Warning = 3; Failing = 1 }
        [pscustomobject] @{ Label = 'Backups current'; Healthy = 46; Whole = 48 }
        [pscustomobject] @{ Label = 'Oldest patch'; Value = '41 days'; Fact = 'FS-02'
            State = 'Warning' }
        [pscustomobject] @{ Label = 'Open alerts'; Value = 5 }
    )
    Attention = @(
        [pscustomobject] @{ State = 'Failing'
            Identity = 'SQL-03'
            Reason = 'Disk D: 97% full'
            Order = 1
                Values = '97%' }
        [pscustomobject] @{ State = 'Warning'
            Identity = 'FS-02'
            Reason = 'Patches 41 days old'
            Order = 2
                Values = '41 d' }
        [pscustomobject] @{ State = 'Warning'
            Identity = 'WEB-07'
            Reason = 'Certificate expires in 9 days'
                Order = 3
            Values = '9 d' }
    )
    Noun = 'server'
    OutOf = 48
    Section = @(
        [pscustomobject] @{ Title = 'Sites'; Facts = 'by location'
            Content = { $sites | New-ConsoleTable } }
        [pscustomobject] @{ Title = 'Backups'; Placement = 'Detail'
            Content = { $backups | New-ConsoleTable } }
    )
    Legend = [ordered] @{ Healthy = 'healthy'; Warning = 'warning'; Failing = 'failing' }
    FooterCount = '48 servers'
}

# Behaviour: print the frame once and return.
New-ConsoleDashboardFrame @frame | Show-Console

Run: ./server-estate.ps1

Terminal
dotnet new console -n ServerEstate
cd ServerEstate
dotnet add package ConsoleMonster
C#Program.cs
// Summarize a server estate in one dashboard frame: facts, meters, problems first, then the details.
// All data is synthetic and inline; no services are contacted or changed.
using ConsoleMonster;
using ConsoleMonster.Frames;
using ConsoleMonster.Widgets;

// All values are synthetic. This example never connects to a host.
// Data: fictional inputs kept in memory.
string[][] sites =
{
    new[] { "London", "22", "21" },
    new[] { "Amsterdam", "16", "15" },
    new[] { "New York", "10", "8" }
};
string[][] backups =
{
    new[] { "Nightly SQL", "02:10", "84 GB" },
    new[] { "Weekly files", "Sun 01:00", "312 GB" }
};

// Layout: the frame places each part for the terminal's width.
DashboardFrame frame = new DashboardFrame("Server estate");
frame.Header.Facts.Add("contoso.example");
frame.Header.Facts.Add("48 servers");
frame.Header.Facts.Add("3 sites");
frame.Header.States.Add(FrameState.Of(HealthState.Failing, 1, "failing"));
frame.Header.States.Add(FrameState.Of(HealthState.Warning, 3, "warning"));
frame.Meters.Add(Meter.Composition("Servers healthy", 44, 3, 1));
frame.Meters.Add(Meter.Ratio("Backups current", 46, 48));
frame.Meters.Add(Meter.Fact("Oldest patch", "41 days", "FS-02", HealthState.Warning));
frame.Meters.Add(Meter.Count("Open alerts", 5));
frame.Attention.Noun = "server";
frame.Attention.OutOf = 48;
(HealthState State, string Identity, string Reason, string Value)[] problems =
{
    (HealthState.Failing, "SQL-03", "Disk D: 97% full", "97%"),
    (HealthState.Warning, "FS-02", "Patches 41 days old", "41 d"),
    (HealthState.Warning, "WEB-07", "Certificate expires in 9 days", "9 d")
};
for (int i = 0; i < problems.Length; i++)
{
    AttentionItem item = new AttentionItem(problems[i].State, problems[i].Identity,
        problems[i].Reason, i + 1.0);
    item.Values.Add(problems[i].Value);
    frame.Attention.Items.Add(item);
}
Table siteTable = new Table().AddColumn("Site").AddColumn("Servers").AddColumn("Healthy");
foreach (string[] site in sites) { siteTable.AddValues(site); }
FrameSection siteSection = new FrameSection("Sites", context => siteTable);
siteSection.Facts.Add("by location");
frame.Sections.Add(siteSection);
Table backupTable = new Table().AddColumn("Job").AddColumn("Last run").AddColumn("Size");
foreach (string[] backup in backups) { backupTable.AddValues(backup); }
frame.Sections.Add(new FrameSection(
    "Backups",
    context => backupTable) { Placement = FramePlacement.Detail });
frame.Legend.Add(HealthState.Healthy, "healthy");
frame.Legend.Add(HealthState.Warning, "warning");
frame.Legend.Add(HealthState.Failing, "failing");
frame.Footer.Counts.Add("48 servers");
frame.Footer.Hint = FrameHint.None;

// Behaviour: print the frame once and return.
Terminal.Write(frame);
Full program
// Summarize a server estate in one dashboard frame: facts, meters, problems first, then the details.
// All data is synthetic and inline; no services are contacted or changed.
using ConsoleMonster;
using ConsoleMonster.Frames;
using ConsoleMonster.Widgets;

// All values are synthetic. This example never connects to a host.
// Data: fictional inputs kept in memory.
string[][] sites =
{
    new[] { "London", "22", "21" },
    new[] { "Amsterdam", "16", "15" },
    new[] { "New York", "10", "8" }
};
string[][] backups =
{
    new[] { "Nightly SQL", "02:10", "84 GB" },
    new[] { "Weekly files", "Sun 01:00", "312 GB" }
};

// Layout: the frame places each part for the terminal's width.
DashboardFrame frame = new DashboardFrame("Server estate");
frame.Header.Facts.Add("contoso.example");
frame.Header.Facts.Add("48 servers");
frame.Header.Facts.Add("3 sites");
frame.Header.States.Add(FrameState.Of(HealthState.Failing, 1, "failing"));
frame.Header.States.Add(FrameState.Of(HealthState.Warning, 3, "warning"));
frame.Meters.Add(Meter.Composition("Servers healthy", 44, 3, 1));
frame.Meters.Add(Meter.Ratio("Backups current", 46, 48));
frame.Meters.Add(Meter.Fact("Oldest patch", "41 days", "FS-02", HealthState.Warning));
frame.Meters.Add(Meter.Count("Open alerts", 5));
frame.Attention.Noun = "server";
frame.Attention.OutOf = 48;
(HealthState State, string Identity, string Reason, string Value)[] problems =
{
    (HealthState.Failing, "SQL-03", "Disk D: 97% full", "97%"),
    (HealthState.Warning, "FS-02", "Patches 41 days old", "41 d"),
    (HealthState.Warning, "WEB-07", "Certificate expires in 9 days", "9 d")
};
for (int i = 0; i < problems.Length; i++)
{
    AttentionItem item = new AttentionItem(problems[i].State, problems[i].Identity,
        problems[i].Reason, i + 1.0);
    item.Values.Add(problems[i].Value);
    frame.Attention.Items.Add(item);
}
Table siteTable = new Table().AddColumn("Site").AddColumn("Servers").AddColumn("Healthy");
foreach (string[] site in sites) { siteTable.AddValues(site); }
FrameSection siteSection = new FrameSection("Sites", context => siteTable);
siteSection.Facts.Add("by location");
frame.Sections.Add(siteSection);
Table backupTable = new Table().AddColumn("Job").AddColumn("Last run").AddColumn("Size");
foreach (string[] backup in backups) { backupTable.AddValues(backup); }
frame.Sections.Add(new FrameSection(
    "Backups",
    context => backupTable) { Placement = FramePlacement.Detail });
frame.Legend.Add(HealthState.Healthy, "healthy");
frame.Legend.Add(HealthState.Warning, "warning");
frame.Legend.Add(HealthState.Failing, "failing");
frame.Footer.Counts.Add("48 servers");
frame.Footer.Hint = FrameHint.None;

// Behaviour: print the frame once and return.
Terminal.Write(frame);

Run: dotnet run

Terminal
dotnet new console -n ServerEstate
cd ServerEstate
dotnet add package ConsoleMonster
C# fluentProgram.cs
// Summarize a server estate in one dashboard frame: facts, meters, problems first, then the details.
// All data is synthetic and inline; no services are contacted or changed.
using ConsoleMonster;
using ConsoleMonster.Frames;
using ConsoleMonster.Widgets;

// All values are synthetic. This example never connects to a host.
// Data: fictional inputs kept in memory.
string[][] sites =
{
    new[] { "London", "22", "21" },
    new[] { "Amsterdam", "16", "15" },
    new[] { "New York", "10", "8" }
};
string[][] backups =
{
    new[] { "Nightly SQL", "02:10", "84 GB" },
    new[] { "Weekly files", "Sun 01:00", "312 GB" }
};

// Layout: the frame places each part for the terminal's width.
DashboardFrame frame = Ui.DashboardFrame("Server estate");
frame.Header.Facts.Add("contoso.example");
frame.Header.Facts.Add("48 servers");
frame.Header.Facts.Add("3 sites");
frame.Header.States.Add(FrameState.Of(HealthState.Failing, 1, "failing"));
frame.Header.States.Add(FrameState.Of(HealthState.Warning, 3, "warning"));
frame.Meters.Add(Meter.Composition("Servers healthy", 44, 3, 1));
frame.Meters.Add(Meter.Ratio("Backups current", 46, 48));
frame.Meters.Add(Meter.Fact("Oldest patch", "41 days", "FS-02", HealthState.Warning));
frame.Meters.Add(Meter.Count("Open alerts", 5));
frame.Attention.Noun("server");
frame.Attention.OutOf(48);
(HealthState State, string Identity, string Reason, string Value)[] problems =
{
    (HealthState.Failing, "SQL-03", "Disk D: 97% full", "97%"),
    (HealthState.Warning, "FS-02", "Patches 41 days old", "41 d"),
    (HealthState.Warning, "WEB-07", "Certificate expires in 9 days", "9 d")
};
for (int i = 0; i < problems.Length; i++)
{
    AttentionItem item = new AttentionItem(problems[i].State, problems[i].Identity,
        problems[i].Reason, i + 1.0);
    item.Values.Add(problems[i].Value);
    frame.Attention.Items.Add(item);
}
Table siteTable = Ui.Table().AddColumn("Site").AddColumn("Servers").AddColumn("Healthy");
foreach (string[] site in sites) { siteTable.AddValues(site); }
FrameSection siteSection = new FrameSection("Sites", context => siteTable);
siteSection.Facts.Add("by location");
frame.Sections.Add(siteSection);
Table backupTable = Ui.Table().AddColumn("Job").AddColumn("Last run").AddColumn("Size");
foreach (string[] backup in backups) { backupTable.AddValues(backup); }
frame.Sections.Add(new FrameSection("Backups", context => backupTable)
    .Placement(FramePlacement.Detail));
frame.Legend.Add(HealthState.Healthy, "healthy");
frame.Legend.Add(HealthState.Warning, "warning");
frame.Legend.Add(HealthState.Failing, "failing");
frame.Footer.Counts.Add("48 servers");
frame.Footer.Hint(FrameHint.None);

// Behaviour: print the frame once and return.
frame.Show();
Full program
// Summarize a server estate in one dashboard frame: facts, meters, problems first, then the details.
// All data is synthetic and inline; no services are contacted or changed.
using ConsoleMonster;
using ConsoleMonster.Frames;
using ConsoleMonster.Widgets;

// All values are synthetic. This example never connects to a host.
// Data: fictional inputs kept in memory.
string[][] sites =
{
    new[] { "London", "22", "21" },
    new[] { "Amsterdam", "16", "15" },
    new[] { "New York", "10", "8" }
};
string[][] backups =
{
    new[] { "Nightly SQL", "02:10", "84 GB" },
    new[] { "Weekly files", "Sun 01:00", "312 GB" }
};

// Layout: the frame places each part for the terminal's width.
DashboardFrame frame = Ui.DashboardFrame("Server estate");
frame.Header.Facts.Add("contoso.example");
frame.Header.Facts.Add("48 servers");
frame.Header.Facts.Add("3 sites");
frame.Header.States.Add(FrameState.Of(HealthState.Failing, 1, "failing"));
frame.Header.States.Add(FrameState.Of(HealthState.Warning, 3, "warning"));
frame.Meters.Add(Meter.Composition("Servers healthy", 44, 3, 1));
frame.Meters.Add(Meter.Ratio("Backups current", 46, 48));
frame.Meters.Add(Meter.Fact("Oldest patch", "41 days", "FS-02", HealthState.Warning));
frame.Meters.Add(Meter.Count("Open alerts", 5));
frame.Attention.Noun("server");
frame.Attention.OutOf(48);
(HealthState State, string Identity, string Reason, string Value)[] problems =
{
    (HealthState.Failing, "SQL-03", "Disk D: 97% full", "97%"),
    (HealthState.Warning, "FS-02", "Patches 41 days old", "41 d"),
    (HealthState.Warning, "WEB-07", "Certificate expires in 9 days", "9 d")
};
for (int i = 0; i < problems.Length; i++)
{
    AttentionItem item = new AttentionItem(problems[i].State, problems[i].Identity,
        problems[i].Reason, i + 1.0);
    item.Values.Add(problems[i].Value);
    frame.Attention.Items.Add(item);
}
Table siteTable = Ui.Table().AddColumn("Site").AddColumn("Servers").AddColumn("Healthy");
foreach (string[] site in sites) { siteTable.AddValues(site); }
FrameSection siteSection = new FrameSection("Sites", context => siteTable);
siteSection.Facts.Add("by location");
frame.Sections.Add(siteSection);
Table backupTable = Ui.Table().AddColumn("Job").AddColumn("Last run").AddColumn("Size");
foreach (string[] backup in backups) { backupTable.AddValues(backup); }
frame.Sections.Add(new FrameSection("Backups", context => backupTable)
    .Placement(FramePlacement.Detail));
frame.Legend.Add(HealthState.Healthy, "healthy");
frame.Legend.Add(HealthState.Warning, "warning");
frame.Legend.Add(HealthState.Failing, "failing");
frame.Footer.Counts.Add("48 servers");
frame.Footer.Hint(FrameHint.None);

// Behaviour: print the frame once and return.
frame.Show();

Run: dotnet run

Server estate 110×2024-bitFull

At 110 columns the attention list and the sites share a row, and the backups, a detail section, follow below.

How it works

Each part is a record in PowerShell and an object in C#. Meters pick their kind from the fields they carry: Healthy with Warning and Failing is a composition bar, Healthy with Whole is a ratio, a number Value is a count and a text Value is a fact.

$frame = @{
    Title = 'Server estate'
    Fact = 'contoso.example', '48 servers'
    Meter = @(
        [pscustomobject]@{ Label = 'Servers healthy'; Healthy = 44; Warning = 3; Failing = 1 }
        [pscustomobject]@{ Label = 'Backups current'; Healthy = 46; Whole = 48 }
    )
    Attention = [pscustomobject]@{
        State = 'Failing'; Identity = 'SQL-03'; Reason = 'Disk D: 97% full'; Values = '97%'
    }
    Noun = 'server'
    OutOf = 48
    Section = [pscustomobject]@{ Title = 'Sites'; Content = { $sites | New-ConsoleTable } }
}
New-ConsoleDashboardFrame @frame | Show-Console
var frame = new DashboardFrame("Server estate");
frame.Header.Facts.Add("contoso.example");
frame.Header.Facts.Add("48 servers");
frame.Meters.Add(Meter.Composition("Servers healthy", 44, 3, 1));
frame.Meters.Add(Meter.Ratio("Backups current", 46, 48));
frame.Attention.Noun = "server";
frame.Attention.OutOf = 48;
frame.Attention.Items.Add(new AttentionItem(HealthState.Failing, "SQL-03", "Disk D: 97% full") {
    Values = { "97%" }
});
frame.Sections.Add(new FrameSection("Sites", context => siteTable));
Terminal.Write(frame);
Ui.DashboardFrame("Server estate")
    .Fact("contoso.example", "48 servers")
    .Meter(Meter.Composition("Servers healthy", 44, 3, 1))
    .Meter(Meter.Ratio("Backups current", 46, 48))
    .AttentionOf("server", 48)
    .Attention(HealthState.Failing, "SQL-03", "Disk D: 97% full", "97%")
    .Section("Sites", c => c.Add(siteTable))
    .Show();
  • Title line. Facts follow the title and states (State, Count, Word) sit at the right edge. When every state counts zero and nothing needs attention, the line says "all healthy". -ObservedAt adds how long ago the data was taken.
  • Meters print in the order given; a narrow row keeps the first ones.
  • Needs attention prints first in the summary rows whenever it has items, worst state first, then by Order, with "3 of 48 servers" from -Noun and -OutOf.
  • Sections build their content once. Summary sections pack side by side while their minimum widths fit, and a later one drops into the shortest column; Placement = 'Detail' prints a section below at full width. MinWidth, MaxWidth, Grow and FullWidth shape the row.
  • Legend and footer. The legend lists the states you name; the footer prints the counts. A frame the script builds has no footer hint; module views use EveryRow ("pipe to Format-Table for every row").

Variations

The same program at 60 columns: the facts move under the title, meters stack two to a row and each section takes the whole width.

dotnet run 60×2924-bitFull
The server estate program at 60 columns.
  • -Length Summary leaves out the detail sections and Full lifts the row caps; without it the frame follows the session's preference.
  • On the ASCII tier every cell inside the frame, including your own widgets, is folded to printable ASCII without changing the layout.
  • A module that owns summary objects declares its frame in C#, next to its data model, and registers it as their default display; DashboardFrame.Compact then gives it a narrow view of its own below 80 columns.

The server estate demo is the program above with its download and run lines, and the readiness report builds a similar summary from ordinary widgets.

Next

Edit this page on GitHub