Files
QuoteForge/bible-local/02-architecture.md
Michael Chus eb7c3739ce Add shared bible submodule, rename local bible to bible-local
- Add bible.git as submodule at bible/
- Rename bible/ → bible-local/ (project-specific architecture)
- Update CLAUDE.md to reference both bible/ and bible-local/
- Add AGENTS.md for Codex with same structure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:41:14 +03:00

7.4 KiB

02 — Architecture

Local-First Principle

SQLite is the single source of truth for the user. MariaDB is a sync server only — it never blocks local operations.

User
  │
  ▼
SQLite (qfs.db)          ← all CRUD operations go here
  │
  │ background sync (every 5 min)
  ▼
MariaDB (RFQ_LOG)        ← pull/push only

Rules:

  • All CRUD operations go through SQLite only
  • If MariaDB is unavailable → local work continues without restrictions
  • Changes are queued in pending_changes and pushed on next sync

Synchronization

Data Flow Diagram

                      [ SERVER / MariaDB ]
                 ┌───────────────────────────┐
                 │ qt_projects               │
                 │ qt_configurations         │
                 │ qt_pricelists             │
                 │ qt_pricelist_items        │
                 │ qt_pricelist_sync_status  │
                 └─────────────┬─────────────┘
                               │
              pull (projects/configs/pricelists)
                               │
          ┌────────────────────┴────────────────────┐
          │                                         │
[ CLIENT A / SQLite ]                   [ CLIENT B / SQLite ]
local_projects                          local_projects
local_configurations                    local_configurations
local_pricelists                        local_pricelists
local_pricelist_items                   local_pricelist_items
pending_changes                         pending_changes
          │                                         │
          └────── push (projects/configs only) ─────┘
                               │
                      [ SERVER / MariaDB ]

Sync Direction by Entity

Entity Direction
Configurations Client ↔ Server ↔ Other Clients
Projects Client ↔ Server ↔ Other Clients
Pricelists Server → Clients only (no push)
Components Server → Clients only

Local pricelists not present on the server and not referenced by active configurations are deleted automatically on sync.

Soft Deletes (Archive Pattern)

Configurations and projects are never hard-deleted. Deletion is archive via is_active = false.

  • DELETE /api/configs/:uuid → sets is_active = false (archived); can be restored via reactivate
  • DELETE /api/projects/:uuid → archives a project variant only (variant field must be non-empty); main projects cannot be deleted via this endpoint

Sync Readiness Guard

Before every push/pull, a preflight check runs:

  1. Is the server (MariaDB) reachable?
  2. Can centralized local DB migrations be applied?
  3. Does the application version satisfy min_app_version of pending migrations?

If the check fails:

  • Local CRUD continues without restriction
  • Sync API returns 423 Locked with reason_code and reason_text
  • UI shows a red indicator with the block reason

Pricing

Principle

Prices come only from local_pricelist_items. Components (local_components) are metadata-only — they contain no pricing information.

Lookup Pattern

// Look up a price for a line item
price, found := s.lookupPriceByPricelistID(pricelistID, lotName)
if found && price > 0 {
    // use price
}

// Inside lookupPriceByPricelistID:
localPL, err := s.localDB.GetLocalPricelistByServerID(pricelistID)
price, err := s.localDB.GetLocalPriceForLot(localPL.ID, lotName)

Multi-Level Pricelists

A configuration can reference up to three pricelists simultaneously:

Field Purpose
pricelist_id Primary (estimate)
warehouse_pricelist_id Warehouse pricing
competitor_pricelist_id Competitor pricing

Pricelist sources: estimate | warehouse | competitor

"Auto" Pricelist Selection

Configurator supports explicit and automatic selection per source (estimate, warehouse, competitor):

  • Explicit mode: concrete pricelist_id is set by user in settings.
  • Auto mode: client sends no explicit ID for that source; backend resolves the current latest active pricelist.

auto must stay auto after price-level refresh and after manual "refresh prices":

  • resolved IDs are runtime-only and must not overwrite user's mode;
  • switching to explicit selection must clear runtime auto resolution for that source.

Latest Pricelist Resolution Rules

For both server (qt_pricelists) and local cache (local_pricelists), "latest by source" is resolved with:

  1. only pricelists that have at least one item (EXISTS ...pricelist_items);
  2. deterministic sort: created_at DESC, id DESC.

This prevents selecting empty/incomplete snapshots and removes nondeterministic ties.


Configuration Versioning

Principle

Append-only for spec+price changes: immutable snapshots are stored in local_configuration_versions.

local_configurations
  └── current_version_id ──► local_configuration_versions (v3)  ← active
                              local_configuration_versions (v2)
                              local_configuration_versions (v1)
  • version_no = max + 1 when configuration spec+price changes
  • Old versions are never modified or deleted in normal flow
  • Rollback does not rewind history — it creates a new version from the snapshot
  • Operational updates (line_no reorder, server count, project move, rename) are synced via pending_changes but do not create a new revision snapshot

Rollback

POST /api/configs/:uuid/rollback
{
  "target_version": 3,
  "note": "optional comment"
}

Result:

  • A new version vN is created with data from the target version
  • change_note = "rollback to v{target_version}" (+ note if provided)
  • current_version_id is switched to the new version
  • Configuration moves to sync_status = pending

Sync Status Flow

local → pending → synced

Project Specification Ordering (Line)

  • Each project configuration has persistent line_no (10,20,30...) in both SQLite and MariaDB.
  • Project list ordering is deterministic: line_no ASC, then created_at DESC, then id DESC.
  • Drag-and-drop reorder in project UI updates line_no for active project configurations.
  • Reorder writes are queued as configuration update events in pending_changes without creating new configuration versions.
  • Backward compatibility: if remote MariaDB schema does not yet include line_no, sync falls back to create/update without line_no instead of failing.

Sync Payload for Versioning

Events in pending_changes for configurations contain:

Field Description
configuration_uuid Identifier
operation create / update / rollback
current_version_id Active version ID
current_version_no Version number
snapshot Current configuration state
idempotency_key For idempotent push
conflict_policy last_write_wins

Background Processes

Process Interval What it does
Sync worker 5 min push pending + pull all
Backup scheduler configurable (backup.time) creates ZIP archives