Files
QuoteForge/CLAUDE.md
Michael Chus 8b8d2f18f9 Update CLAUDE.md with new architecture, remove Docker
- Add development phases (pricelists, projects, local SQLite, price versioning)
- Add new table schemas (qt_pricelists, qt_projects, qt_specifications)
- Add local SQLite database structure for offline work
- Remove Docker files (distributing as binary only)
- Disable RBAC for initial phases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 14:57:23 +03:00

21 KiB
Raw Blame History

QuoteForge - Claude Code Instructions

Project Overview

QuoteForge — корпоративный инструмент для конфигурирования серверов и формирования коммерческих предложений (КП). Приложение работает с серверной базой данных MariaDB (RFQ_LOG) и локальной SQLite для оффлайн-работы.

Development Phases

Phase 1: Pricelists in MariaDB

  • Настройка подключения к БД при первом запуске
  • Таблицы qt_pricelists и qt_pricelist_items
  • CRUD операции для прайслистов (при наличии прав записи)

Phase 2: Projects and Specifications

  • Таблицы qt_projects и qt_specifications
  • Замена qt_configurations на новую структуру
  • Поля: opty, customer_requirement, variant, qty, rev

Phase 3: Local SQLite Database

  • Локальное хранение настроек подключения
  • Кэширование прайслистов
  • Локальные проекты и спецификации
  • Синхронизация с сервером

Phase 4: Price Versioning

  • Привязка спецификаций к версиям прайслистов
  • Актуализация прайслистов с показом разницы цен
  • Автоочистка старых прайслистов (>1 года, usage_count=0)

Tech Stack

  • Language: Go 1.22+
  • Web Framework: Gin (github.com/gin-gonic/gin)
  • ORM: GORM (gorm.io/gorm)
  • Server Database: MariaDB 11 (existing database RFQ_LOG)
  • Local Database: SQLite (github.com/glebarez/sqlite for pure Go)
  • Frontend: HTML templates + htmx + Tailwind CSS (CDN)
  • Excel Export: excelize (github.com/xuri/excelize/v2)

Project Structure

quoteforge/
├── cmd/
│   ├── server/main.go           # Main HTTP server
│   ├── importer/main.go         # Import metadata from lot table
│   └── cron/main.go             # Cron jobs
├── internal/
│   ├── config/
│   │   └── config.go            # Load settings from SQLite
│   ├── db/
│   │   ├── mariadb.go           # Server DB connection
│   │   └── sqlite.go            # Local DB connection
│   ├── models/
│   │   ├── lot.go               # Existing lot tables
│   │   ├── pricelist.go         # Pricelists
│   │   ├── project.go           # Projects
│   │   ├── specification.go     # Specifications
│   │   └── local_models.go      # SQLite models
│   ├── handlers/
│   │   ├── setup_handler.go     # Initial DB setup
│   │   ├── pricelist_handler.go # Pricelist CRUD
│   │   ├── project_handler.go   # Project CRUD
│   │   ├── spec_handler.go      # Specification CRUD
│   │   └── sync_handler.go      # Sync operations
│   ├── services/
│   │   ├── pricelist_service.go # Pricelist business logic
│   │   ├── project_service.go   # Project business logic
│   │   ├── sync_service.go      # Sync with server
│   │   └── price_service.go     # Price calculations
│   ├── middleware/
│   │   └── db_check.go          # Check DB connection
│   └── repository/
│       ├── mariadb_repo.go      # Server DB queries
│       └── sqlite_repo.go       # Local DB queries
├── web/
│   ├── templates/
│   │   ├── setup.html           # DB connection setup
│   │   ├── projects.html        # Project list
│   │   ├── project_detail.html  # Project with specs
│   │   ├── spec_editor.html     # Specification editor
│   │   └── pricelists.html      # Pricelist management
│   └── static/
├── data/                         # SQLite database location
│   └── quoteforge.db
├── migrations/
└── go.mod

Existing Database Tables (READ-ONLY - DO NOT MODIFY)

These tables are used by other systems. Our app only reads from them:

-- Component catalog
CREATE TABLE lot (
    lot_name CHAR(255) PRIMARY KEY,
    lot_description VARCHAR(10000)
);

-- Price history from suppliers
CREATE TABLE lot_log (
    lot_log_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    lot CHAR(255) NOT NULL,
    supplier CHAR(255) NOT NULL,
    date DATE NOT NULL,
    price DOUBLE NOT NULL,
    quality CHAR(255),
    comments VARCHAR(15000),
    FOREIGN KEY (lot) REFERENCES lot(lot_name),
    FOREIGN KEY (supplier) REFERENCES supplier(supplier_name)
);

-- Supplier catalog
CREATE TABLE supplier (
    supplier_name CHAR(255) PRIMARY KEY,
    supplier_comment VARCHAR(10000)
);

New MariaDB Tables (prefix qt_)

Core Tables

-- Component metadata (extends lot table)
CREATE TABLE qt_lot_metadata (
    lot_name CHAR(255) PRIMARY KEY,
    category_id INT,
    model VARCHAR(100),
    specs JSON,
    current_price DECIMAL(12,2),
    price_method ENUM('manual', 'median', 'average', 'weighted_median') DEFAULT 'median',
    price_period_days INT DEFAULT 90,
    price_updated_at TIMESTAMP,
    request_count INT DEFAULT 0,
    last_request_date DATE,
    popularity_score DECIMAL(10,4),
    FOREIGN KEY (lot_name) REFERENCES lot(lot_name)
);

-- Categories
CREATE TABLE qt_categories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    code VARCHAR(20) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    name_ru VARCHAR(100),
    display_order INT DEFAULT 0,
    is_required BOOLEAN DEFAULT FALSE
);

-- Usage statistics
CREATE TABLE qt_component_usage_stats (
    lot_name CHAR(255) PRIMARY KEY,
    quotes_total INT DEFAULT 0,
    quotes_last_30d INT DEFAULT 0,
    quotes_last_7d INT DEFAULT 0,
    total_quantity INT DEFAULT 0,
    total_revenue DECIMAL(14,2) DEFAULT 0,
    trend_direction ENUM('up', 'stable', 'down') DEFAULT 'stable',
    trend_percent DECIMAL(5,2) DEFAULT 0,
    last_used_at TIMESTAMP,
    config_count INT DEFAULT 0              -- Number of configurations using this component
);

Pricelist Tables

-- Pricelists (versioned price snapshots)
CREATE TABLE qt_pricelists (
    id INT AUTO_INCREMENT PRIMARY KEY,
    version VARCHAR(20) NOT NULL,           -- Format: "YYYY-MM-DD-NNN" (e.g., "2024-01-31-001")
    name VARCHAR(200),                       -- Optional display name
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    created_by VARCHAR(100),                 -- Username of creator
    is_active BOOLEAN DEFAULT TRUE,
    usage_count INT DEFAULT 0,               -- How many specifications use this pricelist
    expires_at DATE,                         -- Auto-calculated: created_at + 1 year
    UNIQUE KEY (version)
);

-- Pricelist items
CREATE TABLE qt_pricelist_items (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    pricelist_id INT NOT NULL,
    lot_name CHAR(255) NOT NULL,
    price DECIMAL(12,2) NOT NULL,
    price_method ENUM('manual', 'median', 'average', 'weighted_median'),
    FOREIGN KEY (pricelist_id) REFERENCES qt_pricelists(id) ON DELETE CASCADE,
    FOREIGN KEY (lot_name) REFERENCES lot(lot_name),
    INDEX idx_pricelist_lot (pricelist_id, lot_name)
);

Project Tables

-- Projects (group of specifications)
CREATE TABLE qt_projects (
    id INT AUTO_INCREMENT PRIMARY KEY,
    uuid VARCHAR(36) UNIQUE NOT NULL,
    opty VARCHAR(50),                        -- Opportunity/project number
    customer_requirement TEXT,               -- Link to customer requirements/TZ
    name VARCHAR(200) NOT NULL,
    notes TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- Specifications (replaces qt_configurations)
CREATE TABLE qt_specifications (
    id INT AUTO_INCREMENT PRIMARY KEY,
    uuid VARCHAR(36) UNIQUE NOT NULL,
    project_id INT NOT NULL,
    pricelist_id INT NOT NULL,               -- Bound to specific pricelist version
    variant VARCHAR(50) NOT NULL,            -- Calculation variant (A, B, C, Base, Extended...)
    rev INT DEFAULT 1,                       -- Revision number
    qty INT DEFAULT 1,                       -- Number of servers
    items JSON NOT NULL,                     -- [{"lot_name": "CPU_AMD_9654", "quantity": 2, "unit_price": 11500}]
    total_price DECIMAL(12,2),
    custom_price DECIMAL(12,2),              -- User-defined target price
    notes TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (project_id) REFERENCES qt_projects(id) ON DELETE CASCADE,
    FOREIGN KEY (pricelist_id) REFERENCES qt_pricelists(id),
    UNIQUE KEY (project_id, variant, rev)
);

Legacy Tables (will be deprecated)

-- Users (RBAC disabled in Phase 1-3)
CREATE TABLE qt_users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(100) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    role ENUM('viewer', 'editor', 'pricing_admin', 'admin') DEFAULT 'viewer',
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- Price overrides (for future use)
CREATE TABLE qt_price_overrides (
    id INT AUTO_INCREMENT PRIMARY KEY,
    lot_name CHAR(255) NOT NULL,
    price DECIMAL(12,2) NOT NULL,
    valid_from DATE NOT NULL,
    valid_until DATE,
    reason TEXT,
    created_by INT NOT NULL,
    FOREIGN KEY (lot_name) REFERENCES lot(lot_name)
);

-- Alerts (for future use)
CREATE TABLE qt_pricing_alerts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    lot_name CHAR(255) NOT NULL,
    alert_type ENUM('high_demand_stale_price', 'price_spike', 'price_drop', 'no_recent_quotes', 'trending_no_price') NOT NULL,
    severity ENUM('low', 'medium', 'high', 'critical') DEFAULT 'medium',
    message TEXT NOT NULL,
    details JSON,
    status ENUM('new', 'acknowledged', 'resolved', 'ignored') DEFAULT 'new',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Local SQLite Database

Located at data/quoteforge.db:

-- Application settings (connection credentials stored encrypted)
CREATE TABLE app_settings (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- Keys: db_host, db_port, db_name, db_user, db_password (encrypted), last_sync

-- Cached pricelists from server
CREATE TABLE local_pricelists (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    server_id INT NOT NULL,                  -- ID on MariaDB server
    version TEXT NOT NULL UNIQUE,
    name TEXT,
    created_at TEXT,
    synced_at TEXT DEFAULT CURRENT_TIMESTAMP,
    is_used INTEGER DEFAULT 0                -- 1 if used by any specification
);

CREATE TABLE local_pricelist_items (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    pricelist_id INTEGER NOT NULL,
    lot_name TEXT NOT NULL,
    price REAL NOT NULL,
    FOREIGN KEY (pricelist_id) REFERENCES local_pricelists(id) ON DELETE CASCADE
);

-- Local projects (can be synced to server)
CREATE TABLE local_projects (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    uuid TEXT UNIQUE NOT NULL,
    server_id INTEGER,                       -- NULL if not synced yet
    opty TEXT,
    customer_requirement TEXT,
    name TEXT NOT NULL,
    notes TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
    synced_at TEXT,                          -- NULL if has local changes
    sync_status TEXT DEFAULT 'local'         -- 'local', 'synced', 'modified'
);

-- Local specifications
CREATE TABLE local_specifications (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    uuid TEXT UNIQUE NOT NULL,
    server_id INTEGER,
    project_id INTEGER NOT NULL,
    pricelist_id INTEGER NOT NULL,
    variant TEXT NOT NULL,
    rev INTEGER DEFAULT 1,
    qty INTEGER DEFAULT 1,
    items TEXT NOT NULL,                     -- JSON string
    total_price REAL,
    custom_price REAL,
    notes TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
    synced_at TEXT,
    sync_status TEXT DEFAULT 'local',
    FOREIGN KEY (project_id) REFERENCES local_projects(id) ON DELETE CASCADE,
    FOREIGN KEY (pricelist_id) REFERENCES local_pricelists(id)
);

-- Component cache (for offline search)
CREATE TABLE local_components (
    lot_name TEXT PRIMARY KEY,
    lot_description TEXT,
    category TEXT,
    model TEXT,
    synced_at TEXT DEFAULT CURRENT_TIMESTAMP
);

Key Business Logic

1. Database Connection Setup

// First launch: show setup form
// User provides: host, port, database, username, password
// Credentials are encrypted and stored in SQLite
// Connection is tested before saving

func SetupConnection(host, port, dbName, user, password string) error {
    // Test connection to MariaDB
    // If successful, encrypt and save to SQLite
    // Create/migrate qt_* tables if user has permissions
}

func CheckWritePermission(db *gorm.DB, tableName string) bool {
    // Check if current user can INSERT into table
    // Used to enable/disable pricelist creation UI
}

2. Pricelist Creation

// Create snapshot of current prices from qt_lot_metadata
// Version format: YYYY-MM-DD-NNN (NNN = sequential number for the day)

func CreatePricelist(name string, createdBy string) (*Pricelist, error) {
    version := generateVersion() // e.g., "2024-01-31-001"
    expiresAt := time.Now().AddDate(1, 0, 0) // +1 year

    // Copy all prices from qt_lot_metadata
    // Insert into qt_pricelists and qt_pricelist_items
}

func generateVersion() string {
    today := time.Now().Format("2006-01-02")
    // Count existing pricelists for today
    // Return "YYYY-MM-DD-NNN"
}

3. Pricelist Comparison

type PriceDiff struct {
    LotName      string
    OldPrice     float64
    NewPrice     float64
    Difference   float64
    PercentDiff  float64
}

// Compare two pricelists and return differences
func ComparePricelists(oldID, newID int) ([]PriceDiff, error)

// Compare specification's pricelist with latest available
func GetSpecificationPriceDiff(specUUID string) ([]PriceDiff, float64, error) {
    // Returns item diffs and total price difference
}

4. Specification Upgrade

// Upgrade specification to use newer pricelist
func UpgradeSpecificationPricelist(specUUID string, newPricelistID int) error {
    // Update pricelist_id
    // Recalculate prices from new pricelist
    // Increment revision number
    // Update old pricelist usage_count--
    // Update new pricelist usage_count++
}

5. Pricelist Cleanup

// Cron job: cleanup old unused pricelists
// Run weekly: 0 4 * * 0
func CleanupOldPricelists() error {
    // Delete pricelists where:
    // - expires_at < NOW()
    // - usage_count = 0
    // - is_active = false OR created_at < NOW() - 1 year
}

6. Sync Service

// Sync pricelists from server to local SQLite
func SyncPricelists() error {
    // Fetch all active pricelists from MariaDB
    // Update local_pricelists table
    // For pricelists used by local specs, also sync items
}

// Check if sync is needed
func NeedSync() bool {
    // Compare last_sync timestamp with server
    // Return true if new pricelists available
}

7. Part Number Parsing

// "CPU_AMD_9654" → category="CPU", model="AMD_9654"
// "MB_INTEL_4.Sapphire_2S" → category="MB", model="INTEL_4.Sapphire_2S"

func ParsePartNumber(lotName string) (category, model string) {
    parts := strings.SplitN(lotName, "_", 2)
    if len(parts) >= 1 {
        category = parts[0]
    }
    if len(parts) >= 2 {
        model = parts[1]
    }
    return
}

8. Price Calculation Methods

func CalculateMedian(prices []float64) float64
func CalculateAverage(prices []float64) float64
func CalculateWeightedMedian(prices []PricePoint, decayDays int) float64

9. Price Freshness

func GetPriceFreshness(daysSinceUpdate int, quoteCount int) string {
    if daysSinceUpdate < 30 && quoteCount >= 3 {
        return "fresh"      // green
    } else if daysSinceUpdate < 60 {
        return "normal"     // yellow
    } else if daysSinceUpdate < 90 {
        return "stale"      // orange
    }
    return "critical"       // red
}

API Endpoints

Setup (no auth required)

GET  /setup              → DB connection form (if not configured)
POST /setup              → Save connection settings
POST /setup/test         → Test connection without saving
GET  /setup/status       → Check if configured and connected

Components

GET /api/components                          → List with pagination
GET /api/components?category=CPU&search=AMD  → Filtered list
GET /api/components/:lot_name                → Single component details
GET /api/categories                          → Category list

Pricelists

GET    /api/pricelists                       → List all pricelists
POST   /api/pricelists                       → Create new pricelist (requires write permission)
GET    /api/pricelists/:id                   → Pricelist details
GET    /api/pricelists/:id/items             → Pricelist items with pagination
DELETE /api/pricelists/:id                   → Delete pricelist (if usage_count=0)
GET    /api/pricelists/latest                → Get latest active pricelist
POST   /api/pricelists/compare               → Compare two pricelists

Projects

GET    /api/projects                         → List all projects
POST   /api/projects                         → Create project
GET    /api/projects/:uuid                   → Project with specifications
PUT    /api/projects/:uuid                   → Update project
DELETE /api/projects/:uuid                   → Delete project and specs

Specifications

GET    /api/projects/:uuid/specs             → List specifications
POST   /api/projects/:uuid/specs             → Create specification
GET    /api/specs/:spec_uuid                 → Specification details
PUT    /api/specs/:spec_uuid                 → Update specification
DELETE /api/specs/:spec_uuid                 → Delete specification
POST   /api/specs/:spec_uuid/upgrade         → Upgrade to new pricelist
GET    /api/specs/:spec_uuid/diff            → Show price diff with latest pricelist
POST   /api/specs/:spec_uuid/new-revision    → Create new revision

Sync

GET  /api/sync/status                        → Sync status (last sync, pending changes)
POST /api/sync/pricelists                    → Sync pricelists from server
POST /api/sync/push                          → Push local changes to server
POST /api/sync/pull                          → Pull all data from server

Export

POST /api/export/xlsx    → Export specification as XLSX
POST /api/export/pdf     → Export specification as PDF (future)
GET  /api/specs/:uuid/export → Export single spec
GET  /api/projects/:uuid/export → Export all project specs

htmx Partials

GET /partials/components?category=CPU        → Component list HTML
GET /partials/spec-items/:spec_uuid          → Specification items HTML
GET /partials/price-diff/:spec_uuid          → Price diff table HTML
GET /partials/project-specs/:project_uuid    → Project specifications list

Frontend Guidelines

  • Mobile-first design
  • Use htmx for interactivity (hx-get, hx-post, hx-target, hx-swap)
  • Use Tailwind CSS via CDN
  • Minimal custom JavaScript
  • Color scheme for price freshness:
    • text-green-600 bg-green-50 - fresh
    • text-yellow-600 bg-yellow-50 - normal
    • text-orange-600 bg-orange-50 - stale
    • text-red-600 bg-red-50 - critical
  • Sync status indicator in header
  • Offline mode indicator when server unavailable

Commands

# Run development server
go run ./cmd/server

# Run importer (one-time setup)
go run ./cmd/importer

# Run cron jobs manually
go run ./cmd/cron -job=cleanup-pricelists  # Remove old unused pricelists
go run ./cmd/cron -job=update-prices       # Recalculate all prices
go run ./cmd/cron -job=update-popularity   # Update popularity scores

# Build for production
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/quoteforge ./cmd/server
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/quoteforge-cron ./cmd/cron

# Run tests
go test ./...

Cron Jobs

  • Pricelist cleanup: Weekly on Sunday at 4 AM (0 4 * * 0)
  • Price updates: Daily at 2 AM (0 2 * * *)
  • Popularity score updates: Daily at 3 AM (0 3 * * *)

Code Style

  • Use standard Go formatting (gofmt)
  • Error handling: always check errors, wrap with context
  • Logging: use structured logging (slog)
  • Comments: in Russian or English, be consistent
  • File naming: snake_case for files, PascalCase for types

Migration Notes

From qt_configurations to qt_specifications

When migrating existing data:

  1. Create a default project for orphan configurations
  2. Create initial pricelist from current qt_lot_metadata prices
  3. Convert qt_configurations to qt_specifications with default variant="Base", rev=1
  4. Link all specs to initial pricelist

RBAC Disabled

During Phase 1-3, RBAC is disabled:

  • No login required
  • All users have full access
  • Write permissions determined by MariaDB user privileges
  • qt_users table exists but not used