package settings import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "time" ) // LoadActionWidgets reads and parses node-action-widgets.json from dir. func LoadActionWidgets(dir string) (ActionWidgetStore, error) { path := filepath.Join(dir, NodeActionWidgetsFileName) payload, err := os.ReadFile(path) if err != nil { return ActionWidgetStore{}, err } var store ActionWidgetStore if err := json.Unmarshal(payload, &store); err != nil { return ActionWidgetStore{}, fmt.Errorf("parse action widgets: %w", err) } if store.Widgets == nil { store.Widgets = []ActionWidgetSnapshot{} } return store, nil } // LoadActionWidgetsOrEmpty returns an empty store when the file is missing. func LoadActionWidgetsOrEmpty(dir string) (ActionWidgetStore, error) { store, err := LoadActionWidgets(dir) if err != nil { if errors.Is(err, os.ErrNotExist) { return ActionWidgetStore{Widgets: []ActionWidgetSnapshot{}}, nil } return ActionWidgetStore{}, err } return store, nil } // SaveActionWidgets writes node-action-widgets.json to dir with mode 0640. func SaveActionWidgets(dir string, store ActionWidgetStore) error { if err := ensureConfigDir(dir); err != nil { return err } if store.Widgets == nil { store.Widgets = []ActionWidgetSnapshot{} } payload, err := json.MarshalIndent(store, "", " ") if err != nil { return fmt.Errorf("encode action widgets: %w", err) } payload = append(payload, '\n') path := filepath.Join(dir, NodeActionWidgetsFileName) return os.WriteFile(path, payload, 0o640) } // WidgetsForNode returns snapshots belonging to nodeID. func WidgetsForNode(store ActionWidgetStore, nodeID string) []ActionWidgetSnapshot { out := make([]ActionWidgetSnapshot, 0) for _, widget := range store.Widgets { if widget.NodeID == nodeID { out = append(out, widget) } } return out } // UpsertActionWidgetSnapshot replaces or appends a snapshot keyed by node_id + item_id. func UpsertActionWidgetSnapshot(dir string, snapshot ActionWidgetSnapshot) error { if strings.TrimSpace(snapshot.NodeID) == "" { return fmt.Errorf("node id is required") } if strings.TrimSpace(snapshot.ItemID) == "" { return fmt.Errorf("item id is required") } if snapshot.UpdatedAt.IsZero() { snapshot.UpdatedAt = time.Now().UTC() } store, err := LoadActionWidgetsOrEmpty(dir) if err != nil { return err } found := false for index := range store.Widgets { if store.Widgets[index].NodeID == snapshot.NodeID && store.Widgets[index].ItemID == snapshot.ItemID { store.Widgets[index] = snapshot found = true break } } if !found { store.Widgets = append(store.Widgets, snapshot) } return SaveActionWidgets(dir, store) } // DeleteActionWidgetsForItem removes the snapshot for a node action item, if any. func DeleteActionWidgetsForItem(dir string, nodeID string, itemID string) error { store, err := LoadActionWidgetsOrEmpty(dir) if err != nil { return err } filtered := make([]ActionWidgetSnapshot, 0, len(store.Widgets)) changed := false for _, widget := range store.Widgets { if widget.NodeID == nodeID && widget.ItemID == itemID { changed = true continue } filtered = append(filtered, widget) } if !changed { return nil } store.Widgets = filtered return SaveActionWidgets(dir, store) }