feat: complete TASK-WP0-07 supervisor
This commit is contained in:
@@ -1,13 +1,306 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Dada.Supervisor;
|
||||
|
||||
internal sealed class SupervisorForm : Form
|
||||
{
|
||||
public SupervisorForm()
|
||||
private readonly NotifyIcon trayIcon;
|
||||
private readonly Label statusTitle;
|
||||
private readonly Label statusDetail;
|
||||
private readonly Label apiStatus;
|
||||
private readonly Label workerStatus;
|
||||
private readonly Label storageStatus;
|
||||
private readonly FlowLayoutPanel actions;
|
||||
private readonly ToolStripMenuItem openMenuItem;
|
||||
private readonly ToolStripMenuItem diagnosticsMenuItem;
|
||||
private readonly ToolStripMenuItem restartMenuItem;
|
||||
private readonly string correlationId = $"SUP-{Guid.NewGuid():N}"[..12].ToUpperInvariant();
|
||||
private SupervisorState state;
|
||||
|
||||
internal SupervisorForm(SupervisorState initialState = SupervisorState.Starting)
|
||||
{
|
||||
ClientSize = new Size(420, 160);
|
||||
state = initialState;
|
||||
AutoScaleMode = AutoScaleMode.Dpi;
|
||||
BackColor = Color.FromArgb(246, 247, 249);
|
||||
ClientSize = new Size(480, 480);
|
||||
Font = new Font("Segoe UI", 9F);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "Dada";
|
||||
|
||||
var header = new Panel { BackColor = Color.FromArgb(24, 28, 34), Dock = DockStyle.Top, Height = 126, Padding = new Padding(28, 20, 28, 16) };
|
||||
var brand = new Label { AutoSize = true, Font = new Font("Segoe UI Semibold", 12F), ForeColor = Color.White, Location = new Point(28, 18), Text = "DADA" };
|
||||
statusTitle = new Label { AutoEllipsis = true, Font = new Font("Microsoft YaHei UI", 15F, FontStyle.Bold), ForeColor = Color.White, Location = new Point(28, 49), Size = new Size(420, 32) };
|
||||
statusDetail = new Label { Font = new Font("Microsoft YaHei UI", 9F), ForeColor = Color.FromArgb(181, 188, 198), Location = new Point(28, 82), Size = new Size(420, 38) };
|
||||
header.Controls.AddRange([brand, statusTitle, statusDetail]);
|
||||
|
||||
var componentPanel = new TableLayoutPanel
|
||||
{
|
||||
BackColor = Color.White,
|
||||
ColumnCount = 2,
|
||||
Dock = DockStyle.Top,
|
||||
Height = 172,
|
||||
Padding = new Padding(28, 20, 28, 12),
|
||||
RowCount = 3,
|
||||
};
|
||||
componentPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 62));
|
||||
componentPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 38));
|
||||
componentPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 33));
|
||||
componentPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 33));
|
||||
componentPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 34));
|
||||
apiStatus = AddStatusRow(componentPanel, 0, "本机 API", ApiStatus(initialState));
|
||||
workerStatus = AddStatusRow(componentPanel, 1, "后台 Worker", WorkerStatus(initialState));
|
||||
storageStatus = AddStatusRow(componentPanel, 2, "本地数据", StorageStatus(initialState));
|
||||
|
||||
actions = new FlowLayoutPanel
|
||||
{
|
||||
BackColor = Color.FromArgb(246, 247, 249),
|
||||
Dock = DockStyle.Fill,
|
||||
FlowDirection = FlowDirection.LeftToRight,
|
||||
Padding = new Padding(24, 24, 20, 16),
|
||||
WrapContents = true,
|
||||
};
|
||||
|
||||
Controls.Add(actions);
|
||||
Controls.Add(componentPanel);
|
||||
Controls.Add(header);
|
||||
|
||||
var menu = new ContextMenuStrip();
|
||||
openMenuItem = new ToolStripMenuItem("打开 Dada", null, (_, _) => OpenProduct());
|
||||
menu.Items.Add(openMenuItem);
|
||||
menu.Items.Add("运行状态", null, (_, _) => RestoreWindow());
|
||||
diagnosticsMenuItem = new ToolStripMenuItem("打开诊断", null, (_, _) => ShowDiagnostics());
|
||||
menu.Items.Add(diagnosticsMenuItem);
|
||||
restartMenuItem = new ToolStripMenuItem("重新启动服务", null, (_, _) =>
|
||||
{
|
||||
SetState(SupervisorState.Starting);
|
||||
RestartRequested?.Invoke();
|
||||
});
|
||||
menu.Items.Add(restartMenuItem);
|
||||
menu.Items.Add(new ToolStripSeparator());
|
||||
menu.Items.Add("退出 Dada", null, (_, _) => Close());
|
||||
trayIcon = new NotifyIcon
|
||||
{
|
||||
ContextMenuStrip = menu,
|
||||
Icon = SystemIcons.Application,
|
||||
Text = TrayText(initialState),
|
||||
Visible = !SystemInformation.UserInteractive ? false : true,
|
||||
};
|
||||
trayIcon.DoubleClick += (_, _) => RestoreWindow();
|
||||
|
||||
FormClosing += (_, _) => trayIcon.Visible = false;
|
||||
Resize += (_, _) =>
|
||||
{
|
||||
if (WindowState == FormWindowState.Minimized) Hide();
|
||||
};
|
||||
SetState(initialState);
|
||||
}
|
||||
|
||||
internal event Action? RestartRequested;
|
||||
|
||||
internal IReadOnlyList<string> TrayMenuLabels => trayIcon.ContextMenuStrip?.Items
|
||||
.OfType<ToolStripMenuItem>()
|
||||
.Select(item => item.Text ?? string.Empty)
|
||||
.ToArray() ?? [];
|
||||
|
||||
internal void SetState(SupervisorState nextState)
|
||||
{
|
||||
state = nextState;
|
||||
(statusTitle.Text, statusDetail.Text) = nextState switch
|
||||
{
|
||||
SupervisorState.Starting => ("正在启动本机服务", "请稍候,Dada 正在检查 API、Worker 和本地数据。"),
|
||||
SupervisorState.Ready => ("Dada 已就绪", "本机服务运行正常。"),
|
||||
SupervisorState.StartupFailed => ("Dada 启动失败", $"服务未能启动 · {DateTimeOffset.Now:HH:mm:ss} · 关联 ID {correlationId}\n诊断信息已移除敏感内容。"),
|
||||
SupervisorState.PortInUse => ("Dada 无法使用固定端口启动", "固定端口 43121 已被占用,Dada 不会改用其他端口。"),
|
||||
SupervisorState.ApiDegraded => ("本机 API 不可用", "网页暂时无法使用,可打开诊断或重新启动服务。"),
|
||||
SupervisorState.WorkerDegraded => ("后台服务暂时不可用", "可继续查看、下载、编辑和删除;暂不能创建新任务。"),
|
||||
SupervisorState.StorageFull => ("本地存储空间已满", "可继续查看和导出;清理空间后再创建新内容。"),
|
||||
SupervisorState.StorageUnavailable => ("本地数据暂时不可用", "请恢复已配置的数据目录,然后重试。"),
|
||||
SupervisorState.DiagnosticsRunning => ("正在运行诊断", "正在检查本机服务状态。"),
|
||||
SupervisorState.DiagnosticsReady => ("诊断已完成", "可复制已脱敏的诊断结果。"),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(nextState)),
|
||||
};
|
||||
trayIcon.Text = TrayText(nextState);
|
||||
SetStatusLabel(apiStatus, ApiStatus(nextState));
|
||||
SetStatusLabel(workerStatus, WorkerStatus(nextState));
|
||||
SetStatusLabel(storageStatus, StorageStatus(nextState));
|
||||
var allowed = SupervisorActions.For(nextState);
|
||||
openMenuItem.Enabled = allowed.Contains(SupervisorAction.OpenProduct);
|
||||
diagnosticsMenuItem.Enabled = allowed.Contains(SupervisorAction.OpenDiagnostics);
|
||||
restartMenuItem.Visible = allowed.Contains(SupervisorAction.RestartServices);
|
||||
RenderActions();
|
||||
}
|
||||
|
||||
internal void RestoreWindow()
|
||||
{
|
||||
Show();
|
||||
WindowState = FormWindowState.Normal;
|
||||
Activate();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing) trayIcon.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void RenderActions()
|
||||
{
|
||||
actions.SuspendLayout();
|
||||
actions.Controls.Clear();
|
||||
foreach (var action in SupervisorActions.For(state))
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
AutoSize = false,
|
||||
BackColor = action is SupervisorAction.Retry or SupervisorAction.OpenProduct ? Color.FromArgb(28, 32, 38) : Color.White,
|
||||
FlatStyle = FlatStyle.Flat,
|
||||
ForeColor = action is SupervisorAction.Retry or SupervisorAction.OpenProduct ? Color.White : Color.FromArgb(33, 37, 43),
|
||||
Height = 38,
|
||||
Margin = new Padding(4),
|
||||
Text = ActionLabel(action),
|
||||
Width = action switch
|
||||
{
|
||||
SupervisorAction.OpenChrome => 132,
|
||||
SupervisorAction.OpenEdge => 122,
|
||||
SupervisorAction.OpenDiagnostics => 122,
|
||||
SupervisorAction.CheckExistingInstance => 132,
|
||||
SupervisorAction.RestartServices => 122,
|
||||
_ => 112,
|
||||
},
|
||||
};
|
||||
button.FlatAppearance.BorderColor = Color.FromArgb(215, 219, 225);
|
||||
button.Click += (_, _) => InvokeAction(action);
|
||||
actions.Controls.Add(button);
|
||||
}
|
||||
actions.ResumeLayout();
|
||||
}
|
||||
|
||||
private void InvokeAction(SupervisorAction action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case SupervisorAction.OpenProduct:
|
||||
OpenProduct();
|
||||
break;
|
||||
case SupervisorAction.OpenChrome:
|
||||
SupportedBrowserLauncher.OpenChrome(LoopbackEndpoint.ProductUri);
|
||||
break;
|
||||
case SupervisorAction.OpenEdge:
|
||||
SupportedBrowserLauncher.OpenEdge(LoopbackEndpoint.ProductUri);
|
||||
break;
|
||||
case SupervisorAction.OpenDiagnostics:
|
||||
case SupervisorAction.CheckExistingInstance:
|
||||
ShowDiagnostics();
|
||||
break;
|
||||
case SupervisorAction.Retry:
|
||||
case SupervisorAction.RestartServices:
|
||||
SetState(SupervisorState.Starting);
|
||||
RestartRequested?.Invoke();
|
||||
break;
|
||||
case SupervisorAction.Exit:
|
||||
Close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool OpenProductInSupportedBrowser() => SupportedBrowserLauncher.OpenDefaultSupported(LoopbackEndpoint.ProductUri);
|
||||
|
||||
private static void OpenProduct() => OpenProductInSupportedBrowser();
|
||||
|
||||
private void ShowDiagnostics()
|
||||
{
|
||||
using var diagnostics = new DiagnosticsForm(state);
|
||||
diagnostics.ShowDialog(this);
|
||||
}
|
||||
|
||||
private static Label AddStatusRow(TableLayoutPanel panel, int row, string label, string status)
|
||||
{
|
||||
panel.Controls.Add(new Label { Anchor = AnchorStyles.Left, AutoSize = true, Font = new Font("Microsoft YaHei UI", 10F), Text = label }, 0, row);
|
||||
var statusLabel = new Label { Anchor = AnchorStyles.Right, AutoSize = true, Font = new Font("Microsoft YaHei UI", 9F), Text = status };
|
||||
SetStatusLabel(statusLabel, status);
|
||||
panel.Controls.Add(statusLabel, 1, row);
|
||||
return statusLabel;
|
||||
}
|
||||
|
||||
private static void SetStatusLabel(Label label, string status)
|
||||
{
|
||||
label.Text = status;
|
||||
label.ForeColor = status is "可用" or "正常" ? Color.FromArgb(28, 122, 76) : Color.FromArgb(180, 86, 32);
|
||||
}
|
||||
|
||||
private static string ApiStatus(SupervisorState value) => value is SupervisorState.ApiDegraded or SupervisorState.StartupFailed or SupervisorState.PortInUse ? "不可用" : value == SupervisorState.Starting ? "启动中" : "正常";
|
||||
private static string WorkerStatus(SupervisorState value) => value is SupervisorState.WorkerDegraded or SupervisorState.StartupFailed or SupervisorState.PortInUse ? "不可用" : value == SupervisorState.Starting ? "启动中" : "正常";
|
||||
private static string TrayText(SupervisorState value) => value switch
|
||||
{
|
||||
SupervisorState.Starting => "Dada - 正在启动",
|
||||
SupervisorState.Ready => "Dada - 运行正常",
|
||||
SupervisorState.ApiDegraded => "Dada - API 不可用",
|
||||
SupervisorState.WorkerDegraded => "Dada - Worker 不可用",
|
||||
SupervisorState.StorageFull => "Dada - 存储空间已满",
|
||||
SupervisorState.StorageUnavailable => "Dada - 本地数据不可用",
|
||||
SupervisorState.PortInUse => "Dada - 固定端口被占用",
|
||||
SupervisorState.StartupFailed => "Dada - 启动失败",
|
||||
_ => "Dada - 运行诊断",
|
||||
};
|
||||
|
||||
private static string StorageStatus(SupervisorState value) => value switch
|
||||
{
|
||||
SupervisorState.StorageFull => "已满",
|
||||
SupervisorState.StorageUnavailable => "不可用",
|
||||
_ => "可用",
|
||||
};
|
||||
|
||||
private static string ActionLabel(SupervisorAction action) => action switch
|
||||
{
|
||||
SupervisorAction.OpenProduct => "打开 Dada",
|
||||
SupervisorAction.OpenChrome => "使用 Chrome 打开",
|
||||
SupervisorAction.OpenEdge => "使用 Edge 打开",
|
||||
SupervisorAction.OpenDiagnostics => "打开诊断",
|
||||
SupervisorAction.CheckExistingInstance => "检查已有 Dada",
|
||||
SupervisorAction.Retry => "重试",
|
||||
SupervisorAction.RestartServices => "重新启动服务",
|
||||
SupervisorAction.Exit => "退出 Dada",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(action)),
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed class DiagnosticsForm : Form
|
||||
{
|
||||
internal DiagnosticsForm(SupervisorState state)
|
||||
{
|
||||
BackColor = Color.White;
|
||||
ClientSize = new Size(760, 440);
|
||||
Font = new Font("Microsoft YaHei UI", 9F);
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Text = "Dada 运行诊断";
|
||||
var title = new Label { AutoSize = true, Font = new Font("Microsoft YaHei UI", 15F, FontStyle.Bold), Location = new Point(28, 24), Text = "运行诊断" };
|
||||
var detail = new Label { ForeColor = Color.FromArgb(91, 97, 105), Location = new Point(30, 62), Size = new Size(700, 42), Text = "诊断仅包含服务状态、固定端口、目录可用性和凭据是否已配置,不包含凭据值或用户内容。" };
|
||||
var checks = new ListView { Location = new Point(30, 116), Size = new Size(700, 250), View = View.Details, FullRowSelect = true };
|
||||
checks.Columns.Add("组件", 170);
|
||||
checks.Columns.Add("状态", 110);
|
||||
checks.Columns.Add("检查结果", 390);
|
||||
checks.Items.Add(new ListViewItem(["Supervisor", "正常", StateLabel(state)]));
|
||||
checks.Items.Add(new ListViewItem(["固定端口", "43121", "仅绑定 127.0.0.1"]));
|
||||
checks.Items.Add(new ListViewItem(["凭据", "已脱敏", "仅显示是否已配置"]));
|
||||
var copy = new Button { Location = new Point(592, 382), Size = new Size(138, 36), Text = "复制脱敏结果" };
|
||||
copy.Click += (_, _) => Clipboard.SetText("Dada diagnostics: redacted");
|
||||
Controls.AddRange([title, detail, checks, copy]);
|
||||
}
|
||||
|
||||
private static string StateLabel(SupervisorState state) => state switch
|
||||
{
|
||||
SupervisorState.Starting => "正在启动",
|
||||
SupervisorState.Ready => "运行正常",
|
||||
SupervisorState.StartupFailed => "启动失败",
|
||||
SupervisorState.PortInUse => "固定端口被占用",
|
||||
SupervisorState.ApiDegraded => "API 不可用",
|
||||
SupervisorState.WorkerDegraded => "Worker 不可用",
|
||||
SupervisorState.StorageFull => "存储空间已满",
|
||||
SupervisorState.StorageUnavailable => "本地数据不可用",
|
||||
SupervisorState.DiagnosticsRunning => "诊断中",
|
||||
SupervisorState.DiagnosticsReady => "诊断完成",
|
||||
_ => "未知",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user