Treat current configuration as main

This commit is contained in:
Mikhail Chusavitin
2026-03-17 18:43:49 +03:00
parent 20ce0124be
commit a8d8d7dfa9
4 changed files with 53 additions and 23 deletions

View File

@@ -3,16 +3,48 @@
Дата релиза: 2026-03-16 Дата релиза: 2026-03-16
Тег: `v1.5.4` Тег: `v1.5.4`
Предыдущий релиз: `v1.5.0`
## Ключевые изменения ## Ключевые изменения
- runtime автоматически нормализует `server.host` к `127.0.0.1` и переписывает некорректный локальный конфиг; - pricing tab переработан: закупка и продажа разделены на отдельные таблицы с ценами за 1 шт.;
- экран прайслиста переработан под разные типы источников; удалены misleading-колонки `Поставщик` и `partnumbers`;
- runtime и startup ужесточены: локальный клиент принудительно работает только на loopback, конфиг автоматически нормализуется;
- добавлены действия с вариантом и унифицированы правила именования `_копия` для вариантов и конфигураций; - добавлены действия с вариантом и унифицированы правила именования `_копия` для вариантов и конфигураций;
- исправлен CSV-экспорт прайсинговых таблиц в конфигураторе под Excel-совместимый формат; - исправлен CSV-экспорт прайсинговых таблиц в конфигураторе под Excel-совместимый формат Excel-friendly;
- таблица проектов переработана: новая колонка даты, tooltip с деталями, отдельный автор, компактные действия и ссылка на трекер; - таблица проектов переработана: дата последней правки, tooltip с деталями, отдельный автор, компактные действия и ссылка на трекер;
- sync больше не подменяет `updated_at` проектов временем синхронизации; - sync больше не подменяет `updated_at` проектов временем синхронизации;
- добавлена одноразовая утилита `cmd/migrate_project_updated_at` для пересинхронизации `updated_at` проектов из MariaDB в локальную SQLite. - добавлена одноразовая утилита `cmd/migrate_project_updated_at` для пересинхронизации `updated_at` проектов из MariaDB в локальную SQLite;
- runtime config, release notes и `bible-local/` очищены и приведены к актуальной архитектуре;
- `scripts/release.sh` больше не затирает существующий `RELEASE_NOTES.md`. - `scripts/release.sh` больше не затирает существующий `RELEASE_NOTES.md`.
## Summary
### UI и UX
- вкладка ценообразования теперь разделена на отдельные таблицы закупки и продажи;
- список проектов переработан: новая колонка даты, отдельный автор, tooltip с деталями, компактные действия, ссылка на трекер;
- для вариантов добавлены действия переименования, переноса и копирования;
- копии вариантов и конфигураций теперь именуются единообразно: `_копия`, `_копия2`, `_копия3`.
### Прайслисты и экспорт
- экран прайслиста переработан под разные типы источников;
- из прайслистов убраны misleading-колонки `Поставщик` и `partnumbers`;
- CSV-экспорт прайсинговых таблиц в конфигураторе приведён к Excel-совместимому формату.
### Runtime и sync
- локальный runtime нормализует `server.host` к `127.0.0.1` и переписывает некорректный runtime config;
- sync перестал подменять `updated_at` проектов временем локальной синхронизации;
- добавлена утилита `cmd/migrate_project_updated_at` для восстановления локальных дат проектов с сервера.
### Документация и release tooling
- `bible-local/` сокращён до актуальных архитектурных контрактов;
- release notes и release-структура приведены к одному формату;
- `scripts/release.sh` теперь сохраняет существующий `RELEASE_NOTES.md` и не затирает его шаблоном.
## Затронутые области ## Затронутые области
- `cmd/qfs/`; - `cmd/qfs/`;
@@ -20,6 +52,8 @@
- `internal/localdb/`; - `internal/localdb/`;
- `internal/services/project.go`; - `internal/services/project.go`;
- `internal/services/sync/service.go`; - `internal/services/sync/service.go`;
- `internal/handlers/pricelist.go`;
- `web/templates/pricelist_detail.html`;
- `web/templates/index.html`; - `web/templates/index.html`;
- `web/templates/project_detail.html`; - `web/templates/project_detail.html`;
- `web/templates/projects.html`; - `web/templates/projects.html`;

View File

@@ -135,15 +135,18 @@ async function loadVersions() {
} }
function renderVersions(versions) { function renderVersions(versions) {
if (versions.length === 0) { const currentVersionNo = configData && configData.current_version_no ? Number(configData.current_version_no) : null;
const snapshots = versions.filter(v => Number(v.version_no) !== currentVersionNo);
if (snapshots.length === 0) {
document.getElementById('revisions-list').innerHTML = document.getElementById('revisions-list').innerHTML =
'<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500">Нет ревизий</div>'; '<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500">Нет прошлых снимков. Рабочая версия остается main.</div>';
return; return;
} }
let html = '<div class="bg-white rounded-lg shadow overflow-hidden"><table class="w-full">'; let html = '<div class="bg-white rounded-lg shadow overflow-hidden"><table class="w-full">';
html += '<thead class="bg-gray-50"><tr>'; html += '<thead class="bg-gray-50"><tr>';
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Версия</th>'; html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Снимок</th>';
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Дата</th>'; html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Дата</th>';
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Автор</th>'; html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Автор</th>';
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Артикул</th>'; html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Артикул</th>';
@@ -152,16 +155,14 @@ function renderVersions(versions) {
html += '<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Действия</th>'; html += '<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Действия</th>';
html += '</tr></thead><tbody class="divide-y">'; html += '</tr></thead><tbody class="divide-y">';
versions.forEach((v, idx) => { snapshots.forEach((v) => {
const date = new Date(v.created_at).toLocaleString('ru-RU'); const date = new Date(v.created_at).toLocaleString('ru-RU');
const author = v.created_by || '—'; const author = v.created_by || '—';
const snapshot = parseVersionSnapshot(v); const snapshot = parseVersionSnapshot(v);
const isCurrent = idx === 0;
html += '<tr class="hover:bg-gray-50' + (isCurrent ? ' bg-blue-50' : '') + '">'; html += '<tr class="hover:bg-gray-50">';
html += '<td class="px-4 py-3 text-sm font-medium">'; html += '<td class="px-4 py-3 text-sm font-medium">';
html += 'v' + v.version_no; html += 'v' + v.version_no;
if (isCurrent) html += ' <span class="text-xs text-blue-600 font-normal">(текущая)</span>';
html += '</td>'; html += '</td>';
html += '<td class="px-4 py-3 text-sm text-gray-500">' + escapeHtml(date) + '</td>'; html += '<td class="px-4 py-3 text-sm text-gray-500">' + escapeHtml(date) + '</td>';
html += '<td class="px-4 py-3 text-sm text-gray-500">' + escapeHtml(author) + '</td>'; html += '<td class="px-4 py-3 text-sm text-gray-500">' + escapeHtml(author) + '</td>';
@@ -178,11 +179,8 @@ function renderVersions(versions) {
html += '<button onclick="cloneFromVersion(' + v.version_no + ')" class="text-green-600 hover:text-green-800" title="Скопировать из этой ревизии">'; html += '<button onclick="cloneFromVersion(' + v.version_no + ')" class="text-green-600 hover:text-green-800" title="Скопировать из этой ревизии">';
html += '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg></button>'; html += '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg></button>';
// Rollback (not for current version) html += '<button onclick="rollbackToVersion(' + v.version_no + ')" class="text-orange-600 hover:text-orange-800" title="Восстановить этот снимок как новую main-версию">';
if (!isCurrent) {
html += '<button onclick="rollbackToVersion(' + v.version_no + ')" class="text-orange-600 hover:text-orange-800" title="Восстановить эту ревизию">';
html += '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path></svg></button>'; html += '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path></svg></button>';
}
html += '</td></tr>'; html += '</td></tr>';
}); });
@@ -212,7 +210,7 @@ async function cloneFromVersion(versionNo) {
} }
async function rollbackToVersion(versionNo) { async function rollbackToVersion(versionNo) {
if (!confirm('Восстановить конфигурацию до ревизии v' + versionNo + '?')) return; if (!confirm('Восстановить снимок v' + versionNo + ' как новую рабочую версию main?')) return;
const resp = await fetch('/api/configs/' + configUUID + '/rollback', { const resp = await fetch('/api/configs/' + configUUID + '/rollback', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},

View File

@@ -24,7 +24,7 @@
<span id="breadcrumb-config-name">Конфигуратор</span> <span id="breadcrumb-config-name">Конфигуратор</span>
</a> </a>
<span class="text-gray-400">-</span> <span class="text-gray-400">-</span>
<span id="breadcrumb-config-version">v1</span> <span id="breadcrumb-config-version">main</span>
</div> </div>
</div> </div>
<div id="save-buttons" class="hidden flex items-center space-x-2"> <div id="save-buttons" class="hidden flex items-center space-x-2">
@@ -476,7 +476,7 @@ function updateConfigBreadcrumbs() {
const fullConfigName = configName || 'Конфигурация'; const fullConfigName = configName || 'Конфигурация';
configEl.textContent = truncateBreadcrumbSpecName(fullConfigName); configEl.textContent = truncateBreadcrumbSpecName(fullConfigName);
configEl.title = fullConfigName; configEl.title = fullConfigName;
versionEl.textContent = 'v' + (currentVersionNo || 1); versionEl.textContent = 'main';
const configNameLinkEl = document.getElementById('breadcrumb-config-name-link'); const configNameLinkEl = document.getElementById('breadcrumb-config-name-link');
if (configNameLinkEl && configUUID) { if (configNameLinkEl && configUUID) {
configNameLinkEl.href = '/configs/' + configUUID + '/revisions'; configNameLinkEl.href = '/configs/' + configUUID + '/revisions';
@@ -2172,9 +2172,8 @@ async function saveConfig(showNotification = true) {
const saved = await resp.json(); const saved = await resp.json();
if (saved && saved.current_version_no) { if (saved && saved.current_version_no) {
currentVersionNo = saved.current_version_no; currentVersionNo = saved.current_version_no;
const versionEl = document.getElementById('breadcrumb-config-version');
if (versionEl) versionEl.textContent = 'v' + currentVersionNo;
} }
updateConfigBreadcrumbs();
hasUnsavedChanges = false; hasUnsavedChanges = false;
clearAutosaveDraft(); clearAutosaveDraft();
exitSaveStarted = false; exitSaveStarted = false;

View File

@@ -472,8 +472,7 @@ function renderConfigs(configs) {
html += '<td class="px-4 py-3 text-sm text-gray-500"><input type="number" min="1" value="' + serverCount + '" class="w-16 px-1 py-0.5 border rounded text-center text-sm" data-uuid="' + c.uuid + '" data-prev="' + serverCount + '" onchange="updateConfigServerCount(this)"></td>'; html += '<td class="px-4 py-3 text-sm text-gray-500"><input type="number" min="1" value="' + serverCount + '" class="w-16 px-1 py-0.5 border rounded text-center text-sm" data-uuid="' + c.uuid + '" data-prev="' + serverCount + '" onchange="updateConfigServerCount(this)"></td>';
} }
html += '<td class="px-4 py-3 text-sm text-right" data-total-uuid="' + c.uuid + '">' + formatMoneyNoDecimals(total) + '</td>'; html += '<td class="px-4 py-3 text-sm text-right" data-total-uuid="' + c.uuid + '">' + formatMoneyNoDecimals(total) + '</td>';
const versionNo = c.current_version_no || 1; html += '<td class="px-2 py-3 text-sm text-center text-gray-500 w-12">main</td>';
html += '<td class="px-2 py-3 text-sm text-center text-gray-500 w-12">v' + versionNo + '</td>';
html += '<td class="px-4 py-3 text-sm text-right whitespace-nowrap"><div class="inline-flex items-center justify-end gap-2">'; html += '<td class="px-4 py-3 text-sm text-right whitespace-nowrap"><div class="inline-flex items-center justify-end gap-2">';
if (configStatusMode === 'archived') { if (configStatusMode === 'archived') {
html += '<button onclick="reactivateConfig(\'' + c.uuid + '\')" class="text-emerald-600 hover:text-emerald-800" title="Восстановить">'; html += '<button onclick="reactivateConfig(\'' + c.uuid + '\')" class="text-emerald-600 hover:text-emerald-800" title="Восстановить">';