misc: sds format support, convert limits, dell dedup, supermicro removal, bible updates
Parser / archive: - Add .sds extension as tar-format alias (archive.go) - Add tests for multipart upload size limits (multipart_limits_test.go) - Remove supermicro crashdump parser (ADL-015) Dell parser: - Remove GPU duplicates from PCIeDevices (DCIM_VideoView vs DCIM_PCIDeviceView both list the same GPU; VideoView record is authoritative) Server: - Add LOGPILE_CONVERT_MAX_MB env var for independent convert batch size limit - Improve "file too large" error message with current limit value Web: - Add CONVERT_MAX_FILES_PER_BATCH = 1000 cap - Minor UI copy and CSS fixes Bible: - bible-local/06-parsers.md: add pci.ids enrichment rule (enrich model from pciids when name is empty but vendor_id+device_id are present) - Sync bible submodule and local overview/architecture docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let sourceType = 'archive';
|
||||
let convertFiles = [];
|
||||
let isConvertRunning = false;
|
||||
const CONVERT_MAX_FILES_PER_BATCH = 1000;
|
||||
let supportedUploadExtensions = null;
|
||||
let supportedConvertExtensions = null;
|
||||
let apiConnectPayload = null;
|
||||
@@ -539,12 +540,12 @@ async function loadParsersInfo() {
|
||||
const container = document.getElementById('parsers-info');
|
||||
|
||||
if (data.parsers && data.parsers.length > 0) {
|
||||
let html = '<p class="parsers-title">Поддерживаемые платформы:</p><div class="parsers-list">';
|
||||
let html = '<p class="parsers-title">Подключенные парсеры:</p><div class="parsers-list">';
|
||||
data.parsers.forEach(p => {
|
||||
html += `<div class="parser-item">
|
||||
<span class="parser-name">${escapeHtml(p.name)}</span>
|
||||
<span class="parser-version">v${escapeHtml(p.version)}</span>
|
||||
</div>`;
|
||||
html += `<span class="parser-chip">
|
||||
<span class="parser-chip-name">${escapeHtml(p.name)}</span>
|
||||
<span class="parser-chip-version">v${escapeHtml(p.version)}</span>
|
||||
</span>`;
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
@@ -653,8 +654,10 @@ function renderConvertSummary() {
|
||||
const remaining = supportedFiles.length - previewFiles.length;
|
||||
const previewText = previewFiles.length > 0 ? `Примеры: ${previewFiles.join(', ')}` : '';
|
||||
const skippedText = skippedCount > 0 ? ` Пропущено неподдерживаемых: ${skippedCount}.` : '';
|
||||
const batchCount = Math.ceil(supportedFiles.length / CONVERT_MAX_FILES_PER_BATCH);
|
||||
const batchesText = batchCount > 1 ? ` Будет ${batchCount} прохода(ов) по ${CONVERT_MAX_FILES_PER_BATCH} файлов.` : '';
|
||||
|
||||
summary.innerHTML = `<strong>${supportedFiles.length}</strong> файлов готовы к конвертации.${previewText ? ` ${previewText}` : ''}${remaining > 0 ? ` и ещё ${remaining}` : ''}.${skippedText}`;
|
||||
summary.innerHTML = `<strong>${supportedFiles.length}</strong> файлов готовы к конвертации.${previewText ? ` ${previewText}` : ''}${remaining > 0 ? ` и ещё ${remaining}` : ''}.${skippedText}${batchesText}`;
|
||||
summary.className = 'api-connect-status';
|
||||
}
|
||||
|
||||
@@ -674,58 +677,71 @@ async function runConvertBatch() {
|
||||
renderConvertStatus('В выбранной папке нет файлов поддерживаемого типа', 'error');
|
||||
return;
|
||||
}
|
||||
const batches = chunkFiles(supportedFiles, CONVERT_MAX_FILES_PER_BATCH);
|
||||
|
||||
isConvertRunning = true;
|
||||
runButton.disabled = true;
|
||||
renderConvertProgress(0, 'Подготовка загрузки...');
|
||||
renderConvertStatus('Выполняю пакетную конвертацию...', 'info');
|
||||
|
||||
const formData = new FormData();
|
||||
supportedFiles.forEach(file => {
|
||||
const relativePath = file.webkitRelativePath || file.name || 'file';
|
||||
formData.append('files[]', file, relativePath);
|
||||
});
|
||||
renderConvertStatus(`Выполняю пакетную конвертацию (${batches.length} проходов)...`, 'info');
|
||||
|
||||
try {
|
||||
const startResponse = await uploadConvertBatch(formData, (percent) => {
|
||||
const uploadPercent = Math.round(percent * 0.3);
|
||||
renderConvertProgress(uploadPercent, `Загрузка файлов: ${percent}%`);
|
||||
});
|
||||
|
||||
if (!startResponse.ok) {
|
||||
const errorPayload = parseConvertErrorPayload(startResponse.bodyText);
|
||||
hideConvertProgress();
|
||||
renderConvertStatus(errorPayload.error || 'Пакетная конвертация завершилась с ошибкой', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startResponse.jobId) {
|
||||
hideConvertProgress();
|
||||
renderConvertStatus('Сервер не вернул идентификатор задачи', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForConvertJob(startResponse.jobId, (statusPayload) => {
|
||||
const serverProgress = Number(statusPayload.progress || 0);
|
||||
const combined = 30 + Math.round(Math.max(0, Math.min(100, serverProgress)) * 0.7);
|
||||
renderConvertProgress(combined, `Конвертация: ${serverProgress}%`);
|
||||
});
|
||||
|
||||
renderConvertProgress(100, 'Подготовка выгрузки...');
|
||||
const downloadResponse = await downloadConvertArchive(startResponse.jobId);
|
||||
if (!downloadResponse.ok) {
|
||||
const errorPayload = parseConvertErrorPayload(downloadResponse.bodyText);
|
||||
hideConvertProgress();
|
||||
renderConvertStatus(errorPayload.error || 'Не удалось скачать результат', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = downloadResponse.blob;
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
downloadBlob(blob, `logpile-convert-${timestamp}.zip`);
|
||||
const summary = downloadResponse.summaryHeader || 'Конвертация завершена';
|
||||
const passSummaries = [];
|
||||
|
||||
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
|
||||
const batchFiles = batches[batchIdx];
|
||||
const pass = batchIdx + 1;
|
||||
const passLabel = `Проход ${pass}/${batches.length}`;
|
||||
const passStart = Math.round((batchIdx / batches.length) * 100);
|
||||
const passEnd = Math.round(((batchIdx + 1) / batches.length) * 100);
|
||||
|
||||
const formData = new FormData();
|
||||
batchFiles.forEach(file => {
|
||||
const relativePath = file.webkitRelativePath || file.name || 'file';
|
||||
formData.append('files[]', file, relativePath);
|
||||
});
|
||||
|
||||
const startResponse = await uploadConvertBatch(formData, (percent) => {
|
||||
const clamped = Math.max(0, Math.min(100, Number(percent) || 0));
|
||||
const uploadPhase = passStart + Math.round((passEnd - passStart) * 0.3 * (clamped / 100));
|
||||
renderConvertProgress(uploadPhase, `${passLabel}: загрузка ${clamped}%`);
|
||||
});
|
||||
|
||||
if (!startResponse.ok) {
|
||||
const errorPayload = parseConvertErrorPayload(startResponse.bodyText);
|
||||
hideConvertProgress();
|
||||
renderConvertStatus(`${passLabel}: ${errorPayload.error || 'пакетная конвертация завершилась с ошибкой'}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startResponse.jobId) {
|
||||
hideConvertProgress();
|
||||
renderConvertStatus(`${passLabel}: сервер не вернул идентификатор задачи`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForConvertJob(startResponse.jobId, (statusPayload) => {
|
||||
const serverProgress = Math.max(0, Math.min(100, Number(statusPayload.progress || 0)));
|
||||
const phase = 0.3 + 0.7 * (serverProgress / 100);
|
||||
const combined = passStart + Math.round((passEnd - passStart) * phase);
|
||||
renderConvertProgress(combined, `${passLabel}: конвертация ${serverProgress}%`);
|
||||
});
|
||||
|
||||
const downloadResponse = await downloadConvertArchive(startResponse.jobId);
|
||||
if (!downloadResponse.ok) {
|
||||
const errorPayload = parseConvertErrorPayload(downloadResponse.bodyText);
|
||||
hideConvertProgress();
|
||||
renderConvertStatus(`${passLabel}: ${errorPayload.error || 'не удалось скачать результат'}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const suffix = batches.length > 1 ? `-part${pass}` : '';
|
||||
downloadBlob(downloadResponse.blob, `logpile-convert-${timestamp}${suffix}.zip`);
|
||||
passSummaries.push(downloadResponse.summaryHeader || `${passLabel}: завершено`);
|
||||
}
|
||||
|
||||
hideConvertProgress();
|
||||
renderConvertStatus(summary, 'success');
|
||||
renderConvertStatus(passSummaries.join(' | '), 'success');
|
||||
} catch (err) {
|
||||
hideConvertProgress();
|
||||
renderConvertStatus('Ошибка соединения при конвертации', 'error');
|
||||
@@ -735,6 +751,15 @@ async function runConvertBatch() {
|
||||
}
|
||||
}
|
||||
|
||||
function chunkFiles(files, chunkSize) {
|
||||
const safeChunkSize = Math.max(1, Number(chunkSize) || 1);
|
||||
const chunks = [];
|
||||
for (let i = 0; i < files.length; i += safeChunkSize) {
|
||||
chunks.push(files.slice(i, i + safeChunkSize));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function uploadConvertBatch(formData, onUploadPercent) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
Reference in New Issue
Block a user