From 887d45a771b691acdc5c29272d97cebff9011be7 Mon Sep 17 00:00:00 2001 From: Pi Agent Date: Wed, 2 Sep 2026 16:13:36 +0000 Subject: [PATCH] feat: multi-percentage monitors (per-core CPU), max 8 --- config.yaml | 1 + gen/homelab/v1/homelab.pb.go | 18 ++++++++--- proto/homelab/v1/homelab.proto | 4 ++- scripts/cpu.sh | 24 ++++++++++++-- server/config.go | 3 ++ server/main.go | 47 +++++++++++++++++----------- server/rpc.go | 2 +- web/src/components/MonitorCard.tsx | 44 +++++++++++++++++++------- web/src/gen/homelab/v1/homelab_pb.ts | 10 ++++-- 9 files changed, 112 insertions(+), 41 deletions(-) diff --git a/config.yaml b/config.yaml index a035f32..0e16df7 100644 --- a/config.yaml +++ b/config.yaml @@ -7,6 +7,7 @@ monitor: - name: cpu script: scripts/cpu.sh refresh: 5 + percentages: 5 # общий + по ядру на каждое ядро (max 8) - name: disk script: scripts/disk.sh refresh: 60 diff --git a/gen/homelab/v1/homelab.pb.go b/gen/homelab/v1/homelab.pb.go index d189775..2c4a631 100644 --- a/gen/homelab/v1/homelab.pb.go +++ b/gen/homelab/v1/homelab.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.35.1 +// protoc v6.32.1 // source: homelab/v1/homelab.proto package homelabv1 @@ -229,12 +229,14 @@ func (x *Stat) GetValue() string { return "" } -// Карточка монитора: процент (-1 = скрипт не дал процент) + строки для подписей. +// Карточка монитора: проценты (-1 = скрипт не дал процент) + строки для подписей. +// percentages[0] — основной (большой radial), дальше — дополнительные (per-core и т.п.). type Monitor struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Percentage int32 `protobuf:"varint,2,opt,name=percentage,proto3" json:"percentage,omitempty"` Stats []*Stat `protobuf:"bytes,3,rep,name=stats,proto3" json:"stats,omitempty"` + Percentages []int32 `protobuf:"varint,4,rep,packed,name=percentages,proto3" json:"percentages,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -290,6 +292,13 @@ func (x *Monitor) GetStats() []*Stat { return nil } +func (x *Monitor) GetPercentages() []int32 { + if x != nil { + return x.Percentages + } + return nil +} + type Link struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -1170,13 +1179,14 @@ const file_homelab_v1_homelab_proto_rawDesc = "" + "\targ_types\x18\x05 \x03(\tR\bargTypes\"0\n" + "\x04Stat\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value\"e\n" + + "\x05value\x18\x02 \x01(\tR\x05value\"\x87\x01\n" + "\aMonitor\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1e\n" + "\n" + "percentage\x18\x02 \x01(\x05R\n" + "percentage\x12&\n" + - "\x05stats\x18\x03 \x03(\v2\x10.homelab.v1.StatR\x05stats\",\n" + + "\x05stats\x18\x03 \x03(\v2\x10.homelab.v1.StatR\x05stats\x12 \n" + + "\vpercentages\x18\x04 \x03(\x05R\vpercentages\",\n" + "\x04Link\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + "\x03url\x18\x02 \x01(\tR\x03url\"\x14\n" + diff --git a/proto/homelab/v1/homelab.proto b/proto/homelab/v1/homelab.proto index 3269d78..9f83381 100644 --- a/proto/homelab/v1/homelab.proto +++ b/proto/homelab/v1/homelab.proto @@ -15,11 +15,13 @@ message Stat { string value = 2; } -// Карточка монитора: процент (-1 = скрипт не дал процент) + строки для подписей. +// Карточка монитора: проценты (-1 = скрипт не дал процент) + строки для подписей. +// percentages[0] — основной (большой radial), дальше — дополнительные (per-core и т.п.). message Monitor { string name = 1; int32 percentage = 2; repeated Stat stats = 3; + repeated int32 percentages = 4; } message Link { diff --git a/scripts/cpu.sh b/scripts/cpu.sh index 1aa0d7b..3e3fb68 100755 --- a/scripts/cpu.sh +++ b/scripts/cpu.sh @@ -1,8 +1,26 @@ #!/bin/sh -# CPU: процент загрузки + число ядер. Linux + mac. -# Контракт мониторов: 1-я строка — процент, дальше — key=value. +# CPU: общий процент + по ядру на каждое ядро (Linux). Mac: только общий. +# Контракт мониторов: первые N строк (monitor.percentages, max 8) — проценты, +# дальше — key=value. case "$(uname)" in Darwin) cores=$(sysctl -n hw.ncpu) ;; *) cores=$(nproc) ;; esac -ps -A -o %cpu | awk -v c="$cores" '{s+=$1} END {printf "%d\nядер=%d\n", s/c, c}' +overall=$(ps -A -o %cpu | awk -v c="$cores" '{s+=$1} END {printf "%d", s/c}') +echo "$overall" +# per-core: два среза /proc/stat (user..steal), usage = 1 - Δidle/Δtotal +if [ -r /proc/stat ]; then + a=$(grep '^cpu[0-9]* ' /proc/stat | awk '{i=$5+$6; t=0; for(n=2;n<=8;n++) t+=$n; print i, t}') + sleep 0.5 + # оба среза одним awk: первая половина строк — срез 1, вторая — срез 2 + printf '%s\n%s\n' "$a" "$(grep '^cpu[0-9]* ' /proc/stat | awk '{i=$5+$6; t=0; for(n=2;n<=8;n++) t+=$n; print i, t}')" | awk ' + { ia[NR]=$1; ta[NR]=$2; n=NR } + END { + half=n/2 + for (r=1; r<=half; r++) { + di=ia[r+half]-ia[r]; dt=ta[r+half]-ta[r] + printf "%d\n", (dt>0 ? (1-di/dt)*100 : 0) + } + }' +fi +echo "ядер=$cores" diff --git a/server/config.go b/server/config.go index adcc298..d6416b9 100644 --- a/server/config.go +++ b/server/config.go @@ -47,6 +47,9 @@ func loadConfig(path string) (*Config, error) { if it.Refresh <= 0 { return nil, fmt.Errorf("monitor %q: refresh must be > 0", it.Name) } + if it.Percentages < 0 || it.Percentages > 8 { + return nil, fmt.Errorf("monitor %q: percentages must be 1..8", it.Name) + } } for _, it := range cfg.Exec { if len(it.ArgTypes) != it.Args { diff --git a/server/main.go b/server/main.go index 1d0320e..0814480 100644 --- a/server/main.go +++ b/server/main.go @@ -30,9 +30,10 @@ type Item struct { } type Monitor struct { - Name string `yaml:"name"` - Script string `yaml:"script"` - Refresh int `yaml:"refresh"` // seconds + Name string `yaml:"name"` + Script string `yaml:"script"` + Refresh int `yaml:"refresh"` // seconds + Percentages int `yaml:"percentages"` // сколько процентных строк отдает скрипт (1..8); 0 = 1 } type Link struct { @@ -54,24 +55,31 @@ type server struct { const execTimeout = 10 * time.Second -// monData — распарсенный вывод скрипта-монитора: первая строка — процент, -// остальные — строки key=value для подписей в карточке. +// monData — распарсенный вывод скрипта-монитора: первые строки-проценты +// (первая — основной процент карточки), остальные — key=value для подписей. type monData struct { - percentage int32 // -1 = скрипт не дал процент - stats []*homelabv1.Stat + percentage int32 // -1 = скрипт не дал процент + percentages []int32 // все проценты (основной + дополнительные, max 8) + stats []*homelabv1.Stat } -func parseMonitor(out string) *monData { +func parseMonitor(out string, maxPct int) *monData { d := &monData{percentage: -1} - lines := strings.Split(strings.TrimSpace(out), "\n") - if lines[0] == "" { - return d - } - if n, err := strconv.Atoi(strings.TrimSpace(lines[0])); err == nil { - d.percentage = int32(n) - } - for _, l := range lines[1:] { - if k, v, ok := strings.Cut(strings.TrimSpace(l), "="); ok { + for i, l := range strings.Split(strings.TrimSpace(out), "\n") { + l = strings.TrimSpace(l) + if l == "" { + continue + } + if i < maxPct { + if n, err := strconv.Atoi(l); err == nil { + if len(d.percentages) == 0 { + d.percentage = int32(n) + } + d.percentages = append(d.percentages, int32(n)) + continue + } + } + if k, v, ok := strings.Cut(l, "="); ok { d.stats = append(d.stats, &homelabv1.Stat{Name: strings.TrimSpace(k), Value: strings.TrimSpace(v)}) } } @@ -99,7 +107,10 @@ func (s *server) startMonitors() { log.Printf("monitor %s: %v", sc.Name, err) out = "-\nerror=" + err.Error() } - d := parseMonitor(out) + if sc.Percentages < 1 { + sc.Percentages = 1 + } + d := parseMonitor(out, sc.Percentages) log.Printf("monitor %s -> %d%% (%d stats)", sc.Name, d.percentage, len(d.stats)) s.monVals.Store(sc.Name, d) time.Sleep(time.Duration(sc.Refresh) * time.Second) diff --git a/server/rpc.go b/server/rpc.go index fe74abf..c0e049a 100644 --- a/server/rpc.go +++ b/server/rpc.go @@ -21,7 +21,7 @@ func (s *server) ListContent(ctx context.Context, _ *connect.Request[homelabv1.L m := &homelabv1.Monitor{Name: it.Name} if d, ok := s.monVals.Load(it.Name); ok { md := d.(*monData) - m.Percentage, m.Stats = md.percentage, md.stats + m.Percentage, m.Percentages, m.Stats = md.percentage, md.percentages, md.stats } resp.Monitors = append(resp.Monitors, m) } diff --git a/web/src/components/MonitorCard.tsx b/web/src/components/MonitorCard.tsx index 5a9b6c3..877e0b3 100644 --- a/web/src/components/MonitorCard.tsx +++ b/web/src/components/MonitorCard.tsx @@ -1,9 +1,11 @@ -import type { Item, Monitor } from "../gen/homelab/v1/homelab_pb"; +import type { Monitor } from "../gen/homelab/v1/homelab_pb"; -/** Карточка монитора: percentage → radial-progress, иначе текст. */ +/** Карточка монитора: percentages → radial-progress'ы (первый большой, остальные — по ядрам), иначе текст. */ export function MonitorCard({ s }: { s: Monitor }) { - const pct = s.percentage; - const isPct = !Number.isNaN(pct); + const pcts = + s.percentages.length > 0 ? s.percentages : s.percentage >= 0 ? [s.percentage] : []; + const main = pcts[0]; + const extra = pcts.slice(1); return (
@@ -11,16 +13,34 @@ export function MonitorCard({ s }: { s: Monitor }) {

{s.name}

- {isPct ? ( -
- {pct}% + {main !== undefined && main >= 0 ? ( +
+
+ {main}% +
+ {extra.map((v, i) => ( +
+ {v} +
+ ))}
) : ( -

{s.percentage || "…"}

+

{main || "…"}

)}
diff --git a/web/src/gen/homelab/v1/homelab_pb.ts b/web/src/gen/homelab/v1/homelab_pb.ts index 7e94cdb..6e2238f 100644 --- a/web/src/gen/homelab/v1/homelab_pb.ts +++ b/web/src/gen/homelab/v1/homelab_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file homelab/v1/homelab.proto. */ export const file_homelab_v1_homelab: GenFile = /*@__PURE__*/ - fileDesc("Chhob21lbGFiL3YxL2hvbWVsYWIucHJvdG8SCmhvbWVsYWIudjEiNQoESXRlbRIMCgRuYW1lGAEgASgJEgwKBGFyZ3MYBCABKAUSEQoJYXJnX3R5cGVzGAUgAygJIiMKBFN0YXQSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJMCgdNb25pdG9yEgwKBG5hbWUYASABKAkSEgoKcGVyY2VudGFnZRgCIAEoBRIfCgVzdGF0cxgDIAMoCzIQLmhvbWVsYWIudjEuU3RhdCIhCgRMaW5rEgwKBG5hbWUYASABKAkSCwoDdXJsGAIgASgJIhQKEkxpc3RDb250ZW50UmVxdWVzdCKkAQoTTGlzdENvbnRlbnRSZXNwb25zZRIfCgVsaW5rcxgBIAMoCzIQLmhvbWVsYWIudjEuTGluaxIlCghtb25pdG9ycxgCIAMoCzITLmhvbWVsYWIudjEuTW9uaXRvchIeCgRleGVjGAMgAygLMhAuaG9tZWxhYi52MS5JdGVtEiUKCHNlcnZpY2VzGAQgAygLMhMuaG9tZWxhYi52MS5TZXJ2aWNlIiUKB0RvY01ldGESDAoEbmFtZRgBIAEoCRIMCgRoYXNoGAIgASgJIhEKD0xpc3REb2NzUmVxdWVzdCI1ChBMaXN0RG9jc1Jlc3BvbnNlEiEKBGRvY3MYASADKAsyEy5ob21lbGFiLnYxLkRvY01ldGEiHQoNR2V0RG9jUmVxdWVzdBIMCgRuYW1lGAEgASgJIiEKDkdldERvY1Jlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkiLwoOU2F2ZURvY1JlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdjb250ZW50GAIgASgJIh8KD1NhdmVEb2NSZXNwb25zZRIMCgRoYXNoGAEgASgJIq8BCgdTZXJ2aWNlEiUKBGtpbmQYASABKA4yFy5ob21lbGFiLnYxLlNlcnZpY2VLaW5kEgwKBG5hbWUYAiABKAkSDQoFc3RhdGUYAyABKAkSEAoIc3Vic3RhdGUYBCABKAkSEgoKbG9hZF9zdGF0ZRgFIAEoCRIXCg91bml0X2ZpbGVfc3RhdGUYBiABKAkSEgoKY29udGFpbmVycxgHIAMoCRINCgVlcnJvchgIIAEoCSJuChRTZXJ2aWNlQWN0aW9uUmVxdWVzdBIMCgRuYW1lGAEgASgJEiUKBGtpbmQYAiABKA4yFy5ob21lbGFiLnYxLlNlcnZpY2VLaW5kEiEKAm9wGAMgASgOMhUuaG9tZWxhYi52MS5TZXJ2aWNlT3AiJwoVU2VydmljZUFjdGlvblJlc3BvbnNlEg4KBm91dHB1dBgBIAEoCSJJChJTZXJ2aWNlSW5mb1JlcXVlc3QSDAoEbmFtZRgBIAEoCRIlCgRraW5kGAIgASgOMhcuaG9tZWxhYi52MS5TZXJ2aWNlS2luZCIlChNTZXJ2aWNlSW5mb1Jlc3BvbnNlEg4KBm91dHB1dBgBIAEoCSIuChBSdW5TY3JpcHRSZXF1ZXN0EgwKBG5hbWUYASABKAkSDAoEYXJncxgCIAMoCSIjChFSdW5TY3JpcHRSZXNwb25zZRIOCgZvdXRwdXQYASABKAkqJgoLU2VydmljZUtpbmQSCwoHU1lTVEVNRBAAEgoKBkRPQ0tFUhABKi0KCVNlcnZpY2VPcBIJCgVTVEFSVBAAEggKBFNUT1AQARILCgdSRVNUQVJUEAIynAQKDkhvbWVsYWJTZXJ2aWNlEk4KC0xpc3RDb250ZW50Eh4uaG9tZWxhYi52MS5MaXN0Q29udGVudFJlcXVlc3QaHy5ob21lbGFiLnYxLkxpc3RDb250ZW50UmVzcG9uc2USRQoITGlzdERvY3MSGy5ob21lbGFiLnYxLkxpc3REb2NzUmVxdWVzdBocLmhvbWVsYWIudjEuTGlzdERvY3NSZXNwb25zZRI/CgZHZXREb2MSGS5ob21lbGFiLnYxLkdldERvY1JlcXVlc3QaGi5ob21lbGFiLnYxLkdldERvY1Jlc3BvbnNlEkIKB1NhdmVEb2MSGi5ob21lbGFiLnYxLlNhdmVEb2NSZXF1ZXN0GhsuaG9tZWxhYi52MS5TYXZlRG9jUmVzcG9uc2USVAoNU2VydmljZUFjdGlvbhIgLmhvbWVsYWIudjEuU2VydmljZUFjdGlvblJlcXVlc3QaIS5ob21lbGFiLnYxLlNlcnZpY2VBY3Rpb25SZXNwb25zZRJOCgtTZXJ2aWNlSW5mbxIeLmhvbWVsYWIudjEuU2VydmljZUluZm9SZXF1ZXN0Gh8uaG9tZWxhYi52MS5TZXJ2aWNlSW5mb1Jlc3BvbnNlEkgKCVJ1blNjcmlwdBIcLmhvbWVsYWIudjEuUnVuU2NyaXB0UmVxdWVzdBodLmhvbWVsYWIudjEuUnVuU2NyaXB0UmVzcG9uc2VCKloocHdhLWhvbWVsYWItbW9uL2dlbi9ob21lbGFiL3YxO2hvbWVsYWJ2MWIGcHJvdG8z"); + fileDesc("Chhob21lbGFiL3YxL2hvbWVsYWIucHJvdG8SCmhvbWVsYWIudjEiNQoESXRlbRIMCgRuYW1lGAEgASgJEgwKBGFyZ3MYBCABKAUSEQoJYXJnX3R5cGVzGAUgAygJIiMKBFN0YXQSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJhCgdNb25pdG9yEgwKBG5hbWUYASABKAkSEgoKcGVyY2VudGFnZRgCIAEoBRIfCgVzdGF0cxgDIAMoCzIQLmhvbWVsYWIudjEuU3RhdBITCgtwZXJjZW50YWdlcxgEIAMoBSIhCgRMaW5rEgwKBG5hbWUYASABKAkSCwoDdXJsGAIgASgJIhQKEkxpc3RDb250ZW50UmVxdWVzdCKkAQoTTGlzdENvbnRlbnRSZXNwb25zZRIfCgVsaW5rcxgBIAMoCzIQLmhvbWVsYWIudjEuTGluaxIlCghtb25pdG9ycxgCIAMoCzITLmhvbWVsYWIudjEuTW9uaXRvchIeCgRleGVjGAMgAygLMhAuaG9tZWxhYi52MS5JdGVtEiUKCHNlcnZpY2VzGAQgAygLMhMuaG9tZWxhYi52MS5TZXJ2aWNlIiUKB0RvY01ldGESDAoEbmFtZRgBIAEoCRIMCgRoYXNoGAIgASgJIhEKD0xpc3REb2NzUmVxdWVzdCI1ChBMaXN0RG9jc1Jlc3BvbnNlEiEKBGRvY3MYASADKAsyEy5ob21lbGFiLnYxLkRvY01ldGEiHQoNR2V0RG9jUmVxdWVzdBIMCgRuYW1lGAEgASgJIiEKDkdldERvY1Jlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkiLwoOU2F2ZURvY1JlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdjb250ZW50GAIgASgJIh8KD1NhdmVEb2NSZXNwb25zZRIMCgRoYXNoGAEgASgJIq8BCgdTZXJ2aWNlEiUKBGtpbmQYASABKA4yFy5ob21lbGFiLnYxLlNlcnZpY2VLaW5kEgwKBG5hbWUYAiABKAkSDQoFc3RhdGUYAyABKAkSEAoIc3Vic3RhdGUYBCABKAkSEgoKbG9hZF9zdGF0ZRgFIAEoCRIXCg91bml0X2ZpbGVfc3RhdGUYBiABKAkSEgoKY29udGFpbmVycxgHIAMoCRINCgVlcnJvchgIIAEoCSJuChRTZXJ2aWNlQWN0aW9uUmVxdWVzdBIMCgRuYW1lGAEgASgJEiUKBGtpbmQYAiABKA4yFy5ob21lbGFiLnYxLlNlcnZpY2VLaW5kEiEKAm9wGAMgASgOMhUuaG9tZWxhYi52MS5TZXJ2aWNlT3AiJwoVU2VydmljZUFjdGlvblJlc3BvbnNlEg4KBm91dHB1dBgBIAEoCSJJChJTZXJ2aWNlSW5mb1JlcXVlc3QSDAoEbmFtZRgBIAEoCRIlCgRraW5kGAIgASgOMhcuaG9tZWxhYi52MS5TZXJ2aWNlS2luZCIlChNTZXJ2aWNlSW5mb1Jlc3BvbnNlEg4KBm91dHB1dBgBIAEoCSIuChBSdW5TY3JpcHRSZXF1ZXN0EgwKBG5hbWUYASABKAkSDAoEYXJncxgCIAMoCSIjChFSdW5TY3JpcHRSZXNwb25zZRIOCgZvdXRwdXQYASABKAkqJgoLU2VydmljZUtpbmQSCwoHU1lTVEVNRBAAEgoKBkRPQ0tFUhABKi0KCVNlcnZpY2VPcBIJCgVTVEFSVBAAEggKBFNUT1AQARILCgdSRVNUQVJUEAIynAQKDkhvbWVsYWJTZXJ2aWNlEk4KC0xpc3RDb250ZW50Eh4uaG9tZWxhYi52MS5MaXN0Q29udGVudFJlcXVlc3QaHy5ob21lbGFiLnYxLkxpc3RDb250ZW50UmVzcG9uc2USRQoITGlzdERvY3MSGy5ob21lbGFiLnYxLkxpc3REb2NzUmVxdWVzdBocLmhvbWVsYWIudjEuTGlzdERvY3NSZXNwb25zZRI/CgZHZXREb2MSGS5ob21lbGFiLnYxLkdldERvY1JlcXVlc3QaGi5ob21lbGFiLnYxLkdldERvY1Jlc3BvbnNlEkIKB1NhdmVEb2MSGi5ob21lbGFiLnYxLlNhdmVEb2NSZXF1ZXN0GhsuaG9tZWxhYi52MS5TYXZlRG9jUmVzcG9uc2USVAoNU2VydmljZUFjdGlvbhIgLmhvbWVsYWIudjEuU2VydmljZUFjdGlvblJlcXVlc3QaIS5ob21lbGFiLnYxLlNlcnZpY2VBY3Rpb25SZXNwb25zZRJOCgtTZXJ2aWNlSW5mbxIeLmhvbWVsYWIudjEuU2VydmljZUluZm9SZXF1ZXN0Gh8uaG9tZWxhYi52MS5TZXJ2aWNlSW5mb1Jlc3BvbnNlEkgKCVJ1blNjcmlwdBIcLmhvbWVsYWIudjEuUnVuU2NyaXB0UmVxdWVzdBodLmhvbWVsYWIudjEuUnVuU2NyaXB0UmVzcG9uc2VCKloocHdhLWhvbWVsYWItbW9uL2dlbi9ob21lbGFiL3YxO2hvbWVsYWJ2MWIGcHJvdG8z"); /** * @generated from message homelab.v1.Item @@ -66,7 +66,8 @@ export const StatSchema: GenMessage = /*@__PURE__*/ messageDesc(file_homelab_v1_homelab, 1); /** - * Карточка монитора: процент (-1 = скрипт не дал процент) + строки для подписей. + * Карточка монитора: проценты (-1 = скрипт не дал процент) + строки для подписей. + * percentages[0] — основной (большой radial), дальше — дополнительные (per-core и т.п.). * * @generated from message homelab.v1.Monitor */ @@ -85,6 +86,11 @@ export type Monitor = Message<"homelab.v1.Monitor"> & { * @generated from field: repeated homelab.v1.Stat stats = 3; */ stats: Stat[]; + + /** + * @generated from field: repeated int32 percentages = 4; + */ + percentages: number[]; }; /**