Add Show Widget for library actions with Overview charts from last run output.

This commit is contained in:
2026-07-20 20:48:06 +02:00
parent e047499fed
commit d188ca41b8
20 changed files with 1918 additions and 128 deletions
+126 -13
View File
@@ -14,6 +14,7 @@ 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.
@@ -27,6 +28,8 @@ func DefaultBuiltinActions() []Action {
Kind: ActionKindShell,
Body: "uptime",
Env: []ActionEnvVar{},
ShowWidget: true,
WidgetType: ActionWidgetTypeRaw,
Builtin: true,
CreatedAt: now,
UpdatedAt: now,
@@ -36,11 +39,15 @@ func DefaultBuiltinActions() []Action {
Name: "Disk usage",
Description: "Show filesystem disk space usage on the target.",
Kind: ActionKindShell,
Body: "df -h",
Env: []ActionEnvVar{},
Builtin: true,
CreatedAt: now,
UpdatedAt: now,
Body: "df -h {{env.MOUNT}}",
Env: []ActionEnvVar{
{Name: "MOUNT", Value: "/"},
},
ShowWidget: true,
WidgetType: ActionWidgetTypeDisk,
Builtin: true,
CreatedAt: now,
UpdatedAt: now,
},
{
ID: BuiltinActionSysInfoID,
@@ -52,10 +59,25 @@ if [ -f /etc/os-release ]; then
. /etc/os-release
echo "OS: ${NAME:-unknown} ${VERSION:-}"
fi`,
Env: []ActionEnvVar{},
Builtin: true,
CreatedAt: now,
UpdatedAt: now,
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,
},
}
}
@@ -84,6 +106,7 @@ func LoadActions(dir string) (ActionStore, error) {
}
// 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 {
@@ -96,9 +119,102 @@ func LoadActionsOrSeed(dir string) (ActionStore, error) {
}
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 {
@@ -120,8 +236,5 @@ func SaveActions(dir string, store ActionStore) error {
payload = append(payload, '\n')
path := filepath.Join(dir, ActionsFileName)
if err := os.WriteFile(path, payload, 0o640); err != nil {
return fmt.Errorf("write actions: %w", err)
}
return nil
return os.WriteFile(path, payload, 0o640)
}