话接上一篇快速上手——接入群晖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">&lt;@result.Hostname&gt;</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>
                                                        &darr; @result.RXKbps kbps<br />
                                                        &uarr; @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">&lt;@host.Hostname&gt;</span>
                                    </div>
                                    <div class="table-responsive">
                                        <table class="table table-vcenter">
                                            <thead>
                                                <tr><th>存储卷</th><th>卷名</th><th>使用率</th><th class="w-25">&nbsp;</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>
分类: articles