package localdb import ( "encoding/json" "fmt" "log/slog" "gorm.io/gorm" ) // ConfigTypeDef describes one device configuration type as synced from qt_settings. type ConfigTypeDef struct { Code string `json:"code"` NameRu string `json:"name_ru"` DisplayOrder int `json:"display_order"` Categories []string `json:"categories"` } // TabSection is a named sub-group of categories within a configurator tab. type TabSection struct { Title string `json:"title"` Categories []string `json:"categories"` } // TabDef describes one tab in the configurator as synced from qt_settings. type TabDef struct { Key string `json:"key"` Label string `json:"label"` SingleSelect bool `json:"single_select"` Categories []string `json:"categories"` Sections []TabSection `json:"sections,omitempty"` } // ConfiguratorSettings holds all four server-driven settings consumed by the configurator. // Fields are nil/empty when the corresponding qt_settings key is absent or unparseable; // callers are expected to apply hardcoded fallbacks in that case. type ConfiguratorSettings struct { ConfigTypes []ConfigTypeDef `json:"config_types"` TabConfig []TabDef `json:"tab_config"` AlwaysVisibleTabs []string `json:"always_visible_tabs"` RequiredCategories map[string][]string `json:"required_categories"` } // SyncQtSettings reads all rows from qt_settings on MariaDB and replaces the // local_qt_settings cache in a single SQLite transaction. // If the read fails (no connection, table missing on old server) or the server // returns an empty table, the existing local_qt_settings are preserved so the // configurator keeps working offline or against old server versions. func (l *LocalDB) SyncQtSettings(mariaDB *gorm.DB) error { var rows []LocalQtSetting if err := mariaDB. Table("qt_settings"). Select("name, value"). Find(&rows).Error; err != nil { slog.Warn("qt_settings: read from MariaDB failed, keeping existing local cache", "error", err) return fmt.Errorf("reading qt_settings from MariaDB: %w", err) } if len(rows) == 0 { slog.Warn("qt_settings: server returned empty table, keeping existing local cache") return nil } return l.db.Transaction(func(tx *gorm.DB) error { if err := tx.Exec("DELETE FROM local_qt_settings").Error; err != nil { return fmt.Errorf("clearing local_qt_settings: %w", err) } if err := tx.Create(&rows).Error; err != nil { return fmt.Errorf("inserting local_qt_settings: %w", err) } slog.Info("qt_settings synced", "count", len(rows)) return nil }) } // GetQtSetting returns the raw JSON value for a named setting. // found is false when the key does not exist. func (l *LocalDB) GetQtSetting(name string) (value string, found bool, err error) { var row LocalQtSetting res := l.db.Where("name = ?", name).First(&row) if res.Error != nil { if res.Error == gorm.ErrRecordNotFound { return "", false, nil } return "", false, res.Error } return row.Value, true, nil } // GetConfiguratorSettings reads all four known settings from local_qt_settings and // parses them. Any missing or unparseable key is left as nil/zero in the result; // the caller must apply fallbacks. func (l *LocalDB) GetConfiguratorSettings() (*ConfiguratorSettings, error) { out := &ConfiguratorSettings{} keys := []string{"config_types", "tab_config", "always_visible_tabs", "required_categories"} for _, key := range keys { raw, found, err := l.GetQtSetting(key) if err != nil { return out, fmt.Errorf("reading setting %q: %w", key, err) } if !found || raw == "" { continue } switch key { case "config_types": if err := json.Unmarshal([]byte(raw), &out.ConfigTypes); err != nil { slog.Warn("failed to parse config_types setting", "error", err) } case "tab_config": if err := json.Unmarshal([]byte(raw), &out.TabConfig); err != nil { slog.Warn("failed to parse tab_config setting", "error", err) } case "always_visible_tabs": if err := json.Unmarshal([]byte(raw), &out.AlwaysVisibleTabs); err != nil { slog.Warn("failed to parse always_visible_tabs setting", "error", err) } case "required_categories": if err := json.Unmarshal([]byte(raw), &out.RequiredCategories); err != nil { slog.Warn("failed to parse required_categories setting", "error", err) } } } return out, nil }