Skip to content

Commit 9aff58c

Browse files
Control Panel: Server status page (version, server state, database details, statistics, and configuration warnings - ucStatus parity)
1 parent 4b47b08 commit 9aff58c

4 files changed

Lines changed: 370 additions & 0 deletions

File tree

hmailserver/source/Tools/ControlPanel/App.xaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@
9999
<Setter Property="VerticalAlignment" Value="Top" />
100100
</Style>
101101

102+
<Style x:Key="StatusLabel" TargetType="TextBlock">
103+
<Setter Property="FontSize" Value="12.5" />
104+
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
105+
<Setter Property="Margin" Value="0,0,16,8" />
106+
<Setter Property="VerticalAlignment" Value="Center" />
107+
</Style>
108+
109+
<Style x:Key="StatusValue" TargetType="TextBlock">
110+
<Setter Property="FontSize" Value="13" />
111+
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
112+
<Setter Property="Margin" Value="0,0,0,8" />
113+
<Setter Property="TextWrapping" Value="Wrap" />
114+
<Setter Property="VerticalAlignment" Value="Center" />
115+
</Style>
116+
102117
<Style x:Key="KpiLabel" TargetType="TextBlock">
103118
<Setter Property="FontSize" Value="11" />
104119
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />

hmailserver/source/Tools/ControlPanel/MainWindow.xaml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ private void RegisterPages()
6161
{
6262
pageFactories_["welcome"] = () => new WelcomeView();
6363
pageFactories_["dashboard"] = () => new DashboardView();
64+
pageFactories_["status"] = () => new StatusView();
6465
pageFactories_["queue"] = () => new QueueView();
6566
pageFactories_["logs"] = () => new LogsView();
6667
pageFactories_["domains"] = () => new DomainsView();
@@ -127,6 +128,7 @@ private void BuildNavTree()
127128
NavTree.Items.Add(Item("Welcome", "welcome"));
128129
NavTree.Items.Add(Item("Dashboard", "dashboard"));
129130
NavTree.Items.Add(Group("Status",
131+
Item("Server status", "status"),
130132
Item("Delivery queue", "queue"),
131133
Item("Live logs", "logs")));
132134
NavTree.Items.Add(Item("Domains", "domains"));
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<UserControl
2+
x:Class="hMailServer.ControlPanel.Views.StatusView"
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml">
6+
7+
<Grid Margin="26,20">
8+
<Grid.RowDefinitions>
9+
<RowDefinition Height="Auto" />
10+
<RowDefinition Height="*" />
11+
</Grid.RowDefinitions>
12+
13+
<Grid>
14+
<StackPanel>
15+
<TextBlock Text="Server status" Style="{StaticResource PageTitle}" />
16+
<TextBlock x:Name="SubtitleText" Text="Server, database and health overview."
17+
Style="{StaticResource PageSubtitle}" />
18+
</StackPanel>
19+
<ui:Button Content="Refresh" Click="Refresh_Click" HorizontalAlignment="Right" VerticalAlignment="Top">
20+
<ui:Button.Icon><ui:SymbolIcon Symbol="ArrowClockwise24" /></ui:Button.Icon>
21+
</ui:Button>
22+
</Grid>
23+
24+
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="0,4,0,0">
25+
<StackPanel>
26+
<Grid>
27+
<Grid.ColumnDefinitions>
28+
<ColumnDefinition Width="*" />
29+
<ColumnDefinition Width="*" />
30+
</Grid.ColumnDefinitions>
31+
32+
<!-- Server -->
33+
<Border Style="{StaticResource Card}" Margin="0,0,6,0">
34+
<StackPanel>
35+
<TextBlock Text="Server" FontSize="15" FontWeight="SemiBold" Margin="0,0,0,12" />
36+
<Grid x:Name="ServerGrid">
37+
<Grid.ColumnDefinitions>
38+
<ColumnDefinition Width="Auto" />
39+
<ColumnDefinition Width="*" />
40+
</Grid.ColumnDefinitions>
41+
<Grid.RowDefinitions>
42+
<RowDefinition Height="Auto" />
43+
<RowDefinition Height="Auto" />
44+
<RowDefinition Height="Auto" />
45+
<RowDefinition Height="Auto" />
46+
</Grid.RowDefinitions>
47+
<TextBlock Grid.Row="0" Grid.Column="0" Text="Version" Style="{StaticResource StatusLabel}" />
48+
<TextBlock Grid.Row="0" Grid.Column="1" x:Name="VersionValue" Style="{StaticResource StatusValue}" />
49+
<TextBlock Grid.Row="1" Grid.Column="0" Text="State" Style="{StaticResource StatusLabel}" />
50+
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="StateValue" Style="{StaticResource StatusValue}" />
51+
<TextBlock Grid.Row="2" Grid.Column="0" Text="Started" Style="{StaticResource StatusLabel}" />
52+
<TextBlock Grid.Row="2" Grid.Column="1" x:Name="StartedValue" Style="{StaticResource StatusValue}" />
53+
<TextBlock Grid.Row="3" Grid.Column="0" Text="Uptime" Style="{StaticResource StatusLabel}" />
54+
<TextBlock Grid.Row="3" Grid.Column="1" x:Name="UptimeValue" Style="{StaticResource StatusValue}" />
55+
</Grid>
56+
</StackPanel>
57+
</Border>
58+
59+
<!-- Database -->
60+
<Border Grid.Column="1" Style="{StaticResource Card}" Margin="6,0,0,0">
61+
<StackPanel>
62+
<TextBlock Text="Database" FontSize="15" FontWeight="SemiBold" Margin="0,0,0,12" />
63+
<Grid>
64+
<Grid.ColumnDefinitions>
65+
<ColumnDefinition Width="Auto" />
66+
<ColumnDefinition Width="*" />
67+
</Grid.ColumnDefinitions>
68+
<Grid.RowDefinitions>
69+
<RowDefinition Height="Auto" />
70+
<RowDefinition Height="Auto" />
71+
<RowDefinition Height="Auto" />
72+
<RowDefinition Height="Auto" />
73+
</Grid.RowDefinitions>
74+
<TextBlock Grid.Row="0" Grid.Column="0" Text="Type" Style="{StaticResource StatusLabel}" />
75+
<TextBlock Grid.Row="0" Grid.Column="1" x:Name="DbTypeValue" Style="{StaticResource StatusValue}" />
76+
<TextBlock Grid.Row="1" Grid.Column="0" Text="Host" Style="{StaticResource StatusLabel}" />
77+
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="DbHostValue" Style="{StaticResource StatusValue}" />
78+
<TextBlock Grid.Row="2" Grid.Column="0" Text="Name" Style="{StaticResource StatusLabel}" />
79+
<TextBlock Grid.Row="2" Grid.Column="1" x:Name="DbNameValue" Style="{StaticResource StatusValue}" />
80+
<TextBlock Grid.Row="3" Grid.Column="0" Text="Schema" Style="{StaticResource StatusLabel}" />
81+
<TextBlock Grid.Row="3" Grid.Column="1" x:Name="DbVersionValue" Style="{StaticResource StatusValue}" />
82+
</Grid>
83+
</StackPanel>
84+
</Border>
85+
</Grid>
86+
87+
<!-- Statistics -->
88+
<Border Style="{StaticResource Card}" Margin="0,12,0,0">
89+
<StackPanel>
90+
<TextBlock Text="Statistics" FontSize="15" FontWeight="SemiBold" Margin="0,0,0,12" />
91+
<UniformGrid Columns="3">
92+
<StackPanel Margin="0,0,0,8">
93+
<TextBlock Text="Processed messages" Style="{StaticResource StatusLabel}" />
94+
<TextBlock x:Name="ProcessedValue" FontSize="20" FontWeight="SemiBold"
95+
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
96+
</StackPanel>
97+
<StackPanel Margin="0,0,0,8">
98+
<TextBlock Text="Spam removed" Style="{StaticResource StatusLabel}" />
99+
<TextBlock x:Name="SpamValue" FontSize="20" FontWeight="SemiBold"
100+
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
101+
</StackPanel>
102+
<StackPanel Margin="0,0,0,8">
103+
<TextBlock Text="Viruses removed" Style="{StaticResource StatusLabel}" />
104+
<TextBlock x:Name="VirusValue" FontSize="20" FontWeight="SemiBold"
105+
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
106+
</StackPanel>
107+
<StackPanel Margin="0,8,0,0">
108+
<TextBlock Text="SMTP sessions" Style="{StaticResource StatusLabel}" />
109+
<TextBlock x:Name="SmtpValue" FontSize="20" FontWeight="SemiBold"
110+
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
111+
</StackPanel>
112+
<StackPanel Margin="0,8,0,0">
113+
<TextBlock Text="IMAP sessions" Style="{StaticResource StatusLabel}" />
114+
<TextBlock x:Name="ImapValue" FontSize="20" FontWeight="SemiBold"
115+
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
116+
</StackPanel>
117+
<StackPanel Margin="0,8,0,0">
118+
<TextBlock Text="POP3 sessions" Style="{StaticResource StatusLabel}" />
119+
<TextBlock x:Name="Pop3Value" FontSize="20" FontWeight="SemiBold"
120+
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
121+
</StackPanel>
122+
</UniformGrid>
123+
</StackPanel>
124+
</Border>
125+
126+
<!-- Warnings -->
127+
<Border Style="{StaticResource Card}" Margin="0,12,0,0">
128+
<StackPanel>
129+
<TextBlock Text="Warnings" FontSize="15" FontWeight="SemiBold" Margin="0,0,0,12" />
130+
<StackPanel x:Name="WarningsPanel" />
131+
</StackPanel>
132+
</Border>
133+
</StackPanel>
134+
</ScrollViewer>
135+
</Grid>
136+
</UserControl>
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
using System;
2+
using System.Windows;
3+
using System.Windows.Controls;
4+
using System.Windows.Media;
5+
using hMailServer.ControlPanel.Services;
6+
7+
namespace hMailServer.ControlPanel.Views
8+
{
9+
/// <summary>
10+
/// Server / database / health overview — the parts of the classic Administrator
11+
/// Status pane that the live Dashboard does not already cover (version, database
12+
/// details, server state and the configuration warnings).
13+
/// </summary>
14+
public partial class StatusView : UserControl, IPageLifecycle
15+
{
16+
public StatusView()
17+
{
18+
InitializeComponent();
19+
}
20+
21+
public void OnEnter() => Reload();
22+
23+
public void OnLeave()
24+
{
25+
}
26+
27+
private void Refresh_Click(object sender, RoutedEventArgs e) => Reload();
28+
29+
private static string DatabaseTypeName(int type) => type switch
30+
{
31+
1 => "MySQL / MariaDB",
32+
2 => "Microsoft SQL Server",
33+
3 => "PostgreSQL",
34+
4 => "Built-in (SQL Server Compact)",
35+
_ => "Unknown"
36+
};
37+
38+
private static string ServerStateName(int state) => state switch
39+
{
40+
1 => "Stopped",
41+
2 => "Starting",
42+
3 => "Running",
43+
4 => "Stopping",
44+
_ => "Unknown"
45+
};
46+
47+
private void Reload()
48+
{
49+
dynamic app = ServerSession.Current?.Application;
50+
if (app == null)
51+
return;
52+
53+
// Server + database
54+
try
55+
{
56+
VersionValue.Text = (string) app.Version + " (" + (string) app.VersionArchitecture + ")";
57+
}
58+
catch (Exception) { VersionValue.Text = "-"; }
59+
60+
try { StateValue.Text = ServerStateName((int) app.ServerState); }
61+
catch (Exception) { StateValue.Text = "-"; }
62+
63+
try
64+
{
65+
dynamic db = app.Database;
66+
DbTypeValue.Text = DatabaseTypeName((int) db.DatabaseType);
67+
string host = (string) db.ServerName;
68+
DbHostValue.Text = string.IsNullOrEmpty(host) ? "-" : host;
69+
string name = (string) db.DatabaseName;
70+
DbNameValue.Text = string.IsNullOrEmpty(name) ? "-" : name;
71+
DbVersionValue.Text = ((int) db.CurrentVersion).ToString();
72+
ServerSession.Release(db);
73+
}
74+
catch (Exception)
75+
{
76+
DbTypeValue.Text = DbHostValue.Text = DbNameValue.Text = DbVersionValue.Text = "-";
77+
}
78+
79+
// Statistics + uptime
80+
try
81+
{
82+
var snap = ServerSession.Current.ReadStatus();
83+
ProcessedValue.Text = snap.ProcessedMessages.ToString("N0");
84+
SpamValue.Text = snap.SpamBlocked.ToString("N0");
85+
VirusValue.Text = snap.VirusesRemoved.ToString("N0");
86+
SmtpValue.Text = snap.SmtpSessions.ToString();
87+
ImapValue.Text = snap.ImapSessions.ToString();
88+
Pop3Value.Text = snap.Pop3Sessions.ToString();
89+
StartedValue.Text = string.IsNullOrEmpty(snap.StartTime) ? "-" : snap.StartTime;
90+
UptimeValue.Text = FormatUptime(snap.StartTime);
91+
}
92+
catch (Exception)
93+
{
94+
ProcessedValue.Text = SpamValue.Text = VirusValue.Text = "-";
95+
SmtpValue.Text = ImapValue.Text = Pop3Value.Text = "-";
96+
}
97+
98+
LoadWarnings(app);
99+
}
100+
101+
private void LoadWarnings(dynamic app)
102+
{
103+
WarningsPanel.Children.Clear();
104+
int count = 0;
105+
106+
try
107+
{
108+
dynamic settings = app.Settings;
109+
try
110+
{
111+
if (((string) settings.HostName).Length == 0)
112+
count += AddWarning("High", "No public host name is configured in the SMTP settings.");
113+
114+
if ((bool) settings.DenyMailFromNull)
115+
count += AddWarning("High", "Mail from an empty sender address is denied. Many servers send bounces from <>, which will be rejected.");
116+
117+
dynamic ranges = settings.SecurityRanges;
118+
int autoban = 0;
119+
int rangeCount = (int) ranges.Count;
120+
for (int i = 0; i < rangeCount; i++)
121+
{
122+
dynamic range = ranges.Item[i];
123+
try
124+
{
125+
if ((bool) range.AllowDeliveryFromRemoteToRemote && !(bool) range.RequireSMTPAuthExternalToExternal)
126+
count += AddWarning("Critical", "IP range '" + (string) range.Name + "' allows external-to-external delivery without authentication (open relay risk).");
127+
128+
if ((string) range.LowerIP == "127.0.0.1" && (string) range.UpperIP == "127.0.0.1" && (bool) range.Expires)
129+
count += AddWarning("High", "Localhost is currently banned in the IP ranges.");
130+
131+
if ((bool) range.Expires)
132+
autoban++;
133+
}
134+
finally
135+
{
136+
ServerSession.Release(range);
137+
}
138+
}
139+
ServerSession.Release(ranges);
140+
141+
if (autoban > 0)
142+
count += AddWarning("Medium", "There is a total of " + autoban + " auto-ban IP range(s).");
143+
}
144+
finally
145+
{
146+
ServerSession.Release(settings);
147+
}
148+
}
149+
catch (Exception ex)
150+
{
151+
AddWarning("Info", "Could not evaluate all warnings: " + ex.Message);
152+
return;
153+
}
154+
155+
if (count == 0)
156+
{
157+
var ok = new TextBlock
158+
{
159+
Text = "No configuration warnings.",
160+
FontSize = 13,
161+
Margin = new Thickness(0, 2, 0, 2)
162+
};
163+
ok.SetResourceReference(Control.ForegroundProperty, "TextFillColorSecondaryBrush");
164+
WarningsPanel.Children.Add(ok);
165+
}
166+
}
167+
168+
private int AddWarning(string severity, string text)
169+
{
170+
var row = new Grid { Margin = new Thickness(0, 0, 0, 8) };
171+
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(90) });
172+
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
173+
174+
var badge = new Border
175+
{
176+
Background = SeverityBrush(severity),
177+
CornerRadius = new CornerRadius(4),
178+
Padding = new Thickness(8, 2, 8, 2),
179+
Margin = new Thickness(0, 0, 12, 0),
180+
HorizontalAlignment = HorizontalAlignment.Left,
181+
VerticalAlignment = VerticalAlignment.Top,
182+
Child = new TextBlock { Text = severity, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White }
183+
};
184+
Grid.SetColumn(badge, 0);
185+
row.Children.Add(badge);
186+
187+
var msg = new TextBlock { Text = text, FontSize = 13, TextWrapping = TextWrapping.Wrap };
188+
msg.SetResourceReference(Control.ForegroundProperty, "TextFillColorPrimaryBrush");
189+
Grid.SetColumn(msg, 1);
190+
row.Children.Add(msg);
191+
192+
WarningsPanel.Children.Add(row);
193+
return 1;
194+
}
195+
196+
private static Brush SeverityBrush(string severity) => severity switch
197+
{
198+
"Critical" => new SolidColorBrush(Color.FromRgb(0xC4, 0x18, 0x1E)),
199+
"High" => new SolidColorBrush(Color.FromRgb(0xD2, 0x4F, 0x1A)),
200+
"Medium" => new SolidColorBrush(Color.FromRgb(0xC2, 0x8A, 0x00)),
201+
_ => new SolidColorBrush(Color.FromRgb(0x55, 0x7A, 0x95))
202+
};
203+
204+
private static string FormatUptime(string startTime)
205+
{
206+
if (DateTime.TryParse(startTime, out DateTime started))
207+
{
208+
TimeSpan up = DateTime.Now - started;
209+
if (up.TotalSeconds < 0) return startTime;
210+
if (up.TotalDays >= 1) return (int) up.TotalDays + "d " + up.Hours + "h " + up.Minutes + "m";
211+
if (up.TotalHours >= 1) return up.Hours + "h " + up.Minutes + "m";
212+
return Math.Max(0, (int) up.TotalMinutes) + "m";
213+
}
214+
return string.IsNullOrEmpty(startTime) ? "-" : startTime;
215+
}
216+
}
217+
}

0 commit comments

Comments
 (0)