Treat current configuration as main
This commit is contained in:
@@ -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`;
|
||||||
|
|||||||
@@ -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 += '<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 += '<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 += '</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'},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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="Восстановить">';
|
||||||
|
|||||||
Reference in New Issue
Block a user