# QuoteForge **Server Configuration & Quotation Tool** QuoteForge — корпоративный инструмент для конфигурирования серверов и формирования коммерческих предложений (КП). Приложение работает в strict local-first режиме: пользовательские операции выполняются через локальную SQLite, MariaDB используется для синхронизации и серверного администрирования прайслистов. ![Go Version](https://img.shields.io/badge/Go-1.22+-00ADD8?style=flat&logo=go) ![License](https://img.shields.io/badge/License-Proprietary-red) ![Status](https://img.shields.io/badge/Status-In%20Development-yellow) ## Возможности ### Для пользователей - 📱 **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. Клонирование репозитория ```bash git clone https://github.com/your-company/quoteforge.git cd quoteforge ``` ### 2. Настройка runtime-конфига (опционально) `config.yaml` создаётся автоматически при первом старте в той же user-state папке, где находится `qfs.db`. Если найден старый формат, приложение автоматически мигрирует файл в актуальный runtime-формат (оставляя только используемые секции `server` и `logging`). При необходимости можно создать/отредактировать файл вручную: ```yaml server: host: "0.0.0.0" port: 8080 mode: "release" logging: level: "info" format: "json" output: "stdout" ``` ### 3. Миграции базы данных ```bash go run ./cmd/qfs -migrate ``` ### Мигратор OPS -> проекты (preview/apply) Переносит квоты, чьи названия начинаются с `OPS-xxxx` (где `x` — цифра), в проект `OPS-xxxx`. Если проекта нет, он будет создан; если архивный — реактивирован. Сначала всегда смотрите preview: ```bash go run ./cmd/migrate_ops_projects ``` Применение изменений: ```bash go run ./cmd/migrate_ops_projects -apply ``` Без интерактивного подтверждения: ```bash go run ./cmd/migrate_ops_projects -apply -yes ``` ### Права БД для пользователя приложения #### Полный набор прав для обычного пользователя Чтобы выдать существующему пользователю все необходимые права (без переоздания): ```sql -- Справочные таблицы (только чтение) GRANT SELECT ON RFQ_LOG.lot TO ''@'%'; GRANT SELECT ON RFQ_LOG.qt_lot_metadata TO ''@'%'; GRANT SELECT ON RFQ_LOG.qt_categories TO ''@'%'; GRANT SELECT ON RFQ_LOG.qt_pricelists TO ''@'%'; GRANT SELECT ON RFQ_LOG.qt_pricelist_items TO ''@'%'; -- Таблицы конфигураций и проектов (чтение и запись) GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_configurations TO ''@'%'; GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_projects TO ''@'%'; -- Таблицы синхронизации (только чтение для миграций, чтение+запись для статуса) GRANT SELECT ON RFQ_LOG.qt_client_local_migrations TO ''@'%'; GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_client_schema_state TO ''@'%'; GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_pricelist_sync_status TO ''@'%'; -- Применить изменения FLUSH PRIVILEGES; -- Проверка выданных прав SHOW GRANTS FOR ''@'%'; ``` #### Таблицы и их назначение | Таблица | Назначение | Права | Примечание | |---------|-----------|-------|-----------| | `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 | Отслеживание активности синхронизации | #### При создании нового пользователя Если нужно создать нового пользователя с нуля: ```sql -- 1) Создать пользователя CREATE USER IF NOT EXISTS 'quote_user'@'%' IDENTIFIED BY ''; -- 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 ...@''`, проверьте наличие конфликтующих записей пользователя с разными хостами (user@localhost vs user@'%'). ### 4. Импорт метаданных компонентов ```bash go run ./cmd/importer ``` ### 5. Запуск ```bash # 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 команды:** ```bash 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 показывается красный индикатор и причина блокировки в модалке синхронизации. #### Схема потоков данных синхронизации ```text [ 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-методом: ```bash 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 ```bash # Сборка образа 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 # Пример конфигурации └── go.mod ``` ## Роли пользователей | Роль | Описание | |------|----------| | `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. ## Разработка ```bash # Запуск в режиме разработки (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 | ## Интеграция с существующей БД 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](LICENSE) для подробностей.