From 4a6ece4e7770bf4c54509e2b9eb9ca8b77c0d868 Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Sun, 27 Sep 2026 22:40:33 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E5=9B=BD=E6=97=97=E7=A7=BB?= =?UTF-8?q?=E5=88=B0=E5=90=8D=E7=A7=B0=E5=89=8D=EF=BC=8C=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E8=A1=8C=E5=8F=AA=E6=98=BE=E7=A4=BA=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=EF=BC=9B=E6=B1=87=E6=80=BB=E5=9B=9B=E6=A0=BC=E5=8F=AF=E5=9C=A8?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E8=AE=BE=E7=BD=AE=E4=B8=AD=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 21 +++++++++++++++++---- src/components/NodeCard.tsx | 31 ++++++++++++++++++++----------- src/components/NodeDetail.tsx | 2 +- src/lib/config.ts | 18 ++++++++++++++++++ src/lib/format.test.ts | 6 +++++- src/lib/format.ts | 9 +++++++++ theme.json | 11 ++++++++++- 7 files changed, 80 insertions(+), 18 deletions(-) create mode 100644 src/lib/config.ts diff --git a/src/App.tsx b/src/App.tsx index 30e0593..65216ea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { Summary } from "@/components/Summary" import { Button } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" import { api, groupsOf, useNodes, type Node } from "@/lib/api" +import { loadConfig } from "@/lib/config" type Me = { authed: boolean; github: boolean; site_name: string; public_page: boolean } @@ -92,6 +93,7 @@ export default function App() { const [dark, toggleTheme] = useTheme() const [me, setMe] = useState(null) const [meError, setMeError] = useState("") + const [config, setConfig] = useState | null>(null) const { nodes, error, closed } = useNodes() const [open, go] = useNodeRoute() // The list's group tab, held here so it survives a visit to a node's page. @@ -105,6 +107,9 @@ export default function App() { useEffect(() => { loadMe() + // Requested alongside /me, not after it. It never rejects: any failure + // yields the defaults. + void loadConfig().then(setConfig) // Warmed here rather than left to Suspense, which requests the chunk only // once a render reaches the detail view, itself waiting on /me. Without this // the split trades its first paint for a full-page skeleton over the first @@ -136,7 +141,7 @@ export default function App() { // Only while there is nothing else to show. Once `me` has loaded, a later // failure belongs beside the page rather than over it. - if (!me) return ( + if (!me || !config) return (
{meError ?

加载失败:{meError}

: "加载中…"}
@@ -190,7 +195,13 @@ export default function App() { ))} ) : ( - + )} @@ -199,10 +210,12 @@ export default function App() { // Group tabs appear only once the operator has grouped something, so a hub // without groups keeps the page it always had. The summary follows the tab. -function NodeList({ nodes, group, onGroup, onOpen }: { +function NodeList({ nodes, group, summary, onGroup, onOpen }: { nodes: Node[] /** null is every node, "" the ungrouped. */ group: string | null + /** The operator's `show_summary` setting. */ + summary: boolean onGroup: (group: string | null) => void onOpen: (id: number) => void }) { @@ -242,7 +255,7 @@ function NodeList({ nodes, group, onGroup, onOpen }: { ))} )} - + {summary && } {nodes.length === 0 ? (

还没有节点

) : ( diff --git a/src/components/NodeCard.tsx b/src/components/NodeCard.tsx index e0e4661..1426eb9 100644 --- a/src/components/NodeCard.tsx +++ b/src/components/NodeCard.tsx @@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge" import { Card } from "@/components/ui/card" import { Meter } from "@/components/Meter" import type { Node } from "@/lib/api" -import { bytes, daysUntil, FOREVER, osName, pair, percent, rate, uptime } from "@/lib/format" +import { bytes, daysUntil, FOREVER, osName, osVersion, pair, percent, rate, uptime } from "@/lib/format" import { cn } from "@/lib/utils" // Emitted as files and fetched on first use, so a page carries only the flags its @@ -114,20 +114,25 @@ export function Country({ node }: { node: Node }) { ) } +/** The logo naming the distribution, or none for one the list lacks. */ +function distro(os: string) { + const name = os.toLowerCase() + return DISTROS.find(([key]) => name.includes(key))?.[1] +} + /** * The distribution's logo in its brand colour. Mixed toward white on the dark * theme, where AlmaLinux's black and CentOS's navy would otherwise vanish. */ -function OsIcon({ os }: { os: string }) { - const name = os.toLowerCase() - const icon = DISTROS.find(([key]) => name.includes(key))?.[1] ?? siLinux +function OsIcon({ icon }: { icon: SimpleIcon }) { return ( + {icon.title} ) @@ -159,6 +164,10 @@ function Expiry({ node }: { node: Node }) { export function NodeCard({ node, onOpen }: { node: Node; onOpen: () => void }) { const m = node.metrics + // A known logo already names the distribution, so the text beside it is only + // the release; the penguin names none and keeps the full name. + const icon = node.os ? distro(node.os) : undefined + const os = node.os ? (icon ? osVersion(node.os) : osName(node.os)) : "等待首次上报" return ( void }) {
-

{node.name}

+

{node.name}

-

- {node.os && } +

+ {node.os && } - {node.os ? osName(node.os) : "等待首次上报"} - {node.virt && node.virt !== "none" ? ` · ${node.virt}` : ""} - {node.arch ? ` · ${node.arch}` : ""} + {[os, node.virt !== "none" && node.virt, node.arch] + .filter(Boolean) + .join(" · ")}

diff --git a/src/components/NodeDetail.tsx b/src/components/NodeDetail.tsx index 55c063f..a3fbe85 100644 --- a/src/components/NodeDetail.tsx +++ b/src/components/NodeDetail.tsx @@ -333,8 +333,8 @@ export function NodeDetail({ node }: { node: Node }) { return (
-

{node.name}

+

{node.name}

{node.agent_version && ( diff --git a/src/lib/config.ts b/src/lib/config.ts new file mode 100644 index 0000000..5fea067 --- /dev/null +++ b/src/lib/config.ts @@ -0,0 +1,18 @@ +import manifest from "../../theme.json" +import { api } from "@/lib/api" + +/** + * The operator's settings over the defaults declared in theme.json, which the + * hub does not store. A saved value of another type -- left by an older version + * of this theme -- counts as unsaved. Any failure, a hub predating settings (404) + * included, renders the defaults rather than an error. + */ +export function loadConfig(): Promise> { + // ponytail: checks the type only; a select or a ranged number would need the + // option and min/max checks the panel applies. + const pick = (saved: Record) => + Object.fromEntries( + manifest.config.map((f) => [f.key, typeof saved[f.key] === typeof f.default ? saved[f.key] : f.default]), + ) + return api>(`/themes/${manifest.short}/config`).then(pick).catch(() => pick({})) +} diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index b55b7a3..e496bbc 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -4,7 +4,7 @@ // // Nothing imports it, so the bundle never includes it. import { - axisBytes, axisTop, bytes, cpuName, daysUntil, despike, osName, pair, quarters, timeTicks, uptime, + axisBytes, axisTop, bytes, cpuName, daysUntil, despike, osName, osVersion, pair, quarters, timeTicks, uptime, } from "./format.ts" let failed = 0 @@ -118,6 +118,10 @@ eq(uptime(2 * 86400 + 5 * 3600), "2 天 5 小时", "超过一天不再写分钟" } eq(osName("Debian GNU/Linux 12 (bookworm)"), "Debian 12", "发行版名去掉代号") +eq(osVersion("Debian GNU/Linux 13 (trixie)"), "13", "版本号去掉发行版名") +eq(osVersion("Alpine Linux v3.24"), "3.24", "版本号去掉 v 前缀") +eq(osVersion("Ubuntu 24.04.1 LTS"), "24.04.1 LTS", "版本号保留 LTS") +eq(osVersion("Arch Linux"), "", "滚动发行版没有版本号") eq(cpuName("Intel(R) Xeon(R) CPU E5-2680 8-Core Processor"), "Intel Xeon E5-2680", "CPU 名去掉商标和核数") if (failed) { diff --git a/src/lib/format.ts b/src/lib/format.ts index ea2953c..e43cb21 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -133,6 +133,15 @@ export function osName(name: string): string { return name.replace("GNU/Linux ", "").replace(/\s*\([^)]*\)\s*$/, "") } +/** + * The release alone, for a line whose logo already names the distribution: + * everything from the first number on, "v" dropped. Empty for a rolling release + * such as Arch, which has no number to show. + */ +export function osVersion(name: string): string { + return osName(name).match(/\bv?(\d.*)$/)?.[1] ?? "" +} + export function cpuName(name: string): string { return name .replace(/\((R|TM|r|tm)\)/g, "") diff --git a/theme.json b/theme.json index 5581b26..62cf521 100644 --- a/theme.json +++ b/theme.json @@ -4,5 +4,14 @@ "description": "内置状态面板,黑白配色", "version": "1.1.0", "author": "stqfdyr", - "url": "https://github.com/monitor-probe/monitor-theme-default" + "url": "https://github.com/monitor-probe/monitor-theme-default", + "config": [ + { + "key": "show_summary", + "type": "boolean", + "label": "显示汇总", + "default": true, + "help": "节点列表上方的节点数、最忙节点、流量和网速四格" + } + ] } From e9f9eb51ae92f7332314a7bedeef2138fd93c8bf Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Sun, 27 Sep 2026 23:02:43 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E7=B3=BB=E7=BB=9F=E8=A1=8C?= =?UTF-8?q?=E5=8F=AA=E7=95=99=E5=8F=91=E8=A1=8C=E7=89=88=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=EF=BC=8C=E5=90=8D=E7=A7=B0=E4=B8=8E=E7=89=88=E6=9C=AC=E6=94=BE?= =?UTF-8?q?=E8=BF=9B=E6=82=AC=E5=81=9C=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/NodeCard.tsx | 33 +++++++++++++-------------------- src/lib/format.test.ts | 6 +----- src/lib/format.ts | 9 --------- 3 files changed, 14 insertions(+), 34 deletions(-) diff --git a/src/components/NodeCard.tsx b/src/components/NodeCard.tsx index 1426eb9..e7d0927 100644 --- a/src/components/NodeCard.tsx +++ b/src/components/NodeCard.tsx @@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge" import { Card } from "@/components/ui/card" import { Meter } from "@/components/Meter" import type { Node } from "@/lib/api" -import { bytes, daysUntil, FOREVER, osName, osVersion, pair, percent, rate, uptime } from "@/lib/format" +import { bytes, daysUntil, FOREVER, pair, percent, rate, uptime } from "@/lib/format" import { cn } from "@/lib/utils" // Emitted as files and fetched on first use, so a page carries only the flags its @@ -114,17 +114,14 @@ export function Country({ node }: { node: Node }) { ) } -/** The logo naming the distribution, or none for one the list lacks. */ -function distro(os: string) { - const name = os.toLowerCase() - return DISTROS.find(([key]) => name.includes(key))?.[1] -} - /** - * The distribution's logo in its brand colour. Mixed toward white on the dark - * theme, where AlmaLinux's black and CentOS's navy would otherwise vanish. + * The distribution's logo in its brand colour, standing in for the name, which + * shows on hover. Mixed toward white on the dark theme, where AlmaLinux's black + * and CentOS's navy would otherwise vanish. */ -function OsIcon({ icon }: { icon: SimpleIcon }) { +function OsIcon({ os }: { os: string }) { + const name = os.toLowerCase() + const icon = DISTROS.find(([key]) => name.includes(key))?.[1] ?? siLinux return ( - {icon.title} + {os} ) @@ -164,10 +161,6 @@ function Expiry({ node }: { node: Node }) { export function NodeCard({ node, onOpen }: { node: Node; onOpen: () => void }) { const m = node.metrics - // A known logo already names the distribution, so the text beside it is only - // the release; the penguin names none and keeps the full name. - const icon = node.os ? distro(node.os) : undefined - const os = node.os ? (icon ? osVersion(node.os) : osName(node.os)) : "等待首次上报" return ( void }) {

{node.name}

-

- {node.os && } +

+ {node.os && } - {[os, node.virt !== "none" && node.virt, node.arch] - .filter(Boolean) - .join(" · ")} + {node.os + ? [node.virt !== "none" && node.virt, node.arch].filter(Boolean).join(" · ") + : "等待首次上报"}

diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index e496bbc..b55b7a3 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -4,7 +4,7 @@ // // Nothing imports it, so the bundle never includes it. import { - axisBytes, axisTop, bytes, cpuName, daysUntil, despike, osName, osVersion, pair, quarters, timeTicks, uptime, + axisBytes, axisTop, bytes, cpuName, daysUntil, despike, osName, pair, quarters, timeTicks, uptime, } from "./format.ts" let failed = 0 @@ -118,10 +118,6 @@ eq(uptime(2 * 86400 + 5 * 3600), "2 天 5 小时", "超过一天不再写分钟" } eq(osName("Debian GNU/Linux 12 (bookworm)"), "Debian 12", "发行版名去掉代号") -eq(osVersion("Debian GNU/Linux 13 (trixie)"), "13", "版本号去掉发行版名") -eq(osVersion("Alpine Linux v3.24"), "3.24", "版本号去掉 v 前缀") -eq(osVersion("Ubuntu 24.04.1 LTS"), "24.04.1 LTS", "版本号保留 LTS") -eq(osVersion("Arch Linux"), "", "滚动发行版没有版本号") eq(cpuName("Intel(R) Xeon(R) CPU E5-2680 8-Core Processor"), "Intel Xeon E5-2680", "CPU 名去掉商标和核数") if (failed) { diff --git a/src/lib/format.ts b/src/lib/format.ts index e43cb21..ea2953c 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -133,15 +133,6 @@ export function osName(name: string): string { return name.replace("GNU/Linux ", "").replace(/\s*\([^)]*\)\s*$/, "") } -/** - * The release alone, for a line whose logo already names the distribution: - * everything from the first number on, "v" dropped. Empty for a rolling release - * such as Arch, which has no number to show. - */ -export function osVersion(name: string): string { - return osName(name).match(/\bv?(\d.*)$/)?.[1] ?? "" -} - export function cpuName(name: string): string { return name .replace(/\((R|TM|r|tm)\)/g, "") From f714bf263181b3382a1b2e1de57a9fe07da39d48 Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Sun, 27 Sep 2026 23:17:44 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E8=AE=BE=E7=BD=AE=E5=8F=AA=E9=98=BB?= =?UTF-8?q?=E5=A1=9E=E8=8A=82=E7=82=B9=E5=88=97=E8=A1=A8=EF=BC=8C=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E4=B8=8D=E7=AD=89=E5=AE=83=EF=BC=9B=E6=82=AC?= =?UTF-8?q?=E5=81=9C=E6=8F=90=E7=A4=BA=E4=B8=8E=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E5=86=99=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 4 ++-- src/components/NodeCard.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 65216ea..5c5e734 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -141,7 +141,7 @@ export default function App() { // Only while there is nothing else to show. Once `me` has loaded, a later // failure belongs beside the page rather than over it. - if (!me || !config) return ( + if (!me) return (
{meError ?

加载失败:{meError}

: "加载中…"}
@@ -188,7 +188,7 @@ export default function App() { 节点不存在或未公开。

) - ) : !nodes ? ( + ) : !nodes || !config ? (
{[0, 1, 2].map((i) => ( diff --git a/src/components/NodeCard.tsx b/src/components/NodeCard.tsx index e7d0927..2389943 100644 --- a/src/components/NodeCard.tsx +++ b/src/components/NodeCard.tsx @@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge" import { Card } from "@/components/ui/card" import { Meter } from "@/components/Meter" import type { Node } from "@/lib/api" -import { bytes, daysUntil, FOREVER, pair, percent, rate, uptime } from "@/lib/format" +import { bytes, daysUntil, FOREVER, osName, pair, percent, rate, uptime } from "@/lib/format" import { cn } from "@/lib/utils" // Emitted as files and fetched on first use, so a page carries only the flags its @@ -129,7 +129,7 @@ function OsIcon({ os }: { os: string }) { style={{ "--brand": `#${icon.hex}` } as CSSProperties} className="size-3 shrink-0 fill-(--brand) dark:fill-[color-mix(in_oklab,var(--brand)_60%,white)]" > - {os} + {osName(os)} )