8.8 KiB
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 Lockedwith a machine-readable reason; - local work continues even when sync is blocked.
- sync metadata updates must preserve project
updated_at; sync time belongs insynced_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_syncadvances 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_componentstable has been removed; do not recreate it;- component list for the configurator autocomplete comes from
local_pricelist_itemsviaListComponents; - quote calculation reads prices from
local_pricelist_itemsonly; - 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_namemust useUPPER(lot_name) IN ?orUPPER(lot_name) = ?with an uppercased input — never a bare=orINon 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 → originalindex before the query); GetLocalPricesForLotsis the canonical pattern: it uppercases the input list, queries withUPPER(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_categoryfromlocal_pricelist_itemsis 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_mappingsentry 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 withquantity_per_pnfrom_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:
vendorOrigandvendorOrigUnit(BOM unit/total price) are attached to the first LOT sub-row only;- subsequent sub-rows carry empty
data-vendor-origsosetPricingCustomPriceFromVendorcounts 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
mainstate; - 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, andonly_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
mainis reserved and must not be allowed for non-main variants.
Configuration types
Configurations have a config_type field: "server" (default) or "storage".
Rules:
config_typedefaults 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_typeson 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_settingsis a read-only cache — never written by user actions;- absent or unparseable settings: QF uses hardcoded fallbacks for that key only;
config_types[].categoriesis an allowlist: a category absent from all types is shown everywhere;qt_categories.nameandqt_categories.name_ruare 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, orqt_lot_bundle_items.