Commit Graph

131 Commits

Author SHA1 Message Date
3992dbf919 refactor: убрать qt_pricelist_sync_status, lot_log и лишние права БД
- Удалить все записи в 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>
2026-06-02 16:18:52 +03:00
5d5af07fc5 fix: NeedSync проверяет версии сервера когда онлайн, игнорируя 1-часовой порог
Раньше NeedSync возвращал true сразу если last_sync > 1 часа назад —
до сравнения версий дело не доходило. Это приводило к бесконечным
повторным попыткам синка когда все прайслисты уже скачаны, но
last_pricelist_status застрял в "failed" из-за предыдущего сбоя.

Теперь когда онлайн — всегда сравниваем реальные версии с сервером.
Если все источники совпадают — возвращаем false независимо от времени
последнего синка. Фолбэк на 1-часовой порог только в офлайн-режиме.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 14:06:18 +03:00
8d965bfee9 fix: сбрасывать stale pricelist "failed" когда NeedSync подтверждает актуальность
После сетевого сбоя во время синка прайслистов 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>
2026-06-02 13:55:43 +03:00
0072f2a15f fix: ALTER spam в логах — DDL на qt_client_schema_state только при нужде
Раньше 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>
2026-06-02 13:02:40 +03:00
452811f393 feat: sync_log таблица и список прайслистов в Support Bundle
- Добавлена таблица 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>
2026-06-02 12:57:28 +03:00
84cab011d3 feat: автосинхронизация компонентов для новых пользователей и Support Bundle
- Воркер теперь запускает 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>
2026-06-02 12:50:41 +03:00
caf1732cd3 fix: сортировка категорий в CSV-экспорте без учёта регистра
Поиск в categoryOrder не нормализовал регистр — категория "mem" не совпадала
с ключом "MEM", получала порядок 9999 и строки шли в произвольном порядке.
Заодно заменён bubble-sort на sort.SliceStable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:14:11 +03:00
e58f5774ab fix: /api/categories возвращал display_order=0 для всех категорий
Коды брались из локальных компонентов, но display_order не проставлялся —
поэтому categoryOrderMap на фронте был пустым и порядок в итоговой
конфигурации не соблюдался. Теперь display_order берётся из DefaultCategories.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:09:15 +03:00
ddc00523e0 refactor: убрать categoryRepo из ExportService, порядок из DefaultCategories
Категория лота приходит из прайслиста — запрашивать её из серверной БД
нарушало принцип local-first. Сигнатура NewExportService упрощена,
все call-sites обновлены.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:06:39 +03:00
ff262822e1 fix: использовать DefaultCategories как fallback для сортировки в CSV-экспорте
categoryRepo всегда nil (передаётся null при инициализации), поэтому
categoryOrder был пустым и сортировка по категориям не работала.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:05:09 +03:00
6049334323 refactor: переработать порядок категорий (MB→CPU→MEM→RAID→drives→GPU→NIC→HBA→PSU→ACC)
SeedCategories теперь обновляет display_order у существующих записей,
поэтому новый порядок применяется при следующем запуске без ручных миграций.
MaxKnownDisplayOrder повышен до 200.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:03:19 +03:00
5d4e1b44f6 feat: импорт собственного CSV QuoteForge + fix обновления цен
Добавлен парсер собственного 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>
2026-05-24 18:54:08 +03:00
67a761345f feat: поддержка импорта BOM Inspur в формате PN*qty
Добавлен парсер для текстового формата Inspur (опциональный '|' в начале
строки, разделитель '*' перед количеством). На BOM-вкладке вставка такого
текста автоматически определяется и разбивается на колонки P/N + Qty без
ручного выбора типов. На бэкенде тот же формат поддерживается через
POST /api/projects/:uuid/vendor-import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:04:10 +03:00
Mikhail Chusavitin
55acbe138b refactor: унифицировать CSV-экспорт, перенести pricing на сервер
- Вынести 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>
2026-05-19 12:37:47 +03:00
Mikhail Chusavitin
e1f34ae81b fix: compressArticle used hard-coded indices, PSU rendered as NIC when GPU absent
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>
2026-05-19 12:36:55 +03:00
Mikhail Chusavitin
66ff7e25a6 Fix pricelist sync upsert and refresh tests 2026-04-28 16:54:36 +03:00
Mikhail Chusavitin
f18df01618 Persist pricing state and refresh storage sync 2026-04-15 18:56:40 +03:00
Mikhail Chusavitin
df3cd62cb5 Fix storage sync and configurator category visibility 2026-04-15 18:40:34 +03:00
Mikhail Chusavitin
89ce001906 fix: abbreviate GPU architecture suffixes in article token
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>
2026-04-09 15:08:47 +03:00
Mikhail Chusavitin
6a41c957cc fix: include model number and ADA suffix in GPU article token
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>
2026-04-09 15:07:25 +03:00
Mikhail Chusavitin
19b1abf4c8 feat: add СХД configuration type with storage-specific tabs and LOT catalog guide
- 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>
2026-04-08 18:01:23 +03:00
Mikhail Chusavitin
7966ece7a6 feat: redesign project pricing export — FOB/DDP basis, variant filename, article column
- Add FOB/DDP basis to export options; DDP multiplies all prices ×1.3
- Rename export file from "pricing" to "{FOB|DDP} {variant}" (e.g. "FOB v1")
- Fix server article missing from CSV summary row (PN вендора column)
- Skip per-row breakdown when neither LOT nor BOM is selected
- Remove empty separator rows between configurations
- Redesign export modal: split into Артикул / Цены / Базис поставки sections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 17:55:26 +03:00
Mikhail Chusavitin
ae7d8911c6 perf: enable WAL mode, batch price lookup, add DB diagnostics to schema_state
- Set PRAGMA journal_mode=WAL + synchronous=NORMAL on SQLite open;
  eliminates read blocking during background pricelist sync writes
- Replace N+1 per-lot price loop in QuoteService local fallback with
  GetLocalPricesForLots batch query (120 queries → 3 per price-levels call)
- Add CountAllPricelistItems, CountComponents, DBFileSizeBytes to LocalDB
- Report local_pricelist_count, pricelist_items_count, components_count,
  db_size_bytes in qt_client_schema_state for performance diagnostics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 10:53:36 +03:00
Mikhail Chusavitin
82dcee74c5 fix: prevent config creation hang on pricelist sync
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>
2026-03-30 12:34:57 +03:00
Mikhail Chusavitin
1745c8fdd6 fix: block renaming main project variant; dynamic page titles
- 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>
2026-03-24 17:29:02 +03:00
Mikhail Chusavitin
65641ae49a fix: pricelist selection preserved when opening configurations
- 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>
2026-03-24 15:24:57 +03:00
Mikhail Chusavitin
0b0b38c29d Show build version in page footer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 17:51:13 +03:00
Mikhail Chusavitin
c20da96788 Make sync status non-blocking 2026-03-17 18:34:28 +03:00
Mikhail Chusavitin
5848eebf4c Version BOM and pricing changes 2026-03-17 18:24:09 +03:00
Mikhail Chusavitin
407ef52d28 Fix incomplete pricelist sync status 2026-03-17 12:05:02 +03:00
Mikhail Chusavitin
73e7f0ce11 fix(qfs): project ui, config naming, sync timestamps - v1.5.4 2026-03-16 08:32:15 +03:00
Mikhail Chusavitin
98d8b40282 Simplify project documentation and release notes 2026-03-15 16:43:06 +03:00
Mikhail Chusavitin
8e7da97394 Harden local runtime safety and error handling 2026-03-15 16:28:32 +03:00
Mikhail Chusavitin
c02286a407 Redesign pricelist detail: differentiated layout by source type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 13:14:14 +03:00
Mikhail Chusavitin
a3c26f015b Fix competitor price display and pricelist item deduplication
- 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>
2026-03-13 10:33:04 +03:00
Mikhail Chusavitin
84013c9dc4 Local-first runtime cleanup and recovery hardening 2026-03-07 23:18:07 +03:00
Mikhail Chusavitin
d026c28ea7 Add vendor workspace import and pricing export workflow 2026-03-07 21:03:40 +03:00
Mikhail Chusavitin
08a8113949 Fix article generator producing 1xINTEL in GPU segment
MB_ lots (e.g. MB_INTEL_..._GPU8) are incorrectly categorized as GPU
in the pricelist. Two fixes:
- Skip MB_ lots in buildGPUSegment regardless of pricelist category
- Add INTEL to vendor token skip list in parseGPUModel (was missing,
  unlike AMD/NV/NVIDIA which were already skipped)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 10:53:31 +03:00
d82590c34b Fix price levels returning empty in offline mode
CalculatePriceLevels now falls back to localDB when pricelistRepo is nil
(offline mode) to resolve the latest pricelist ID per source. Previously
all price lookups were skipped, resulting in empty prices on the pricing tab.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 12:47:32 +03:00
Mikhail Chusavitin
ed0916d3d1 fix(bom): preserve local vendor spec on config import 2026-02-27 10:11:20 +03:00
Mikhail Chusavitin
370bd949a5 refactor(bom): enforce canonical lot_mappings persistence 2026-02-27 09:47:46 +03:00
Mikhail Chusavitin
2b63f9ec14 feat(bom): canonical lot mappings and updated vendor spec docs 2026-02-25 19:07:27 +03:00
Mikhail Chusavitin
69049eba69 Fix project line numbering and reorder bootstrap 2026-02-25 17:19:26 +03:00
Mikhail Chusavitin
ea5223dee5 fix(pricelists): tolerate restricted DB grants and use embedded assets only 2026-02-25 17:19:26 +03:00
Mikhail Chusavitin
c76a31f171 fix(sync): backfill missing items for existing local pricelists 2026-02-25 17:18:57 +03:00
512b9ca04b feat(vendor-spec): BOM import, LOT autocomplete, pricing, partnumber_seen push
- BOM paste: auto-detect columns by content (price, qty, PN, description);
  handles $5,114.00 and European comma-decimal formats
- LOT input: HTML5 datalist rebuilt on each renderBOMTable from allComponents;
  oninput updates data only (no re-render), onchange validates+resolves
- BOM persistence: PUT handler explicitly marshals VendorSpec to JSON string
  (GORM Update does not reliably call driver.Valuer for custom types)
- BOM autosave after every resolveBOM() call
- Pricing tab: async renderPricingTab() calls /api/quote/price-levels for all
  resolved LOTs directly — Estimate prices shown even before cart apply
- Unresolved PNs pushed to qt_vendor_partnumber_seen via POST
  /api/sync/partnumber-seen (fire-and-forget from JS)
- sync.PushPartnumberSeen(): upsert with ON DUPLICATE KEY UPDATE last_seen_at
- partnumber_books: pull ALL books (not only is_active=1); re-pull items when
  header exists but item count is 0; fallback for missing description column
- partnumber_books UI: collapsible snapshot section (collapsed by default),
  pagination (10/page), sync button always visible in header
- vendorSpec handlers: use GetConfigurationByUUID + IsActive check (removed
  original_username from WHERE — GetUsername returns "" without JWT)
- bible/09-vendor-spec.md: updated with all architectural decisions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:21:13 +03:00
5bad4f86e4 fix: use AutoMigrate for new SQLite tables instead of hardcoded migrations
LocalPartnumberBook and LocalPartnumberBookItem added to AutoMigrate list
in localdb.go — consistent with all other local tables. Removed incorrectly
added addPartnumberBooks/addVendorSpecColumn functions from migrations.go
(vendor_spec column is handled by AutoMigrate via the LocalConfiguration model field).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 17:07:44 +03:00
e3d322d1f1 feat: implement vendor spec BOM import and PN→LOT resolution (Phase 1)
- Migration 029: local_partnumber_books, local_partnumber_book_items,
  vendor_spec TEXT column on local_configurations
- Models: LocalPartnumberBook, LocalPartnumberBookItem, VendorSpec,
  VendorSpecItem with JSON Valuer/Scanner
- Repository: PartnumberBookRepository (GetActiveBook, FindLotByPartnumber,
  SaveBook/Items, ListBooks, CountBookItems)
- Service: VendorSpecResolver 3-step resolution (book → manual suggestion
  → unresolved) + AggregateLOTs with is_primary_pn qty logic
- Sync: PullPartnumberBooks append-only pull from qt_partnumber_books
- Handlers: VendorSpecHandler (GET/PUT/resolve/apply), PartnumberBooksHandler
- Routes: /api/configs/:uuid/vendor-spec*, /api/partnumber-books,
  /api/sync/partnumber-books, /partnumber-books page
- UI: 3 top-level tabs [Estimate][BOM вендора][Ценообразование]; Excel paste,
  PN resolution, inline LOT autocomplete, pricing table
- Bible: 03-database.md updated, 09-vendor-spec.md added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 10:22:22 +03:00
f3d8e653f8 Implement persistent Line ordering for project specs and update bible 2026-02-21 07:09:38 +03:00
Mikhail Chusavitin
c1993a37cf Fix auto pricelist resolution and latest-price selection; update Bible 2026-02-20 19:15:24 +03:00