Compare commits
8
Commits
4f98490224
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bf7fbbd97 | ||
|
|
771e143eff | ||
|
|
6834cc141a | ||
|
|
135e2b613d | ||
|
|
cbd9de926f | ||
|
|
3302155ecd | ||
|
|
681adda585 | ||
|
|
887d45a771 |
@@ -0,0 +1,21 @@
|
|||||||
|
root = "."
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
# отдельный бинарь, чтобы не путать с make server (bin/hub-api)
|
||||||
|
cmd = "go build -o ./bin/hub-api-dev ./server"
|
||||||
|
bin = "./bin/hub-api-dev"
|
||||||
|
include_ext = ["go"]
|
||||||
|
exclude_dir = ["web", "dist", "gen", "bin", "tmp", "proto", "docs"]
|
||||||
|
# порт берётся из env PORT самим сервером: PORT=8081 air
|
||||||
|
delay = 500
|
||||||
|
# упавший билд НЕ убивает air: стейдж продолжает отдавать последний рабочий бинарь
|
||||||
|
stop_on_error = false
|
||||||
|
send_interrupt = true
|
||||||
|
kill_delay = "2s"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
time = true
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = true
|
||||||
@@ -2,3 +2,5 @@ bin/
|
|||||||
dist/
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
tmp/
|
||||||
|
air.log
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
.PHONY: proto web server release dev test
|
PORT ?= 8080 # порт бэка; в dev еще прокидывается в vite-прокси (BACKEND_PORT)
|
||||||
|
STAGE_BACKEND ?= 8081 # air-бэкенд стейджа
|
||||||
|
STAGE_VITE ?= 8584 # vite стейджа — сюда смотрит домен (Caddy → localhost:8584)
|
||||||
|
|
||||||
|
.PHONY: proto web server release dev dev-srv dev-web watch test
|
||||||
|
|
||||||
proto: ## Regenerate Go + TS code from proto
|
proto: ## Regenerate Go + TS code from proto
|
||||||
PATH="$(shell go env GOPATH)/bin:$$PATH" protoc \
|
PATH="$(shell go env GOPATH)/bin:$$PATH" protoc \
|
||||||
@@ -18,14 +22,24 @@ server: ## Build Go binary → bin/hub-api
|
|||||||
release: ## CI build: static binary for the box (run on the box itself, native arch)
|
release: ## CI build: static binary for the box (run on the box itself, native arch)
|
||||||
CGO_ENABLED=0 go build -o bin/hub-api ./server
|
CGO_ENABLED=0 go build -o bin/hub-api ./server
|
||||||
|
|
||||||
dev: ## Dev: Go server + Vite on :5173 (use http://localhost:5173)
|
dev: ## Dev: Go server + Vite on :5173 (use http://localhost:5173); PORT=8081 make dev для второй копии
|
||||||
go run ./server & cd web && pnpm dev
|
PORT=$(PORT) go run ./server & cd web && BACKEND_PORT=$(PORT) pnpm dev
|
||||||
|
|
||||||
dev-srv:
|
dev-srv:
|
||||||
go run ./server
|
PORT=$(PORT) go run ./server
|
||||||
|
|
||||||
dev-web:
|
dev-web:
|
||||||
cd web && pnpm dev
|
cd web && BACKEND_PORT=$(PORT) pnpm dev
|
||||||
|
|
||||||
|
watch: ## Hot-reload сервера (air); порт как у dev: PORT=8081 make watch
|
||||||
|
PORT=$(PORT) air
|
||||||
|
|
||||||
|
stage: ## Staging: air(бэк :$(STAGE_BACKEND)) + vite(:$(STAGE_VITE)) одной командой — для hub-stage.service
|
||||||
|
PORT=$(STAGE_BACKEND) air & a=$$!; \
|
||||||
|
cd web && BACKEND_PORT=$(STAGE_BACKEND) pnpm dev --port $(STAGE_VITE) --strictPort --host & v=$$!; \
|
||||||
|
while kill -0 $$a 2>/dev/null && kill -0 $$v 2>/dev/null; do sleep 2; done; \
|
||||||
|
kill $$a $$v 2>/dev/null; wait $$a $$v 2>/dev/null; \
|
||||||
|
exit 1 # один из процессов умер — пусть systemd перезапустит сервис
|
||||||
|
|
||||||
test: ## Smoke-test the RPC endpoints
|
test: ## Smoke-test the RPC endpoints
|
||||||
@go build -o bin/hub-api ./server
|
@go build -o bin/hub-api ./server
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ monitor:
|
|||||||
- name: cpu
|
- name: cpu
|
||||||
script: scripts/cpu.sh
|
script: scripts/cpu.sh
|
||||||
refresh: 5
|
refresh: 5
|
||||||
|
percentages: 5 # общий + по ядру на каждое ядро (max 8)
|
||||||
- name: disk
|
- name: disk
|
||||||
script: scripts/disk.sh
|
script: scripts/disk.sh
|
||||||
refresh: 60
|
refresh: 60
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.12
|
// protoc-gen-go v1.36.12
|
||||||
// protoc v7.35.1
|
// protoc v6.32.1
|
||||||
// source: homelab/v1/homelab.proto
|
// source: homelab/v1/homelab.proto
|
||||||
|
|
||||||
package homelabv1
|
package homelabv1
|
||||||
@@ -229,12 +229,14 @@ func (x *Stat) GetValue() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Карточка монитора: процент (-1 = скрипт не дал процент) + строки для подписей.
|
// Карточка монитора: проценты (-1 = скрипт не дал процент) + строки для подписей.
|
||||||
|
// percentages[0] — основной (большой radial), дальше — дополнительные (per-core и т.п.).
|
||||||
type Monitor struct {
|
type Monitor struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
Percentage int32 `protobuf:"varint,2,opt,name=percentage,proto3" json:"percentage,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"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -290,6 +292,13 @@ func (x *Monitor) GetStats() []*Stat {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Monitor) GetPercentages() []int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Percentages
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Link struct {
|
type Link struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
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" +
|
"\targ_types\x18\x05 \x03(\tR\bargTypes\"0\n" +
|
||||||
"\x04Stat\x12\x12\n" +
|
"\x04Stat\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\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" +
|
"\aMonitor\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1e\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1e\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"percentage\x18\x02 \x01(\x05R\n" +
|
"percentage\x18\x02 \x01(\x05R\n" +
|
||||||
"percentage\x12&\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" +
|
"\x04Link\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||||
"\x03url\x18\x02 \x01(\tR\x03url\"\x14\n" +
|
"\x03url\x18\x02 \x01(\tR\x03url\"\x14\n" +
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ message Stat {
|
|||||||
string value = 2;
|
string value = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Карточка монитора: процент (-1 = скрипт не дал процент) + строки для подписей.
|
// Карточка монитора: проценты (-1 = скрипт не дал процент) + строки для подписей.
|
||||||
|
// percentages[0] — основной (большой radial), дальше — дополнительные (per-core и т.п.).
|
||||||
message Monitor {
|
message Monitor {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
int32 percentage = 2;
|
int32 percentage = 2;
|
||||||
repeated Stat stats = 3;
|
repeated Stat stats = 3;
|
||||||
|
repeated int32 percentages = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Link {
|
message Link {
|
||||||
|
|||||||
+21
-3
@@ -1,8 +1,26 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# CPU: процент загрузки + число ядер. Linux + mac.
|
# CPU: общий процент + по ядру на каждое ядро (Linux). Mac: только общий.
|
||||||
# Контракт мониторов: 1-я строка — процент, дальше — key=value.
|
# Контракт мониторов: первые N строк (monitor.percentages, max 8) — проценты,
|
||||||
|
# дальше — key=value.
|
||||||
case "$(uname)" in
|
case "$(uname)" in
|
||||||
Darwin) cores=$(sysctl -n hw.ncpu) ;;
|
Darwin) cores=$(sysctl -n hw.ncpu) ;;
|
||||||
*) cores=$(nproc) ;;
|
*) cores=$(nproc) ;;
|
||||||
esac
|
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"
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ func loadConfig(path string) (*Config, error) {
|
|||||||
if it.Refresh <= 0 {
|
if it.Refresh <= 0 {
|
||||||
return nil, fmt.Errorf("monitor %q: refresh must be > 0", it.Name)
|
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 {
|
for _, it := range cfg.Exec {
|
||||||
if len(it.ArgTypes) != it.Args {
|
if len(it.ArgTypes) != it.Args {
|
||||||
|
|||||||
+29
-18
@@ -30,9 +30,10 @@ type Item struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Monitor struct {
|
type Monitor struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Script string `yaml:"script"`
|
Script string `yaml:"script"`
|
||||||
Refresh int `yaml:"refresh"` // seconds
|
Refresh int `yaml:"refresh"` // seconds
|
||||||
|
Percentages int `yaml:"percentages"` // сколько процентных строк отдает скрипт (1..8); 0 = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
type Link struct {
|
type Link struct {
|
||||||
@@ -54,24 +55,31 @@ type server struct {
|
|||||||
|
|
||||||
const execTimeout = 10 * time.Second
|
const execTimeout = 10 * time.Second
|
||||||
|
|
||||||
// monData — распарсенный вывод скрипта-монитора: первая строка — процент,
|
// monData — распарсенный вывод скрипта-монитора: первые строки-проценты
|
||||||
// остальные — строки key=value для подписей в карточке.
|
// (первая — основной процент карточки), остальные — key=value для подписей.
|
||||||
type monData struct {
|
type monData struct {
|
||||||
percentage int32 // -1 = скрипт не дал процент
|
percentage int32 // -1 = скрипт не дал процент
|
||||||
stats []*homelabv1.Stat
|
percentages []int32 // все проценты (основной + дополнительные, max 8)
|
||||||
|
stats []*homelabv1.Stat
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMonitor(out string) *monData {
|
func parseMonitor(out string, maxPct int) *monData {
|
||||||
d := &monData{percentage: -1}
|
d := &monData{percentage: -1}
|
||||||
lines := strings.Split(strings.TrimSpace(out), "\n")
|
for i, l := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||||
if lines[0] == "" {
|
l = strings.TrimSpace(l)
|
||||||
return d
|
if l == "" {
|
||||||
}
|
continue
|
||||||
if n, err := strconv.Atoi(strings.TrimSpace(lines[0])); err == nil {
|
}
|
||||||
d.percentage = int32(n)
|
if i < maxPct {
|
||||||
}
|
if n, err := strconv.Atoi(l); err == nil {
|
||||||
for _, l := range lines[1:] {
|
if len(d.percentages) == 0 {
|
||||||
if k, v, ok := strings.Cut(strings.TrimSpace(l), "="); ok {
|
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)})
|
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)
|
log.Printf("monitor %s: %v", sc.Name, err)
|
||||||
out = "-\nerror=" + err.Error()
|
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))
|
log.Printf("monitor %s -> %d%% (%d stats)", sc.Name, d.percentage, len(d.stats))
|
||||||
s.monVals.Store(sc.Name, d)
|
s.monVals.Store(sc.Name, d)
|
||||||
time.Sleep(time.Duration(sc.Refresh) * time.Second)
|
time.Sleep(time.Duration(sc.Refresh) * time.Second)
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ func (s *server) ListContent(ctx context.Context, _ *connect.Request[homelabv1.L
|
|||||||
m := &homelabv1.Monitor{Name: it.Name}
|
m := &homelabv1.Monitor{Name: it.Name}
|
||||||
if d, ok := s.monVals.Load(it.Name); ok {
|
if d, ok := s.monVals.Load(it.Name); ok {
|
||||||
md := d.(*monData)
|
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)
|
resp.Monitors = append(resp.Monitors, m)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-8
@@ -142,14 +142,18 @@ function Dashboard({
|
|||||||
monitors: Monitor[];
|
monitors: Monitor[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-2 mt-4">
|
<div class="flex flex-col md:flex-row justify-center gap-2 mt-4">
|
||||||
<DashboardCatName name="Links" />
|
<div>
|
||||||
<LinkList links={links} />
|
<DashboardCatName name="Links" />
|
||||||
<DashboardCatName name="Monitors" />
|
<LinkList links={links} />
|
||||||
<div class="flex flex-col gap-2">
|
</div>
|
||||||
{monitors.map((it) => (
|
<div>
|
||||||
<MonitorCard s={it} />
|
<DashboardCatName name="Monitors" />
|
||||||
))}
|
<div class="flex flex-col gap-2">
|
||||||
|
{monitors.map((it) => (
|
||||||
|
<MonitorCard s={it} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import type { Item, Monitor } from "../gen/homelab/v1/homelab_pb";
|
import type { Monitor } from "../gen/homelab/v1/homelab_pb";
|
||||||
|
|
||||||
/** Карточка монитора: percentage → radial-progress, иначе текст. */
|
/** Карточка монитора: 1 процент → большой radial, несколько → грид маленьких (по ядрам, без общего). */
|
||||||
export function MonitorCard({ s }: { s: Monitor }) {
|
export function MonitorCard({ s }: { s: Monitor }) {
|
||||||
const pct = s.percentage;
|
const pcts =
|
||||||
const isPct = !Number.isNaN(pct);
|
s.percentages.length > 0 ? s.percentages : s.percentage >= 0 ? [s.percentage] : [];
|
||||||
|
const multi = pcts.length > 1;
|
||||||
|
const bars = multi ? pcts.slice(1) : pcts; // при нескольких процентах общий не показываем
|
||||||
|
const main = bars[0];
|
||||||
|
const extra = bars.slice(1);
|
||||||
return (
|
return (
|
||||||
<div class="card bg-base-200 shadow">
|
<div class="card bg-base-200 shadow">
|
||||||
<div class="card-body p-4 flex flex-row justify-between align-center">
|
<div class="card-body p-4 flex flex-row justify-between align-center">
|
||||||
@@ -11,16 +15,53 @@ export function MonitorCard({ s }: { s: Monitor }) {
|
|||||||
<h2 class="card-title text-sm font-medium text-base-content/60">
|
<h2 class="card-title text-sm font-medium text-base-content/60">
|
||||||
{s.name}
|
{s.name}
|
||||||
</h2>
|
</h2>
|
||||||
{isPct ? (
|
{multi ? (
|
||||||
<div
|
<div class="grid w-fit grid-cols-2 gap-2">
|
||||||
class="radial-progress text-primary"
|
{bars.map((v, i) => (
|
||||||
style={{ "--value": pct } as preact.JSX.CSSProperties}
|
<div
|
||||||
role="progressbar"
|
class="radial-progress text-primary"
|
||||||
>
|
style={
|
||||||
{pct}%
|
{
|
||||||
|
"--value": v,
|
||||||
|
"--size": "2.6rem",
|
||||||
|
"--thickness": "3px",
|
||||||
|
} as preact.JSX.CSSProperties
|
||||||
|
}
|
||||||
|
role="progressbar"
|
||||||
|
title={`cpu${i}`}
|
||||||
|
>
|
||||||
|
<span class="text-[0.6rem]">{v}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : main !== undefined && main >= 0 ? (
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="radial-progress text-primary"
|
||||||
|
style={{ "--value": main } as preact.JSX.CSSProperties}
|
||||||
|
role="progressbar"
|
||||||
|
>
|
||||||
|
{main}%
|
||||||
|
</div>
|
||||||
|
{extra.map((v, i) => (
|
||||||
|
<div
|
||||||
|
class="radial-progress text-primary"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--value": v,
|
||||||
|
"--size": "2.6rem",
|
||||||
|
"--thickness": "3px",
|
||||||
|
} as preact.JSX.CSSProperties
|
||||||
|
}
|
||||||
|
role="progressbar"
|
||||||
|
title={`cpu${i}`}
|
||||||
|
>
|
||||||
|
<span class="text-[0.6rem]">{v}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p class="font-mono text-lg break-all">{s.percentage || "…"}</p>
|
<p class="font-mono text-lg break-all">{main || "…"}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
|
|||||||
* Describes the file homelab/v1/homelab.proto.
|
* Describes the file homelab/v1/homelab.proto.
|
||||||
*/
|
*/
|
||||||
export const file_homelab_v1_homelab: GenFile = /*@__PURE__*/
|
export const file_homelab_v1_homelab: GenFile = /*@__PURE__*/
|
||||||
fileDesc("Chhob21lbGFiL3YxL2hvbWVsYWIucHJvdG8SCmhvbWVsYWIudjEiNQoESXRlbRIMCgRuYW1lGAEgASgJEgwKBGFyZ3MYBCABKAUSEQoJYXJnX3R5cGVzGAUgAygJIiMKBFN0YXQSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJMCgdNb25pdG9yEgwKBG5hbWUYASABKAkSEgoKcGVyY2VudGFnZRgCIAEoBRIfCgVzdGF0cxgDIAMoCzIQLmhvbWVsYWIudjEuU3RhdCIhCgRMaW5rEgwKBG5hbWUYASABKAkSCwoDdXJsGAIgASgJIhQKEkxpc3RDb250ZW50UmVxdWVzdCKkAQoTTGlzdENvbnRlbnRSZXNwb25zZRIfCgVsaW5rcxgBIAMoCzIQLmhvbWVsYWIudjEuTGluaxIlCghtb25pdG9ycxgCIAMoCzITLmhvbWVsYWIudjEuTW9uaXRvchIeCgRleGVjGAMgAygLMhAuaG9tZWxhYi52MS5JdGVtEiUKCHNlcnZpY2VzGAQgAygLMhMuaG9tZWxhYi52MS5TZXJ2aWNlIiUKB0RvY01ldGESDAoEbmFtZRgBIAEoCRIMCgRoYXNoGAIgASgJIhEKD0xpc3REb2NzUmVxdWVzdCI1ChBMaXN0RG9jc1Jlc3BvbnNlEiEKBGRvY3MYASADKAsyEy5ob21lbGFiLnYxLkRvY01ldGEiHQoNR2V0RG9jUmVxdWVzdBIMCgRuYW1lGAEgASgJIiEKDkdldERvY1Jlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkiLwoOU2F2ZURvY1JlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdjb250ZW50GAIgASgJIh8KD1NhdmVEb2NSZXNwb25zZRIMCgRoYXNoGAEgASgJIq8BCgdTZXJ2aWNlEiUKBGtpbmQYASABKA4yFy5ob21lbGFiLnYxLlNlcnZpY2VLaW5kEgwKBG5hbWUYAiABKAkSDQoFc3RhdGUYAyABKAkSEAoIc3Vic3RhdGUYBCABKAkSEgoKbG9hZF9zdGF0ZRgFIAEoCRIXCg91bml0X2ZpbGVfc3RhdGUYBiABKAkSEgoKY29udGFpbmVycxgHIAMoCRINCgVlcnJvchgIIAEoCSJuChRTZXJ2aWNlQWN0aW9uUmVxdWVzdBIMCgRuYW1lGAEgASgJEiUKBGtpbmQYAiABKA4yFy5ob21lbGFiLnYxLlNlcnZpY2VLaW5kEiEKAm9wGAMgASgOMhUuaG9tZWxhYi52MS5TZXJ2aWNlT3AiJwoVU2VydmljZUFjdGlvblJlc3BvbnNlEg4KBm91dHB1dBgBIAEoCSJJChJTZXJ2aWNlSW5mb1JlcXVlc3QSDAoEbmFtZRgBIAEoCRIlCgRraW5kGAIgASgOMhcuaG9tZWxhYi52MS5TZXJ2aWNlS2luZCIlChNTZXJ2aWNlSW5mb1Jlc3BvbnNlEg4KBm91dHB1dBgBIAEoCSIuChBSdW5TY3JpcHRSZXF1ZXN0EgwKBG5hbWUYASABKAkSDAoEYXJncxgCIAMoCSIjChFSdW5TY3JpcHRSZXNwb25zZRIOCgZvdXRwdXQYASABKAkqJgoLU2VydmljZUtpbmQSCwoHU1lTVEVNRBAAEgoKBkRPQ0tFUhABKi0KCVNlcnZpY2VPcBIJCgVTVEFSVBAAEggKBFNUT1AQARILCgdSRVNUQVJUEAIynAQKDkhvbWVsYWJTZXJ2aWNlEk4KC0xpc3RDb250ZW50Eh4uaG9tZWxhYi52MS5MaXN0Q29udGVudFJlcXVlc3QaHy5ob21lbGFiLnYxLkxpc3RDb250ZW50UmVzcG9uc2USRQoITGlzdERvY3MSGy5ob21lbGFiLnYxLkxpc3REb2NzUmVxdWVzdBocLmhvbWVsYWIudjEuTGlzdERvY3NSZXNwb25zZRI/CgZHZXREb2MSGS5ob21lbGFiLnYxLkdldERvY1JlcXVlc3QaGi5ob21lbGFiLnYxLkdldERvY1Jlc3BvbnNlEkIKB1NhdmVEb2MSGi5ob21lbGFiLnYxLlNhdmVEb2NSZXF1ZXN0GhsuaG9tZWxhYi52MS5TYXZlRG9jUmVzcG9uc2USVAoNU2VydmljZUFjdGlvbhIgLmhvbWVsYWIudjEuU2VydmljZUFjdGlvblJlcXVlc3QaIS5ob21lbGFiLnYxLlNlcnZpY2VBY3Rpb25SZXNwb25zZRJOCgtTZXJ2aWNlSW5mbxIeLmhvbWVsYWIudjEuU2VydmljZUluZm9SZXF1ZXN0Gh8uaG9tZWxhYi52MS5TZXJ2aWNlSW5mb1Jlc3BvbnNlEkgKCVJ1blNjcmlwdBIcLmhvbWVsYWIudjEuUnVuU2NyaXB0UmVxdWVzdBodLmhvbWVsYWIudjEuUnVuU2NyaXB0UmVzcG9uc2VCKloocHdhLWhvbWVsYWItbW9uL2dlbi9ob21lbGFiL3YxO2hvbWVsYWJ2MWIGcHJvdG8z");
|
fileDesc("Chhob21lbGFiL3YxL2hvbWVsYWIucHJvdG8SCmhvbWVsYWIudjEiNQoESXRlbRIMCgRuYW1lGAEgASgJEgwKBGFyZ3MYBCABKAUSEQoJYXJnX3R5cGVzGAUgAygJIiMKBFN0YXQSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJhCgdNb25pdG9yEgwKBG5hbWUYASABKAkSEgoKcGVyY2VudGFnZRgCIAEoBRIfCgVzdGF0cxgDIAMoCzIQLmhvbWVsYWIudjEuU3RhdBITCgtwZXJjZW50YWdlcxgEIAMoBSIhCgRMaW5rEgwKBG5hbWUYASABKAkSCwoDdXJsGAIgASgJIhQKEkxpc3RDb250ZW50UmVxdWVzdCKkAQoTTGlzdENvbnRlbnRSZXNwb25zZRIfCgVsaW5rcxgBIAMoCzIQLmhvbWVsYWIudjEuTGluaxIlCghtb25pdG9ycxgCIAMoCzITLmhvbWVsYWIudjEuTW9uaXRvchIeCgRleGVjGAMgAygLMhAuaG9tZWxhYi52MS5JdGVtEiUKCHNlcnZpY2VzGAQgAygLMhMuaG9tZWxhYi52MS5TZXJ2aWNlIiUKB0RvY01ldGESDAoEbmFtZRgBIAEoCRIMCgRoYXNoGAIgASgJIhEKD0xpc3REb2NzUmVxdWVzdCI1ChBMaXN0RG9jc1Jlc3BvbnNlEiEKBGRvY3MYASADKAsyEy5ob21lbGFiLnYxLkRvY01ldGEiHQoNR2V0RG9jUmVxdWVzdBIMCgRuYW1lGAEgASgJIiEKDkdldERvY1Jlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkiLwoOU2F2ZURvY1JlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdjb250ZW50GAIgASgJIh8KD1NhdmVEb2NSZXNwb25zZRIMCgRoYXNoGAEgASgJIq8BCgdTZXJ2aWNlEiUKBGtpbmQYASABKA4yFy5ob21lbGFiLnYxLlNlcnZpY2VLaW5kEgwKBG5hbWUYAiABKAkSDQoFc3RhdGUYAyABKAkSEAoIc3Vic3RhdGUYBCABKAkSEgoKbG9hZF9zdGF0ZRgFIAEoCRIXCg91bml0X2ZpbGVfc3RhdGUYBiABKAkSEgoKY29udGFpbmVycxgHIAMoCRINCgVlcnJvchgIIAEoCSJuChRTZXJ2aWNlQWN0aW9uUmVxdWVzdBIMCgRuYW1lGAEgASgJEiUKBGtpbmQYAiABKA4yFy5ob21lbGFiLnYxLlNlcnZpY2VLaW5kEiEKAm9wGAMgASgOMhUuaG9tZWxhYi52MS5TZXJ2aWNlT3AiJwoVU2VydmljZUFjdGlvblJlc3BvbnNlEg4KBm91dHB1dBgBIAEoCSJJChJTZXJ2aWNlSW5mb1JlcXVlc3QSDAoEbmFtZRgBIAEoCRIlCgRraW5kGAIgASgOMhcuaG9tZWxhYi52MS5TZXJ2aWNlS2luZCIlChNTZXJ2aWNlSW5mb1Jlc3BvbnNlEg4KBm91dHB1dBgBIAEoCSIuChBSdW5TY3JpcHRSZXF1ZXN0EgwKBG5hbWUYASABKAkSDAoEYXJncxgCIAMoCSIjChFSdW5TY3JpcHRSZXNwb25zZRIOCgZvdXRwdXQYASABKAkqJgoLU2VydmljZUtpbmQSCwoHU1lTVEVNRBAAEgoKBkRPQ0tFUhABKi0KCVNlcnZpY2VPcBIJCgVTVEFSVBAAEggKBFNUT1AQARILCgdSRVNUQVJUEAIynAQKDkhvbWVsYWJTZXJ2aWNlEk4KC0xpc3RDb250ZW50Eh4uaG9tZWxhYi52MS5MaXN0Q29udGVudFJlcXVlc3QaHy5ob21lbGFiLnYxLkxpc3RDb250ZW50UmVzcG9uc2USRQoITGlzdERvY3MSGy5ob21lbGFiLnYxLkxpc3REb2NzUmVxdWVzdBocLmhvbWVsYWIudjEuTGlzdERvY3NSZXNwb25zZRI/CgZHZXREb2MSGS5ob21lbGFiLnYxLkdldERvY1JlcXVlc3QaGi5ob21lbGFiLnYxLkdldERvY1Jlc3BvbnNlEkIKB1NhdmVEb2MSGi5ob21lbGFiLnYxLlNhdmVEb2NSZXF1ZXN0GhsuaG9tZWxhYi52MS5TYXZlRG9jUmVzcG9uc2USVAoNU2VydmljZUFjdGlvbhIgLmhvbWVsYWIudjEuU2VydmljZUFjdGlvblJlcXVlc3QaIS5ob21lbGFiLnYxLlNlcnZpY2VBY3Rpb25SZXNwb25zZRJOCgtTZXJ2aWNlSW5mbxIeLmhvbWVsYWIudjEuU2VydmljZUluZm9SZXF1ZXN0Gh8uaG9tZWxhYi52MS5TZXJ2aWNlSW5mb1Jlc3BvbnNlEkgKCVJ1blNjcmlwdBIcLmhvbWVsYWIudjEuUnVuU2NyaXB0UmVxdWVzdBodLmhvbWVsYWIudjEuUnVuU2NyaXB0UmVzcG9uc2VCKloocHdhLWhvbWVsYWItbW9uL2dlbi9ob21lbGFiL3YxO2hvbWVsYWJ2MWIGcHJvdG8z");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message homelab.v1.Item
|
* @generated from message homelab.v1.Item
|
||||||
@@ -66,7 +66,8 @@ export const StatSchema: GenMessage<Stat> = /*@__PURE__*/
|
|||||||
messageDesc(file_homelab_v1_homelab, 1);
|
messageDesc(file_homelab_v1_homelab, 1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Карточка монитора: процент (-1 = скрипт не дал процент) + строки для подписей.
|
* Карточка монитора: проценты (-1 = скрипт не дал процент) + строки для подписей.
|
||||||
|
* percentages[0] — основной (большой radial), дальше — дополнительные (per-core и т.п.).
|
||||||
*
|
*
|
||||||
* @generated from message homelab.v1.Monitor
|
* @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;
|
* @generated from field: repeated homelab.v1.Stat stats = 3;
|
||||||
*/
|
*/
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: repeated int32 percentages = 4;
|
||||||
|
*/
|
||||||
|
percentages: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+7
-1
@@ -2,12 +2,18 @@ import { defineConfig } from "vite";
|
|||||||
import preact from "@preact/preset-vite";
|
import preact from "@preact/preset-vite";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
// Бэк для дев-прокси выбирается портом, не правкой кода:
|
||||||
|
// BACKEND_PORT=8081 pnpm dev -> прокси на вторую (рабочую/дев) копию.
|
||||||
|
const apiTarget = `http://localhost:${process.env.BACKEND_PORT ?? 8080}`;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [preact(), tailwindcss()],
|
plugins: [preact(), tailwindcss()],
|
||||||
server: {
|
server: {
|
||||||
|
// стейджинг смотрит доменом на vite — пускаем свой duckdns (точка = любые поддомены)
|
||||||
|
allowedHosts: [".alexey-homelab.duckdns.org"],
|
||||||
proxy: {
|
proxy: {
|
||||||
// Connect RPC calls in dev go straight to the Go server.
|
// Connect RPC calls in dev go straight to the Go server.
|
||||||
"/homelab.v1.HomelabService": "http://localhost:8080",
|
"/homelab.v1.HomelabService": apiTarget,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user