package inventory import ( "context" "encoding/json" "errors" "time" "github.com/jackc/pgx/v5" ) // What a change replaces (novox/hq ADR 0217, to-be 44): the settings layer a set or a clear replaced, // and a summary of what a machine was last sent. Both were missing when a change took effect unseen. // Layer is one settings layer as it stands — the whole mesh's when nodeName is empty — and whether // there is one. A layer is replaced whole when set; reading it first is how one key is changed // without losing the others. func (i *Inventory) Layer(ctx context.Context, nodeName, module string) (map[string]any, bool, error) { nodeID, err := i.layerNode(ctx, nodeName) if err != nil { return nil, false, err } var raw []byte err = i.store.Pool().QueryRow(ctx, `select values from settings where module = $1 and node is not distinct from $2::uuid`, module, nodeID).Scan(&raw) if errors.Is(err, pgx.ErrNoRows) { return nil, false, nil } if err != nil { return nil, false, err } values := map[string]any{} if err := json.Unmarshal(raw, &values); err != nil { return nil, false, err } return values, true, nil } // PastLayer is a layer that was replaced or cleared. type PastLayer struct { Values map[string]any SetAt *time.Time ReplacedAt time.Time // ReplacedBy is "set" or "clear". ReplacedBy string } // SettingsHistory is every layer of one module on one machine — the whole mesh's when nodeName is // empty — that was replaced or cleared, the latest first. func (i *Inventory) SettingsHistory(ctx context.Context, nodeName, module string) ([]PastLayer, error) { nodeID, err := i.layerNode(ctx, nodeName) if err != nil { return nil, err } rows, err := i.store.Pool().Query(ctx, `select values, set_at, replaced_at, replaced_by from settings_history where module = $1 and node is not distinct from $2::uuid order by replaced_at desc`, module, nodeID) if err != nil { return nil, err } defer rows.Close() var out []PastLayer for rows.Next() { var raw []byte var p PastLayer if err := rows.Scan(&raw, &p.SetAt, &p.ReplacedAt, &p.ReplacedBy); err != nil { return nil, err } if err := json.Unmarshal(raw, &p.Values); err != nil { return nil, err } out = append(out, p) } return out, rows.Err() } // keepReplaced writes down the layer about to be replaced or cleared, if there is one. func (i *Inventory) keepReplaced(ctx context.Context, nodeID *string, module, how string) error { _, err := i.store.Pool().Exec(ctx, keepReplacedSQL, module, nodeID, how) return err } // keepReplacedSQL copies a layer into the history before it is replaced or cleared; run in the same // transaction as the write where there is one, so a write that fails leaves no history of it. const keepReplacedSQL = `insert into settings_history (node, module, values, set_at, replaced_by) select node, module, values, set_at, $3 from settings where module = $1 and node is not distinct from $2::uuid` // layerNode is the node id of a layer, nil for the whole mesh's. func (i *Inventory) layerNode(ctx context.Context, nodeName string) (*string, error) { if nodeName == "" { return nil, nil } n, err := i.NodeByName(ctx, nodeName) if err != nil { return nil, err } return &n.ID, nil } // RecordSentSummary keeps what a machine was last sent, summarised (migration 0057): read only to // compare what would be sent with it, never as what the machine should be. func (i *Inventory) RecordSentSummary(ctx context.Context, node string, summary []byte) error { _, err := i.store.Pool().Exec(ctx, `update node set sent_summary = $2 where id = $1`, node, summary) return err } // SentSummary is what a machine was last sent, summarised, by its name; empty for a machine sent // nothing since the summary was first kept. func (i *Inventory) SentSummary(ctx context.Context, name string) ([]byte, error) { var raw []byte err := i.store.Pool().QueryRow(ctx, `select sent_summary from node where name = $1`, name).Scan(&raw) if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return raw, err }