Root cause: Projects with duplicate (code, variant) pairs fail to sync due to unique constraint on server. Example: multiple "OPS-1934" projects with variant="Dell" where one already exists on server. Fixes: 1. Sync service now detects duplicate (code, variant) on server and links local project to existing server project instead of failing 2. Local repair checks for duplicate (code, variant) pairs and deduplicates by appending UUID suffix to variant 3. Modal now scrollable with fixed header/footer (max-h-90vh) This allows users to sync projects that were created offline with conflicting codes/variants without losing data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
QuoteForge
Server Configuration & Quotation Tool
QuoteForge — корпоративный инструмент для конфигурирования серверов и формирования коммерческих предложений (КП).
Приложение работает в strict local-first режиме: пользовательские операции выполняются через локальную SQLite, MariaDB используется для синхронизации и серверного администрирования прайслистов.
Возможности
Для пользователей
- 📱 Mobile-first интерфейс — удобная работа с телефона и планшета
- 🖥️ Конфигуратор серверов — пошаговый выбор компонентов с проверкой совместимости
- 💰 Автоматический расчёт цен — актуальные цены на основе истории закупок
- 📊 Экспорт в CSV/XLSX — готовые спецификации для клиентов
- 💾 Сохранение конфигураций — история и шаблоны для повторного использования
- 🔌 Полная офлайн-работа — можно продолжать работу без сети и синхронизировать позже
- 🛡️ Защищенная синхронизация — sync блокируется preflight-проверкой, если локальная схема не готова
Для ценовых администраторов
- 📈 Умный расчёт цен — медиана, взвешенная медиана, среднее
- 🎯 Система алертов — уведомления о популярных компонентах с устаревшими ценами
- 📉 Аналитика использования — какие компоненты востребованы в КП
- ⚙️ Гибкие настройки — периоды расчёта, методы, ручные переопределения
Индикация актуальности цен
| Цвет | Статус | Условие |
|---|---|---|
| 🟢 Зелёный | Свежая | < 30 дней, ≥ 3 источника |
| 🟡 Жёлтый | Нормальная | 30-60 дней |
| 🟠 Оранжевый | Устаревающая | 60-90 дней |
| 🔴 Красный | Устаревшая | > 90 дней или нет данных |
Технологии
- Backend: Go 1.22+, Gin, GORM
- Frontend: HTML, Tailwind CSS, htmx
- Database: SQLite (runtime/local-first), MariaDB 11+ (sync + server admin)
- Export: excelize (XLSX), encoding/csv
Требования
- Go 1.22 или выше
- MariaDB 11.x (или MySQL 8.x)
- ~50 MB дискового пространства
Установка
1. Клонирование репозитория
git clone https://github.com/your-company/quoteforge.git
cd quoteforge
2. Настройка runtime-конфига (опционально)
config.yaml создаётся автоматически при первом старте в той же user-state папке, где находится qfs.db.
Если найден старый формат, приложение автоматически мигрирует файл в актуальный runtime-формат
(оставляя только используемые секции server и logging).
При необходимости можно создать/отредактировать файл вручную:
server:
host: "0.0.0.0"
port: 8080
mode: "release"
logging:
level: "info"
format: "json"
output: "stdout"
3. Миграции базы данных
go run ./cmd/qfs -migrate
Мигратор OPS -> проекты (preview/apply)
Переносит квоты, чьи названия начинаются с OPS-xxxx (где x — цифра), в проект OPS-xxxx.
Если проекта нет, он будет создан; если архивный — реактивирован.
Сначала всегда смотрите preview:
go run ./cmd/migrate_ops_projects
Применение изменений:
go run ./cmd/migrate_ops_projects -apply
Без интерактивного подтверждения:
go run ./cmd/migrate_ops_projects -apply -yes
Права БД для пользователя приложения
Полный набор прав для обычного пользователя
Чтобы выдать существующему пользователю все необходимые права (без переоздания):
-- Справочные таблицы (только чтение)
GRANT SELECT ON RFQ_LOG.lot TO '<DB_USER>'@'%';
GRANT SELECT ON RFQ_LOG.qt_lot_metadata TO '<DB_USER>'@'%';
GRANT SELECT ON RFQ_LOG.qt_categories TO '<DB_USER>'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelists TO '<DB_USER>'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelist_items TO '<DB_USER>'@'%';
-- Таблицы конфигураций и проектов (чтение и запись)
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_configurations TO '<DB_USER>'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_projects TO '<DB_USER>'@'%';
-- Таблицы синхронизации (только чтение для миграций, чтение+запись для статуса)
GRANT SELECT ON RFQ_LOG.qt_client_local_migrations TO '<DB_USER>'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_client_schema_state TO '<DB_USER>'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_pricelist_sync_status TO '<DB_USER>'@'%';
-- Применить изменения
FLUSH PRIVILEGES;
-- Проверка выданных прав
SHOW GRANTS FOR '<DB_USER>'@'%';
Таблицы и их назначение
| Таблица | Назначение | Права | Примечание |
|---|---|---|---|
lot |
Справочник компонентов | SELECT | Существующая таблица |
qt_lot_metadata |
Расширенные данные компонентов | SELECT | Метаданные компонентов |
qt_categories |
Категории компонентов | SELECT | Справочник |
qt_pricelists |
Прайслисты | SELECT | Управляется сервером |
qt_pricelist_items |
Позиции прайслистов | SELECT | Управляется сервером |
qt_configurations |
Сохранённые конфигурации | SELECT, INSERT, UPDATE | Основная таблица работы |
qt_projects |
Проекты | SELECT, INSERT, UPDATE | Для группировки конфигураций |
qt_client_local_migrations |
Справочник миграций БД | SELECT | Только чтение (управляется админом) |
qt_client_schema_state |
Состояние локальной схемы | SELECT, INSERT, UPDATE | Отслеживание примененных миграций |
qt_pricelist_sync_status |
Статус синхронизации | SELECT, INSERT, UPDATE | Отслеживание активности синхронизации |
При создании нового пользователя
Если нужно создать нового пользователя с нуля:
-- 1) Создать пользователя
CREATE USER IF NOT EXISTS 'quote_user'@'%' IDENTIFIED BY '<DB_PASSWORD>';
-- 2) Выдать все необходимые права
GRANT SELECT ON RFQ_LOG.lot TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_lot_metadata TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_categories TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelists TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelist_items TO 'quote_user'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_configurations TO 'quote_user'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_projects TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_client_local_migrations TO 'quote_user'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_client_schema_state TO 'quote_user'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_pricelist_sync_status TO 'quote_user'@'%';
-- 3) Применить изменения
FLUSH PRIVILEGES;
-- 4) Проверить права
SHOW GRANTS FOR 'quote_user'@'%';
Важные замечания
- Таблицы синхронизации должны быть созданы администратором БД один раз. Приложение не требует прав CREATE TABLE.
- Прайслисты (
qt_pricelists,qt_pricelist_items) — справочные таблицы, управляются сервером, пользователь имеет только SELECT. - Конфигурации и проекты — таблицы, в которые пишет само приложение (INSERT, UPDATE при сохранении изменений).
- Таблицы миграций нужны для синхронизации: приложение читает список миграций и отчитывается о применённых.
- Если видите ошибку
Access denied for user ...@'<ip>', проверьте наличие конфликтующих записей пользователя с разными хостами (user@localhost vs user@'%').
4. Импорт метаданных компонентов
go run ./cmd/importer
5. Запуск
# Development
go run ./cmd/qfs
# Production (with Makefile - recommended)
make build-release # Builds with version info
./bin/qfs -version # Check version
# Production (manual)
VERSION=$(git describe --tags --always --dirty)
CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$VERSION" -o bin/qfs ./cmd/qfs
./bin/qfs -version
Makefile команды:
make build-release # Оптимизированная сборка с версией
make build-all # Сборка для всех платформ (Linux, macOS, Windows)
make build-windows # Только для Windows
make run # Запуск dev сервера
make test # Запуск тестов
make install-hooks # Установить git hooks (блокировка коммита с секретами)
make clean # Очистка bin/
make help # Показать все команды
Приложение будет доступно по адресу: http://localhost:8080
Локальная SQLite база (state)
Локальная база приложения хранится в профиле пользователя и не зависит от расположения бинарника.
Имя файла: qfs.db.
- macOS:
~/Library/Application Support/QuoteForge/qfs.db - Linux:
$XDG_STATE_HOME/quoteforge/qfs.db(или~/.local/state/quoteforge/qfs.db) - Windows:
%LOCALAPPDATA%\\QuoteForge\\qfs.db
Можно переопределить путь через -localdb или переменную окружения QFS_DB_PATH.
Sync readiness guard
Перед push/pull выполняется preflight-проверка:
- доступен ли сервер (MariaDB);
- можно ли проверить и применить централизованные миграции локальной БД;
- подходит ли версия приложения под
min_app_versionмиграций.
Если проверка не пройдена:
- локальная работа (CRUD) продолжается;
- sync API возвращает
423 Lockedсreason_codeиreason_text; - в UI показывается красный индикатор и причина блокировки в модалке синхронизации.
Схема потоков данных синхронизации
[ SERVER / MariaDB ]
┌───────────────────────────┐
│ qt_projects │
│ qt_configurations │
│ qt_pricelists │
│ qt_pricelist_items │
│ qt_pricelist_sync_status │
└─────────────┬─────────────┘
│
pull (projects/configs/pricelists)
│
┌──────────────────┴──────────────────┐
│ │
[ CLIENT A / local SQLite ] [ CLIENT B / local SQLite ]
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ local_projects │ │ local_projects │
│ local_configurations │ │ local_configurations │
│ local_pricelists │ │ local_pricelists │
│ local_pricelist_items │ │ local_pricelist_items │
│ pending_changes (proj/config) │ │ pending_changes (proj/config) │
└───────────────┬───────────────┘ └───────────────┬───────────────┘
│ │
push (projects/configurations only) push (projects/configurations only)
│ │
└──────────────────┬────────────────────┘
│
[ SERVER / MariaDB ]
По сущностям:
- Конфигурации:
Client <-> Server <-> Other Clients - Проекты:
Client <-> Server <-> Other Clients - Прайслисты:
Server -> Clients only(локальный push отсутствует) - Локальная очистка прайслистов на клиенте: удаляются записи, которых нет на сервере и которые не используются активными локальными конфигурациями
Версионность конфигураций (local-first)
Для local_configurations используется append-only versioning через полные snapshot-версии:
- таблица:
local_configuration_versions - для каждого изменения создаётся новая версия (
version_no = max + 1) local_configurations.current_version_idуказывает на активную версию- старые версии не изменяются и не удаляются в обычном потоке
- rollback не "перематывает" историю, а создаёт новую версию из выбранного snapshot
При backfill (миграция 006_add_local_configuration_versions.sql) для существующих конфигураций создаётся v1 и проставляется current_version_id.
Rollback
Rollback выполняется API-методом:
POST /api/configs/:uuid/rollback
{
"target_version": 3,
"note": "optional"
}
Результат:
- создаётся новая версия
vNсdataиз целевой версии change_note = "rollback to v{target_version}"(+ note, если передан)current_version_idпереключается на новую версию- конфигурация уходит в
sync_status = pending
Локальный config.yaml
По умолчанию qfs ищет config.yaml в той же user-state папке, где лежит qfs.db (а не рядом с бинарником).
Если файла нет, он создаётся автоматически. Если формат устарел, он автоматически мигрируется в runtime-формат (server + logging).
Можно переопределить путь через -config или QFS_CONFIG_PATH.
Docker
# Сборка образа
docker build -t quoteforge .
# Запуск с docker-compose
docker-compose up -d
Структура проекта
quoteforge/
├── cmd/
│ ├── server/main.go # Main HTTP server
│ └── importer/main.go # Import metadata from lot table
├── internal/
│ ├── config/ # Конфигурация
│ ├── models/ # GORM модели
│ ├── handlers/ # HTTP handlers
│ ├── services/ # Бизнес-логика
│ ├── middleware/ # Auth, CORS, etc.
│ └── repository/ # Работа с БД
├── web/
│ ├── templates/ # HTML шаблоны
│ └── static/ # CSS, JS, изображения
├── migrations/ # SQL миграции
├── config.example.yaml # Пример конфигурации
├── releases/
│ └── memory/ # Changelog между тегами (v1.2.1.md, v1.2.2.md, ...)
└── go.mod
Releases & Changelog
Change log между версиями хранится в releases/memory/ каталоге в файлах вида v{major}.{minor}.{patch}.md.
Каждый файл содержит:
- Список коммитов между версиями
- Описание изменений и их влияния
- Breaking changes и заметки о миграции
Перед работой над кодом проверьте последний файл в этой папке, чтобы понять текущее состояние проекта.
Роли пользователей
| Роль | Описание |
|---|---|
viewer |
Просмотр, создание квот, экспорт |
editor |
+ сохранение конфигураций |
pricing_admin |
+ управление ценами и алертами |
admin |
Полный доступ, управление пользователями |
API
Документация API доступна по адресу /api/docs (в разработке).
Основные endpoints:
POST /api/auth/login # Авторизация
GET /api/components # Список компонентов
POST /api/quote/calculate # Расчёт цены
POST /api/export/xlsx # Экспорт в Excel
GET /api/configs # Сохранённые конфигурации
GET /api/configs/:uuid/versions # Список версий конфигурации
GET /api/configs/:uuid/versions/:version # Получить конкретную версию
POST /api/configs/:uuid/rollback # Rollback на указанную версию
POST /api/configs/:uuid/reactivate # Вернуть архивную конфигурацию в активные
GET /api/sync/readiness # Статус readiness guard (ready|blocked|unknown)
GET /api/sync/status # Сводный статус синхронизации
GET /api/sync/info # Данные для модалки синхронизации
POST /api/sync/push # Push pending changes (423, если blocked)
POST /api/sync/all # Full sync push+pull (423, если blocked)
POST /api/sync/components # Pull components (423, если blocked)
POST /api/sync/pricelists # Pull pricelists (423, если blocked)
Краткая карта sync API
| Endpoint | Назначение | Поток |
|---|---|---|
POST /api/sync/push |
Отправить локальные pending-изменения | SQLite -> MariaDB |
POST /api/sync/components |
Подтянуть справочник компонентов | MariaDB -> SQLite |
POST /api/sync/pricelists |
Подтянуть прайслисты и позиции | MariaDB -> SQLite |
POST /api/sync/all |
Полный цикл: push + pull + импорт проектов/конфигураций | двунаправленно |
GET /api/sync/readiness |
Статус preflight/readiness | read-only |
GET /api/sync/status / GET /api/sync/info |
Сводка статуса и данных синхронизации | read-only |
Sync payload для versioning
События в pending_changes для конфигураций содержат:
configuration_uuidoperation(create/update/rollback)current_version_idиcurrent_version_nosnapshot(текущее состояние конфигурации)idempotency_keyиconflict_policy(last_write_wins)
Это позволяет push-слою отправлять на сервер актуальное состояние и готовит основу для будущего conflict resolution.
Разработка
# Запуск в режиме разработки (hot reload)
go run ./cmd/qfs
# Запуск тестов
go test ./...
# Сборка для Linux
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/qfs ./cmd/qfs
Переменные окружения
| Переменная | Описание | По умолчанию |
|---|---|---|
QF_DB_HOST |
Хост базы данных | localhost |
QF_DB_PORT |
Порт базы данных | 3306 |
QF_DB_NAME |
Имя базы данных | RFQ_LOG |
QF_DB_USER |
Пользователь БД | — |
QF_DB_PASSWORD |
Пароль БД | — |
QF_JWT_SECRET |
Секрет для JWT | — |
QF_SERVER_PORT |
Порт сервера | 8080 |
QFS_DB_PATH |
Полный путь к локальной SQLite БД | OS-specific user state dir |
QFS_STATE_DIR |
Каталог state (если QFS_DB_PATH не задан) |
OS-specific user state dir |
QFS_CONFIG_PATH |
Полный путь к config.yaml |
OS-specific user state dir |
QFS_BACKUP_DIR |
Каталог для ротационных бэкапов локальных данных | <db dir>/backups |
QFS_BACKUP_DISABLE |
Отключить автоматические бэкапы (1/true/yes) |
— |
Интеграция с существующей БД
QuoteForge интегрируется с существующей базой RFQ_LOG:
lot— справочник компонентов (только чтение)lot_log— история цен от поставщиков (только чтение)supplier— справочник поставщиков (только чтение)
Новые таблицы QuoteForge имеют префикс qt_:
qt_users— пользователи приложенияqt_lot_metadata— расширенные данные компонентовqt_configurations— сохранённые конфигурацииqt_pricing_alerts— алерты для администраторов
Поддержка
По вопросам работы приложения обращайтесь:
- Email: mike@mchus.pro
- Internal: @mchus
Лицензия
Данное программное обеспечение является собственностью компании и предназначено исключительно для внутреннего использования. Распространение, копирование или модификация без письменного разрешения запрещены.
См. файл LICENSE для подробностей.