// Server toolbox: menus, selections, review, live work and actionable results. // Synthetic data and timings; reports and notifications stay in memory. No remote effects. using ConsoleMonster; using ConsoleMonster.Interactive; using ConsoleMonster.Glyphs; using ConsoleMonster.Layout; using ConsoleMonster.Styling; using ConsoleMonster.Text; using ConsoleMonster.Widgets; // Data: replace these inventories, operations and findings with your own sources. Node[] inventory = { new("FS01", "lab.example.test", "Warsaw", "File", "Windows Server 2022", ""), new("FS02", "lab.example.test", "Warsaw", "File", "Windows Server 2022", ""), new("WEB01", "lab.example.test", "Warsaw", "Web", "Windows Server 2025", ""), new("SQL01", "lab.example.test", "Warsaw", "Database", "Windows Server 2019", ""), new("FS11", "lab.example.test", "Berlin", "File", "Windows Server 2022", ""), new("WEB11", "lab.example.test", "Berlin", "Web", "Windows Server 2025", ""), new("PRN11", "lab.example.test", "Berlin", "Print", "Windows Server 2016", "offline since 09:12"), new("BKP21", "lab.example.test", "Dublin", "Backup", "Windows Server 2022", ""), new("SQL21", "lab.example.test", "Dublin", "Database", "Windows Server 2022", ""), new("LAB01", "lab.example.test", "Lab", "Test", "Windows Server 2025", ""), new("LAB02", "lab.example.test", "Lab", "Test", "Windows Server 2025", "maintenance until Friday"), }; Check[] catalog = { new("DiskCleanup", "Disk cleanup", "Maintenance", "Removes temporary files and old update caches.", "cache under 1 GB", 3, true), new("LogRotation", "Log rotation", "Maintenance", "Compresses logs older than 7 days.", "retention 90 days", 2, true), new("RestartPending", "Pending restart check", "Maintenance", "Reports restarts waiting after updates.", "no restart pending", 1, false), new("PatchStatus", "Patch status", "Security", "Lists missing security updates.", "0 missing", 2, false), new("Certificates", "Certificate expiry", "Security", "Finds certificates expiring within 30 days.", "30+ days", 1, false), new("LocalAdmins", "Local administrators audit", "Security", "Compares local Administrators with the approved list.", "approved list", 1, false), new("Firewall", "Firewall rules review", "Security", "Flags inbound allow rules open to any address.", "scoped rules", 2, false), new("EventErrors", "Event log errors", "Health", "Summarizes errors from the last 24 hours.", "under 20", 1, false), new("Services", "Service health", "Health", "Checks and restarts stopped automatic services.", "all running", 1, true), new("DiskSpace", "Disk space", "Health", "Warns below 15% and fails below 5% free.", "15% free", 1, false), new("Uptime", "Uptime and reboots", "Health", "Reports unexpected reboots in the last 30 days.", "0 unexpected", 1, false), }; Dictionary findings = new() { ["FS02/PatchStatus"] = (StatusKind.Fail, "3 updates missing for 41 days"), ["WEB01/Certificates"] = (StatusKind.Warn, "expires in 24 days"), ["SQL01/DiskSpace"] = (StatusKind.Fail, "4% free on data volume"), ["FS11/Services"] = (StatusKind.Warn, "1 service restarted"), ["FS01/DiskCleanup"] = (StatusKind.Warn, "1.4 GB remains"), }; List history = new() { new("Mon 19 Oct 21:04", 3, 4, 10, 1, 1, TimeSpan.FromSeconds(372)), new("Tue 20 Oct 07:30", 2, 3, 6, 0, 0, TimeSpan.FromSeconds(66)) }; string reportRoot = OperatingSystem.IsWindows() ? @"C:\Reports\Toolbox" : "/tmp/reports/toolbox"; Node[] targets = inventory.Where(n => n.Domain == "lab.example.test" && n.Offline == "").Take(4).ToArray(); Check[] checks = catalog.Where(c => new[] { "DiskCleanup", "PatchStatus", "Certificates", "DiskSpace" }.Contains(c.Id)).ToArray(); string domain = "lab.example.test", format = "Html", report = reportRoot, email = "ops@example.test"; int parallel = 3; bool notify = false, dryRun = false; bool snapshot = args.Contains("--snapshot") || (!args.Contains("--interactive") && Console.IsInputRedirected); int stepAt = Array.IndexOf(args, "--step"); int step = stepAt < 0 ? 0 : int.Parse(args[stepAt + 1], System.Globalization.CultureInfo.InvariantCulture); if (step < 0 || step > 5) throw new ArgumentOutOfRangeException(nameof(step)); List Plan() => checks.SelectMany(c => targets.Select(n => { var outcome = findings.GetValueOrDefault(n.Name + "/" + c.Id, (StatusKind.Pass, c.Expected)); return new Work(n, c, outcome.Item1, outcome.Item2); })).ToList(); StatusKind Verdict(IEnumerable work) => work.Any(w => w.Status == StatusKind.Fail) ? StatusKind.Fail : work.Any(w => w.Status == StatusKind.Warn) ? StatusKind.Warn : StatusKind.Pass; Table TableOf(string[] headers) { Table table = new() { Border = TableBorder.Minimal, RecordViewRows = 0 }; foreach (string header in headers) table.AddColumn(header, HorizontalAlignment.Left); return table; } IWidget Findings(IEnumerable work) { Table table = TableOf(new[] { "Server", "Task", "Expected", "Actual" }); table.Columns[3].Flexible = true; foreach (Work w in work.Where(w => w.Status != StatusKind.Pass)) table.AddRow(w.Node.Name, w.Check.Name, w.Check.Expected, TableCell.FromValue(RichText.Plain(w.Actual, new RichStyle(w.Status == StatusKind.Fail ? ThemeToken.Fail : ThemeToken.Warn)))); return table; } IWidget History() { Table table = TableOf(new[] { "When", "Servers", "Tasks", "Result", "Duration" }); foreach (HistoryRow h in history.AsEnumerable().Reverse()) table.AddRow(h.When, h.Servers.ToString(), h.Checks.ToString(), $"{h.Passed} passed · {h.Warnings} warn · {h.Failed} failed", h.Elapsed.ToString(@"hh\:mm\:ss")); return table; } IWidget Preview(Check c) => new Rows(new TextBlock(Markup.Escape(c.Description)), new KeyValueList() .Add("Group", c.Category).Add("Expected", c.Expected).Add("Duration", "about " + c.Minutes + " min per server") .Add("Changes", c.Changes ? "[warn]yes[/]" : "[pass]no[/]").Add("Parameter", c.Id)) { Gap = 1 }; // Layout: snapshots and live work use this same widget tree. IWidget Body(List work, int done, int phase) { Work[] finished = work.Take(done).ToArray(); Work[] active = phase == 2 ? work.Skip(done).TakeWhile(w => w.Check.Id == work[done].Check.Id).Take(parallel).ToArray() : Array.Empty(); SummaryBar summary = new SummaryBar { Elapsed = TimeSpan.FromSeconds(done * .24 / parallel) }.SetProgress(done, work.Count) .SetCounter("passed", finished.Count(w => w.Status == StatusKind.Pass), StatusKind.Pass) .SetCounter("warnings", finished.Count(w => w.Status == StatusKind.Warn), StatusKind.Warn) .SetCounter("failed", finished.Count(w => w.Status == StatusKind.Fail), StatusKind.Fail); Phases phases = new(); string[] names = { "Discover", "Connect", "Tasks", "Report" }; for (int i = 0; i < names.Length; i++) phases.Set(names[i], i < phase ? (i == 2 ? Verdict(finished) : StatusKind.Pass) : i == phase ? StatusKind.Running : StatusKind.Pending); Lanes lanes = new("lanes"); foreach (Node node in targets) { Work[] planned = work.Where(w => w.Node == node).ToArray(); Work[] ready = finished.Where(w => w.Node == node).ToArray(); Lane lane = lanes.Add(node.Name, RichText.Plain(node.Name + " " + node.Site), planned.Length); lane.Done = ready.Length; lane.Passed = ready.Count(w => w.Status == StatusKind.Pass); lane.Warned = ready.Count(w => w.Status == StatusKind.Warn); lane.Failed = ready.Count(w => w.Status == StatusKind.Fail); lane.Status = ready.Length == planned.Length ? Verdict(ready) : active.Any(w => w.Node == node) ? StatusKind.Running : StatusKind.Pending; lane.Detail = active.FirstOrDefault(w => w.Node == node)?.Check.Name ?? ready.LastOrDefault()?.Check.Name; } LogRegion log = new() { ShowTime = false, PreferredHeight = 7 }; log.Write($"Inventory: {domain} · {targets.Length} servers · {work.Count} tasks"); if (phase >= 1) log.Write($"Connecting up to {parallel} servers at a time"); foreach (Work w in finished.TakeLast(5)) log.Write($"{w.Node.Name} · {w.Check.Name}: {w.Actual}", w.Status == StatusKind.Fail ? "err" : w.Status == StatusKind.Warn ? "warn" : "info"); if (phase >= 3) log.Write("Preparing " + format.ToUpperInvariant() + " report: " + report); if (phase >= 4) log.Write(notify ? "Report ready · recipient " + email : "Report ready · " + finished.Count(w => w.Status != StatusKind.Pass) + " findings to review"); KeyValueList facts = new KeyValueList().Add("Targets", targets.Length + " servers").Add("Tasks", checks.Length + " per server") .Add("Parallel", parallel.ToString()).Add("Report", format.ToUpperInvariant()).Add("Dry run", dryRun ? "yes" : "no"); Columns info = new(new Panel(lanes, "Servers"), new Panel(facts, "Run options")) { CollapseBelow = 90 }; return new Rows(summary, phases, info, new Panel(Findings(finished), "Findings"), new Panel(log, "Log")); } Dashboard Dashboard(List work, int done, int phase) => new("[bold]Server toolbox[/]", Body(work, done, phase)) { Subtitle = $"{domain} · {targets.Length} servers · {checks.Length} tasks", Footer = "Ctrl+C cancel · results keep the findings" }; // Behaviour: native form/menu navigation, then a bounded run with cancellation and history. void Execute(MenuContext context, List work) { Slot body = new("body", Body(work, 0, 0)); Dashboard dashboard = new("[bold]Server toolbox[/]", body) { Subtitle = domain, Footer = "Ctrl+C cancel · results keep the findings" }; bool completed = false; RunResult outcome = context.Run(dashboard, run => { void Show(int done, int phase) { body.Handle(run.Session).Set(Body(work, done, phase)); if (run.CancellationToken.WaitHandle.WaitOne(240)) run.CancellationToken.ThrowIfCancellationRequested(); } Show(0, 0); Show(0, 1); int done = 0; foreach (var operation in work.GroupBy(w => w.Check.Id)) { foreach (Work[] batch in operation.Chunk(parallel)) { Show(done, 2); done += batch.Length; } } Show(done, 3); Show(done, 4); RunResult result = Result(work); completed = true; return result; }); if (!completed) return; history.Add(new("Tue 20 Oct 09:30", work.Select(w => w.Node.Name).Distinct().Count(), work.Select(w => w.Check.Id).Distinct().Count(), work.Count(w => w.Status == StatusKind.Pass), work.Count(w => w.Status == StatusKind.Warn), work.Count(w => w.Status == StatusKind.Fail), outcome.Elapsed)); } RunResult Result(List work) { RunResult result = new("Run finished", Verdict(work)) { Summary = $"{work.Count} tasks on {work.Select(w => w.Node.Name).Distinct().Count()} servers. Review the findings and choose the next action.", Details = Findings(work) }; result.Figure("passed", work.Count(w => w.Status == StatusKind.Pass).ToString(), StatusKind.Pass); result.Figure("warnings", work.Count(w => w.Status == StatusKind.Warn).ToString(), work.Any(w => w.Status == StatusKind.Warn) ? StatusKind.Warn : StatusKind.Pass); result.Figure("failed", work.Count(w => w.Status == StatusKind.Fail).ToString(), work.Any(w => w.Status == StatusKind.Fail) ? StatusKind.Fail : StatusKind.Pass); result.Next("View report", ctx => ctx.ShowResult(new RunResult("Report · " + format.ToUpperInvariant(), Verdict(work)) { Summary = report, Details = Findings(work) })); if (work.Any(w => w.Status == StatusKind.Fail)) result.Next("Rerun failed tasks", ctx => Execute(ctx, work.Where(w => w.Status == StatusKind.Fail).ToList())); result.Next("View history", ctx => ctx.ShowResult(new RunResult("Run history") { Details = History() })); return result; } Form Options() { Form form = new("Options") { Review = false, SubmitText = "Save" }; FormPage page = form.Page("Options", "Parallelism, reports and notifications."); NumberField throttle = page.Number("Parallel", "Servers at a time"); throttle.Default = parallel; throttle.Minimum = 1; throttle.Maximum = 16; throttle.WholeNumber = true; throttle.Parameter = "ThrottleLimit"; PathField folder = page.Path("Report", "Report folder"); folder.Default = report; folder.PathKind = PathKind.Folder; folder.Parameter = "ReportPath"; ChoiceField output = page.Select("Format", "Report format", new[] { new Choice("Html", "HTML"), new Choice("Csv", "CSV"), new Choice("Json", "JSON") }); output.Default = format; output.Parameter = "Format"; ToggleField send = page.Toggle("Notify", "Email the report"); send.Parameter = "Notify"; TextField recipient = page.Text("Email", "Recipient"); recipient.Default = email; recipient.Parameter = "To"; recipient.Required = true; recipient.VisibleWhen = v => v.Get("Notify"); recipient.Validator = (value, _) => value is string s && s.Contains('@') ? null : "Enter an email address"; ToggleField dry = page.Toggle("DryRun", "Dry run"); dry.Default = false; dry.Parameter = "WhatIf"; return form; } Form options = Options(); void ApplyOptions(FormValues values) { parallel = (int)values.Get("Parallel", 3m); report = values.Get("Report", reportRoot); format = values.Get("Format", "Html"); notify = values.Get("Notify"); email = values.Get("Email", "ops@example.test"); dryRun = values.Get("DryRun"); } Menu menu = new("Server toolbox") { Layout = MenuLayout.Shell, Subtitle = "Tue 20 Oct 2026 · 09:30" }; MenuItem targetItem = new MenuItem("Targets").Hotkey('t').WithIcon(GlyphIcon.Targets).Describe("Inventory grouped by site; unavailable servers stay visible.") .LoadChoices(async (_, token) => { await Task.Delay(700, token); return Choice.From(inventory, n => n.Name, n => n.Site, description: n => n.Detail, badge: n => RichText.Plain(n.Role, new RichStyle(ThemeToken.Muted)), disabled: n => n.Offline); }); targetItem.SetSelection(targets.Cast()); targetItem.Minimum = 1; targetItem.Preview = c => { Node n = (Node)c.Value!; return new KeyValueList().Add("Server", n.Name).Add("Site", n.Site).Add("Role", n.Role).Add("OS", n.Detail); }; MenuItem taskItem = new MenuItem("Tasks").Hotkey('k').WithIcon(GlyphIcon.Tasks).MultiSelect(catalog.Select(c => new Choice(c.Id, c.Name) { Group = c.Category, Description = c.Description, Badge = RichText.Plain((c.Changes ? "changes · " : "") + "~" + c.Minutes + " min", new RichStyle(c.Changes ? ThemeToken.Warn : ThemeToken.Muted)), Selected = checks.Contains(c) })); taskItem.Minimum = 1; taskItem.Preview = c => Preview(catalog.First(k => k.Id == (string)c.Value!)); menu.Add(targetItem); menu.Add(taskItem); MenuItem optionItem = new MenuItem("Options").Hotkey('o').WithIcon(GlyphIcon.Settings).Form(options, (_, result) => ApplyOptions(result.Values)); menu.Add(optionItem); menu.Preset("Morning · Warsaw", p => p.Selection(targetItem, inventory.Where(n => n.Site == "Warsaw").Cast()) .Selection(taskItem, new object?[] { "PatchStatus", "Certificates", "DiskSpace" }).Form(options, new Dictionary { ["Parallel"] = 2m, ["DryRun"] = true }) .WithPreview(new KeyValueList().Add("Scope", "Warsaw servers").Add("Tasks", "Security and disk space").Add("Dry run", "yes"))); menu.Preset("Health · all sites", p => p.Selection(targetItem, inventory.Where(n => n.Offline == "").Cast()) .Selection(taskItem, new object?[] { "EventErrors", "Services", "DiskSpace", "Uptime" })); menu.Item("Presets", i => i.Hotkey('p').WithIcon(GlyphIcon.Favorite).Presets()); menu.Separator(); menu.Item("Review & run", i => i.Hotkey('r').WithIcon(GlyphIcon.Run).Review(_ => { targets = inventory.Where(n => targetItem.GetSelection().Contains(n) && n.Offline == "").ToArray(); checks = catalog.Where(c => taskItem.GetSelection().Contains(c.Id)).ToArray(); ApplyOptions(options.GetValues()); ReviewModel review = new("Review & run") { Description = "Everything the run uses; the command carries the same options." }; review.Row("Targets", string.Join(", ", targets.Select(n => n.Name))); review.Row("Tasks", string.Join(", ", checks.Select(c => c.Name))); review.Row("Servers at a time", parallel.ToString()); review.Row("Report", report + " (" + format.ToUpperInvariant() + ")"); review.Row("Email", notify ? email : "off"); review.Row("Dry run", dryRun ? "yes" : "no"); if (targets.Length == 0) review.Problem("Pick at least one target", targetItem); if (checks.Length == 0) review.Problem("Pick at least one task", taskItem); if (notify && !email.Contains('@')) review.Problem("Enter the email recipient in Options", optionItem); review.Command = "Invoke-ServerToolbox " + CommandLine.Argument("ComputerName", targets.Select(n => n.Name).ToList(), CommandStyle.PowerShell) + " " + CommandLine.Argument("Task", checks.Select(c => c.Id).ToList(), CommandStyle.PowerShell) + " " + options.BuildCommand(); review.Run = ctx => Execute(ctx, Plan()); return review; })); menu.Item("History", i => i.Hotkey('h').WithIcon(GlyphIcon.List).Page(_ => History()).State(() => history.Count + " runs")); menu.Separator(); menu.Exit("Quit"); if (snapshot) { List work = Plan(); int done = step < 2 ? 0 : step == 2 ? Math.Min(work.Count, targets.Length + 2) : step == 3 ? work.Count * 2 / 3 : work.Count; int phase = step < 2 ? step : step < 4 ? 2 : step == 4 ? 3 : 4; Terminal.Write(Dashboard(work, done, phase)); } else menu.Run(new MenuOptions { Layout = MenuLayout.Shell, AppName = "Server toolbox gallery" }); record Node(string Name, string Domain, string Site, string Role, string Detail, string Offline) { public override string ToString() => Name; } record Check(string Id, string Name, string Category, string Description, string Expected, double Minutes, bool Changes); record Work(Node Node, Check Check, StatusKind Status, string Actual); record HistoryRow(string When, int Servers, int Checks, int Passed, int Warnings, int Failed, TimeSpan Elapsed);