From 8b8d2f18f9e6404b3f39c3b6c313337c6ca7ed6a Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Sat, 31 Jan 2026 14:57:23 +0300 Subject: [PATCH] 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 --- .dockerignore | 33 --- CLAUDE.md | 644 +++++++++++++++++++++++++++++++-------------- Dockerfile | 68 ----- docker-compose.yml | 30 --- 4 files changed, 445 insertions(+), 330 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile delete mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 25390a9..0000000 --- a/.dockerignore +++ /dev/null @@ -1,33 +0,0 @@ -# Git -.git -.gitignore - -# IDE -.idea -.vscode -*.swp -*.swo - -# Build artifacts -server -*.exe -bin/ - -# Config with secrets -config.yaml - -# Documentation -*.md -LICENSE - -# Claude -.claude - -# Test files -*_test.go -test_*.csv -test_*.xlsx - -# Misc -.DS_Store -*.log diff --git a/CLAUDE.md b/CLAUDE.md index 90bcda6..116f21b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,17 +2,40 @@ ## Project Overview -QuoteForge — корпоративный инструмент для конфигурирования серверов и формирования коммерческих предложений (КП). Приложение интегрируется с существующей базой данных RFQ_LOG. +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) -- **Database:** MariaDB 11 (existing database RFQ_LOG) +- **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) -- **Auth:** JWT (github.com/golang-jwt/jwt/v5) ## Project Structure @@ -20,19 +43,47 @@ QuoteForge — корпоративный инструмент для конфи quoteforge/ ├── cmd/ │ ├── server/main.go # Main HTTP server -│ └── importer/main.go # Import metadata from lot table +│ ├── importer/main.go # Import metadata from lot table +│ └── cron/main.go # Cron jobs ├── internal/ -│ ├── config/config.go # YAML config loading -│ ├── models/ # GORM models -│ ├── handlers/ # Gin HTTP handlers -│ ├── services/ # Business logic -│ ├── middleware/ # Auth, CORS, roles -│ └── repository/ # Database queries +│ ├── 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/ # Go HTML templates -│ └── static/ # CSS, JS -├── migrations/ # SQL migration files -├── config.yaml +│ ├── 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 ``` @@ -43,15 +94,15 @@ These tables are used by other systems. Our app only reads from them: ```sql -- Component catalog CREATE TABLE lot ( - lot_name CHAR(255) PRIMARY KEY, -- e.g., "CPU_AMD_9654", "MB_INTEL_4.Sapphire_2S" + 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, -- FK → lot.lot_name - supplier CHAR(255) NOT NULL, -- FK → supplier.supplier_name + lot CHAR(255) NOT NULL, + supplier CHAR(255) NOT NULL, date DATE NOT NULL, price DOUBLE NOT NULL, quality CHAR(255), @@ -67,28 +118,16 @@ CREATE TABLE supplier ( ); ``` -## New Tables (prefix qt_) +## New MariaDB Tables (prefix qt_) -QuoteForge creates these tables: +### Core Tables ```sql --- Users -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 -); - -- Component metadata (extends lot table) CREATE TABLE qt_lot_metadata ( lot_name CHAR(255) PRIMARY KEY, category_id INT, - model VARCHAR(100), -- Parsed: CPU_AMD_9654 → "9654" + model VARCHAR(100), specs JSON, current_price DECIMAL(12,2), price_method ENUM('manual', 'median', 'average', 'weighted_median') DEFAULT 'median', @@ -103,54 +142,13 @@ CREATE TABLE qt_lot_metadata ( -- Categories CREATE TABLE qt_categories ( id INT AUTO_INCREMENT PRIMARY KEY, - code VARCHAR(20) UNIQUE NOT NULL, -- MB, CPU, MEM, GPU, SSD, HDD, RAID, NIC, HCA, HBA, DPU, PS + 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 ); --- Saved configurations -CREATE TABLE qt_configurations ( - id INT AUTO_INCREMENT PRIMARY KEY, - uuid VARCHAR(36) UNIQUE NOT NULL, - user_id INT NOT NULL, - name VARCHAR(200) NOT NULL, - 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 (for discounts) - notes TEXT, - is_template BOOLEAN DEFAULT FALSE, - server_count INT DEFAULT 1, -- Number of servers in configuration - price_updated_at TIMESTAMP, -- Last time prices were refreshed - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES qt_users(id) -); - --- Price overrides -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 pricing admins -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 -); - -- Usage statistics CREATE TABLE qt_component_usage_stats ( lot_name CHAR(255) PRIMARY KEY, @@ -161,20 +159,306 @@ CREATE TABLE qt_component_usage_stats ( 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 + last_used_at TIMESTAMP, + config_count INT DEFAULT 0 -- Number of configurations using this component +); +``` + +### Pricelist Tables + +```sql +-- 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 + +```sql +-- 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) + +```sql +-- 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`: + +```sql +-- 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. Part Number Parsing +### 1. Database Connection Setup + +```go +// 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 + +```go +// 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 + +```go +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 + +```go +// 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 + +```go +// 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 + +```go +// 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 -Extract category and model from lot_name: ```go // "CPU_AMD_9654" → category="CPU", model="AMD_9654" // "MB_INTEL_4.Sapphire_2S" → category="MB", model="INTEL_4.Sapphire_2S" -// "MEM_DDR5_64G_5600" → category="MEM", model="DDR5_64G_5600" -// "GPU_NV_RTX_4090_PCIe" → category="GPU", model="NV_RTX_4090_PCIe" func ParsePartNumber(lotName string) (category, model string) { parts := strings.SplitN(lotName, "_", 2) @@ -188,28 +472,17 @@ func ParsePartNumber(lotName string) (category, model string) { } ``` -### 2. Price Calculation Methods +### 8. Price Calculation Methods ```go -// Median - simple median of prices in period func CalculateMedian(prices []float64) float64 - -// Average - arithmetic mean func CalculateAverage(prices []float64) float64 - -// Weighted Median - recent prices have higher weight (exponential decay) -// weight = e^(-days_since_quote / decay_days) func CalculateWeightedMedian(prices []PricePoint, decayDays int) float64 ``` -### 3. Price Freshness (color coding) +### 9. Price Freshness ```go -// Green: < 30 days AND >= 3 quotes -// Yellow: 30-60 days OR 1-2 quotes -// Orange: 60-90 days -// Red: > 90 days OR no price - func GetPriceFreshness(daysSinceUpdate int, quoteCount int) string { if daysSinceUpdate < 30 && quoteCount >= 3 { return "fresh" // green @@ -222,94 +495,80 @@ func GetPriceFreshness(daysSinceUpdate int, quoteCount int) string { } ``` -### 4. Component Sorting - -Sort by: popularity + price freshness. Components without prices go to the bottom. - -```go -// Sort score = popularity_score * 10 + freshness_bonus - no_price_penalty -// freshness_bonus: fresh=100, normal=50, stale=10, critical=0 -// no_price_penalty: -1000 if current_price is NULL or 0 -``` - -### 5. Alert Generation - -Generate alerts when: -- **high_demand_stale_price** (CRITICAL): >= 5 quotes/month AND price > 60 days old -- **trending_no_price** (HIGH): trend_percent > 50% AND no price set -- **no_recent_quotes** (MEDIUM): popular component, no supplier quotes > 90 days - ## API Endpoints -### Auth +### Setup (no auth required) ``` -POST /api/auth/login → {"username", "password"} → {"token", "refresh_token"} -POST /api/auth/logout -POST /api/auth/refresh -GET /api/auth/me → current user info +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&vendor=AMD → filtered -GET /api/components/:lot_name → single component details -GET /api/categories → category list +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 ``` -### Quote Builder +### Pricelists ``` -POST /api/quote/validate → {"items": [...]} → {"valid": bool, "errors": [], "warnings": []} -POST /api/quote/calculate → {"items": [...]} → {"items": [...], "total": 45000.00} +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/csv → {"items": [...], "name": "Config 1"} → CSV file -POST /api/export/xlsx → {"items": [...], "name": "Config 1"} → XLSX file -``` - -### Configurations -``` -GET /api/configs → list user's configurations -POST /api/configs → save new configuration -GET /api/configs/:uuid → get by UUID -PUT /api/configs/:uuid → update -POST /api/configs/:uuid/refresh-prices → refresh prices for all components -DELETE /api/configs/:uuid → delete -GET /api/configs/:uuid/export → export as JSON -``` - -### Pricing Admin (requires role: pricing_admin or admin) -``` -GET /admin/pricing/stats → dashboard stats -GET /admin/pricing/components → components with pricing info -GET /admin/pricing/components/:lot_name → component pricing details -POST /admin/pricing/update → update price method/value -POST /admin/pricing/recalculate-all → recalculate all prices - -GET /admin/pricing/alerts → list alerts -POST /admin/pricing/alerts/:id/acknowledge → mark as seen -POST /admin/pricing/alerts/:id/resolve → mark as resolved -POST /admin/pricing/alerts/:id/ignore → dismiss alert +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&vendor=AMD → HTML fragment -GET /partials/cart → cart HTML -GET /partials/summary → price summary HTML +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 ``` -## User Roles - -| Role | Permissions | -|------|-------------| -| viewer | View components, create quotes, export | -| editor | + save/load configurations | -| pricing_admin | + manage prices, view alerts | -| admin | + manage users | - ## Frontend Guidelines - **Mobile-first** design @@ -321,6 +580,8 @@ GET /partials/summary → price summary HTML - `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 @@ -332,10 +593,9 @@ go run ./cmd/server go run ./cmd/importer # Run cron jobs manually -go run ./cmd/cron -job=alerts # Check and generate alerts -go run ./cmd/cron -job=update-prices # Recalculate all prices -go run ./cmd/cron -job=reset-counters # Reset usage counters -go run ./cmd/cron -job=update-popularity # Update popularity scores +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 @@ -343,50 +603,36 @@ CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/quoteforge-cron ./cmd/cron # Run tests go test ./... -```` +``` ## Cron Jobs -QuoteForge now includes automated cron jobs for maintenance tasks. These can be run using the built-in cron functionality in the Docker container. - -### Docker Compose Setup - -The Docker setup includes a dedicated cron service that runs the following jobs: - -- **Alerts check**: Every hour (0 * * * *) +- **Pricelist cleanup**: Weekly on Sunday at 4 AM (0 4 * * 0) - **Price updates**: Daily at 2 AM (0 2 * * *) -- **Usage counter reset**: Weekly on Sunday at 1 AM (0 1 * * 0) - **Popularity score updates**: Daily at 3 AM (0 3 * * *) -### Manual Cron Job Execution - -You can also run cron jobs manually using the quoteforge-cron binary: - -```bash -# Check and generate alerts -go run ./cmd/cron -job=alerts - -# Recalculate all prices -go run ./cmd/cron -job=update-prices - -# Reset usage counters -go run ./cmd/cron -job=reset-counters - -# Update popularity scores -go run ./cmd/cron -job=update-popularity -``` - -### Cron Job Details - -- **Alerts check**: Generates alerts for components with high demand and stale prices, trending components without prices, and components with no recent quotes -- **Price updates**: Recalculates prices for all components using configured methods (median, weighted median, average) -- **Usage counter reset**: Resets weekly and monthly usage counters for components -- **Popularity score updates**: Recalculates popularity scores based on supplier quote activity - ## Code Style - Use standard Go formatting (gofmt) - Error handling: always check errors, wrap with context -- Logging: use structured logging (slog or zerolog) +- 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 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 4bf3406..0000000 --- a/Dockerfile +++ /dev/null @@ -1,68 +0,0 @@ -# Build stage -FROM golang:1.24-alpine AS builder - -RUN apk add --no-cache git ca-certificates tzdata - -WORKDIR /app - -# Copy go mod files first for better caching -COPY go.mod go.sum ./ -RUN go mod download - -# Copy source code -COPY . . - -# Build the main binary -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ - -ldflags="-s -w" \ - -o /app/quoteforge \ - ./cmd/server - -# Build the cron binary -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ - -ldflags="-s -w" \ - -o /app/quoteforge-cron \ - ./cmd/cron - -# Final stage -FROM alpine:3.19 - -RUN apk add --no-cache ca-certificates tzdata cron - -# Create non-root user -RUN adduser -D -g '' appuser - -WORKDIR /app - -# Copy binary from builder -COPY --from=builder /app/quoteforge . -COPY --from=builder /app/quoteforge-cron . - -# Copy cron job configuration -COPY crontab /etc/crontabs/appuser -RUN chmod 0600 /etc/crontabs/appuser - -# Create log directory -RUN mkdir -p /var/log/cron - -# Copy web templates and static files -COPY --from=builder /app/web ./web - -# Copy migrations -COPY --from=builder /app/migrations ./migrations - -# Copy example config (actual config should be mounted) -COPY --from=builder /app/config.example.yaml ./config.example.yaml - -# Set ownership -RUN chown -R appuser:appuser /app - -USER appuser - -EXPOSE 8080 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 - -ENTRYPOINT ["/app/quoteforge"] diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index b04e12a..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,30 +0,0 @@ -version: '3.8' - -services: - quoteforge: - build: . - container_name: quoteforge - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - ./config.yaml:/app/config.yaml:ro - environment: - - TZ=Europe/Moscow - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] - interval: 30s - timeout: 3s - retries: 3 - start_period: 5s - - quoteforge-cron: - build: . - container_name: quoteforge-cron - restart: unless-stopped - volumes: - - ./config.yaml:/app/config.yaml:ro - - ./logs:/app/logs - command: /usr/sbin/crond -f -l 8 - environment: - - TZ=Europe/Moscow