Files
QuoteForge/bible-local/02-architecture.md

8.8 KiB
Raw Blame History

02 - Architecture

Local-first rule

SQLite is the runtime source of truth. MariaDB is sync transport plus setup and migration tooling.

browser -> Gin handlers -> SQLite
                        -> pending_changes
background sync <------> MariaDB

Rules:

  • user CRUD must continue when MariaDB is offline;
  • runtime handlers and pages must read and write SQLite only;
  • MariaDB access in runtime code is allowed only inside sync and setup flows;
  • no live MariaDB fallback for reads that already exist in local cache.

Sync contract

Bidirectional:

  • projects;
  • configurations;
  • vendor_spec;
  • pending change metadata.

Pull-only:

  • components;
  • pricelists and pricelist items;
  • partnumber books and partnumber book items.

Readiness guard:

  • every sync push/pull runs a preflight check;
  • blocked sync returns 423 Locked with a machine-readable reason;
  • local work continues even when sync is blocked.
  • sync metadata updates must preserve project updated_at; sync time belongs in synced_at, not in the user-facing last-modified timestamp.
  • pricelist pull must persist a new local snapshot atomically: header and items appear together, and last_pricelist_sync advances only after item download succeeds.
  • UI sync status must distinguish "last sync failed" from "up to date"; if the app can prove newer server pricelist data exists, the indicator must say local cache is incomplete.

Pricing contract

local_pricelist_items is the single source of truth for both prices and component catalog (lot_name + lot_category). There is no separate component catalog table.

Rules:

  • local_components table has been removed; do not recreate it;
  • component list for the configurator autocomplete comes from local_pricelist_items via ListComponents;
  • quote calculation reads prices from local_pricelist_items only;
  • latest pricelist selection ignores snapshots without items;
  • auto pricelist mode stays auto and must not be persisted as an explicit resolved ID.

lot_name case handling

lot_names in local_pricelist_items may be stored in mixed case in databases synced before normalization was enforced. NormalizeLotName (uppercase + trim) is applied at sync time via PricelistItemToLocal, but existing rows are not retroactively updated.

Rules:

  • all SQLite queries that filter by lot_name must use UPPER(lot_name) IN ? or UPPER(lot_name) = ? with an uppercased input — never a bare = or IN on a string that may have been sourced from user input or a legacy row;
  • result map keys must preserve the original case passed by the caller (build a uppercase → original index before the query);
  • GetLocalPricesForLots is the canonical pattern: it uppercases the input list, queries with UPPER(lot_name) IN ?, and returns keys that match the input lot_names;
  • frontend JS must never infer a component category from the lot_name prefix; lot_category from local_pricelist_items is the only valid source; items without a category fall into the "Other" tab.

Pricing tab layout

The Pricing tab (Ценообразование) has two tables: Buy (Цена покупки) and Sale (Цена продажи).

Column order (both tables):

PN вендора | Описание | LOT | Кол-во | Estimate | Склад | Конкуренты | Ручная цена

Per-LOT row expansion rules:

  • each lot_mappings entry in a BOM row becomes its own table row with its own quantity and prices;
  • baseLot (resolved LOT without an explicit mapping) is treated as the first sub-row with quantity_per_pn from _getRowLotQtyPerPN;
  • when one vendor PN expands into N LOT sub-rows, PN вендора and Описание cells use rowspan="N" and appear only on the first sub-row;
  • a visual top border (border-t border-gray-200) separates each vendor PN group.

Vendor price attachment:

  • vendorOrig and vendorOrigUnit (BOM unit/total price) are attached to the first LOT sub-row only;
  • subsequent sub-rows carry empty data-vendor-orig so setPricingCustomPriceFromVendor counts each vendor PN exactly once.

Controls terminology:

  • custom price input is labeled Ручная цена (not "Своя цена");
  • the button that fills custom price from BOM totals is labeled BOM Цена (not "Проставить цены BOM").

CSV export reads PN вендора, Описание, and LOT from data-vendor-pn, data-desc, data-lot row attributes to bypass the rowspan cell offset problem.

Configuration versioning

Configuration revisions are append-only snapshots stored in local_configuration_versions.

Rules:

  • the editable working configuration is always the implicit head named main; UI must not switch the user to a numbered revision after save;
  • create a new revision when spec, BOM, or pricing content changes;
  • revision history is retrospective: the revisions page shows past snapshots, not the current main state;
  • rollback creates a new head revision from an old snapshot;
  • rename, reorder, project move, and similar operational edits do not create a new revision snapshot;
  • revision deduplication includes items, server_count, total_price, custom_price, vendor_spec, pricelist selectors, disable_price_refresh, and only_in_stock;
  • BOM updates must use version-aware save flow, not a direct SQL field update;
  • current revision pointer must be recoverable if legacy or damaged rows are found locally.

Sync UX

UI-facing sync status must never block on live MariaDB calls.

Rules:

  • navbar sync indicator and sync info modal read only local cached state from SQLite/app settings;
  • background/manual sync may talk to MariaDB, but polling endpoints must stay fast even on slow or broken connections;
  • any MariaDB timeout/invalid-connection during sync must invalidate the cached remote handle immediately so UI stops treating the connection as healthy.

Naming collisions

UI-driven rename and copy flows use one suffix convention for conflicts.

Rules:

  • configuration and variant names must auto-resolve collisions with опия, then опия2, опия3, and so on;
  • 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 = дисковая полка без контроллера.
  • the available config types and their localized names flow from qt_settings.config_types on the server; QF falls back to hardcoded "server/Сервер" and "storage/СХД" when the setting is absent.

Server-driven configurator settings (qt_settings)

QF reads four settings from qt_settings (MariaDB) and caches them in local_qt_settings (SQLite). They are synced during every component sync. See bible-local/server-contract-qt-settings.md for the full contract and JSON schemas.

Setting key Effect in QF
config_types New-config modal buttons; category allowlist per config type
tab_config Configurator tab structure, sections, singleSelect
always_visible_tabs Which tabs are shown even when empty
required_categories Per-config-type badge on tabs with unfilled required categories

Rules:

  • sync runs as part of the pricelist pull; failure is non-fatal (Warn log only);
  • local_qt_settings is a read-only cache — never written by user actions;
  • absent or unparseable settings: QF uses hardcoded fallbacks for that key only;
  • config_types[].categories is an allowlist: a category absent from all types is shown everywhere;
  • qt_categories.name and qt_categories.name_ru are not used by QF runtime; do not depend on them.

Vendor BOM contract

Vendor BOM is stored in vendor_spec on the configuration row.

Rules:

  • PN to LOT resolution uses the active local partnumber book;
  • canonical persisted mapping is lot_mappings[];
  • QuoteForge does not use legacy BOM tables such as qt_bom, qt_lot_bundles, or qt_lot_bundle_items.