diff --git a/bible-local/02-architecture.md b/bible-local/02-architecture.md index a71e29e..219f3cd 100644 --- a/bible-local/02-architecture.md +++ b/bible-local/02-architecture.md @@ -106,6 +106,17 @@ Rules: - copy checkboxes and copy modals must prefill `_копия`, not ` (копия)`; - the literal variant name `main` is reserved and must not be allowed for non-main variants. +## Configuration types + +Configurations have a `config_type` field: `"server"` (default) or `"storage"`. + +Rules: +- `config_type` defaults to `"server"` for all existing and new configurations unless explicitly set; +- the configurator page is shared for both types; the SW tab is always visible regardless of type; +- storage configurations use the same vendor_spec + PN→LOT + pricing flow as server configurations; +- storage component categories map to existing tabs: `ENC`/`DKC`/`CTL` → Base, `HIC` → PCI (HIC-карты СХД; `HBA`/`NIC` — серверные, не смешивать), `SSD`/`HDD` → Storage (используют существующие серверные LOT), `ACC` → Accessories (используют существующие серверные LOT), `SW` → SW. +- `DKC` = контроллерная полка (модель СХД + тип дисков + кол-во слотов + кол-во контроллеров); `CTL` = контроллер (кэш + встроенные порты); `ENC` = дисковая полка без контроллера. + ## Vendor BOM contract Vendor BOM is stored in `vendor_spec` on the configuration row. diff --git a/docs/storage-components-guide.docx b/docs/storage-components-guide.docx new file mode 100644 index 0000000..6135734 Binary files /dev/null and b/docs/storage-components-guide.docx differ diff --git a/docs/storage-components-guide.md b/docs/storage-components-guide.md new file mode 100644 index 0000000..790d193 --- /dev/null +++ b/docs/storage-components-guide.md @@ -0,0 +1,213 @@ +# Руководство по составлению каталога лотов СХД + +## Что такое LOT и зачем он нужен + +LOT — это внутренний идентификатор типа компонента в системе QuoteForge. + +Каждый LOT представляет одну рыночную позицию и хранит **средневзвешенную рыночную цену**, рассчитанную по историческим данным от поставщиков. Это позволяет получать актуальную оценку стоимости независимо от конкретного поставщика или прайс-листа. + +Партномера вендора (Part Number, Feature Code) сами по себе не имеют цены в системе — они **переводятся в LOT** через книгу партномеров. Именно через LOT происходит расценка конфигурации. + +**Пример:** Feature Code `B4B9` и Part Number `4C57A14368` — это два разных обозначения одной и той же HIC-карты от Lenovo. Оба маппируются на один LOT `HIC_4pFC32`, у которого есть рыночная цена. + +--- + +## Категории и вкладки конфигуратора + +Категория LOT определяет, в какой вкладке конфигуратора он появится. + +| Код категории | Название | Вкладка | Что сюда относится | +|---|---|---|---| +| `ENC` | Storage Enclosure | **Base** | Дисковая полка без контроллера | +| `DKC` | Disk/Controller Enclosure | **Base** | Контроллерная полка: модель СХД + тип дисков + кол-во слотов + кол-во контроллеров | +| `CTL` | Storage Controller | **Base** | Контроллер СХД: объём кэша + встроенные хост-порты | +| `HIC` | Host Interface Card | **PCI** | HIC-карты СХД: интерфейсы подключения (FC, iSCSI, SAS) | +| `HDD` | HDD | **Storage** | Жёсткие диски (HDD) | +| `SSD` | SSD | **Storage** | Твердотельные диски (SSD, NVMe) | +| `ACC` | Accessories | **Accessories** | Кабели подключения, кабели питания | +| `SW` | Software | **SW** | Программные лицензии | +| *(прочее)* | — | **Other** | Гарантийные опции, инсталляция | + +--- + +## Правила именования LOT + +Формат: `КАТЕГОРИЯ_МОДЕЛЬСХД_СПЕЦИФИКА` + +- только латиница, цифры и знак `_` +- регистр — ВЕРХНИЙ +- без пробелов, дефисов, точек +- каждый LOT уникален — два разных компонента не могут иметь одинаковое имя + +### DKC — контроллерная полка + +Специфика: `ТИПДИСКА_СЛОТЫ_NCTRL` + +| Пример | Расшифровка | +|---|---| +| `DKC_DE4000H_SFF_24_2CTRL` | DE4000H, 24 слота SFF (2.5"), 2 контроллера | +| `DKC_DE4000H_LFF_12_2CTRL` | DE4000H, 12 слотов LFF (3.5"), 2 контроллера | +| `DKC_DE4000H_SFF_24_1CTRL` | DE4000H, 24 слота SFF, 1 контроллер (симплекс) | + +Обозначения типа диска: `SFF` — 2.5", `LFF` — 3.5", `NVMe` — U.2/U.3. + +### CTL — контроллер + +Специфика: `КЭШГБ_ПОРТЫТИП` (если встроенные порты есть) или `КЭШГБ_BASE` (если без портов, добавляются через HIC) + +| Пример | Расшифровка | +|---|---| +| `CTL_DE4000H_32GB_BASE` | 32GB кэш, без встроенных хост-портов | +| `CTL_DE4000H_8GB_BASE` | 8GB кэш, без встроенных хост-портов | +| `CTL_MSA2060_8GB_ISCSI10G_4P` | 8GB кэш, встроенные 4× iSCSI 10GbE | + +### HIC — HIC-карты (интерфейс подключения) + +Специфика: `NpПРОТОКОЛ` — без привязки к модели СХД, по аналогии с серверными `HBA_2pFC16`, `HBA_4pFC32_Gen6`. + +| Пример | Расшифровка | +|---|---| +| `HIC_4pFC32` | 4 порта FC 32Gb | +| `HIC_4pFC16` | 4 порта FC 16G/10GbE | +| `HIC_4p25G_iSCSI` | 4 порта 25G iSCSI | +| `HIC_4p12G_SAS` | 4 порта SAS 12Gb | +| `HIC_2p10G_BaseT` | 2 порта 10G Base-T | + +### HDD / SSD / NVMe — диски + +Диски **не привязываются к модели СХД** — используются существующие LOT из серверного каталога (`HDD_...`, `SSD_...`, `NVME_...`). Новые LOT для дисков СХД не создаются; партномера дисков маппируются на уже существующие серверные LOT. + +### ACC — кабели + +Кабели **не привязываются к модели СХД**. Формат: `ACC_CABLE_{ТИП}_{ДЛИНА}` — универсальные LOT, одинаковые для серверов и СХД. + +| Пример | Расшифровка | +|---|---| +| `ACC_CABLE_CAT6_10M` | Кабель CAT6 10м | +| `ACC_CABLE_FC_OM4_3M` | Кабель FC LC-LC OM4 до 3м | +| `ACC_CABLE_PWR_C13C14_15M` | Кабель питания C13–C14 1.5м | + +### SW — программные лицензии + +Специфика: краткое название функции. + +| Пример | Расшифровка | +|---|---| +| `SW_DE4000H_ASYNC_MIRROR` | Async Mirroring | +| `SW_DE4000H_SNAPSHOT_512` | Snapshot 512 | + +--- + +## Таблица лотов: DE4000H (пример заполнения) + +### DKC — контроллерная полка + +| lot_name | vendor | model | description | disk_slots | disk_type | controllers | +|---|---|---|---|---|---|---| +| `DKC_DE4000H_SFF_24_2CTRL` | Lenovo | DE4000H 2U24 | DE4000H, 24× SFF, 2 контроллера | 24 | SFF | 2 | +| `DKC_DE4000H_LFF_12_2CTRL` | Lenovo | DE4000H 2U12 | DE4000H, 12× LFF, 2 контроллера | 12 | LFF | 2 | + +### CTL — контроллер + +| lot_name | vendor | model | description | cache_gb | host_ports | +|---|---|---|---|---|---| +| `CTL_DE4000H_32GB_BASE` | Lenovo | DE4000 Controller 32GB Gen2 | Контроллер DE4000, 32GB кэш, без встроенных портов | 32 | — | +| `CTL_DE4000H_8GB_BASE` | Lenovo | DE4000 Controller 8GB Gen2 | Контроллер DE4000, 8GB кэш, без встроенных портов | 8 | — | + +### HIC — HIC-карты + +| lot_name | vendor | model | description | +|---|---|---|---| +| `HIC_2p10G_BaseT` | Lenovo | HIC 10GBASE-T 2-Ports | HIC 10GBASE-T, 2 порта | +| `HIC_4p25G_iSCSI` | Lenovo | HIC 10/25GbE iSCSI 4-ports | HIC iSCSI 10/25GbE, 4 порта | +| `HIC_4p12G_SAS` | Lenovo | HIC 12Gb SAS 4-ports | HIC SAS 12Gb, 4 порта | +| `HIC_4pFC32` | Lenovo | HIC 32Gb FC 4-ports | HIC FC 32Gb, 4 порта | +| `HIC_4pFC16` | Lenovo | HIC 16G FC/10GbE 4-ports | HIC FC 16G/10GbE, 4 порта | + +### HDD / SSD / NVMe / ACC — диски и кабели + +Для дисков и кабелей новые LOT не создаются. Партномера маппируются на существующие серверные LOT из каталога. + +### SW — программные лицензии + +| lot_name | vendor | model | description | +|---|---|---|---| +| `SW_DE4000H_ASYNC_MIRROR` | Lenovo | DE4000H Asynchronous Mirroring | Лицензия Async Mirroring | +| `SW_DE4000H_SNAPSHOT_512` | Lenovo | DE4000H Snapshot Upgrade 512 | Лицензия Snapshot 512 | +| `SW_DE4000H_SYNC_MIRROR` | Lenovo | DE4000 Synchronous Mirroring | Лицензия Sync Mirroring | + +--- + +## Таблица партномеров: DE4000H (пример заполнения) + +Каждый Feature Code и Part Number должен быть привязан к своему LOT. +Если у компонента есть оба — добавить две строки. + +| partnumber | lot_name | описание | +|---|---|---| +| `BEY7` | `ENC_2U24_CHASSIS` | Lenovo ThinkSystem Storage 2U24 Chassis | +| `BQA0` | `CTL_DE4000H_32GB_BASE` | DE4000 Controller 32GB Gen2 | +| `BQ9Z` | `CTL_DE4000H_8GB_BASE` | DE4000 Controller 8GB Gen2 | +| `B4B1` | `HIC_2p10G_BaseT` | HIC 10GBASE-T 2-Ports | +| `4C57A14376` | `HIC_2p10G_BaseT` | HIC 10GBASE-T 2-Ports | +| `B4BA` | `HIC_4p25G_iSCSI` | HIC 10/25GbE iSCSI 4-ports | +| `4C57A14369` | `HIC_4p25G_iSCSI` | HIC 10/25GbE iSCSI 4-ports | +| `B4B8` | `HIC_4p12G_SAS` | HIC 12Gb SAS 4-ports | +| `4C57A14367` | `HIC_4p12G_SAS` | HIC 12Gb SAS 4-ports | +| `B4B9` | `HIC_4pFC32` | HIC 32Gb FC 4-ports | +| `4C57A14368` | `HIC_4pFC32` | HIC 32Gb FC 4-ports | +| `B4B7` | `HIC_4pFC16` | HIC 16G FC/10GbE 4-ports | +| `4C57A14366` | `HIC_4pFC16` | HIC 16G FC/10GbE 4-ports | +| `BW12` | `HDD_SAS_02.4TB` | 2.4TB 10K 2.5" HDD 2U24 | +| `4XB7A88046` | `HDD_SAS_02.4TB` | 2.4TB 10K 2.5" HDD 2U24 | +| `B4C0` | `HDD_SAS_01.8TB` | 1.8TB 10K 2.5" HDD SED FIPS | +| `4XB7A14114` | `HDD_SAS_01.8TB` | 1.8TB 10K 2.5" HDD SED FIPS | +| `BW13` | `HDD_SAS_02.4TB` | 2.4TB 10K 2.5" HDD FIPS | +| `4XB7A88048` | `HDD_SAS_02.4TB` | 2.4TB 10K 2.5" HDD FIPS | +| `BKUQ` | `SSD_SAS_0.960T` | 960GB 1DWD 2.5" SSD | +| `4XB7A74948` | `SSD_SAS_0.960T` | 960GB 1DWD 2.5" SSD | +| `BKUT` | `SSD_SAS_01.92T` | 1.92TB 1DWD 2.5" SSD | +| `4XB7A74951` | `SSD_SAS_01.92T` | 1.92TB 1DWD 2.5" SSD | +| `BKUK` | `SSD_SAS_03.84T` | 3.84TB 1DWD 2.5" SSD | +| `4XB7A74955` | `SSD_SAS_03.84T` | 3.84TB 1DWD 2.5" SSD | +| `B4RY` | `SSD_SAS_07.68T` | 7.68TB 1DWD 2.5" SSD | +| `4XB7A14176` | `SSD_SAS_07.68T` | 7.68TB 1DWD 2.5" SSD | +| `B4CD` | `SSD_SAS_15.36T` | 15.36TB 1DWD 2.5" SSD | +| `4XB7A14110` | `SSD_SAS_15.36T` | 15.36TB 1DWD 2.5" SSD | +| `BWCJ` | `SSD_SAS_03.84T` | 3.84TB 1DWD 2.5" SSD FIPS | +| `4XB7A88469` | `SSD_SAS_03.84T` | 3.84TB 1DWD 2.5" SSD FIPS | +| `BW2B` | `SSD_SAS_15.36T` | 15.36TB 1DWD 2.5" SSD SED | +| `4XB7A88466` | `SSD_SAS_15.36T` | 15.36TB 1DWD 2.5" SSD SED | +| `AVFW` | `ACC_CABLE_CAT6_1M` | CAT6 0.75-1.5m | +| `A1MT` | `ACC_CABLE_CAT6_10M` | CAT6 10m | +| `90Y3718` | `ACC_CABLE_CAT6_10M` | CAT6 10m | +| `A1MW` | `ACC_CABLE_CAT6_25M` | CAT6 25m | +| `90Y3727` | `ACC_CABLE_CAT6_25M` | CAT6 25m | +| `39Y7937` | `ACC_CABLE_PWR_C13C14_15M` | C13–C14 1.5m | +| `39Y7938` | `ACC_CABLE_PWR_C13C20_28M` | C13–C20 2.8m | +| `4L67A08371` | `ACC_CABLE_PWR_C13C14_43M` | C13–C14 4.3m | +| `C932` | `SW_DE4000H_ASYNC_MIRROR` | DE4000H Asynchronous Mirroring | +| `00WE123` | `SW_DE4000H_ASYNC_MIRROR` | DE4000H Asynchronous Mirroring | +| `C930` | `SW_DE4000H_SNAPSHOT_512` | DE4000H Snapshot Upgrade 512 | +| `C931` | `SW_DE4000H_SYNC_MIRROR` | DE4000 Synchronous Mirroring | + +--- + +## Шаблон для новых моделей СХД + +``` +DKC_МОДЕЛЬ_ТИПДИСКА_СЛОТЫ_NCTRL — контроллерная полка +CTL_МОДЕЛЬ_КЭШГБ_ПОРТЫ — контроллер +HIC_МОДЕЛЬ_ПРОТОКОЛ_СКОРОСТЬ_ПОРТЫ — HIC-карта (интерфейс подключения) +SW_МОДЕЛЬ_ФУНКЦИЯ — лицензия +``` + +Диски (HDD/SSD/NVMe) и кабели (ACC) — маппируются на существующие серверные LOT, новые не создаются. + +Пример для HPE MSA 2060: +``` +DKC_MSA2060_SFF_24_2CTRL +CTL_MSA2060_8GB_ISCSI10G_4P +HIC_MSA2060_FC32G_2P +SW_MSA2060_REMOTE_SNAP +``` diff --git a/internal/localdb/converters.go b/internal/localdb/converters.go index 77cb3e1..f0bdf2a 100644 --- a/internal/localdb/converters.go +++ b/internal/localdb/converters.go @@ -34,6 +34,7 @@ func ConfigurationToLocal(cfg *models.Configuration) *LocalConfiguration { PricelistID: cfg.PricelistID, WarehousePricelistID: cfg.WarehousePricelistID, CompetitorPricelistID: cfg.CompetitorPricelistID, + ConfigType: cfg.ConfigType, VendorSpec: modelVendorSpecToLocal(cfg.VendorSpec), DisablePriceRefresh: cfg.DisablePriceRefresh, OnlyInStock: cfg.OnlyInStock, @@ -82,6 +83,7 @@ func LocalToConfiguration(local *LocalConfiguration) *models.Configuration { PricelistID: local.PricelistID, WarehousePricelistID: local.WarehousePricelistID, CompetitorPricelistID: local.CompetitorPricelistID, + ConfigType: local.ConfigType, VendorSpec: localVendorSpecToModel(local.VendorSpec), DisablePriceRefresh: local.DisablePriceRefresh, OnlyInStock: local.OnlyInStock, diff --git a/internal/localdb/models.go b/internal/localdb/models.go index 47cdc9c..a636e9e 100644 --- a/internal/localdb/models.go +++ b/internal/localdb/models.go @@ -110,6 +110,7 @@ type LocalConfiguration struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` SyncedAt *time.Time `json:"synced_at"` + ConfigType string `gorm:"default:server" json:"config_type"` // "server" | "storage" SyncStatus string `gorm:"default:'local'" json:"sync_status"` // 'local', 'synced', 'modified' OriginalUserID uint `json:"original_user_id"` // UserID from MariaDB for reference OriginalUsername string `gorm:"not null;default:'';index" json:"original_username"` diff --git a/internal/models/configuration.go b/internal/models/configuration.go index 08d5309..b15ca5c 100644 --- a/internal/models/configuration.go +++ b/internal/models/configuration.go @@ -111,6 +111,7 @@ type Configuration struct { WarehousePricelistID *uint `gorm:"index" json:"warehouse_pricelist_id,omitempty"` CompetitorPricelistID *uint `gorm:"index" json:"competitor_pricelist_id,omitempty"` VendorSpec VendorSpec `gorm:"type:json" json:"vendor_spec,omitempty"` + ConfigType string `gorm:"size:20;default:server" json:"config_type"` // "server" | "storage" DisablePriceRefresh bool `gorm:"default:false" json:"disable_price_refresh"` OnlyInStock bool `gorm:"default:false" json:"only_in_stock"` Line int `gorm:"column:line_no;index" json:"line"` diff --git a/internal/models/models.go b/internal/models/models.go index f0006cf..7680633 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -31,7 +31,9 @@ func Migrate(db *gorm.DB) error { errStr := err.Error() if strings.Contains(errStr, "Can't DROP") || strings.Contains(errStr, "Duplicate key name") || - strings.Contains(errStr, "check that it exists") { + strings.Contains(errStr, "check that it exists") || + strings.Contains(errStr, "Cannot change column") || + strings.Contains(errStr, "used in a foreign key constraint") { slog.Warn("migration warning (skipped)", "model", model, "error", errStr) continue } diff --git a/internal/services/configuration.go b/internal/services/configuration.go index d1a0224..8fcaec6 100644 --- a/internal/services/configuration.go +++ b/internal/services/configuration.go @@ -58,6 +58,7 @@ type CreateConfigRequest struct { PricelistID *uint `json:"pricelist_id,omitempty"` WarehousePricelistID *uint `json:"warehouse_pricelist_id,omitempty"` CompetitorPricelistID *uint `json:"competitor_pricelist_id,omitempty"` + ConfigType string `json:"config_type,omitempty"` // "server" | "storage" DisablePriceRefresh bool `json:"disable_price_refresh"` OnlyInStock bool `json:"only_in_stock"` } @@ -103,9 +104,13 @@ func (s *ConfigurationService) Create(ownerUsername string, req *CreateConfigReq PricelistID: pricelistID, WarehousePricelistID: req.WarehousePricelistID, CompetitorPricelistID: req.CompetitorPricelistID, + ConfigType: req.ConfigType, DisablePriceRefresh: req.DisablePriceRefresh, OnlyInStock: req.OnlyInStock, } + if config.ConfigType == "" { + config.ConfigType = "server" + } if err := s.configRepo.Create(config); err != nil { return nil, err diff --git a/internal/services/local_configuration.go b/internal/services/local_configuration.go index 5ea9b7d..313292a 100644 --- a/internal/services/local_configuration.go +++ b/internal/services/local_configuration.go @@ -101,10 +101,14 @@ func (s *LocalConfigurationService) Create(ownerUsername string, req *CreateConf PricelistID: pricelistID, WarehousePricelistID: req.WarehousePricelistID, CompetitorPricelistID: req.CompetitorPricelistID, + ConfigType: req.ConfigType, DisablePriceRefresh: req.DisablePriceRefresh, OnlyInStock: req.OnlyInStock, CreatedAt: time.Now(), } + if cfg.ConfigType == "" { + cfg.ConfigType = "server" + } // Convert to local model localCfg := localdb.ConfigurationToLocal(cfg) diff --git a/migrations/029_add_config_type.sql b/migrations/029_add_config_type.sql new file mode 100644 index 0000000..d41625d --- /dev/null +++ b/migrations/029_add_config_type.sql @@ -0,0 +1,2 @@ +ALTER TABLE qt_configurations + ADD COLUMN config_type VARCHAR(20) NOT NULL DEFAULT 'server'; diff --git a/web/templates/configs.html b/web/templates/configs.html index dc497fc..2d3488d 100644 --- a/web/templates/configs.html +++ b/web/templates/configs.html @@ -53,6 +53,19 @@

Новая конфигурация

+
+ +
+ + +
+
Accessories + + +
+