话接上一篇快速上手——接入群晖Synology WEB API监控NAS状态

前端框架用的是基于Bootstrap的Tabler,预览网址:https://preview.tabler.io/
1. Controller
public async Task<IActionResult> Index()
{
var results = new List<dynamic>();
var credentials = new List<(string Host, string User, string Pass)>
{
( "192.168.x.x", "userX", "PassX" ),
( "192.168.y.y", "userY", "PassY" )
};
using HttpClient http = new();
foreach (var login in credentials)
{
string urlAuth = $"https://{login.Host}:5001/webapi/auth.cgi?api=SYNO.API.Auth&version=3&method=login" +
$"&account={HttpUtility.UrlEncode(login.User)}&passwd={HttpUtility.UrlEncode(login.Pass)}&session=Core&format=sid";
//不做异常处理了,假定所有请求都成功,登录失败和成功返回的都是json
var jsonAuth = JsonNode.Parse(await http.GetStringAsync(urlAuth));
if (!jsonAuth["success"].GetValue<bool>()) return Content($"登录{login.Host}失败");
var sid = jsonAuth["data"]["sid"].GetValue<string>();
var prefix = $"https://{login.Host}:5001/webapi/entry.cgi?";
string[] urls = [
$"{prefix}api=SYNO.Core.System.SystemHealth&method=get&version=1&_sid={sid}",
$"{prefix}api=SYNO.Core.System.Utilization&version=1&method=get&" +
$"type=current&resource=[\"cpu\",\"memory\",\"network\"]&_sid={sid}",
$"{prefix}api=SYNO.Storage.CGI.Storage&method=load_info&version=1&_sid={sid}",
$"{prefix}api=SYNO.Backup.Task&method=list&version=1&additional=" +
$"[\"last_bkp_time\",\"last_bkp_result\",\"next_bkp_time\"]&_sid={sid}"
];
//4个API的返回结果: 系统状态,系统资源,存储,备份任务
var nodes = await Task.WhenAll(urls.Select(
async url =>
{
return JsonNode.Parse(await http.GetStringAsync(url));
}));
var dataHealth = nodes[0]["data"];
var dataSys = nodes[1]["data"];
var dataStorage = nodes[2]["data"];
var dataBackups = nodes[3]["success"].GetValue<bool>() ? nodes[3]["data"] : null;
string hostname = dataHealth["hostname"].GetValue<string>();
int cpu = dataSys["cpu"]["user_load"].GetValue<int>() + dataSys["cpu"]["system_load"].GetValue<int>();
var netNode = dataSys["network"].AsArray().FirstOrDefault(n => n["device"].ToString() == "total");
var strUptime = dataHealth["uptime"].GetValue<string>().Split(':');
TimeSpan uptime = new(Convert.ToInt32(strUptime[0]), Convert.ToInt32(strUptime[1]), Convert.ToInt32(strUptime[2]));
string _uptime = $"{uptime.Days}天 {uptime.Hours}小时 {uptime.Minutes}分钟 {uptime.Seconds}秒";
var disks = dataStorage["disks"].AsArray().Select(n => new
{
Name = n["name"].GetValue<string>(),
Vendor = n["vendor"].GetValue<string>(),
Temp = n["temp"].GetValue<int>(),
Uncorrectable = n["unc"].GetValue<int>(),
RemainLife = n["remain_life"] is JsonValue v ? v.GetValue<int>() : n["remain_life"]["value"]?.GetValue<int>(),
IsSSD = n["isSsd"].GetValue<bool>(),
SizeTotalGB = Convert.ToInt64(n["size_total"].GetValue<string>()) >> 30
}).ToArray();
var backups = dataBackups is null ? null :
dataBackups["task_list"].AsArray().Select(b => new
{
Type = b["type"].GetValue<string>(),
Name = b["name"].GetValue<string>(),
LastEnd = b["last_bkp_end_time"]?.GetValue<string>() ?? "",
LastResult = b["last_bkp_result"].GetValue<string>(),
Next = b["next_bkp_time"]?.GetValue<string>() ?? "",
}).ToArray();
var volumes = dataStorage["volumes"].AsArray().Select(v => new
{
Path = v["vol_path"].GetValue<string>(),
Total = Convert.ToInt64(v["size"]["total"].GetValue<string>()) >> 30,
Used = Convert.ToInt64(v["size"]["used"].GetValue<string>()) >> 30,
}).ToArray();
results.Add(new
{
Hostname = hostname,
Ip = login.Host,
Uptime = _uptime,
CPU = cpu,
Memory = dataSys["memory"]["real_usage"].GetValue<int>(),
MemoryTotal = dataSys["memory"]["memory_size"].GetValue<long>() >> 20,
RXKbps = (netNode["rx"]?.GetValue<long>() ?? 0) >> 8,
TXKbps = (netNode["tx"]?.GetValue<long>() ?? 0) >> 8,
Updated = DateTimeOffset.FromUnixTimeSeconds(dataSys["time"].GetValue<long>()).ToLocalTime(),
Disks = disks,
Volumes = volumes,
Backups = backups
});
}
ViewBag.Results = results;
return View();
}
2. View,不应用模板了,直接在View里写完整页面
@{
Layout = null;
List<dynamic> results = ViewBag.Results;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Synology Status Monitor</title>
<meta name="msapplication-TileColor" content="#066fd1">
<meta name="theme-color" content="#066fd1">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="MobileOptimized" content="320">
<link rel="icon" href="https://preview.tabler.io/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@@tabler/core@1.4.0/dist/css/tabler.min.css" />
<style>
@@import url("https://rsms.me/inter/inter.css");
</style>
</head>
<body>
<div class="page">
<div class="page-wrapper">
<div class="page-header d-print-none">
<div class="container">
<h2 class="page-title">Synology Status Monitor</h2>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<div class="card">
<div class="card-header bg-azure text-white">
<h3 class="card-title">摘要</h3>
<span class="card-actions"><small class="text-light">Last checked: @DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")</small></span>
</div>
<div class="table-responsive">
<table class="table table-vcenter">
<thead><tr><th>Host</th><th>Resources</th><th>Network</th></tr></thead>
<tbody>
@for (int i = 0; i < results.Count; i++)
{
var result = results[i];
<tr>
<td>
<h4>@result.Ip <small class="text-secondary"><@result.Hostname></small></h4>
<small class="text-secondary">UPTIME: @result.Uptime</small>
</td>
<td>
<div class="progressbg">
<div class="progress progress-3 progressbg-progress">
<div class="progress-bar bg-primary-lt" style="width: @(result.CPU)%" role="progressbar" aria-valuenow="@result.CPU" aria-valuemin="0" aria-valuemax="100" aria-label="@(result.CPU)% Complete">
<span class="visually-hidden">@result.CPU %</span>
</div>
</div>
<div class="progressbg-text">CPU: @result.CPU %</div>
</div>
<div class="progressbg">
<div class="progress progress-3 progressbg-progress">
<div class="progress-bar bg-primary-lt" style="width: @(result.Memory)%" role="progressbar" aria-valuenow="@result.Memory" aria-valuemin="0" aria-valuemax="100" aria-label="@(result.Memory)% Complete">
<span class="visually-hidden">@result.Memory %</span>
</div>
</div>
<div class="progressbg-text">MEM: @result.Memory % of @result.MemoryTotal GB</div>
</div>
</td>
<td>
↓ @result.RXKbps kbps<br />
↑ @result.TXKbps kbps
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@for (int i = 0; i < results.Count; i++)
{
var host = results[i];
<div class="col-md-6 col-sm-12">
<div class="card">
<div class="card-header bg-azure text-white">
<div class="card-title">
<h3>@host.Ip</h3>
</div>
<span class="card-actions text-light"><@host.Hostname></span>
</div>
<div class="table-responsive">
<table class="table table-vcenter">
<thead>
<tr><th>存储卷</th><th>卷名</th><th>使用率</th><th class="w-25"> </th></tr>
</thead>
@foreach (var vol in host.Volumes)
{
long _used = (long)vol.Used;
string used = _used > 1024 ? $"{_used / 1024D:0.0} TB" : $"{_used} GB";
long _total = (long)vol.Total;
string total = _total > 1024 ? $"{(_total >> 10)} TB" : $"{_total} GB";
string path = vol.Path;
<tr>
<td>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chart-pie" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10 3.2a9 9 0 1 0 10.8 10.8a1 1 0 0 0 -1 -1h-6.8a2 2 0 0 1 -2 -2v-7a0.9 .9 0 0 0 -1 -.8"></path>
<path d="M15 3.5a9 9 0 0 1 5.5 5.5h-4.5a1 1 0 0 1 -1 -1v-4.5"></path>
</svg>
</td>
<td>@vol.Path </td>
<td><span class="text-secondary">@used of @total</span></td>
<td>
<div class="progress progress-xs">
<div class="progress-bar bg-primary" style="width: @(100D * _used / _total)%"></div>
</div>
</td>
</tr>
}
</table>
</div>
<div class="table-responsive mt-3">
<table class="table table-vcenter">
<thead>
<tr><th>硬盘</th><th>名称</th><th>品牌</th><th>温度(℃)</th><th>容量</th><th>健康度</th></tr>
</thead>
@foreach (var disk in host.Disks)
{
long _cap = (int)disk.SizeTotalGB;
bool ssd = (bool)disk.IsSSD;
<tr>
<td>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-server" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="3" y="4" width="18" height="8" rx="3"></rect>
<rect x="3" y="12" width="18" height="8" rx="3"></rect>
<line x1="7" y1="8" x2="7" y2="8.01"></line>
<line x1="7" y1="16" x2="7" y2="16.01"></line>
</svg>
</td>
<td>
@disk.Name @Html.Raw(ssd ? "<span class='badge bg-blue text-blue-fg'>固态</span>" : "")<br />
</td>
<td>@disk.Vendor</td>
<td>@disk.Temp</td>
<td>@_cap GB</td>
<td>
@if (ssd)
{
<span>寿命 @disk.RemainLife %</span>
}
else
{
<span>@disk.Uncorrectable 坏道</span>
}
</td>
</tr>
}
</table>
</div>
@if (host.Backups is not null)
{
<div class="table-responsive mt-3">
<table class="table table-vcenter">
<thead>
<tr>
<th>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-stack-push">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6 10l-2 1l8 4l8 -4l-2 -1" />
<path d="M4 15l8 4l8 -4" />
<path d="M12 4v7" />
<path d="M15 8l-3 3l-3 -3" />
</svg> 备份任务
</th>
<th>上次完成</th>
<th>下次运行</th>
</tr>
</thead>
@foreach (var task in host.Backups)
{
<tr>
<td>@Html.Raw(task.Name)</td>
<td><span class="badge bg-@(task.LastResult == "done" ? "green" : "red") ms-auto"></span> @task.LastEnd</td>
<td>@task.Next</td>
</tr>
}
</table>
</div>
}
</div>
</div>
}
</div>
</div>
</div>
<footer class="footer footer-transparent d-print-none">
<div class="container-xl">
<div class="row text-center align-items-center flex-row-reverse">
<div class="col-lg-auto ms-lg-auto">
<ul class="list-inline list-inline-dots mb-0">
<li class="list-inline-item"><a href="https://preview.tabler.io/license.html" class="link-secondary">License</a></li>
<li class="list-inline-item">
<a href="https://github.com/tabler/tabler" target="_blank" class="link-secondary" rel="noopener">Source code</a>
</li>
<li class="list-inline-item">
<a href="https://github.com/sponsors/codecalm" target="_blank" class="link-secondary" rel="noopener">
<!-- Download SVG icon from http://tabler.io/icons/icon/heart -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon text-pink icon-inline icon-4">
<path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572"></path>
</svg>
Sponsor
</a>
</li>
</ul>
</div>
<div class="col-12 col-lg-auto mt-3 mt-lg-0">
<ul class="list-inline list-inline-dots mb-0">
<li class="list-inline-item">
Copyright © 2025
<a href="https://preview.tabler.io/" class="link-secondary">Tabler</a>. All rights reserved.
</li>
<li class="list-inline-item">
<a href="https://preview.tabler.io/changelog.html" class="link-secondary" rel="noopener"> v1.4.0 </a>
</li>
</ul>
</div>
</div>
</div>
</footer>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@@tabler/core@1.4.0/dist/js/tabler.min.js"></script>
</body>
</html>