Michael Chus 1606143b9f Fix sync errors for duplicate projects and add modal scrolling
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>
2026-02-16 21:25:22 +03:00
2026-02-11 19:50:35 +03:00
2026-01-22 17:29:47 +03:00
2026-02-11 19:48:40 +03:00

QuoteForge

Server Configuration & Quotation Tool

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

Go Version License Status

Возможности

Для пользователей

  • 📱 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_uuid
  • operation (create / update / rollback)
  • current_version_id и current_version_no
  • snapshot (текущее состояние конфигурации)
  • 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 — алерты для администраторов

Поддержка

По вопросам работы приложения обращайтесь:

Лицензия

Данное программное обеспечение является собственностью компании и предназначено исключительно для внутреннего использования. Распространение, копирование или модификация без письменного разрешения запрещены.

См. файл LICENSE для подробностей.

Description
No description provided
Readme 226 MiB
Languages
Go 64.8%
HTML 34.4%
Shell 0.5%
Makefile 0.3%