Сохранение:
- restoreAutosaveDraftIfAny теперь восстанавливает pricing_ui из notes драфта
- saveConfigOnExit привязан к pagehide и visibilitychange — цены сохраняются
на сервер при уходе со страницы без явного нажатия «Сохранить»
Экспорт CSV:
- exportPricingCSV передаёт manual_price (buy для FOB, sale для DDP)
- ProjectPricingExportOptions.ManualPrice *float64 — новое поле
- distributeManualPrice распределяет ручную цену пропорционально estimate
с коррекцией остатка на последней строке
- Колонка «Ручная цена» в CSV (заголовок, строки, итог конфига)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
buildPricingExportBlock теперь создаёт одну строку на каждый LOT mapping,
а не одну строку на BOM-строку. BOM-цена ставится только в первую подстроку
(как vendorOrig в фронтенде). Добавлен computeSingleLotTotal, удалён
неиспользуемый formatLotDisplay.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Добавлен новый вариант импорта спеки: quantity-first формат, где каждая
строка начинается с `<qty>x <description>` (например, «2x Intel Xeon 8570»).
Порядок детекции: Inspur → Nx → Text BOM. Заголовок «, в составе:» работает
так же, как в Text BOM — последний токен перед запятой становится server_model.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
modernc.org/sqlite (glebarez/sqlite) не приводит BLOB к TEXT при LIKE,
в отличие от нативного sqlite3. lots_json хранится как BLOB (json.Marshal
возвращает []byte), поэтому `lots_json LIKE ?` всегда возвращал 0.
Исправлено на CAST(lots_json AS TEXT) LIKE — теперь поиск по LOT имени
работает корректно.
Заодно обновлён плейсхолдер поля поиска: «PN, LOT или описание».
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ensureConfigurationProject: если project не найден ни на сервере, ни локально
(stale UUID после удаления), падаем в fallback «Без проекта» вместо вечной ошибки
- PushPendingChanges: автоматически вызывает RepairPendingChanges() перед циклом,
чтобы локально-исправимые проблемы чинились до попытки отправки
- maxPendingChangeAttempts=20: после 20 неудачных попыток change считается
unrecoverable и удаляется из очереди (логируется ERROR)
- pushSingleChange/pushConfigurationChange: unknown entity type / operation
теперь дропается с warn вместо вечного error в цикле
- latestSyncErrorState: last_sync_error_text в qt_client_schema_state теперь
содержит JSON-массив с type/uuid/op/attempts/error по всем застрявшим changes
(до 20 штук) вместо текста только последней ошибки
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PullPartnumberBooks вызывается автоматически после каждой синхронизации
прайслистов — в фоновом воркере, при ручном триггере /api/sync/pricelists
и при полной синхронизации /api/sync/all. Отдельная кнопка «Синхронизировать»
на странице Партномера удалена.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
При сохранении vendor-spec строки с заполненным lot_mappings автоматически
отправляются на сервер и пишутся в новый столбец lot_suggestion. Столбец
хранит JSON-массив [{lot_name, qty}] — тот же формат, что qt_partnumber_book_items.lots_json.
Если миграция ещё не прошла (столбец отсутствует), приложение логирует WARN
и записывает строку без столбца; сбоя нет.
Контракт для инструмента создания partnumber-books описан в bible-local/11-lot-suggestions.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Паста BOM на странице конфигурирования теперь распознаёт текстовый и
Inspur-форматы: вместо дублирования парсера на JS добавлен stateless
эндпоинт POST /api/vendor-spec/parse-text, который использует те же
детекторы и парсеры, что и импорт файла (KISS — один парсер на оба
входа). JS-копии _parseInspurBOMText/_isInspurBOMText удалены.
Заголовок конфигурации определяется по маркеру ", в составе:" с любым
префиксом ("Сервер X3" и "Вычислительный GPU сервер X3" → модель X3);
строки тримятся, пробел в начале не попадает в P/N; запятые и дефисы
внутри описания сохраняются (RAID0,1,10; 8-GPU-2304GB).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Новый формат vendor-import: опциональный заголовок "Сервер <модель>,
в составе:" и строки вида "<описание> - <кол-во> шт." (дефис/тире,
пробел перед "шт" и точка опциональны). Количество якорится в конце
строки, поэтому дефисы и цифры внутри описания (8-GPU-2304GB) сохраняются.
Описание пишется и в vendor_partnumber, и в description: строки
резолвятся через активную книгу партномеров, иначе остаются
нерезолвленными и редактируемыми. Весь файл — одна конфигурация.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Удалены модели, репозитории и авто-миграции для трёх таблиц, которые
никогда не использовались в продакшн-коде. Убраны StatsRepository и
RecordUsage из сервисов, сигнатуры конструкторов упрощены.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ExportConfigCSV и ExportConfigPricingCSV: GetByUUID → GetByUUIDNoAuth,
чтобы не падать с ErrConfigForbidden на конфигах с чужим OriginalUsername
- ConfigurationGetter: добавлен GetByUUIDNoAuth в интерфейс
- Шаблоны: toLocaleString('en-US') → 'ru-RU' во всех местах отображения цен;
процентные значения: .toFixed(1) → .toFixed(1).replace('.', ',')
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Удалить все записи в qt_pricelist_sync_status (RecordSyncHeartbeat и
ensureUserSyncStatusTable); ListUserSyncStatuses переведён на
qt_client_schema_state
- Подавлять устаревший OFFLINE_UNVERIFIED_SCHEMA в UI когда соединение
уже восстановлено
- Удалить все обращения к lot_log: repository/price.go, сортировка
quote_count в component.go, UpdatePopularityScores в stats.go,
модель LotLog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Раньше NeedSync возвращал true сразу если last_sync > 1 часа назад —
до сравнения версий дело не доходило. Это приводило к бесконечным
повторным попыткам синка когда все прайслисты уже скачаны, но
last_pricelist_status застрял в "failed" из-за предыдущего сбоя.
Теперь когда онлайн — всегда сравниваем реальные версии с сервером.
Если все источники совпадают — возвращаем false независимо от времени
последнего синка. Фолбэк на 1-часовой порог только в офлайн-режиме.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
После сетевого сбоя во время синка прайслистов last_pricelist_status
мог оставаться "failed" навсегда, даже если все прайслисты реально
скачались и NeedSync() возвращает false (всё актуально).
В SyncPricelistsIfNeeded: если NeedSync() == false и статус "failed" —
сбрасываем в success и обновляем last_sync_time, чтобы UI убрал "Не докачано".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Раньше ensureClientSchemaStateTable запускался на каждом цикле синка
(каждые 5 минут) и пытался ALTER TABLE, даже если все колонки уже были.
Для пользователей без DDL-прав это давало WARN-спам в каждом цикле.
Два изменения:
- schemaOnce (sync.Once) на Service: ensureClientSchemaStateTable
вызывается не более одного раза за жизнь процесса
- columnExists() проверяет information_schema.COLUMNS перед каждым
ALTER — если колонка уже есть, ALTER пропускается без ошибки
Если таблица уже мигрирована сервером, клиент молча пропускает все DDL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Добавлена таблица sync_log (до 100 записей на тип): фиксирует каждый
запуск синхронизации с типом, статусом, ошибкой, кол-вом и временем
- AppendSyncLog вызывается из SyncComponents, SyncPricelists (service и
handler), SyncAll и SyncComponentsIfEmpty
- Bundle теперь включает sync_log.json (200 последних записей) и
pricelists.json (все скачанные прайслисты, сгруппированные по source)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Воркер теперь запускает SyncComponents при пустой local_components,
чтобы новый пользователь получил каталог компонентов без ручного действия
- Результат синхронизации компонентов персистируется в app_settings
(last_component_sync_status/error/attempt_at) по аналогии с прайслистами
- Добавлен эндпоинт GET /api/support-bundle: скачивает ZIP с диагностикой
(app_info, local_db_stats, db_connection с TCP-пингом, sync_readiness,
system_metrics с памятью и диском, schema_migrations, app.log)
- Кнопка-иконка в шапке рядом с юзернеймом для скачивания бандла
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Поиск в categoryOrder не нормализовал регистр — категория "mem" не совпадала
с ключом "MEM", получала порядок 9999 и строки шли в произвольном порядке.
Заодно заменён bubble-sort на sort.SliceStable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Коды брались из локальных компонентов, но display_order не проставлялся —
поэтому categoryOrderMap на фронте был пустым и порядок в итоговой
конфигурации не соблюдался. Теперь display_order берётся из DefaultCategories.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Категория лота приходит из прайслиста — запрашивать её из серверной БД
нарушало принцип local-first. Сигнатура NewExportService упрощена,
все call-sites обновлены.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
categoryRepo всегда nil (передаётся null при инициализации), поэтому
categoryOrder был пустым и сортировка по категориям не работала.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SeedCategories теперь обновляет display_order у существующих записей,
поэтому новый порядок применяется при следующем запуске без ручных миграций.
MaxKnownDisplayOrder повышен до 200.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Добавлен парсер собственного CSV-экспорта QuoteForge (IsQuoteForgeCSV /
parseQuoteForgeCSV). Формат: UTF-8 BOM + заголовок Line;Type;p/n;...,
блоки сервер → компоненты. DirectItems создаются напрямую без прохода
через VendorSpecResolver. Модальное окно импорта принимает .csv/.txt/.xml.
Fix кнопки «Обновить цены» на странице варианта: после синхронизации
прайс-листов запрашивается актуальный estimate-прайслист и передаётся
явным pricelist_id в каждый POST /api/configs/:uuid/refresh-prices.
Ранее использовался устаревший ID, сохранённый в конфигурации.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Добавлен парсер для текстового формата Inspur (опциональный '|' в начале
строки, разделитель '*' перед количеством). На BOM-вкладке вставка такого
текста автоматически определяется и разбивается на колонки P/N + Qty без
ручного выбора типов. На бэкенде тот же формат поддерживается через
POST /api/projects/:uuid/vendor-import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Вынести sortConfigsByLine() — устранить дублирование sort.Slice
в ProjectToExportData и ProjectToPricingExportData
- Добавить ConfigToPricingExportData() и ExportConfigPricingCSV handler
- Зарегистрировать POST /api/configs/:uuid/export/pricing
- Заменить клиентский DOM-скрапинг exportPricingCSV() на fetch к новому
endpoint; артикул теперь включается через pricingConfigSummaryRow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
compressArticle assumed a fixed segment layout [model,cpu,mem,gpu,disk,net,psu,support].
Build() skips empty segments, so without GPU the PSU slot shifted to index 5. Step 2 of
compression called compressNetSegment on the PSU value ("2x1kW") which returned "2xNIC".
Replace positional indexing with namedSeg{group,value} tagged segments; compressArticle
now looks up each segment by group name via findSegGroup(), regardless of array length.
Regression test TestBuild_CompressArticle_NoGPU_PSUNotNIC reproduces the exact config
from OPS-2445 (NF5280M6 + 2xPSU, no GPU) that triggered the bug.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ampere, Hopper, Blackwell now produce AMP/HOP/BWL suffixes (like ADA)
so RTX cards across generations are distinguishable: RTX6000ADA vs
RTX6000BWL. LOVELACE remains a skip token as it duplicates ADA info.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RTX 6000 ADA and A6000 are distinct cards — RTX_4000_ADA_SFF now
produces RTX4000ADA instead of RTX, avoiding visual ambiguity with
the segment separator (10xRTX4000ADA vs 10xRTX-1x…).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add config_type field ("server"|"storage") to Configuration and LocalConfiguration
- Create modal: Сервер/СХД segmented control in configs.html and project_detail.html
- Configurator: ENC/DKC/CTL categories in Base tab, HIC section in PCI tab hidden for server configs
- Add SW tab (categories: SW) to configurator, visible only when components present
- TAB_CONFIG.pci: add HIC section for storage HIC adapters (separate from server HBA/NIC)
- Migration 029: ALTER TABLE qt_configurations ADD COLUMN config_type
- Fix: skip Error 1833 (Cannot change column used in FK) in GORM AutoMigrate
- Operator guide: docs/storage-components-guide.md with LOT naming rules and DE4000H catalog template
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SyncPricelistsIfNeeded was called synchronously in Create(), blocking
the HTTP response for several seconds while pricelist data was fetched.
Users clicking multiple times caused 6+ duplicate configurations.
- Run SyncPricelistsIfNeeded in a goroutine so Create() returns immediately
- Add TryLock mutex to SyncPricelistsIfNeeded to skip concurrent calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ErrCannotRenameMainVariant; ProjectService.Update now returns
this error if the caller tries to change the Variant of a main
project (empty Variant) — ensures there is always exactly one main
- Handle ErrCannotRenameMainVariant in PUT /api/projects/:uuid with 400
- Set document.title dynamically from breadcrumb data:
- Configurator: "CODE / variant / Config name — QuoteForge"
- Project detail: "CODE / variant — QuoteForge"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove 'auto (latest active)' option from pricelist dropdowns; new
configs pre-select the first active pricelist instead
- Stop resetting stored pricelist_id to null when it is not in the
active list (deactivated pricelists are shown as inactive options)
- RefreshPricesNoAuth now accepts an optional pricelist_id; uses the
UI-selected pricelist, then the config's stored pricelist, then
latest as a last-resort fallback — no longer silently overwrites
the stored pricelist on every price refresh
- Same fix applied to RefreshPrices (with auth)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Render competitor prices in Pricing tab (all three row branches)
- Add footer total accumulation for competitor column
- Deduplicate local_pricelist_items via migration + unique index
- Use ON CONFLICT DO NOTHING in SaveLocalPricelistItems to prevent duplicates on concurrent sync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>