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>
This commit is contained in:
@@ -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
|
|
||||||
644
CLAUDE.md
644
CLAUDE.md
@@ -2,17 +2,40 @@
|
|||||||
|
|
||||||
## Project Overview
|
## 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
|
## Tech Stack
|
||||||
|
|
||||||
- **Language:** Go 1.22+
|
- **Language:** Go 1.22+
|
||||||
- **Web Framework:** Gin (github.com/gin-gonic/gin)
|
- **Web Framework:** Gin (github.com/gin-gonic/gin)
|
||||||
- **ORM:** GORM (gorm.io/gorm)
|
- **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)
|
- **Frontend:** HTML templates + htmx + Tailwind CSS (CDN)
|
||||||
- **Excel Export:** excelize (github.com/xuri/excelize/v2)
|
- **Excel Export:** excelize (github.com/xuri/excelize/v2)
|
||||||
- **Auth:** JWT (github.com/golang-jwt/jwt/v5)
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -20,19 +43,47 @@ QuoteForge — корпоративный инструмент для конфи
|
|||||||
quoteforge/
|
quoteforge/
|
||||||
├── cmd/
|
├── cmd/
|
||||||
│ ├── server/main.go # Main HTTP server
|
│ ├── 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/
|
├── internal/
|
||||||
│ ├── config/config.go # YAML config loading
|
│ ├── config/
|
||||||
│ ├── models/ # GORM models
|
│ │ └── config.go # Load settings from SQLite
|
||||||
│ ├── handlers/ # Gin HTTP handlers
|
│ ├── db/
|
||||||
│ ├── services/ # Business logic
|
│ │ ├── mariadb.go # Server DB connection
|
||||||
│ ├── middleware/ # Auth, CORS, roles
|
│ │ └── sqlite.go # Local DB connection
|
||||||
│ └── repository/ # Database queries
|
│ ├── 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/
|
├── web/
|
||||||
│ ├── templates/ # Go HTML templates
|
│ ├── templates/
|
||||||
│ └── static/ # CSS, JS
|
│ │ ├── setup.html # DB connection setup
|
||||||
├── migrations/ # SQL migration files
|
│ │ ├── projects.html # Project list
|
||||||
├── config.yaml
|
│ │ ├── 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
|
└── go.mod
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -43,15 +94,15 @@ These tables are used by other systems. Our app only reads from them:
|
|||||||
```sql
|
```sql
|
||||||
-- Component catalog
|
-- Component catalog
|
||||||
CREATE TABLE lot (
|
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)
|
lot_description VARCHAR(10000)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Price history from suppliers
|
-- Price history from suppliers
|
||||||
CREATE TABLE lot_log (
|
CREATE TABLE lot_log (
|
||||||
lot_log_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
lot_log_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
lot CHAR(255) NOT NULL, -- FK → lot.lot_name
|
lot CHAR(255) NOT NULL,
|
||||||
supplier CHAR(255) NOT NULL, -- FK → supplier.supplier_name
|
supplier CHAR(255) NOT NULL,
|
||||||
date DATE NOT NULL,
|
date DATE NOT NULL,
|
||||||
price DOUBLE NOT NULL,
|
price DOUBLE NOT NULL,
|
||||||
quality CHAR(255),
|
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
|
```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)
|
-- Component metadata (extends lot table)
|
||||||
CREATE TABLE qt_lot_metadata (
|
CREATE TABLE qt_lot_metadata (
|
||||||
lot_name CHAR(255) PRIMARY KEY,
|
lot_name CHAR(255) PRIMARY KEY,
|
||||||
category_id INT,
|
category_id INT,
|
||||||
model VARCHAR(100), -- Parsed: CPU_AMD_9654 → "9654"
|
model VARCHAR(100),
|
||||||
specs JSON,
|
specs JSON,
|
||||||
current_price DECIMAL(12,2),
|
current_price DECIMAL(12,2),
|
||||||
price_method ENUM('manual', 'median', 'average', 'weighted_median') DEFAULT 'median',
|
price_method ENUM('manual', 'median', 'average', 'weighted_median') DEFAULT 'median',
|
||||||
@@ -103,54 +142,13 @@ CREATE TABLE qt_lot_metadata (
|
|||||||
-- Categories
|
-- Categories
|
||||||
CREATE TABLE qt_categories (
|
CREATE TABLE qt_categories (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
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 VARCHAR(100) NOT NULL,
|
||||||
name_ru VARCHAR(100),
|
name_ru VARCHAR(100),
|
||||||
display_order INT DEFAULT 0,
|
display_order INT DEFAULT 0,
|
||||||
is_required BOOLEAN DEFAULT FALSE
|
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
|
-- Usage statistics
|
||||||
CREATE TABLE qt_component_usage_stats (
|
CREATE TABLE qt_component_usage_stats (
|
||||||
lot_name CHAR(255) PRIMARY KEY,
|
lot_name CHAR(255) PRIMARY KEY,
|
||||||
@@ -161,20 +159,306 @@ CREATE TABLE qt_component_usage_stats (
|
|||||||
total_revenue DECIMAL(14,2) DEFAULT 0,
|
total_revenue DECIMAL(14,2) DEFAULT 0,
|
||||||
trend_direction ENUM('up', 'stable', 'down') DEFAULT 'stable',
|
trend_direction ENUM('up', 'stable', 'down') DEFAULT 'stable',
|
||||||
trend_percent DECIMAL(5,2) DEFAULT 0,
|
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
|
## 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
|
```go
|
||||||
// "CPU_AMD_9654" → category="CPU", model="AMD_9654"
|
// "CPU_AMD_9654" → category="CPU", model="AMD_9654"
|
||||||
// "MB_INTEL_4.Sapphire_2S" → category="MB", model="INTEL_4.Sapphire_2S"
|
// "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) {
|
func ParsePartNumber(lotName string) (category, model string) {
|
||||||
parts := strings.SplitN(lotName, "_", 2)
|
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
|
```go
|
||||||
// Median - simple median of prices in period
|
|
||||||
func CalculateMedian(prices []float64) float64
|
func CalculateMedian(prices []float64) float64
|
||||||
|
|
||||||
// Average - arithmetic mean
|
|
||||||
func CalculateAverage(prices []float64) float64
|
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
|
func CalculateWeightedMedian(prices []PricePoint, decayDays int) float64
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Price Freshness (color coding)
|
### 9. Price Freshness
|
||||||
|
|
||||||
```go
|
```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 {
|
func GetPriceFreshness(daysSinceUpdate int, quoteCount int) string {
|
||||||
if daysSinceUpdate < 30 && quoteCount >= 3 {
|
if daysSinceUpdate < 30 && quoteCount >= 3 {
|
||||||
return "fresh" // green
|
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
|
## API Endpoints
|
||||||
|
|
||||||
### Auth
|
### Setup (no auth required)
|
||||||
```
|
```
|
||||||
POST /api/auth/login → {"username", "password"} → {"token", "refresh_token"}
|
GET /setup → DB connection form (if not configured)
|
||||||
POST /api/auth/logout
|
POST /setup → Save connection settings
|
||||||
POST /api/auth/refresh
|
POST /setup/test → Test connection without saving
|
||||||
GET /api/auth/me → current user info
|
GET /setup/status → Check if configured and connected
|
||||||
```
|
```
|
||||||
|
|
||||||
### Components
|
### Components
|
||||||
```
|
```
|
||||||
GET /api/components → list with pagination
|
GET /api/components → List with pagination
|
||||||
GET /api/components?category=CPU&vendor=AMD → filtered
|
GET /api/components?category=CPU&search=AMD → Filtered list
|
||||||
GET /api/components/:lot_name → single component details
|
GET /api/components/:lot_name → Single component details
|
||||||
GET /api/categories → category list
|
GET /api/categories → Category list
|
||||||
```
|
```
|
||||||
|
|
||||||
### Quote Builder
|
### Pricelists
|
||||||
```
|
```
|
||||||
POST /api/quote/validate → {"items": [...]} → {"valid": bool, "errors": [], "warnings": []}
|
GET /api/pricelists → List all pricelists
|
||||||
POST /api/quote/calculate → {"items": [...]} → {"items": [...], "total": 45000.00}
|
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
|
### Export
|
||||||
```
|
```
|
||||||
POST /api/export/csv → {"items": [...], "name": "Config 1"} → CSV file
|
POST /api/export/xlsx → Export specification as XLSX
|
||||||
POST /api/export/xlsx → {"items": [...], "name": "Config 1"} → XLSX file
|
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
|
||||||
### 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### htmx Partials
|
### htmx Partials
|
||||||
```
|
```
|
||||||
GET /partials/components?category=CPU&vendor=AMD → HTML fragment
|
GET /partials/components?category=CPU → Component list HTML
|
||||||
GET /partials/cart → cart HTML
|
GET /partials/spec-items/:spec_uuid → Specification items HTML
|
||||||
GET /partials/summary → price summary 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
|
## Frontend Guidelines
|
||||||
|
|
||||||
- **Mobile-first** design
|
- **Mobile-first** design
|
||||||
@@ -321,6 +580,8 @@ GET /partials/summary → price summary HTML
|
|||||||
- `text-yellow-600 bg-yellow-50` - normal
|
- `text-yellow-600 bg-yellow-50` - normal
|
||||||
- `text-orange-600 bg-orange-50` - stale
|
- `text-orange-600 bg-orange-50` - stale
|
||||||
- `text-red-600 bg-red-50` - critical
|
- `text-red-600 bg-red-50` - critical
|
||||||
|
- Sync status indicator in header
|
||||||
|
- Offline mode indicator when server unavailable
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -332,10 +593,9 @@ go run ./cmd/server
|
|||||||
go run ./cmd/importer
|
go run ./cmd/importer
|
||||||
|
|
||||||
# Run cron jobs manually
|
# Run cron jobs manually
|
||||||
go run ./cmd/cron -job=alerts # Check and generate alerts
|
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-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=update-popularity # Update popularity scores
|
|
||||||
|
|
||||||
# Build for production
|
# 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 ./cmd/server
|
||||||
@@ -343,50 +603,36 @@ CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/quoteforge-cron ./cmd/cron
|
|||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
go test ./...
|
go test ./...
|
||||||
````
|
```
|
||||||
|
|
||||||
## Cron Jobs
|
## 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.
|
- **Pricelist cleanup**: Weekly on Sunday at 4 AM (0 4 * * 0)
|
||||||
|
|
||||||
### Docker Compose Setup
|
|
||||||
|
|
||||||
The Docker setup includes a dedicated cron service that runs the following jobs:
|
|
||||||
|
|
||||||
- **Alerts check**: Every hour (0 * * * *)
|
|
||||||
- **Price updates**: Daily at 2 AM (0 2 * * *)
|
- **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 * * *)
|
- **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
|
## Code Style
|
||||||
|
|
||||||
- Use standard Go formatting (gofmt)
|
- Use standard Go formatting (gofmt)
|
||||||
- Error handling: always check errors, wrap with context
|
- 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
|
- Comments: in Russian or English, be consistent
|
||||||
- File naming: snake_case for files, PascalCase for types
|
- 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
|
||||||
|
|||||||
68
Dockerfile
68
Dockerfile
@@ -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"]
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user