refactor: unified ingest pipeline + modular Redfish profile framework
Implement the full architectural plan: unified ingest.Service entry point for archive and Redfish payloads, modular redfishprofile package with composable profiles (generic, ami-family, msi, supermicro, dell, hgx-topology), score-based profile matching with fallback expansion mode, and profile-driven acquisition/analysis plans. Vendor-specific logic moved out of common executors and into profile hooks. GPU chassis lookup strategies and known storage recovery collections (IntelVROC/HA-RAID/MRVL) now live in ResolvedAnalysisPlan, populated by profiles at analysis time. Replay helpers read from the plan; no hardcoded path lists remain in generic code. Also splits redfish_replay.go into domain modules (gpu, storage, inventory, fru, profiles) and adds full fixture/matcher/directive test coverage including Dell, AMI, unknown-vendor fallback, and deterministic ordering. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -422,7 +422,12 @@ function startCollectionJob(payload) {
|
||||
id: body.job_id,
|
||||
status: normalizeJobStatus(body.status || 'queued'),
|
||||
progress: 0,
|
||||
currentPhase: '',
|
||||
etaSeconds: null,
|
||||
logs: [],
|
||||
activeModules: [],
|
||||
moduleScores: [],
|
||||
debugInfo: null,
|
||||
payload
|
||||
};
|
||||
appendJobLog(body.message || 'Задача поставлена в очередь');
|
||||
@@ -460,7 +465,12 @@ function pollCollectionJobStatus() {
|
||||
const prevStatus = collectionJob.status;
|
||||
collectionJob.status = normalizeJobStatus(body.status || collectionJob.status);
|
||||
collectionJob.progress = Number.isFinite(body.progress) ? body.progress : collectionJob.progress;
|
||||
collectionJob.currentPhase = body.current_phase || collectionJob.currentPhase || '';
|
||||
collectionJob.etaSeconds = Number.isFinite(body.eta_seconds) ? body.eta_seconds : collectionJob.etaSeconds;
|
||||
collectionJob.error = body.error || '';
|
||||
collectionJob.activeModules = Array.isArray(body.active_modules) ? body.active_modules : collectionJob.activeModules;
|
||||
collectionJob.moduleScores = Array.isArray(body.module_scores) ? body.module_scores : collectionJob.moduleScores;
|
||||
collectionJob.debugInfo = body.debug_info || collectionJob.debugInfo || null;
|
||||
syncServerLogs(body.logs);
|
||||
renderCollectionJob();
|
||||
|
||||
@@ -528,9 +538,14 @@ function renderCollectionJob() {
|
||||
const progressValue = document.getElementById('job-progress-value');
|
||||
const etaValue = document.getElementById('job-eta-value');
|
||||
const progressBar = document.getElementById('job-progress-bar');
|
||||
const activeModulesBlock = document.getElementById('job-active-modules');
|
||||
const activeModulesList = document.getElementById('job-active-modules-list');
|
||||
const debugInfoBlock = document.getElementById('job-debug-info');
|
||||
const debugSummary = document.getElementById('job-debug-summary');
|
||||
const phaseTelemetryNode = document.getElementById('job-phase-telemetry');
|
||||
const logsList = document.getElementById('job-logs-list');
|
||||
const cancelButton = document.getElementById('cancel-job-btn');
|
||||
if (!jobStatusBlock || !jobIdValue || !statusValue || !progressValue || !etaValue || !progressBar || !logsList || !cancelButton) {
|
||||
if (!jobStatusBlock || !jobIdValue || !statusValue || !progressValue || !etaValue || !progressBar || !activeModulesBlock || !activeModulesList || !debugInfoBlock || !debugSummary || !phaseTelemetryNode || !logsList || !cancelButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -558,6 +573,8 @@ function renderCollectionJob() {
|
||||
etaValue.textContent = eta;
|
||||
progressBar.style.width = `${progressPercent}%`;
|
||||
progressBar.textContent = `${progressPercent}%`;
|
||||
renderJobActiveModules(activeModulesBlock, activeModulesList);
|
||||
renderJobDebugInfo(debugInfoBlock, debugSummary, phaseTelemetryNode);
|
||||
|
||||
logsList.innerHTML = [...collectionJob.logs].reverse().map((log) => (
|
||||
`<li><span class="log-time">${escapeHtml(log.time)}</span><span class="log-message">${escapeHtml(log.message)}</span></li>`
|
||||
@@ -568,6 +585,9 @@ function renderCollectionJob() {
|
||||
}
|
||||
|
||||
function latestCollectionActivityMessage() {
|
||||
if (collectionJob && collectionJob.currentPhase) {
|
||||
return humanizeCollectionPhase(collectionJob.currentPhase);
|
||||
}
|
||||
if (!collectionJob || !Array.isArray(collectionJob.logs) || collectionJob.logs.length === 0) {
|
||||
return 'Сбор данных...';
|
||||
}
|
||||
@@ -584,6 +604,9 @@ function latestCollectionActivityMessage() {
|
||||
}
|
||||
|
||||
function latestCollectionETA() {
|
||||
if (collectionJob && Number.isFinite(collectionJob.etaSeconds) && collectionJob.etaSeconds > 0) {
|
||||
return formatDurationSeconds(collectionJob.etaSeconds);
|
||||
}
|
||||
if (!collectionJob || !Array.isArray(collectionJob.logs) || collectionJob.logs.length === 0) {
|
||||
return '-';
|
||||
}
|
||||
@@ -649,6 +672,94 @@ function normalizeJobStatus(status) {
|
||||
return String(status || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function humanizeCollectionPhase(phase) {
|
||||
const value = String(phase || '').trim().toLowerCase();
|
||||
return {
|
||||
discovery: 'Discovery',
|
||||
snapshot: 'Snapshot',
|
||||
snapshot_postprobe_nvme: 'Snapshot NVMe post-probe',
|
||||
snapshot_postprobe_collections: 'Snapshot collection post-probe',
|
||||
prefetch: 'Prefetch critical endpoints',
|
||||
critical_plan_b: 'Critical plan-B',
|
||||
profile_plan_b: 'Profile plan-B'
|
||||
}[value] || value || 'Сбор данных...';
|
||||
}
|
||||
|
||||
function formatDurationSeconds(totalSeconds) {
|
||||
const seconds = Math.max(0, Math.round(Number(totalSeconds) || 0));
|
||||
if (seconds <= 0) {
|
||||
return '-';
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
if (minutes === 0) {
|
||||
return `${remainingSeconds}s`;
|
||||
}
|
||||
if (remainingSeconds === 0) {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
|
||||
function renderJobActiveModules(activeModulesBlock, activeModulesList) {
|
||||
const activeModules = collectionJob && Array.isArray(collectionJob.activeModules) ? collectionJob.activeModules : [];
|
||||
if (activeModules.length === 0) {
|
||||
activeModulesBlock.classList.add('hidden');
|
||||
activeModulesList.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
activeModulesBlock.classList.remove('hidden');
|
||||
activeModulesList.innerHTML = activeModules.map((module) => {
|
||||
const score = Number.isFinite(module.score) ? module.score : 0;
|
||||
return `<span class="job-module-chip" title="${escapeHtml(moduleTitle(module))}">
|
||||
<span class="job-module-chip-name">${escapeHtml(module.name || '-')}</span>
|
||||
<span class="job-module-chip-score">${escapeHtml(String(score))}</span>
|
||||
</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderJobDebugInfo(debugInfoBlock, debugSummary, phaseTelemetryNode) {
|
||||
const debug = collectionJob && collectionJob.debugInfo ? collectionJob.debugInfo : null;
|
||||
if (!debug) {
|
||||
debugInfoBlock.classList.add('hidden');
|
||||
debugSummary.innerHTML = '';
|
||||
phaseTelemetryNode.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
debugInfoBlock.classList.remove('hidden');
|
||||
const throttled = debug.adaptive_throttled ? 'on' : 'off';
|
||||
const prefetchEnabled = typeof debug.prefetch_enabled === 'boolean' ? String(debug.prefetch_enabled) : 'auto';
|
||||
debugSummary.innerHTML = `adaptive_throttling=<strong>${escapeHtml(throttled)}</strong>, snapshot_workers=<strong>${escapeHtml(String(debug.snapshot_workers || 0))}</strong>, prefetch_workers=<strong>${escapeHtml(String(debug.prefetch_workers || 0))}</strong>, prefetch_enabled=<strong>${escapeHtml(prefetchEnabled)}</strong>`;
|
||||
|
||||
const phases = Array.isArray(debug.phase_telemetry) ? debug.phase_telemetry : [];
|
||||
if (phases.length === 0) {
|
||||
phaseTelemetryNode.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
phaseTelemetryNode.innerHTML = phases.map((item) => (
|
||||
`<div class="job-phase-row">
|
||||
<span class="job-phase-name">${escapeHtml(humanizeCollectionPhase(item.phase || ''))}</span>
|
||||
<span class="job-phase-metric">req=${escapeHtml(String(item.requests || 0))}</span>
|
||||
<span class="job-phase-metric">err=${escapeHtml(String(item.errors || 0))}</span>
|
||||
<span class="job-phase-metric">avg=${escapeHtml(String(item.avg_ms || 0))}ms</span>
|
||||
<span class="job-phase-metric">p95=${escapeHtml(String(item.p95_ms || 0))}ms</span>
|
||||
</div>`
|
||||
)).join('');
|
||||
}
|
||||
|
||||
function moduleTitle(activeModule) {
|
||||
const name = String(activeModule && activeModule.name || '').trim();
|
||||
const scores = collectionJob && Array.isArray(collectionJob.moduleScores) ? collectionJob.moduleScores : [];
|
||||
const full = scores.find((item) => String(item && item.name || '').trim() === name);
|
||||
if (!full) {
|
||||
return name;
|
||||
}
|
||||
const state = full.active ? 'active' : 'inactive';
|
||||
return `${name}: score=${Number.isFinite(full.score) ? full.score : 0}, priority=${Number.isFinite(full.priority) ? full.priority : 0}, ${state}`;
|
||||
}
|
||||
|
||||
async function loadDataFromStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/status');
|
||||
|
||||
Reference in New Issue
Block a user