241 lines
6.7 KiB
Go
241 lines
6.7 KiB
Go
package settings
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// Stable IDs for seeded built-in actions.
|
|
const (
|
|
BuiltinActionUptimeID = "00000000-0000-4000-8000-000000000001"
|
|
BuiltinActionDiskUsageID = "00000000-0000-4000-8000-000000000002"
|
|
BuiltinActionSysInfoID = "00000000-0000-4000-8000-000000000003"
|
|
BuiltinActionMemoryID = "00000000-0000-4000-8000-000000000004"
|
|
)
|
|
|
|
// DefaultBuiltinActions returns the read-only seed actions.
|
|
func DefaultBuiltinActions() []Action {
|
|
now := time.Now().UTC()
|
|
return []Action{
|
|
{
|
|
ID: BuiltinActionUptimeID,
|
|
Name: "Uptime",
|
|
Description: "Show how long the target system has been running.",
|
|
Kind: ActionKindShell,
|
|
Body: "uptime",
|
|
Env: []ActionEnvVar{},
|
|
ShowWidget: true,
|
|
WidgetType: ActionWidgetTypeRaw,
|
|
Builtin: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
{
|
|
ID: BuiltinActionDiskUsageID,
|
|
Name: "Disk usage",
|
|
Description: "Show filesystem disk space usage on the target.",
|
|
Kind: ActionKindShell,
|
|
Body: "df -h {{env.MOUNT}}",
|
|
Env: []ActionEnvVar{
|
|
{Name: "MOUNT", Value: "/"},
|
|
},
|
|
ShowWidget: true,
|
|
WidgetType: ActionWidgetTypeDisk,
|
|
Builtin: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
{
|
|
ID: BuiltinActionSysInfoID,
|
|
Name: "System info",
|
|
Description: "Print kernel and OS release details from the target.",
|
|
Kind: ActionKindScript,
|
|
Body: `uname -a
|
|
if [ -f /etc/os-release ]; then
|
|
. /etc/os-release
|
|
echo "OS: ${NAME:-unknown} ${VERSION:-}"
|
|
fi`,
|
|
Env: []ActionEnvVar{},
|
|
ShowWidget: true,
|
|
WidgetType: ActionWidgetTypeRaw,
|
|
Builtin: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
{
|
|
ID: BuiltinActionMemoryID,
|
|
Name: "Memory",
|
|
Description: "Show memory usage on the target (free -m).",
|
|
Kind: ActionKindShell,
|
|
Body: "free -m",
|
|
Env: []ActionEnvVar{},
|
|
ShowWidget: true,
|
|
WidgetType: ActionWidgetTypeMemory,
|
|
Builtin: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
}
|
|
}
|
|
|
|
// LoadActions reads and parses actions.json from dir.
|
|
func LoadActions(dir string) (ActionStore, error) {
|
|
path := filepath.Join(dir, ActionsFileName)
|
|
payload, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return ActionStore{}, err
|
|
}
|
|
|
|
var store ActionStore
|
|
if err := json.Unmarshal(payload, &store); err != nil {
|
|
return ActionStore{}, fmt.Errorf("parse actions: %w", err)
|
|
}
|
|
if store.Actions == nil {
|
|
store.Actions = []Action{}
|
|
}
|
|
for index := range store.Actions {
|
|
if store.Actions[index].Env == nil {
|
|
store.Actions[index].Env = []ActionEnvVar{}
|
|
}
|
|
}
|
|
return store, nil
|
|
}
|
|
|
|
// LoadActionsOrSeed returns seeded builtins (and persists them) when actions.json is missing.
|
|
// Existing installs are updated via EnsureBuiltinActions.
|
|
func LoadActionsOrSeed(dir string) (ActionStore, error) {
|
|
store, err := LoadActions(dir)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
seeded := ActionStore{Actions: DefaultBuiltinActions()}
|
|
if saveErr := SaveActions(dir, seeded); saveErr != nil {
|
|
return ActionStore{}, saveErr
|
|
}
|
|
return seeded, nil
|
|
}
|
|
return ActionStore{}, err
|
|
}
|
|
updated, changed := EnsureBuiltinActions(store)
|
|
if changed {
|
|
if saveErr := SaveActions(dir, updated); saveErr != nil {
|
|
return ActionStore{}, saveErr
|
|
}
|
|
return updated, nil
|
|
}
|
|
return store, nil
|
|
}
|
|
|
|
// EnsureBuiltinActions merges expected builtin definitions into the store.
|
|
// Custom (non-builtin) actions are preserved. Builtin widget settings already
|
|
// customized by the user are kept when the action already exists with matching ID;
|
|
// body/env/name for known builtins are refreshed to the current seed values.
|
|
// Returns the updated store and whether anything changed.
|
|
func EnsureBuiltinActions(store ActionStore) (ActionStore, bool) {
|
|
if store.Actions == nil {
|
|
store.Actions = []Action{}
|
|
}
|
|
defaults := DefaultBuiltinActions()
|
|
byID := make(map[string]int, len(store.Actions))
|
|
for index := range store.Actions {
|
|
byID[store.Actions[index].ID] = index
|
|
}
|
|
|
|
changed := false
|
|
now := time.Now().UTC()
|
|
for _, desired := range defaults {
|
|
index, exists := byID[desired.ID]
|
|
if !exists {
|
|
desired.CreatedAt = now
|
|
desired.UpdatedAt = now
|
|
store.Actions = append(store.Actions, desired)
|
|
byID[desired.ID] = len(store.Actions) - 1
|
|
changed = true
|
|
continue
|
|
}
|
|
current := store.Actions[index]
|
|
// Preserve user-chosen widget toggle/type on existing builtins.
|
|
showWidget := current.ShowWidget
|
|
widgetType := current.WidgetType
|
|
if !current.Builtin {
|
|
// ID collision with a custom action: leave it alone.
|
|
continue
|
|
}
|
|
needsUpdate := current.Name != desired.Name ||
|
|
current.Description != desired.Description ||
|
|
current.Kind != desired.Kind ||
|
|
current.Body != desired.Body ||
|
|
!envVarsEqual(current.Env, desired.Env) ||
|
|
current.RequiresSudo != desired.RequiresSudo
|
|
if !needsUpdate {
|
|
// Still ensure widget defaults if never set and seed wants widgets.
|
|
if !current.ShowWidget && desired.ShowWidget && current.WidgetType == "" {
|
|
current.ShowWidget = desired.ShowWidget
|
|
current.WidgetType = desired.WidgetType
|
|
current.UpdatedAt = now
|
|
store.Actions[index] = current
|
|
changed = true
|
|
}
|
|
continue
|
|
}
|
|
current.Name = desired.Name
|
|
current.Description = desired.Description
|
|
current.Kind = desired.Kind
|
|
current.Body = desired.Body
|
|
current.Env = append([]ActionEnvVar(nil), desired.Env...)
|
|
current.RequiresSudo = desired.RequiresSudo
|
|
current.Builtin = true
|
|
// Keep existing widget prefs; only fill defaults when unset.
|
|
if current.WidgetType == "" && desired.WidgetType != "" {
|
|
current.ShowWidget = desired.ShowWidget
|
|
current.WidgetType = desired.WidgetType
|
|
} else {
|
|
current.ShowWidget = showWidget
|
|
current.WidgetType = widgetType
|
|
}
|
|
current.UpdatedAt = now
|
|
store.Actions[index] = current
|
|
changed = true
|
|
}
|
|
return store, changed
|
|
}
|
|
|
|
func envVarsEqual(left, right []ActionEnvVar) bool {
|
|
if len(left) != len(right) {
|
|
return false
|
|
}
|
|
for index := range left {
|
|
if left[index].Name != right[index].Name || left[index].Value != right[index].Value {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// SaveActions writes actions.json to dir with mode 0640.
|
|
func SaveActions(dir string, store ActionStore) error {
|
|
if err := ensureConfigDir(dir); err != nil {
|
|
return err
|
|
}
|
|
if store.Actions == nil {
|
|
store.Actions = []Action{}
|
|
}
|
|
for index := range store.Actions {
|
|
if store.Actions[index].Env == nil {
|
|
store.Actions[index].Env = []ActionEnvVar{}
|
|
}
|
|
}
|
|
|
|
payload, err := json.MarshalIndent(store, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("encode actions: %w", err)
|
|
}
|
|
payload = append(payload, '\n')
|
|
|
|
path := filepath.Join(dir, ActionsFileName)
|
|
return os.WriteFile(path, payload, 0o640)
|
|
}
|