Rework: 4 tabs by func
Deploy / deploy (push) Successful in 10s

This commit is contained in:
2026-08-29 13:33:17 +05:00
parent dfa70e96e3
commit 0857d77a01
18 changed files with 1239 additions and 314 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
- name: Deploy to box - name: Deploy to box
run: | run: |
mkdir -p ~/apps/hub/dist ~/apps/hub/scripts mkdir -p ~/apps/hub/dist ~/apps/hub/scripts ~/apps/hub/docs
cp bin/hub-api ~/apps/hub/hub-api.new cp bin/hub-api ~/apps/hub/hub-api.new
mv -f ~/apps/hub/hub-api.new ~/apps/hub/hub-api mv -f ~/apps/hub/hub-api.new ~/apps/hub/hub-api
chmod +x ~/apps/hub/hub-api chmod +x ~/apps/hub/hub-api
+31
View File
@@ -0,0 +1,31 @@
require("custom.runner").setup({
commands = {
-- Bottom terminal (focused after exec)
-- run = function()
-- local command = require("custom.runner").command
-- command:add({
-- cmd = "make",
-- flags = { "run" },
-- })
-- command.target = { position = "bottom" }
-- command.focus = true
-- command:run()
-- end,
-- float-job (no target): executes in a float
-- debug = function()
-- local command = require("custom.runner").command
-- command:add({
-- cmd = "odin build .",
-- flags = { "-debug" },
-- })
-- command:run()
-- end,
},
-- additional excludes for snacks picker
exclude = {
"gen",
"*.png",
"go.mod",
"go.sum",
},
})
+12 -4
View File
@@ -15,14 +15,20 @@ PWA для менеджмента homelab: монорепозиторий Go-с
``` ```
proto/homelab/v1/ контракт (единственный источник правды) proto/homelab/v1/ контракт (единственный источник правды)
gen/ сгенерённый Go-код (коммитим) gen/ сгенерённый Go-код (коммитим)
server/main.go весь сервер: конфиг, мониторы, RPC, статика server/main.go весь сервер: конфиг, мониторы, RPC, статика, docs API
web/src/App.tsx layout, сайдбар секций, поллинг ListSections (3s) web/src/App.tsx layout, топбар + hamburger-drawer, 4 хардкод-таба, поллинг ListContent (3s)
web/src/components/ MonitorCard (мониторы), ScriptCard (response-форма + модалка результата) web/src/components/ MonitorCard, ScriptCard, Docs (ридер + textarea-редактор)
web/src/docsStore.ts localStorage доков (hub:doc:*), dirty-флаги, syncDocs: pull по хэшам + flush раз в 5s
web/src/gen/ сгенерированный TS-клиент web/src/gen/ сгенерированный TS-клиент
web/public/ PWA-обвязка: manifest, sw.js, иконки web/public/ PWA-обвязка: manifest, sw.js, иконки
scripts/ bash-скрипты, пути из config.yaml scripts/ bash-скрипты, пути из config.yaml
docs/ markdown-доки, НЕ в репо — живут только на сервере рядом с бинарьём
``` ```
## Категории
Хардкод в бандле (App.tsx CATEGORIES): Links (из config links), Monitor (config monitor), Exec (config exec), Docs (папка docs/, API ListDocs/GetDoc/SaveDoc). Оффлайн: контент-стейт держит последний ответ, доки — localStorage; правки в оффлайне помечаются dirty и флашатся на сервер каждые 5s (client authority — последняя запись побеждает, без base-hash check).
## Команды (всё через make) ## Команды (всё через make)
- `make dev` — сервер :8080 + vite :5173 (работать на http://localhost:5173) - `make dev` — сервер :8080 + vite :5173 (работать на http://localhost:5173)
@@ -46,7 +52,9 @@ scripts/ bash-скрипты, пути из config.yaml
- `int64` в proto → `bigint` в TS (не `number`!). - `int64` в proto → `bigint` в TS (не `number`!).
- Поля protobuf в TS — camelCase (`arg_types` → `argTypes`). - Поля protobuf в TS — camelCase (`arg_types` → `argTypes`).
- Новый item-тип или поле в yaml = обновить proto + валидацию в `loadConfig` + компонент на фронте. - Новый item-тип или поле в yaml = обновить proto + валидацию в `loadConfig` + компонент на фронте.
- Иконки секций: строка из конфига → ручной маппинг `ICONS` в `App.tsx` (не тянуть весь lucide через `icons` — раздувает бандл в 5 раз). - Иконки: хардкод в компонентах из lucide-preact поимённо (не тянуть весь lucide через `icons` — раздувает бандл в 5 раз).
- Маркдаун: `marked` (~10KB gzip) + dangerouslySetInnerHTML без санитайзера (контент свой). Если редактор перестанет нравиться — **переход на MDXEditor** ( agreed upgrade path, изолирован в компоненте Docs).
- index.html и sw.js отдаются с `Cache-Control: no-cache` — иначе iOS не подхватит новый бандл.
- Логи сервера: monitors и RunScript пишут в stdout; `ListSections` (поллинг-шум) исключён из HTTP-лога echo. - Логи сервера: monitors и RunScript пишут в stdout; `ListSections` (поллинг-шум) исключён из HTTP-лога echo.
- SW: `web/public/sw.js`, версия кэша `homelab-shell-vN` — **бампить при изменении** офлайн-логики, иначе iOS не обновит. - SW: `web/public/sw.js`, версия кэша `homelab-shell-vN` — **бампить при изменении** офлайн-логики, иначе iOS не обновит.
- Тесты: `make test` (smoke RPC) + `tsc --noEmit` внутри `make web`. Не плодить фреймворки. - Тесты: `make test` (smoke RPC) + `tsc --noEmit` внутри `make web`. Не плодить фреймворки.
+2 -1
View File
@@ -30,6 +30,7 @@ dev-web:
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
@./bin/hub-api & sleep 1; \ @./bin/hub-api & sleep 1; \
curl -sf -X POST localhost:8080/homelab.v1.HomelabService/ListSections -H 'Content-Type: application/json' -d '{}' | grep -q '"cpu"' || exit 1; \ curl -sf -X POST localhost:8080/homelab.v1.HomelabService/ListContent -H 'Content-Type: application/json' -d '{}' | grep -q '"cpu"' || exit 1; \
curl -sf -X POST localhost:8080/homelab.v1.HomelabService/ListDocs -H 'Content-Type: application/json' -d '{}' | grep -q 'test.md' || exit 1; \
curl -sf -X POST localhost:8080/homelab.v1.HomelabService/RunScript -H 'Content-Type: application/json' -d '{"name":"greet","args":["pony","42"]}' | grep -q 'arg1: pony' || exit 1; \ curl -sf -X POST localhost:8080/homelab.v1.HomelabService/RunScript -H 'Content-Type: application/json' -d '{"name":"greet","args":["pony","42"]}' | grep -q 'arg1: pony' || exit 1; \
kill %1; echo OK kill %1; echo OK
+5 -6
View File
@@ -1,14 +1,13 @@
sections: links:
- name: Overview - { name: Proxmox, url: "https://pve.local:8006" }
icon: gauge - { name: Router, url: "http://192.168.1.1" }
items: monitor:
- name: cpu - name: cpu
type: monitor
script: scripts/cpu.sh script: scripts/cpu.sh
refresh: 5 refresh: 5
data: percentage data: percentage
exec:
- name: greet - name: greet
type: response
script: scripts/greet.sh script: scripts/greet.sh
args: 2 args: 2
arg_types: [text, number] arg_types: [text, number]
+1
View File
@@ -0,0 +1 @@
# Second doc
+12
View File
@@ -0,0 +1,12 @@
# Test doc
Hello **world**, list:
- one
- two
`code` and [link](https://example.com)
Edited OFFLINE
Edited OFFLINE v3
+439 -90
View File
@@ -24,11 +24,10 @@ const (
type Item struct { type Item 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"`
Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` // monitor | response Data string `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` // monitor: text | percentage
Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` // monitor: text | percentage Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` // monitor: last output
Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` // monitor: last output Args int32 `protobuf:"varint,4,opt,name=args,proto3" json:"args,omitempty"` // exec: arg count
Args int32 `protobuf:"varint,5,opt,name=args,proto3" json:"args,omitempty"` // response: arg count ArgTypes []string `protobuf:"bytes,5,rep,name=arg_types,json=argTypes,proto3" json:"arg_types,omitempty"` // exec: text | number per arg
ArgTypes []string `protobuf:"bytes,6,rep,name=arg_types,json=argTypes,proto3" json:"arg_types,omitempty"` // response: text | number per arg
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -70,13 +69,6 @@ func (x *Item) GetName() string {
return "" return ""
} }
func (x *Item) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *Item) GetData() string { func (x *Item) GetData() string {
if x != nil { if x != nil {
return x.Data return x.Data
@@ -105,29 +97,28 @@ func (x *Item) GetArgTypes() []string {
return nil return nil
} }
type Section 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"`
Icon string `protobuf:"bytes,2,opt,name=icon,proto3" json:"icon,omitempty"` Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"`
Items []*Item `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Section) Reset() { func (x *Link) Reset() {
*x = Section{} *x = Link{}
mi := &file_homelab_v1_homelab_proto_msgTypes[1] mi := &file_homelab_v1_homelab_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
func (x *Section) String() string { func (x *Link) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*Section) ProtoMessage() {} func (*Link) ProtoMessage() {}
func (x *Section) ProtoReflect() protoreflect.Message { func (x *Link) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[1] mi := &file_homelab_v1_homelab_proto_msgTypes[1]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -139,52 +130,45 @@ func (x *Section) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use Section.ProtoReflect.Descriptor instead. // Deprecated: Use Link.ProtoReflect.Descriptor instead.
func (*Section) Descriptor() ([]byte, []int) { func (*Link) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{1} return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{1}
} }
func (x *Section) GetName() string { func (x *Link) GetName() string {
if x != nil { if x != nil {
return x.Name return x.Name
} }
return "" return ""
} }
func (x *Section) GetIcon() string { func (x *Link) GetUrl() string {
if x != nil { if x != nil {
return x.Icon return x.Url
} }
return "" return ""
} }
func (x *Section) GetItems() []*Item { type ListContentRequest struct {
if x != nil {
return x.Items
}
return nil
}
type ListSectionsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *ListSectionsRequest) Reset() { func (x *ListContentRequest) Reset() {
*x = ListSectionsRequest{} *x = ListContentRequest{}
mi := &file_homelab_v1_homelab_proto_msgTypes[2] mi := &file_homelab_v1_homelab_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
func (x *ListSectionsRequest) String() string { func (x *ListContentRequest) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*ListSectionsRequest) ProtoMessage() {} func (*ListContentRequest) ProtoMessage() {}
func (x *ListSectionsRequest) ProtoReflect() protoreflect.Message { func (x *ListContentRequest) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[2] mi := &file_homelab_v1_homelab_proto_msgTypes[2]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -196,32 +180,34 @@ func (x *ListSectionsRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use ListSectionsRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ListContentRequest.ProtoReflect.Descriptor instead.
func (*ListSectionsRequest) Descriptor() ([]byte, []int) { func (*ListContentRequest) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{2} return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{2}
} }
type ListSectionsResponse struct { type ListContentResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Sections []*Section `protobuf:"bytes,1,rep,name=sections,proto3" json:"sections,omitempty"` Links []*Link `protobuf:"bytes,1,rep,name=links,proto3" json:"links,omitempty"`
Monitors []*Item `protobuf:"bytes,2,rep,name=monitors,proto3" json:"monitors,omitempty"`
Exec []*Item `protobuf:"bytes,3,rep,name=exec,proto3" json:"exec,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *ListSectionsResponse) Reset() { func (x *ListContentResponse) Reset() {
*x = ListSectionsResponse{} *x = ListContentResponse{}
mi := &file_homelab_v1_homelab_proto_msgTypes[3] mi := &file_homelab_v1_homelab_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
func (x *ListSectionsResponse) String() string { func (x *ListContentResponse) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*ListSectionsResponse) ProtoMessage() {} func (*ListContentResponse) ProtoMessage() {}
func (x *ListSectionsResponse) ProtoReflect() protoreflect.Message { func (x *ListContentResponse) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[3] mi := &file_homelab_v1_homelab_proto_msgTypes[3]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -233,18 +219,348 @@ func (x *ListSectionsResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use ListSectionsResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ListContentResponse.ProtoReflect.Descriptor instead.
func (*ListSectionsResponse) Descriptor() ([]byte, []int) { func (*ListContentResponse) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{3} return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{3}
} }
func (x *ListSectionsResponse) GetSections() []*Section { func (x *ListContentResponse) GetLinks() []*Link {
if x != nil { if x != nil {
return x.Sections return x.Links
} }
return nil return nil
} }
func (x *ListContentResponse) GetMonitors() []*Item {
if x != nil {
return x.Monitors
}
return nil
}
func (x *ListContentResponse) GetExec() []*Item {
if x != nil {
return x.Exec
}
return nil
}
type DocMeta struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Hash string `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"` // sha256 hex of content
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DocMeta) Reset() {
*x = DocMeta{}
mi := &file_homelab_v1_homelab_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DocMeta) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DocMeta) ProtoMessage() {}
func (x *DocMeta) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DocMeta.ProtoReflect.Descriptor instead.
func (*DocMeta) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{4}
}
func (x *DocMeta) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *DocMeta) GetHash() string {
if x != nil {
return x.Hash
}
return ""
}
type ListDocsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListDocsRequest) Reset() {
*x = ListDocsRequest{}
mi := &file_homelab_v1_homelab_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListDocsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListDocsRequest) ProtoMessage() {}
func (x *ListDocsRequest) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListDocsRequest.ProtoReflect.Descriptor instead.
func (*ListDocsRequest) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{5}
}
type ListDocsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Docs []*DocMeta `protobuf:"bytes,1,rep,name=docs,proto3" json:"docs,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListDocsResponse) Reset() {
*x = ListDocsResponse{}
mi := &file_homelab_v1_homelab_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListDocsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListDocsResponse) ProtoMessage() {}
func (x *ListDocsResponse) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListDocsResponse.ProtoReflect.Descriptor instead.
func (*ListDocsResponse) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{6}
}
func (x *ListDocsResponse) GetDocs() []*DocMeta {
if x != nil {
return x.Docs
}
return nil
}
type GetDocRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetDocRequest) Reset() {
*x = GetDocRequest{}
mi := &file_homelab_v1_homelab_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetDocRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetDocRequest) ProtoMessage() {}
func (x *GetDocRequest) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetDocRequest.ProtoReflect.Descriptor instead.
func (*GetDocRequest) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{7}
}
func (x *GetDocRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type GetDocResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetDocResponse) Reset() {
*x = GetDocResponse{}
mi := &file_homelab_v1_homelab_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetDocResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetDocResponse) ProtoMessage() {}
func (x *GetDocResponse) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetDocResponse.ProtoReflect.Descriptor instead.
func (*GetDocResponse) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{8}
}
func (x *GetDocResponse) GetContent() string {
if x != nil {
return x.Content
}
return ""
}
type SaveDocRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SaveDocRequest) Reset() {
*x = SaveDocRequest{}
mi := &file_homelab_v1_homelab_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SaveDocRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SaveDocRequest) ProtoMessage() {}
func (x *SaveDocRequest) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SaveDocRequest.ProtoReflect.Descriptor instead.
func (*SaveDocRequest) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{9}
}
func (x *SaveDocRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *SaveDocRequest) GetContent() string {
if x != nil {
return x.Content
}
return ""
}
type SaveDocResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Hash string `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SaveDocResponse) Reset() {
*x = SaveDocResponse{}
mi := &file_homelab_v1_homelab_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SaveDocResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SaveDocResponse) ProtoMessage() {}
func (x *SaveDocResponse) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SaveDocResponse.ProtoReflect.Descriptor instead.
func (*SaveDocResponse) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{10}
}
func (x *SaveDocResponse) GetHash() string {
if x != nil {
return x.Hash
}
return ""
}
type RunScriptRequest struct { type RunScriptRequest 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"`
@@ -255,7 +571,7 @@ type RunScriptRequest struct {
func (x *RunScriptRequest) Reset() { func (x *RunScriptRequest) Reset() {
*x = RunScriptRequest{} *x = RunScriptRequest{}
mi := &file_homelab_v1_homelab_proto_msgTypes[4] mi := &file_homelab_v1_homelab_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -267,7 +583,7 @@ func (x *RunScriptRequest) String() string {
func (*RunScriptRequest) ProtoMessage() {} func (*RunScriptRequest) ProtoMessage() {}
func (x *RunScriptRequest) ProtoReflect() protoreflect.Message { func (x *RunScriptRequest) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[4] mi := &file_homelab_v1_homelab_proto_msgTypes[11]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -280,7 +596,7 @@ func (x *RunScriptRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RunScriptRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RunScriptRequest.ProtoReflect.Descriptor instead.
func (*RunScriptRequest) Descriptor() ([]byte, []int) { func (*RunScriptRequest) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{4} return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{11}
} }
func (x *RunScriptRequest) GetName() string { func (x *RunScriptRequest) GetName() string {
@@ -306,7 +622,7 @@ type RunScriptResponse struct {
func (x *RunScriptResponse) Reset() { func (x *RunScriptResponse) Reset() {
*x = RunScriptResponse{} *x = RunScriptResponse{}
mi := &file_homelab_v1_homelab_proto_msgTypes[5] mi := &file_homelab_v1_homelab_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -318,7 +634,7 @@ func (x *RunScriptResponse) String() string {
func (*RunScriptResponse) ProtoMessage() {} func (*RunScriptResponse) ProtoMessage() {}
func (x *RunScriptResponse) ProtoReflect() protoreflect.Message { func (x *RunScriptResponse) ProtoReflect() protoreflect.Message {
mi := &file_homelab_v1_homelab_proto_msgTypes[5] mi := &file_homelab_v1_homelab_proto_msgTypes[12]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -331,7 +647,7 @@ func (x *RunScriptResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RunScriptResponse.ProtoReflect.Descriptor instead. // Deprecated: Use RunScriptResponse.ProtoReflect.Descriptor instead.
func (*RunScriptResponse) Descriptor() ([]byte, []int) { func (*RunScriptResponse) Descriptor() ([]byte, []int) {
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{5} return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{12}
} }
func (x *RunScriptResponse) GetOutput() string { func (x *RunScriptResponse) GetOutput() string {
@@ -346,28 +662,46 @@ var File_homelab_v1_homelab_proto protoreflect.FileDescriptor
const file_homelab_v1_homelab_proto_rawDesc = "" + const file_homelab_v1_homelab_proto_rawDesc = "" +
"\n" + "\n" +
"\x18homelab/v1/homelab.proto\x12\n" + "\x18homelab/v1/homelab.proto\x12\n" +
"homelab.v1\"\x89\x01\n" + "homelab.v1\"u\n" +
"\x04Item\x12\x12\n" + "\x04Item\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
"\x04type\x18\x02 \x01(\tR\x04type\x12\x12\n" + "\x04data\x18\x02 \x01(\tR\x04data\x12\x14\n" +
"\x04data\x18\x03 \x01(\tR\x04data\x12\x14\n" + "\x05value\x18\x03 \x01(\tR\x05value\x12\x12\n" +
"\x05value\x18\x04 \x01(\tR\x05value\x12\x12\n" + "\x04args\x18\x04 \x01(\x05R\x04args\x12\x1b\n" +
"\x04args\x18\x05 \x01(\x05R\x04args\x12\x1b\n" + "\targ_types\x18\x05 \x03(\tR\bargTypes\",\n" +
"\targ_types\x18\x06 \x03(\tR\bargTypes\"Y\n" + "\x04Link\x12\x12\n" +
"\aSection\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
"\x03url\x18\x02 \x01(\tR\x03url\"\x14\n" +
"\x12ListContentRequest\"\x91\x01\n" +
"\x13ListContentResponse\x12&\n" +
"\x05links\x18\x01 \x03(\v2\x10.homelab.v1.LinkR\x05links\x12,\n" +
"\bmonitors\x18\x02 \x03(\v2\x10.homelab.v1.ItemR\bmonitors\x12$\n" +
"\x04exec\x18\x03 \x03(\v2\x10.homelab.v1.ItemR\x04exec\"1\n" +
"\aDocMeta\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
"\x04icon\x18\x02 \x01(\tR\x04icon\x12&\n" + "\x04hash\x18\x02 \x01(\tR\x04hash\"\x11\n" +
"\x05items\x18\x03 \x03(\v2\x10.homelab.v1.ItemR\x05items\"\x15\n" + "\x0fListDocsRequest\";\n" +
"\x13ListSectionsRequest\"G\n" + "\x10ListDocsResponse\x12'\n" +
"\x14ListSectionsResponse\x12/\n" + "\x04docs\x18\x01 \x03(\v2\x13.homelab.v1.DocMetaR\x04docs\"#\n" +
"\bsections\x18\x01 \x03(\v2\x13.homelab.v1.SectionR\bsections\":\n" + "\rGetDocRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"*\n" +
"\x0eGetDocResponse\x12\x18\n" +
"\acontent\x18\x01 \x01(\tR\acontent\">\n" +
"\x0eSaveDocRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" +
"\acontent\x18\x02 \x01(\tR\acontent\"%\n" +
"\x0fSaveDocResponse\x12\x12\n" +
"\x04hash\x18\x01 \x01(\tR\x04hash\":\n" +
"\x10RunScriptRequest\x12\x12\n" + "\x10RunScriptRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
"\x04args\x18\x02 \x03(\tR\x04args\"+\n" + "\x04args\x18\x02 \x03(\tR\x04args\"+\n" +
"\x11RunScriptResponse\x12\x16\n" + "\x11RunScriptResponse\x12\x16\n" +
"\x06output\x18\x01 \x01(\tR\x06output2\xad\x01\n" + "\x06output\x18\x01 \x01(\tR\x06output2\xf6\x02\n" +
"\x0eHomelabService\x12Q\n" + "\x0eHomelabService\x12N\n" +
"\fListSections\x12\x1f.homelab.v1.ListSectionsRequest\x1a .homelab.v1.ListSectionsResponse\x12H\n" + "\vListContent\x12\x1e.homelab.v1.ListContentRequest\x1a\x1f.homelab.v1.ListContentResponse\x12E\n" +
"\bListDocs\x12\x1b.homelab.v1.ListDocsRequest\x1a\x1c.homelab.v1.ListDocsResponse\x12?\n" +
"\x06GetDoc\x12\x19.homelab.v1.GetDocRequest\x1a\x1a.homelab.v1.GetDocResponse\x12B\n" +
"\aSaveDoc\x12\x1a.homelab.v1.SaveDocRequest\x1a\x1b.homelab.v1.SaveDocResponse\x12H\n" +
"\tRunScript\x12\x1c.homelab.v1.RunScriptRequest\x1a\x1d.homelab.v1.RunScriptResponseB*Z(pwa-homelab-mon/gen/homelab/v1;homelabv1b\x06proto3" "\tRunScript\x12\x1c.homelab.v1.RunScriptRequest\x1a\x1d.homelab.v1.RunScriptResponseB*Z(pwa-homelab-mon/gen/homelab/v1;homelabv1b\x06proto3"
var ( var (
@@ -382,27 +716,42 @@ func file_homelab_v1_homelab_proto_rawDescGZIP() []byte {
return file_homelab_v1_homelab_proto_rawDescData return file_homelab_v1_homelab_proto_rawDescData
} }
var file_homelab_v1_homelab_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_homelab_v1_homelab_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
var file_homelab_v1_homelab_proto_goTypes = []any{ var file_homelab_v1_homelab_proto_goTypes = []any{
(*Item)(nil), // 0: homelab.v1.Item (*Item)(nil), // 0: homelab.v1.Item
(*Section)(nil), // 1: homelab.v1.Section (*Link)(nil), // 1: homelab.v1.Link
(*ListSectionsRequest)(nil), // 2: homelab.v1.ListSectionsRequest (*ListContentRequest)(nil), // 2: homelab.v1.ListContentRequest
(*ListSectionsResponse)(nil), // 3: homelab.v1.ListSectionsResponse (*ListContentResponse)(nil), // 3: homelab.v1.ListContentResponse
(*RunScriptRequest)(nil), // 4: homelab.v1.RunScriptRequest (*DocMeta)(nil), // 4: homelab.v1.DocMeta
(*RunScriptResponse)(nil), // 5: homelab.v1.RunScriptResponse (*ListDocsRequest)(nil), // 5: homelab.v1.ListDocsRequest
(*ListDocsResponse)(nil), // 6: homelab.v1.ListDocsResponse
(*GetDocRequest)(nil), // 7: homelab.v1.GetDocRequest
(*GetDocResponse)(nil), // 8: homelab.v1.GetDocResponse
(*SaveDocRequest)(nil), // 9: homelab.v1.SaveDocRequest
(*SaveDocResponse)(nil), // 10: homelab.v1.SaveDocResponse
(*RunScriptRequest)(nil), // 11: homelab.v1.RunScriptRequest
(*RunScriptResponse)(nil), // 12: homelab.v1.RunScriptResponse
} }
var file_homelab_v1_homelab_proto_depIdxs = []int32{ var file_homelab_v1_homelab_proto_depIdxs = []int32{
0, // 0: homelab.v1.Section.items:type_name -> homelab.v1.Item 1, // 0: homelab.v1.ListContentResponse.links:type_name -> homelab.v1.Link
1, // 1: homelab.v1.ListSectionsResponse.sections:type_name -> homelab.v1.Section 0, // 1: homelab.v1.ListContentResponse.monitors:type_name -> homelab.v1.Item
2, // 2: homelab.v1.HomelabService.ListSections:input_type -> homelab.v1.ListSectionsRequest 0, // 2: homelab.v1.ListContentResponse.exec:type_name -> homelab.v1.Item
4, // 3: homelab.v1.HomelabService.RunScript:input_type -> homelab.v1.RunScriptRequest 4, // 3: homelab.v1.ListDocsResponse.docs:type_name -> homelab.v1.DocMeta
3, // 4: homelab.v1.HomelabService.ListSections:output_type -> homelab.v1.ListSectionsResponse 2, // 4: homelab.v1.HomelabService.ListContent:input_type -> homelab.v1.ListContentRequest
5, // 5: homelab.v1.HomelabService.RunScript:output_type -> homelab.v1.RunScriptResponse 5, // 5: homelab.v1.HomelabService.ListDocs:input_type -> homelab.v1.ListDocsRequest
4, // [4:6] is the sub-list for method output_type 7, // 6: homelab.v1.HomelabService.GetDoc:input_type -> homelab.v1.GetDocRequest
2, // [2:4] is the sub-list for method input_type 9, // 7: homelab.v1.HomelabService.SaveDoc:input_type -> homelab.v1.SaveDocRequest
2, // [2:2] is the sub-list for extension type_name 11, // 8: homelab.v1.HomelabService.RunScript:input_type -> homelab.v1.RunScriptRequest
2, // [2:2] is the sub-list for extension extendee 3, // 9: homelab.v1.HomelabService.ListContent:output_type -> homelab.v1.ListContentResponse
0, // [0:2] is the sub-list for field type_name 6, // 10: homelab.v1.HomelabService.ListDocs:output_type -> homelab.v1.ListDocsResponse
8, // 11: homelab.v1.HomelabService.GetDoc:output_type -> homelab.v1.GetDocResponse
10, // 12: homelab.v1.HomelabService.SaveDoc:output_type -> homelab.v1.SaveDocResponse
12, // 13: homelab.v1.HomelabService.RunScript:output_type -> homelab.v1.RunScriptResponse
9, // [9:14] is the sub-list for method output_type
4, // [4:9] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
} }
func init() { file_homelab_v1_homelab_proto_init() } func init() { file_homelab_v1_homelab_proto_init() }
@@ -416,7 +765,7 @@ func file_homelab_v1_homelab_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_homelab_v1_homelab_proto_rawDesc), len(file_homelab_v1_homelab_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_homelab_v1_homelab_proto_rawDesc), len(file_homelab_v1_homelab_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 6, NumMessages: 13,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
@@ -33,9 +33,15 @@ const (
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a // reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period. // period.
const ( const (
// HomelabServiceListSectionsProcedure is the fully-qualified name of the HomelabService's // HomelabServiceListContentProcedure is the fully-qualified name of the HomelabService's
// ListSections RPC. // ListContent RPC.
HomelabServiceListSectionsProcedure = "/homelab.v1.HomelabService/ListSections" HomelabServiceListContentProcedure = "/homelab.v1.HomelabService/ListContent"
// HomelabServiceListDocsProcedure is the fully-qualified name of the HomelabService's ListDocs RPC.
HomelabServiceListDocsProcedure = "/homelab.v1.HomelabService/ListDocs"
// HomelabServiceGetDocProcedure is the fully-qualified name of the HomelabService's GetDoc RPC.
HomelabServiceGetDocProcedure = "/homelab.v1.HomelabService/GetDoc"
// HomelabServiceSaveDocProcedure is the fully-qualified name of the HomelabService's SaveDoc RPC.
HomelabServiceSaveDocProcedure = "/homelab.v1.HomelabService/SaveDoc"
// HomelabServiceRunScriptProcedure is the fully-qualified name of the HomelabService's RunScript // HomelabServiceRunScriptProcedure is the fully-qualified name of the HomelabService's RunScript
// RPC. // RPC.
HomelabServiceRunScriptProcedure = "/homelab.v1.HomelabService/RunScript" HomelabServiceRunScriptProcedure = "/homelab.v1.HomelabService/RunScript"
@@ -43,7 +49,10 @@ const (
// HomelabServiceClient is a client for the homelab.v1.HomelabService service. // HomelabServiceClient is a client for the homelab.v1.HomelabService service.
type HomelabServiceClient interface { type HomelabServiceClient interface {
ListSections(context.Context, *connect.Request[v1.ListSectionsRequest]) (*connect.Response[v1.ListSectionsResponse], error) ListContent(context.Context, *connect.Request[v1.ListContentRequest]) (*connect.Response[v1.ListContentResponse], error)
ListDocs(context.Context, *connect.Request[v1.ListDocsRequest]) (*connect.Response[v1.ListDocsResponse], error)
GetDoc(context.Context, *connect.Request[v1.GetDocRequest]) (*connect.Response[v1.GetDocResponse], error)
SaveDoc(context.Context, *connect.Request[v1.SaveDocRequest]) (*connect.Response[v1.SaveDocResponse], error)
RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error) RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error)
} }
@@ -58,10 +67,28 @@ func NewHomelabServiceClient(httpClient connect.HTTPClient, baseURL string, opts
baseURL = strings.TrimRight(baseURL, "/") baseURL = strings.TrimRight(baseURL, "/")
homelabServiceMethods := v1.File_homelab_v1_homelab_proto.Services().ByName("HomelabService").Methods() homelabServiceMethods := v1.File_homelab_v1_homelab_proto.Services().ByName("HomelabService").Methods()
return &homelabServiceClient{ return &homelabServiceClient{
listSections: connect.NewClient[v1.ListSectionsRequest, v1.ListSectionsResponse]( listContent: connect.NewClient[v1.ListContentRequest, v1.ListContentResponse](
httpClient, httpClient,
baseURL+HomelabServiceListSectionsProcedure, baseURL+HomelabServiceListContentProcedure,
connect.WithSchema(homelabServiceMethods.ByName("ListSections")), connect.WithSchema(homelabServiceMethods.ByName("ListContent")),
connect.WithClientOptions(opts...),
),
listDocs: connect.NewClient[v1.ListDocsRequest, v1.ListDocsResponse](
httpClient,
baseURL+HomelabServiceListDocsProcedure,
connect.WithSchema(homelabServiceMethods.ByName("ListDocs")),
connect.WithClientOptions(opts...),
),
getDoc: connect.NewClient[v1.GetDocRequest, v1.GetDocResponse](
httpClient,
baseURL+HomelabServiceGetDocProcedure,
connect.WithSchema(homelabServiceMethods.ByName("GetDoc")),
connect.WithClientOptions(opts...),
),
saveDoc: connect.NewClient[v1.SaveDocRequest, v1.SaveDocResponse](
httpClient,
baseURL+HomelabServiceSaveDocProcedure,
connect.WithSchema(homelabServiceMethods.ByName("SaveDoc")),
connect.WithClientOptions(opts...), connect.WithClientOptions(opts...),
), ),
runScript: connect.NewClient[v1.RunScriptRequest, v1.RunScriptResponse]( runScript: connect.NewClient[v1.RunScriptRequest, v1.RunScriptResponse](
@@ -75,13 +102,31 @@ func NewHomelabServiceClient(httpClient connect.HTTPClient, baseURL string, opts
// homelabServiceClient implements HomelabServiceClient. // homelabServiceClient implements HomelabServiceClient.
type homelabServiceClient struct { type homelabServiceClient struct {
listSections *connect.Client[v1.ListSectionsRequest, v1.ListSectionsResponse] listContent *connect.Client[v1.ListContentRequest, v1.ListContentResponse]
listDocs *connect.Client[v1.ListDocsRequest, v1.ListDocsResponse]
getDoc *connect.Client[v1.GetDocRequest, v1.GetDocResponse]
saveDoc *connect.Client[v1.SaveDocRequest, v1.SaveDocResponse]
runScript *connect.Client[v1.RunScriptRequest, v1.RunScriptResponse] runScript *connect.Client[v1.RunScriptRequest, v1.RunScriptResponse]
} }
// ListSections calls homelab.v1.HomelabService.ListSections. // ListContent calls homelab.v1.HomelabService.ListContent.
func (c *homelabServiceClient) ListSections(ctx context.Context, req *connect.Request[v1.ListSectionsRequest]) (*connect.Response[v1.ListSectionsResponse], error) { func (c *homelabServiceClient) ListContent(ctx context.Context, req *connect.Request[v1.ListContentRequest]) (*connect.Response[v1.ListContentResponse], error) {
return c.listSections.CallUnary(ctx, req) return c.listContent.CallUnary(ctx, req)
}
// ListDocs calls homelab.v1.HomelabService.ListDocs.
func (c *homelabServiceClient) ListDocs(ctx context.Context, req *connect.Request[v1.ListDocsRequest]) (*connect.Response[v1.ListDocsResponse], error) {
return c.listDocs.CallUnary(ctx, req)
}
// GetDoc calls homelab.v1.HomelabService.GetDoc.
func (c *homelabServiceClient) GetDoc(ctx context.Context, req *connect.Request[v1.GetDocRequest]) (*connect.Response[v1.GetDocResponse], error) {
return c.getDoc.CallUnary(ctx, req)
}
// SaveDoc calls homelab.v1.HomelabService.SaveDoc.
func (c *homelabServiceClient) SaveDoc(ctx context.Context, req *connect.Request[v1.SaveDocRequest]) (*connect.Response[v1.SaveDocResponse], error) {
return c.saveDoc.CallUnary(ctx, req)
} }
// RunScript calls homelab.v1.HomelabService.RunScript. // RunScript calls homelab.v1.HomelabService.RunScript.
@@ -91,7 +136,10 @@ func (c *homelabServiceClient) RunScript(ctx context.Context, req *connect.Reque
// HomelabServiceHandler is an implementation of the homelab.v1.HomelabService service. // HomelabServiceHandler is an implementation of the homelab.v1.HomelabService service.
type HomelabServiceHandler interface { type HomelabServiceHandler interface {
ListSections(context.Context, *connect.Request[v1.ListSectionsRequest]) (*connect.Response[v1.ListSectionsResponse], error) ListContent(context.Context, *connect.Request[v1.ListContentRequest]) (*connect.Response[v1.ListContentResponse], error)
ListDocs(context.Context, *connect.Request[v1.ListDocsRequest]) (*connect.Response[v1.ListDocsResponse], error)
GetDoc(context.Context, *connect.Request[v1.GetDocRequest]) (*connect.Response[v1.GetDocResponse], error)
SaveDoc(context.Context, *connect.Request[v1.SaveDocRequest]) (*connect.Response[v1.SaveDocResponse], error)
RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error) RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error)
} }
@@ -102,10 +150,28 @@ type HomelabServiceHandler interface {
// and JSON codecs. They also support gzip compression. // and JSON codecs. They also support gzip compression.
func NewHomelabServiceHandler(svc HomelabServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { func NewHomelabServiceHandler(svc HomelabServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
homelabServiceMethods := v1.File_homelab_v1_homelab_proto.Services().ByName("HomelabService").Methods() homelabServiceMethods := v1.File_homelab_v1_homelab_proto.Services().ByName("HomelabService").Methods()
homelabServiceListSectionsHandler := connect.NewUnaryHandler( homelabServiceListContentHandler := connect.NewUnaryHandler(
HomelabServiceListSectionsProcedure, HomelabServiceListContentProcedure,
svc.ListSections, svc.ListContent,
connect.WithSchema(homelabServiceMethods.ByName("ListSections")), connect.WithSchema(homelabServiceMethods.ByName("ListContent")),
connect.WithHandlerOptions(opts...),
)
homelabServiceListDocsHandler := connect.NewUnaryHandler(
HomelabServiceListDocsProcedure,
svc.ListDocs,
connect.WithSchema(homelabServiceMethods.ByName("ListDocs")),
connect.WithHandlerOptions(opts...),
)
homelabServiceGetDocHandler := connect.NewUnaryHandler(
HomelabServiceGetDocProcedure,
svc.GetDoc,
connect.WithSchema(homelabServiceMethods.ByName("GetDoc")),
connect.WithHandlerOptions(opts...),
)
homelabServiceSaveDocHandler := connect.NewUnaryHandler(
HomelabServiceSaveDocProcedure,
svc.SaveDoc,
connect.WithSchema(homelabServiceMethods.ByName("SaveDoc")),
connect.WithHandlerOptions(opts...), connect.WithHandlerOptions(opts...),
) )
homelabServiceRunScriptHandler := connect.NewUnaryHandler( homelabServiceRunScriptHandler := connect.NewUnaryHandler(
@@ -116,8 +182,14 @@ func NewHomelabServiceHandler(svc HomelabServiceHandler, opts ...connect.Handler
) )
return "/homelab.v1.HomelabService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return "/homelab.v1.HomelabService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
case HomelabServiceListSectionsProcedure: case HomelabServiceListContentProcedure:
homelabServiceListSectionsHandler.ServeHTTP(w, r) homelabServiceListContentHandler.ServeHTTP(w, r)
case HomelabServiceListDocsProcedure:
homelabServiceListDocsHandler.ServeHTTP(w, r)
case HomelabServiceGetDocProcedure:
homelabServiceGetDocHandler.ServeHTTP(w, r)
case HomelabServiceSaveDocProcedure:
homelabServiceSaveDocHandler.ServeHTTP(w, r)
case HomelabServiceRunScriptProcedure: case HomelabServiceRunScriptProcedure:
homelabServiceRunScriptHandler.ServeHTTP(w, r) homelabServiceRunScriptHandler.ServeHTTP(w, r)
default: default:
@@ -129,8 +201,20 @@ func NewHomelabServiceHandler(svc HomelabServiceHandler, opts ...connect.Handler
// UnimplementedHomelabServiceHandler returns CodeUnimplemented from all methods. // UnimplementedHomelabServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedHomelabServiceHandler struct{} type UnimplementedHomelabServiceHandler struct{}
func (UnimplementedHomelabServiceHandler) ListSections(context.Context, *connect.Request[v1.ListSectionsRequest]) (*connect.Response[v1.ListSectionsResponse], error) { func (UnimplementedHomelabServiceHandler) ListContent(context.Context, *connect.Request[v1.ListContentRequest]) (*connect.Response[v1.ListContentResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("homelab.v1.HomelabService.ListSections is not implemented")) return nil, connect.NewError(connect.CodeUnimplemented, errors.New("homelab.v1.HomelabService.ListContent is not implemented"))
}
func (UnimplementedHomelabServiceHandler) ListDocs(context.Context, *connect.Request[v1.ListDocsRequest]) (*connect.Response[v1.ListDocsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("homelab.v1.HomelabService.ListDocs is not implemented"))
}
func (UnimplementedHomelabServiceHandler) GetDoc(context.Context, *connect.Request[v1.GetDocRequest]) (*connect.Response[v1.GetDocResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("homelab.v1.HomelabService.GetDoc is not implemented"))
}
func (UnimplementedHomelabServiceHandler) SaveDoc(context.Context, *connect.Request[v1.SaveDocRequest]) (*connect.Response[v1.SaveDocResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("homelab.v1.HomelabService.SaveDoc is not implemented"))
} }
func (UnimplementedHomelabServiceHandler) RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error) { func (UnimplementedHomelabServiceHandler) RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error) {
+44 -16
View File
@@ -6,22 +6,55 @@ option go_package = "pwa-homelab-mon/gen/homelab/v1;homelabv1";
message Item { message Item {
string name = 1; string name = 1;
string type = 2; // monitor | response string data = 2; // monitor: text | percentage
string data = 3; // monitor: text | percentage string value = 3; // monitor: last output
string value = 4; // monitor: last output int32 args = 4; // exec: arg count
int32 args = 5; // response: arg count repeated string arg_types = 5; // exec: text | number per arg
repeated string arg_types = 6; // response: text | number per arg
} }
message Section { message Link {
string name = 1; string name = 1;
string icon = 2; string url = 2;
repeated Item items = 3;
} }
message ListSectionsRequest {} message ListContentRequest {}
message ListSectionsResponse { message ListContentResponse {
repeated Section sections = 1; repeated Link links = 1;
repeated Item monitors = 2;
repeated Item exec = 3;
}
message DocMeta {
string name = 1;
string hash = 2; // sha256 hex of content
}
message ListDocsRequest {}
message ListDocsResponse {
repeated DocMeta docs = 1;
}
message GetDocRequest {
string name = 1;
}
message GetDocResponse {
string content = 1;
}
message SaveDocRequest {
string name = 1;
string content = 2;
}
message SaveDocResponse {
string hash = 1;
}
service HomelabService {
rpc ListContent(ListContentRequest) returns (ListContentResponse);
rpc ListDocs(ListDocsRequest) returns (ListDocsResponse);
rpc GetDoc(GetDocRequest) returns (GetDocResponse);
rpc SaveDoc(SaveDocRequest) returns (SaveDocResponse);
rpc RunScript(RunScriptRequest) returns (RunScriptResponse);
} }
message RunScriptRequest { message RunScriptRequest {
@@ -31,8 +64,3 @@ message RunScriptRequest {
message RunScriptResponse { message RunScriptResponse {
string output = 1; string output = 1;
} }
service HomelabService {
rpc ListSections(ListSectionsRequest) returns (ListSectionsResponse);
rpc RunScript(RunScriptRequest) returns (RunScriptResponse);
}
+115 -58
View File
@@ -1,14 +1,15 @@
package main package main
//test
import ( import (
"context" "context"
"crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -25,23 +26,22 @@ import (
type Item struct { type Item struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Type string `yaml:"type"` // monitor | response
Script string `yaml:"script"` Script string `yaml:"script"`
Refresh int `yaml:"refresh"` // monitor: seconds Refresh int `yaml:"refresh"` // monitor: seconds
Data string `yaml:"data"` // monitor: text | percentage Data string `yaml:"data"` // monitor: text | percentage
Args int `yaml:"args"` // response: arg count Args int `yaml:"args"` // exec: arg count
ArgTypes []string `yaml:"arg_types"` // response: text | number per arg ArgTypes []string `yaml:"arg_types"` // exec: text | number per arg
} }
// Section is a full view on the web page: a list of items plus sidebar metadata. type Link struct {
type Section struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Icon string `yaml:"icon"` URL string `yaml:"url"`
Items []Item `yaml:"items"`
} }
type Config struct { type Config struct {
Sections []Section `yaml:"sections"` Links []Link `yaml:"links"`
Monitor []Item `yaml:"monitor"`
Exec []Item `yaml:"exec"`
} }
func loadConfig(path string) (*Config, error) { func loadConfig(path string) (*Config, error) {
@@ -56,27 +56,21 @@ func loadConfig(path string) (*Config, error) {
if err := dec.Decode(&cfg); err != nil { if err := dec.Decode(&cfg); err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err) return nil, fmt.Errorf("parse %s: %w", path, err)
} }
for _, sec := range cfg.Sections { for _, it := range cfg.Monitor {
for _, it := range sec.Items {
switch it.Type {
case "monitor":
if it.Refresh <= 0 { if it.Refresh <= 0 {
return nil, fmt.Errorf("item %q: refresh must be > 0", it.Name) return nil, fmt.Errorf("monitor %q: refresh must be > 0", it.Name)
} }
if it.Data != "text" && it.Data != "percentage" { if it.Data != "text" && it.Data != "percentage" {
return nil, fmt.Errorf("item %q: data must be text|percentage", it.Name) return nil, fmt.Errorf("monitor %q: data must be text|percentage", it.Name)
} }
case "response": }
for _, it := range cfg.Exec {
if len(it.ArgTypes) != it.Args { if len(it.ArgTypes) != it.Args {
return nil, fmt.Errorf("item %q: arg_types must have args entries", it.Name) return nil, fmt.Errorf("exec %q: arg_types must have args entries", it.Name)
} }
for _, t := range it.ArgTypes { for _, t := range it.ArgTypes {
if t != "text" && t != "number" { if t != "text" && t != "number" {
return nil, fmt.Errorf("item %q: arg type must be text|number", it.Name) return nil, fmt.Errorf("exec %q: arg type must be text|number", it.Name)
}
}
default:
return nil, fmt.Errorf("item %q: unknown type %q", it.Name, it.Type)
} }
} }
} }
@@ -87,8 +81,9 @@ func loadConfig(path string) (*Config, error) {
type server struct { type server struct {
cfg *Config cfg *Config
scripts map[string]*Item // by item name, read-only after start exec map[string]*Item // by item name, read-only after start
monVals sync.Map // item name -> string (last monitor output) monVals sync.Map // monitor item name -> string (last output)
docsDir string
} }
const execTimeout = 10 * time.Second const execTimeout = 10 * time.Second
@@ -104,12 +99,8 @@ func runScript(s *Item, args ...string) (string, error) {
} }
func (s *server) startMonitors() { func (s *server) startMonitors() {
for _, sec := range s.cfg.Sections { for i := range s.cfg.Monitor {
for _, it := range sec.Items { sc := &s.cfg.Monitor[i]
if it.Type != "monitor" {
continue
}
sc := it
log.Printf("monitor started: %s (%s) every %ds", sc.Name, sc.Script, sc.Refresh) log.Printf("monitor started: %s (%s) every %ds", sc.Name, sc.Script, sc.Refresh)
go func(sc *Item) { go func(sc *Item) {
for { for {
@@ -123,37 +114,98 @@ func (s *server) startMonitors() {
s.monVals.Store(sc.Name, v) s.monVals.Store(sc.Name, v)
time.Sleep(time.Duration(sc.Refresh) * time.Second) time.Sleep(time.Duration(sc.Refresh) * time.Second)
} }
}(&sc) }(sc)
}
} }
} }
func toProto(it *Item, val string) *homelabv1.Item { // --- docs ---
p := &homelabv1.Item{Name: it.Name, Type: it.Type, Data: it.Data, Args: int32(it.Args), ArgTypes: it.ArgTypes}
if it.Type == "monitor" { func docHash(b []byte) string {
p.Value = val h := sha256.Sum256(b)
} return hex.EncodeToString(h[:])
return p
} }
func (s *server) ListSections(context.Context, *connect.Request[homelabv1.ListSectionsRequest]) (*connect.Response[homelabv1.ListSectionsResponse], error) { // docPath resolves a doc name safely: plain file name only, no separators, always inside docsDir.
resp := &homelabv1.ListSectionsResponse{} func (s *server) docPath(name string) (string, error) {
for _, sec := range s.cfg.Sections { if filepath.Base(name) != name || filepath.Ext(name) != ".md" {
ps := &homelabv1.Section{Name: sec.Name, Icon: sec.Icon} return "", fmt.Errorf("bad doc name %q", name)
for _, it := range sec.Items { }
p := filepath.Join(s.docsDir, name)
if filepath.Dir(p) != s.docsDir {
return "", fmt.Errorf("bad doc name %q", name)
}
return p, nil
}
func (s *server) ListDocs(context.Context, *connect.Request[homelabv1.ListDocsRequest]) (*connect.Response[homelabv1.ListDocsResponse], error) {
resp := &homelabv1.ListDocsResponse{}
entries, err := os.ReadDir(s.docsDir)
if err != nil {
return connect.NewResponse(resp), nil // no docs dir = no docs
}
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
continue
}
b, err := os.ReadFile(filepath.Join(s.docsDir, e.Name()))
if err != nil {
continue
}
resp.Docs = append(resp.Docs, &homelabv1.DocMeta{Name: e.Name(), Hash: docHash(b)})
}
return connect.NewResponse(resp), nil
}
func (s *server) GetDoc(_ context.Context, req *connect.Request[homelabv1.GetDocRequest]) (*connect.Response[homelabv1.GetDocResponse], error) {
p, err := s.docPath(req.Msg.GetName())
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
b, err := os.ReadFile(p)
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return connect.NewResponse(&homelabv1.GetDocResponse{Content: string(b)}), nil
}
func (s *server) SaveDoc(_ context.Context, req *connect.Request[homelabv1.SaveDocRequest]) (*connect.Response[homelabv1.SaveDocResponse], error) {
p, err := s.docPath(req.Msg.GetName())
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
content := []byte(req.Msg.GetContent())
// ponytail: client authority — last write wins, no base-hash check; add optimistic locking if concurrent edits appear
if err := os.WriteFile(p, content, 0o644); err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
log.Printf("doc saved: %s (%d bytes)", filepath.Base(p), len(content))
return connect.NewResponse(&homelabv1.SaveDocResponse{Hash: docHash(content)}), nil
}
// --- content RPC ---
func (s *server) ListContent(context.Context, *connect.Request[homelabv1.ListContentRequest]) (*connect.Response[homelabv1.ListContentResponse], error) {
resp := &homelabv1.ListContentResponse{}
for _, l := range s.cfg.Links {
resp.Links = append(resp.Links, &homelabv1.Link{Name: l.Name, Url: l.URL})
}
for i := range s.cfg.Monitor {
it := &s.cfg.Monitor[i]
val, _ := s.monVals.Load(it.Name) val, _ := s.monVals.Load(it.Name)
v, _ := val.(string) v, _ := val.(string)
ps.Items = append(ps.Items, toProto(&it, v)) resp.Monitors = append(resp.Monitors, &homelabv1.Item{Name: it.Name, Data: it.Data, Value: v})
} }
resp.Sections = append(resp.Sections, ps) for i := range s.cfg.Exec {
it := &s.cfg.Exec[i]
resp.Exec = append(resp.Exec, &homelabv1.Item{Name: it.Name, Args: int32(it.Args), ArgTypes: it.ArgTypes})
} }
return connect.NewResponse(resp), nil return connect.NewResponse(resp), nil
} }
func (s *server) RunScript(_ context.Context, req *connect.Request[homelabv1.RunScriptRequest]) (*connect.Response[homelabv1.RunScriptResponse], error) { func (s *server) RunScript(_ context.Context, req *connect.Request[homelabv1.RunScriptRequest]) (*connect.Response[homelabv1.RunScriptResponse], error) {
it, ok := s.scripts[req.Msg.GetName()] it, ok := s.exec[req.Msg.GetName()]
if !ok || it.Type != "response" { if !ok {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("no response script %q", req.Msg.GetName())) return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("no exec item %q", req.Msg.GetName()))
} }
if len(req.Msg.GetArgs()) != it.Args { if len(req.Msg.GetArgs()) != it.Args {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("%q takes %d args, got %d", it.Name, it.Args, len(req.Msg.GetArgs()))) return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("%q takes %d args, got %d", it.Name, it.Args, len(req.Msg.GetArgs())))
@@ -180,22 +232,26 @@ func main() {
os.Exit(1) os.Exit(1)
} }
svc := &server{cfg: cfg, scripts: map[string]*Item{}} docsDir := "docs"
for _, sec := range cfg.Sections { if err := os.MkdirAll(docsDir, 0o755); err != nil {
for i := range sec.Items { fmt.Fprintln(os.Stderr, "docs:", err)
svc.scripts[sec.Items[i].Name] = &sec.Items[i] os.Exit(1)
} }
svc := &server{cfg: cfg, exec: map[string]*Item{}, docsDir: docsDir}
for i := range cfg.Exec {
svc.exec[cfg.Exec[i].Name] = &cfg.Exec[i]
} }
svc.startMonitors() svc.startMonitors()
log.Printf("config %s: %d sections, %d items", cfgPath, len(cfg.Sections), len(svc.scripts)) log.Printf("config %s: %d links, %d monitors, %d exec", cfgPath, len(cfg.Links), len(cfg.Monitor), len(cfg.Exec))
e := echo.New() e := echo.New()
e.HideBanner = true e.HideBanner = true
// HTTP request log, except the ListSections heartbeat (every 3s per client — pure noise). // HTTP request log, except the ListContent heartbeat (every 3s per client — pure noise).
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Skipper: func(c echo.Context) bool { Skipper: func(c echo.Context) bool {
return c.Request().URL.Path == "/homelab.v1.HomelabService/ListSections" return c.Request().URL.Path == "/homelab.v1.HomelabService/ListContent"
}, },
})) }))
@@ -204,10 +260,11 @@ func main() {
mux.Handle(homelabv1connect.NewHomelabServiceHandler(svc)) mux.Handle(homelabv1connect.NewHomelabServiceHandler(svc))
e.Any("/homelab.v1.HomelabService/*", echo.WrapHandler(mux)) e.Any("/homelab.v1.HomelabService/*", echo.WrapHandler(mux))
// sw.js must never be cached, otherwise iOS serves a stale service worker. // sw.js and index.html must never be cached: stale index = stale asset references = iOS never updates.
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
if c.Request().URL.Path == "/sw.js" || c.Request().URL.Path == "/manifest.webmanifest" { p := c.Request().URL.Path
if p == "/sw.js" || p == "/manifest.webmanifest" || p == "/index.html" || p == "/" {
c.Response().Header().Set("Cache-Control", "no-cache") c.Response().Header().Set("Cache-Control", "no-cache")
} }
return next(c) return next(c)
+1
View File
@@ -15,6 +15,7 @@
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"daisyui": "^5.7.22", "daisyui": "^5.7.22",
"lucide-preact": "^1.35.0", "lucide-preact": "^1.35.0",
"marked": "^18.0.11",
"preact": "^10.29.8", "preact": "^10.29.8",
"tailwindcss": "^4.3.3" "tailwindcss": "^4.3.3"
}, },
+10
View File
@@ -26,6 +26,9 @@ importers:
lucide-preact: lucide-preact:
specifier: ^1.35.0 specifier: ^1.35.0
version: 1.35.0(preact@10.29.8) version: 1.35.0(preact@10.29.8)
marked:
specifier: ^18.0.11
version: 18.0.11
preact: preact:
specifier: ^10.29.8 specifier: ^10.29.8
version: 10.29.8 version: 10.29.8
@@ -808,6 +811,11 @@ packages:
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
marked@18.0.11:
resolution: {integrity: sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw==}
engines: {node: '>= 20'}
hasBin: true
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -1569,6 +1577,8 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/sourcemap-codec': 1.6.0
marked@18.0.11: {}
ms@2.1.3: {} ms@2.1.3: {}
nanoid@3.3.18: {} nanoid@3.3.18: {}
+1 -1
View File
@@ -1,5 +1,5 @@
/* Offline shell: navigations are network-first, static assets cache-first. */ /* Offline shell: navigations are network-first, static assets cache-first. */
const CACHE = "homelab-shell-v2"; const CACHE = "homelab-shell-v3";
self.addEventListener("install", (e) => { self.addEventListener("install", (e) => {
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(["/", "/index.html"]))); e.waitUntil(caches.open(CACHE).then((c) => c.addAll(["/", "/index.html"])));
+93 -36
View File
@@ -1,66 +1,123 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { createClient } from "@connectrpc/connect"; import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web"; import { createConnectTransport } from "@connectrpc/connect-web";
import { Gauge, FlaskConical } from "lucide-preact"; import { BookOpen, Gauge, Link as LinkIcon, Menu, Play } from "lucide-preact";
import type { Section } from "./gen/homelab/v1/homelab_pb"; import type { Item, Link } from "./gen/homelab/v1/homelab_pb";
import { HomelabService } from "./gen/homelab/v1/homelab_pb"; import { HomelabService } from "./gen/homelab/v1/homelab_pb";
import { ScriptCard } from "./components/ScriptCard";
import { MonitorCard } from "./components/MonitorCard"; import { MonitorCard } from "./components/MonitorCard";
import { ScriptCard } from "./components/ScriptCard";
import { DocsView, DocView, localDocs } from "./components/Docs";
import { syncDocs } from "./docsStore";
const client = createClient(HomelabService, createConnectTransport({ baseUrl: "" })); const client = createClient(HomelabService, createConnectTransport({ baseUrl: "" }));
// Иконки секций: строка из конфига → компонент. Новый icon в yaml = добавить сюда. const CATEGORIES = [
const ICONS: Record<string, preact.JSX.ElementType> = { gauge: Gauge, "flask-conical": FlaskConical }; { id: "links", label: "Links", icon: LinkIcon },
{ id: "monitor", label: "Monitor", icon: Gauge },
{ id: "exec", label: "Exec", icon: Play },
{ id: "docs", label: "Docs", icon: BookOpen },
] as const;
function DynIcon({ name, size = 20 }: { name: string; size?: number }) { type Category = (typeof CATEGORIES)[number]["id"];
const Cmp = ICONS[name] ?? Gauge;
return <Cmp size={size} />; function LinkList({ links }: { links: Link[] }) {
if (links.length === 0) return <div class="alert mt-4">No links in config</div>;
return (
<div class="flex flex-col gap-2 mt-4">
{links.map((l) => (
<a href={l.url} target="_blank" rel="noreferrer" class="card bg-base-200 shadow">
<div class="card-body p-4 py-3 flex-row items-center gap-2">
<LinkIcon size={14} class="text-primary" />
<span class="flex-1 truncate">{l.name}</span>
<span class="text-xs text-base-content/40 truncate max-w-40">{l.url}</span>
</div>
</a>
))}
</div>
);
} }
export function App() { export function App() {
const [sections, setSections] = useState<Section[]>([]); const [content, setContent] = useState<{ links: Link[]; monitors: Item[]; exec: Item[] }>({
const [active, setActive] = useState(0); links: [],
monitors: [],
exec: [],
});
const [category, setCategory] = useState<Category>("monitor");
const [drawerOpen, setDrawerOpen] = useState(false);
const [docs, setDocs] = useState(localDocs());
const [openDoc, setOpenDoc] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const poll = () => client.listSections({}).then((r) => setSections(r.sections)); const poll = () =>
client
.listContent({})
.then((r) => setContent({ links: r.links, monitors: r.monitors, exec: r.exec }))
.catch(() => {}); // offline: keep last state
poll(); poll();
const id = setInterval(poll, 3000); const id = setInterval(poll, 3000);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
const sec = sections[active]; // Docs sync + offline dirty flush, every 5s.
useEffect(() => {
const tick = () => syncDocs().then(() => setDocs(localDocs())).catch(() => {});
tick();
const id = setInterval(tick, 5000);
return () => clearInterval(id);
}, []);
const cat = CATEGORIES.find((c) => c.id === category)!;
const open = docs.find((d) => d.name === openDoc);
return ( return (
<div class="flex min-h-screen"> <div class="drawer min-h-screen">
<input id="nav-drawer" type="checkbox" class="drawer-toggle" checked={drawerOpen} onChange={() => setDrawerOpen((v) => !v)} />
<div class="drawer-content flex flex-col">
{/* Top bar */}
<div class="navbar bg-base-200 px-2 pt-[env(safe-area-inset-top)]">
<label class="btn btn-square btn-ghost" onClick={() => setDrawerOpen(true)}>
<Menu size={22} />
</label>
<span class="text-lg font-bold mx-auto">{open && category === "docs" ? open.name.replace(/\.md$/, "") : cat.label}</span>
<span class="w-11" />
</div>
<div class="max-w-md w-full mx-auto px-4 pb-[env(safe-area-inset-bottom)]">
{category === "links" && <LinkList links={content.links} />}
{category === "monitor" && (
<div class="flex flex-col gap-3 mt-4">
{content.monitors.map((it) => <MonitorCard s={it} />)}
</div>
)}
{category === "exec" && content.exec.map((it) => <ScriptCard s={it} />)}
{category === "docs" &&
(open ? (
<DocView doc={open} onBack={() => setOpenDoc(null)} />
) : (
<DocsView docs={docs} onOpen={(d) => setOpenDoc(d.name)} />
))}
</div>
</div>
{/* Sidebar */} {/* Sidebar */}
<nav class="bg-base-200 w-20 flex flex-col items-center gap-1 py-4 pt-[calc(env(safe-area-inset-top)+1rem)]"> <div class="drawer-side z-20">
{sections.map((s, i) => ( <label class="drawer-overlay" onClick={() => setDrawerOpen(false)} />
<nav class="bg-base-200 min-h-full w-56 p-3 pt-[calc(env(safe-area-inset-top)+1rem)] flex flex-col gap-1">
{CATEGORIES.map((c) => (
<button <button
class={`btn btn-square btn-ghost ${i === active ? "btn-primary" : ""}`} class={`btn btn-ghost justify-start ${c.id === category ? "btn-primary" : ""}`}
title={s.name} onClick={() => {
onClick={() => setActive(i)} setCategory(c.id);
setOpenDoc(null);
setDrawerOpen(false);
}}
> >
<DynIcon name={s.icon} /> <c.icon size={18} /> {c.label}
</button> </button>
))} ))}
</nav> </nav>
{/* Active section view */}
<div class="flex-1 max-w-md mx-auto px-4 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]">
<div class="navbar px-0">
<div class="navbar-start" />
<div class="navbar-center">
<span class="text-lg font-bold">{sec?.name ?? "Homelab"}</span>
</div>
<div class="navbar-end" />
</div>
{sec && (
<div class="flex flex-col gap-4 pb-6">
{sec.items.filter((it) => it.type === "monitor").map((it) => <MonitorCard s={it} />)}
{sec.items.filter((it) => it.type === "response").map((it) => <ScriptCard s={it} />)}
</div>
)}
</div> </div>
</div> </div>
); );
+71
View File
@@ -0,0 +1,71 @@
import { useState } from "preact/hooks";
import { marked } from "marked";
import { ArrowLeft, Pencil } from "lucide-preact";
import { editDoc, localDocs, type StoredDoc } from "../docsStore";
// ponytail: no sanitizer — docs come from our own server; add DOMPurify if foreign content ever lands here
const render = (md: string) => marked.parse(md, { async: false });
/** Просмотр одного дока: рендер сверху, textarea-редактор ниже. Правки — в localStorage (client authority). */
export function DocView({ doc, onBack }: { doc: StoredDoc; onBack: () => void }) {
const [content, setContent] = useState(doc.content);
const [dirty, setDirty] = useState(doc.dirty);
return (
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<button class="btn btn-sm btn-ghost" onClick={onBack}>
<ArrowLeft size={16} /> Docs
</button>
<span class="font-semibold flex-1 truncate">{doc.name}</span>
{dirty && <span class="badge badge-warning badge-sm">изменён</span>}
</div>
<div class="card bg-base-200 shadow">
<div
class="prose prose-sm p-4 max-w-none break-words"
dangerouslySetInnerHTML={{ __html: render(content) }}
/>
</div>
<div class="card bg-base-200 shadow">
<div class="card-body p-4 gap-2">
<h2 class="card-title text-sm font-medium text-base-content/60">
<Pencil size={14} /> Edit
</h2>
<textarea
class="textarea textarea-bordered w-full font-mono text-sm h-64"
value={content}
onInput={(e) => {
const v = (e.target as HTMLTextAreaElement).value;
setContent(v);
editDoc(doc.name, v);
setDirty(true);
}}
/>
</div>
</div>
</div>
);
}
/** Список доков из localStorage (offline-first), обновляется через App-поллинг. */
export function DocsView({ docs, onOpen }: { docs: StoredDoc[]; onOpen: (d: StoredDoc) => void }) {
if (docs.length === 0) {
return <div class="alert mt-4">No docs yet (offline without cache, or empty docs dir)</div>;
}
return (
<div class="flex flex-col gap-2 mt-4">
{docs.map((d) => (
<button class="card bg-base-200 shadow text-left" onClick={() => onOpen(d)}>
<div class="card-body p-4 py-3 flex-row items-center gap-2">
<span class="flex-1 truncate">{d.name.replace(/\.md$/, "")}</span>
{d.dirty && <span class="badge badge-warning badge-sm">*</span>}
</div>
</button>
))}
</div>
);
}
export { localDocs };
+67
View File
@@ -0,0 +1,67 @@
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { HomelabService } from "./gen/homelab/v1/homelab_pb";
const client = createClient(HomelabService, createConnectTransport({ baseUrl: "" }));
// localStorage: hub:doc:<name> = {hash, content}, hub:doc-dirty:<name> = "1"
export type StoredDoc = { name: string; hash: string; content: string; dirty: boolean };
const key = (name: string) => `hub:doc:${name}`;
const dirtyKey = (name: string) => `hub:doc-dirty:${name}`;
function get(name: string): StoredDoc | null {
const raw = localStorage.getItem(key(name));
if (!raw) return null;
const { hash, content } = JSON.parse(raw);
return { name, hash, content, dirty: localStorage.getItem(dirtyKey(name)) === "1" };
}
function put(name: string, hash: string, content: string, dirty: boolean) {
localStorage.setItem(key(name), JSON.stringify({ hash, content }));
if (dirty) localStorage.setItem(dirtyKey(name), "1");
else localStorage.removeItem(dirtyKey(name));
}
/** All locally known docs (the offline source of truth). */
export function localDocs(): StoredDoc[] {
const out: StoredDoc[] = [];
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i)!;
if (k.startsWith("hub:doc:")) {
const d = get(k.slice(8));
if (d) out.push(d);
}
}
return out.sort((a, b) => a.name.localeCompare(b.name));
}
export function editDoc(name: string, content: string) {
const d = get(name);
if (d) put(name, d.hash, content, true);
}
export async function syncDocs(): Promise<void> {
// Pull: download new/changed docs (unless we have local edits — client authority).
const metas = await client.listDocs({});
for (const m of metas.docs) {
const local = get(m.name);
if (local && !local.dirty && local.hash !== m.hash) {
const { content } = await client.getDoc({ name: m.name });
put(m.name, m.hash, content, false);
} else if (!local) {
const { content } = await client.getDoc({ name: m.name });
put(m.name, m.hash, content, false);
}
}
// Push: flush dirty docs, every tick until server accepts.
for (const d of localDocs().filter((d) => d.dirty)) {
try {
const { hash } = await client.saveDoc({ name: d.name, content: d.content });
put(d.name, hash, d.content, false);
} catch {
// offline or server error — stays dirty, retried next tick
}
}
}
+193 -44
View File
@@ -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("Chhob21lbGFiL3YxL2hvbWVsYWIucHJvdG8SCmhvbWVsYWIudjEiYAoESXRlbRIMCgRuYW1lGAEgASgJEgwKBHR5cGUYAiABKAkSDAoEZGF0YRgDIAEoCRINCgV2YWx1ZRgEIAEoCRIMCgRhcmdzGAUgASgFEhEKCWFyZ190eXBlcxgGIAMoCSJGCgdTZWN0aW9uEgwKBG5hbWUYASABKAkSDAoEaWNvbhgCIAEoCRIfCgVpdGVtcxgDIAMoCzIQLmhvbWVsYWIudjEuSXRlbSIVChNMaXN0U2VjdGlvbnNSZXF1ZXN0Ij0KFExpc3RTZWN0aW9uc1Jlc3BvbnNlEiUKCHNlY3Rpb25zGAEgAygLMhMuaG9tZWxhYi52MS5TZWN0aW9uIi4KEFJ1blNjcmlwdFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIMCgRhcmdzGAIgAygJIiMKEVJ1blNjcmlwdFJlc3BvbnNlEg4KBm91dHB1dBgBIAEoCTKtAQoOSG9tZWxhYlNlcnZpY2USUQoMTGlzdFNlY3Rpb25zEh8uaG9tZWxhYi52MS5MaXN0U2VjdGlvbnNSZXF1ZXN0GiAuaG9tZWxhYi52MS5MaXN0U2VjdGlvbnNSZXNwb25zZRJICglSdW5TY3JpcHQSHC5ob21lbGFiLnYxLlJ1blNjcmlwdFJlcXVlc3QaHS5ob21lbGFiLnYxLlJ1blNjcmlwdFJlc3BvbnNlQipaKHB3YS1ob21lbGFiLW1vbi9nZW4vaG9tZWxhYi92MTtob21lbGFidjFiBnByb3RvMw"); fileDesc("Chhob21lbGFiL3YxL2hvbWVsYWIucHJvdG8SCmhvbWVsYWIudjEiUgoESXRlbRIMCgRuYW1lGAEgASgJEgwKBGRhdGEYAiABKAkSDQoFdmFsdWUYAyABKAkSDAoEYXJncxgEIAEoBRIRCglhcmdfdHlwZXMYBSADKAkiIQoETGluaxIMCgRuYW1lGAEgASgJEgsKA3VybBgCIAEoCSIUChJMaXN0Q29udGVudFJlcXVlc3QiegoTTGlzdENvbnRlbnRSZXNwb25zZRIfCgVsaW5rcxgBIAMoCzIQLmhvbWVsYWIudjEuTGluaxIiCghtb25pdG9ycxgCIAMoCzIQLmhvbWVsYWIudjEuSXRlbRIeCgRleGVjGAMgAygLMhAuaG9tZWxhYi52MS5JdGVtIiUKB0RvY01ldGESDAoEbmFtZRgBIAEoCRIMCgRoYXNoGAIgASgJIhEKD0xpc3REb2NzUmVxdWVzdCI1ChBMaXN0RG9jc1Jlc3BvbnNlEiEKBGRvY3MYASADKAsyEy5ob21lbGFiLnYxLkRvY01ldGEiHQoNR2V0RG9jUmVxdWVzdBIMCgRuYW1lGAEgASgJIiEKDkdldERvY1Jlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkiLwoOU2F2ZURvY1JlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdjb250ZW50GAIgASgJIh8KD1NhdmVEb2NSZXNwb25zZRIMCgRoYXNoGAEgASgJIi4KEFJ1blNjcmlwdFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIMCgRhcmdzGAIgAygJIiMKEVJ1blNjcmlwdFJlc3BvbnNlEg4KBm91dHB1dBgBIAEoCTL2AgoOSG9tZWxhYlNlcnZpY2USTgoLTGlzdENvbnRlbnQSHi5ob21lbGFiLnYxLkxpc3RDb250ZW50UmVxdWVzdBofLmhvbWVsYWIudjEuTGlzdENvbnRlbnRSZXNwb25zZRJFCghMaXN0RG9jcxIbLmhvbWVsYWIudjEuTGlzdERvY3NSZXF1ZXN0GhwuaG9tZWxhYi52MS5MaXN0RG9jc1Jlc3BvbnNlEj8KBkdldERvYxIZLmhvbWVsYWIudjEuR2V0RG9jUmVxdWVzdBoaLmhvbWVsYWIudjEuR2V0RG9jUmVzcG9uc2USQgoHU2F2ZURvYxIaLmhvbWVsYWIudjEuU2F2ZURvY1JlcXVlc3QaGy5ob21lbGFiLnYxLlNhdmVEb2NSZXNwb25zZRJICglSdW5TY3JpcHQSHC5ob21lbGFiLnYxLlJ1blNjcmlwdFJlcXVlc3QaHS5ob21lbGFiLnYxLlJ1blNjcmlwdFJlc3BvbnNlQipaKHB3YS1ob21lbGFiLW1vbi9nZW4vaG9tZWxhYi92MTtob21lbGFidjFiBnByb3RvMw");
/** /**
* @generated from message homelab.v1.Item * @generated from message homelab.v1.Item
@@ -21,38 +21,31 @@ export type Item = Message<"homelab.v1.Item"> & {
*/ */
name: string; name: string;
/**
* monitor | response
*
* @generated from field: string type = 2;
*/
type: string;
/** /**
* monitor: text | percentage * monitor: text | percentage
* *
* @generated from field: string data = 3; * @generated from field: string data = 2;
*/ */
data: string; data: string;
/** /**
* monitor: last output * monitor: last output
* *
* @generated from field: string value = 4; * @generated from field: string value = 3;
*/ */
value: string; value: string;
/** /**
* response: arg count * exec: arg count
* *
* @generated from field: int32 args = 5; * @generated from field: int32 args = 4;
*/ */
args: number; args: number;
/** /**
* response: text | number per arg * exec: text | number per arg
* *
* @generated from field: repeated string arg_types = 6; * @generated from field: repeated string arg_types = 5;
*/ */
argTypes: string[]; argTypes: string[];
}; };
@@ -65,62 +58,194 @@ export const ItemSchema: GenMessage<Item> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 0); messageDesc(file_homelab_v1_homelab, 0);
/** /**
* @generated from message homelab.v1.Section * @generated from message homelab.v1.Link
*/ */
export type Section = Message<"homelab.v1.Section"> & { export type Link = Message<"homelab.v1.Link"> & {
/** /**
* @generated from field: string name = 1; * @generated from field: string name = 1;
*/ */
name: string; name: string;
/** /**
* @generated from field: string icon = 2; * @generated from field: string url = 2;
*/ */
icon: string; url: string;
/**
* @generated from field: repeated homelab.v1.Item items = 3;
*/
items: Item[];
}; };
/** /**
* Describes the message homelab.v1.Section. * Describes the message homelab.v1.Link.
* Use `create(SectionSchema)` to create a new message. * Use `create(LinkSchema)` to create a new message.
*/ */
export const SectionSchema: GenMessage<Section> = /*@__PURE__*/ export const LinkSchema: GenMessage<Link> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 1); messageDesc(file_homelab_v1_homelab, 1);
/** /**
* @generated from message homelab.v1.ListSectionsRequest * @generated from message homelab.v1.ListContentRequest
*/ */
export type ListSectionsRequest = Message<"homelab.v1.ListSectionsRequest"> & { export type ListContentRequest = Message<"homelab.v1.ListContentRequest"> & {
}; };
/** /**
* Describes the message homelab.v1.ListSectionsRequest. * Describes the message homelab.v1.ListContentRequest.
* Use `create(ListSectionsRequestSchema)` to create a new message. * Use `create(ListContentRequestSchema)` to create a new message.
*/ */
export const ListSectionsRequestSchema: GenMessage<ListSectionsRequest> = /*@__PURE__*/ export const ListContentRequestSchema: GenMessage<ListContentRequest> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 2); messageDesc(file_homelab_v1_homelab, 2);
/** /**
* @generated from message homelab.v1.ListSectionsResponse * @generated from message homelab.v1.ListContentResponse
*/ */
export type ListSectionsResponse = Message<"homelab.v1.ListSectionsResponse"> & { export type ListContentResponse = Message<"homelab.v1.ListContentResponse"> & {
/** /**
* @generated from field: repeated homelab.v1.Section sections = 1; * @generated from field: repeated homelab.v1.Link links = 1;
*/ */
sections: Section[]; links: Link[];
/**
* @generated from field: repeated homelab.v1.Item monitors = 2;
*/
monitors: Item[];
/**
* @generated from field: repeated homelab.v1.Item exec = 3;
*/
exec: Item[];
}; };
/** /**
* Describes the message homelab.v1.ListSectionsResponse. * Describes the message homelab.v1.ListContentResponse.
* Use `create(ListSectionsResponseSchema)` to create a new message. * Use `create(ListContentResponseSchema)` to create a new message.
*/ */
export const ListSectionsResponseSchema: GenMessage<ListSectionsResponse> = /*@__PURE__*/ export const ListContentResponseSchema: GenMessage<ListContentResponse> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 3); messageDesc(file_homelab_v1_homelab, 3);
/**
* @generated from message homelab.v1.DocMeta
*/
export type DocMeta = Message<"homelab.v1.DocMeta"> & {
/**
* @generated from field: string name = 1;
*/
name: string;
/**
* sha256 hex of content
*
* @generated from field: string hash = 2;
*/
hash: string;
};
/**
* Describes the message homelab.v1.DocMeta.
* Use `create(DocMetaSchema)` to create a new message.
*/
export const DocMetaSchema: GenMessage<DocMeta> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 4);
/**
* @generated from message homelab.v1.ListDocsRequest
*/
export type ListDocsRequest = Message<"homelab.v1.ListDocsRequest"> & {
};
/**
* Describes the message homelab.v1.ListDocsRequest.
* Use `create(ListDocsRequestSchema)` to create a new message.
*/
export const ListDocsRequestSchema: GenMessage<ListDocsRequest> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 5);
/**
* @generated from message homelab.v1.ListDocsResponse
*/
export type ListDocsResponse = Message<"homelab.v1.ListDocsResponse"> & {
/**
* @generated from field: repeated homelab.v1.DocMeta docs = 1;
*/
docs: DocMeta[];
};
/**
* Describes the message homelab.v1.ListDocsResponse.
* Use `create(ListDocsResponseSchema)` to create a new message.
*/
export const ListDocsResponseSchema: GenMessage<ListDocsResponse> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 6);
/**
* @generated from message homelab.v1.GetDocRequest
*/
export type GetDocRequest = Message<"homelab.v1.GetDocRequest"> & {
/**
* @generated from field: string name = 1;
*/
name: string;
};
/**
* Describes the message homelab.v1.GetDocRequest.
* Use `create(GetDocRequestSchema)` to create a new message.
*/
export const GetDocRequestSchema: GenMessage<GetDocRequest> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 7);
/**
* @generated from message homelab.v1.GetDocResponse
*/
export type GetDocResponse = Message<"homelab.v1.GetDocResponse"> & {
/**
* @generated from field: string content = 1;
*/
content: string;
};
/**
* Describes the message homelab.v1.GetDocResponse.
* Use `create(GetDocResponseSchema)` to create a new message.
*/
export const GetDocResponseSchema: GenMessage<GetDocResponse> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 8);
/**
* @generated from message homelab.v1.SaveDocRequest
*/
export type SaveDocRequest = Message<"homelab.v1.SaveDocRequest"> & {
/**
* @generated from field: string name = 1;
*/
name: string;
/**
* @generated from field: string content = 2;
*/
content: string;
};
/**
* Describes the message homelab.v1.SaveDocRequest.
* Use `create(SaveDocRequestSchema)` to create a new message.
*/
export const SaveDocRequestSchema: GenMessage<SaveDocRequest> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 9);
/**
* @generated from message homelab.v1.SaveDocResponse
*/
export type SaveDocResponse = Message<"homelab.v1.SaveDocResponse"> & {
/**
* @generated from field: string hash = 1;
*/
hash: string;
};
/**
* Describes the message homelab.v1.SaveDocResponse.
* Use `create(SaveDocResponseSchema)` to create a new message.
*/
export const SaveDocResponseSchema: GenMessage<SaveDocResponse> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 10);
/** /**
* @generated from message homelab.v1.RunScriptRequest * @generated from message homelab.v1.RunScriptRequest
*/ */
@@ -141,7 +266,7 @@ export type RunScriptRequest = Message<"homelab.v1.RunScriptRequest"> & {
* Use `create(RunScriptRequestSchema)` to create a new message. * Use `create(RunScriptRequestSchema)` to create a new message.
*/ */
export const RunScriptRequestSchema: GenMessage<RunScriptRequest> = /*@__PURE__*/ export const RunScriptRequestSchema: GenMessage<RunScriptRequest> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 4); messageDesc(file_homelab_v1_homelab, 11);
/** /**
* @generated from message homelab.v1.RunScriptResponse * @generated from message homelab.v1.RunScriptResponse
@@ -158,19 +283,43 @@ export type RunScriptResponse = Message<"homelab.v1.RunScriptResponse"> & {
* Use `create(RunScriptResponseSchema)` to create a new message. * Use `create(RunScriptResponseSchema)` to create a new message.
*/ */
export const RunScriptResponseSchema: GenMessage<RunScriptResponse> = /*@__PURE__*/ export const RunScriptResponseSchema: GenMessage<RunScriptResponse> = /*@__PURE__*/
messageDesc(file_homelab_v1_homelab, 5); messageDesc(file_homelab_v1_homelab, 12);
/** /**
* @generated from service homelab.v1.HomelabService * @generated from service homelab.v1.HomelabService
*/ */
export const HomelabService: GenService<{ export const HomelabService: GenService<{
/** /**
* @generated from rpc homelab.v1.HomelabService.ListSections * @generated from rpc homelab.v1.HomelabService.ListContent
*/ */
listSections: { listContent: {
methodKind: "unary"; methodKind: "unary";
input: typeof ListSectionsRequestSchema; input: typeof ListContentRequestSchema;
output: typeof ListSectionsResponseSchema; output: typeof ListContentResponseSchema;
},
/**
* @generated from rpc homelab.v1.HomelabService.ListDocs
*/
listDocs: {
methodKind: "unary";
input: typeof ListDocsRequestSchema;
output: typeof ListDocsResponseSchema;
},
/**
* @generated from rpc homelab.v1.HomelabService.GetDoc
*/
getDoc: {
methodKind: "unary";
input: typeof GetDocRequestSchema;
output: typeof GetDocResponseSchema;
},
/**
* @generated from rpc homelab.v1.HomelabService.SaveDoc
*/
saveDoc: {
methodKind: "unary";
input: typeof SaveDocRequestSchema;
output: typeof SaveDocResponseSchema;
}, },
/** /**
* @generated from rpc homelab.v1.HomelabService.RunScript * @generated from rpc homelab.v1.HomelabService.RunScript