diff --git a/config/config.go b/config/config.go index 1cfadd0..e5005f8 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "bytes" "encoding/json" "fmt" "os" @@ -115,6 +116,44 @@ func LoadBotUserConfig(configPath string) (*BotUserConfig, error) { return &cfg, nil } +// SaveBotNodeConfig persists the given node UUIDs to the bot node config file. +// Entries already present in the file are always preserved: a UUID Komari no +// longer reports on a given fetch is kept rather than deleted, so per-node +// settings for stale nodes survive a node-list refresh that does not include +// them. UUIDs seen for the first time are added with an empty BotNodeOptions +// entry. The resulting JSON has its object keys emitted in sorted order by +// encoding/json, keeping the file deterministic across writes. The path is +// supplied by the caller (typically global.ConfigPath.BotNodeConfig). +func SaveBotNodeConfig(configPath string, uuids []string) error { + // Start from whatever is already on disk so nothing is dropped. An empty or + // missing file is treated as an empty map. + members := make(BotNodeMembers) + if data, err := os.ReadFile(configPath); err == nil && len(bytes.TrimSpace(data)) > 0 { + if err := json.Unmarshal(data, &members); err != nil { + return fmt.Errorf("failed to parse existing bot node config %s: %w", configPath, err) + } + } + + // Add every currently-fetched UUID, but never overwrite an entry that is + // already configured. + for _, uuid := range uuids { + if _, ok := members[uuid]; !ok { + members[uuid] = BotNodeOptions{} + } + } + + data, err := json.MarshalIndent(members, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal bot node config: %w", err) + } + data = append(data, '\n') + + if err := os.WriteFile(configPath, data, 0o644); err != nil { + return fmt.Errorf("failed to write bot node config %s: %w", configPath, err) + } + return nil +} + // IsDebugMode returns whether debug mode is enabled. func IsDebugMode() bool { if C_globalConfig == nil { diff --git a/config/variables.go b/config/variables.go index 44b50c7..d5c4a60 100644 --- a/config/variables.go +++ b/config/variables.go @@ -170,3 +170,16 @@ type BotUserConfig struct { } var C_botUserConfig *BotUserConfig + +// BotNodeOptions holds per-node options stored in bot_node_config.json. The +// struct is currently empty; per-node settings can be added here later without +// invalidating existing files. +type BotNodeOptions struct { +} + +// BotNodeMembers maps a node UUID (as reported by Komari) to its per-node +// options. A plain struct value marshals to an empty JSON object, so +// bot_node_config.json reads like: +// +// {"": {}, "": {}} +type BotNodeMembers map[string]BotNodeOptions diff --git a/internal/komari/client.go b/internal/komari/client.go index 80460a6..542079a 100644 --- a/internal/komari/client.go +++ b/internal/komari/client.go @@ -13,6 +13,7 @@ import ( "time" "nukumizu-backend/config" + "nukumizu-backend/global" "nukumizu-backend/internal/node" "nukumizu-backend/postLog" ) @@ -225,6 +226,20 @@ func (c *Client) FetchNodes() ([]NodeInfo, error) { nodes = append(nodes, n) } + // Persist the fetched node UUIDs to bot_node_config.json on every fetch + // (startup login, WebSocket reconnect and periodic refresh all funnel + // through FetchNodes) so the file always reflects the nodes Komari + // currently manages. A persistence failure is only logged: the node data + // itself was fetched successfully and must not be discarded over a disk + // write problem. + uuids := make([]string, len(nodes)) + for i, n := range nodes { + uuids[i] = n.UUID + } + if err := config.SaveBotNodeConfig(global.ConfigPath.BotNodeConfig, uuids); err != nil { + postLog.Warning("Failed to save bot node config: " + err.Error()) + } + postLog.Info(fmt.Sprintf("Fetched %d nodes from Komari", len(nodes))) return nodes, nil }