diff --git a/backend/routers/coverage.py b/backend/routers/coverage.py index db69729..29766bd 100644 --- a/backend/routers/coverage.py +++ b/backend/routers/coverage.py @@ -1339,22 +1339,41 @@ def get_onboarding_status(db: Session = Depends(get_db)): rules_for_src = rule_by_source.get(src.source_name, []) rules_firing = any(firing_cache.get(r, 0) > 0 for r in rules_for_src) - stages = [ + has_detection_rules = len(rules_for_src) > 0 + + # Core stages (apply to every source) + core_stages = [ {"stage": "Data Received", "done": (src.event_count or 0) > 0}, {"stage": "Parser File Exists", "done": parser_info is not None}, {"stage": "Parser Active", "done": parser_active}, {"stage": "Source Labeled", "done": has_ds_name and parser_active}, - {"stage": "Detection Rules", "done": len(rules_for_src) > 0}, - {"stage": "Rules Firing", "done": rules_firing}, ] - completed = sum(1 for s in stages if s["done"]) + # Detection stages (only meaningful when detection rules exist) + detection_stages = [ + {"stage": "Detection Rules", "done": has_detection_rules, "na": False}, + {"stage": "Rules Firing", "done": rules_firing, "na": False}, + ] + + if has_detection_rules: + stages = core_stages + detection_stages + total = 6 + else: + # Mark detection stages as N/A — don't count against progress + stages = core_stages + [ + {"stage": "Detection Rules", "done": False, "na": True}, + {"stage": "Rules Firing", "done": False, "na": True}, + ] + total = 4 # progress calculated over core stages only + + completed = sum(1 for s in stages if s.get("done") and not s.get("na")) out.append({ "source": src.source_name, "event_count": src.event_count, "stages": stages, "completed": completed, - "total": len(stages), - "pct": round(completed / len(stages) * 100), + "total": total, + "has_detection_rules": has_detection_rules, + "pct": round(completed / total * 100) if total else 0, }) # Sort: incomplete first, then by event volume diff --git a/frontend/index.html b/frontend/index.html index 23f88b4..f454702 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -973,7 +973,6 @@ function obCopy() { // ── Onboarding Pipeline ─────────────────────────────────────────────────── -let _obShowCompleted = false let _obPipelineData = null @@ -992,13 +991,17 @@ async function loadOnboardingPipeline() { ○ Not Started: ${data.not_started}` const STAGE_ICONS = ['📥','📄','⚙️','🏷️','🔍','🔔'] - const incomplete = sources.filter(s => s.completed < s.total) - const complete = sources.filter(s => s.completed === s.total) + const STAGE_LABELS = ['Data Received','Parser File','Parser Active','Source Labeled','Detection Rules','Rules Firing'] + + // Split into two groups + const withRules = sources.filter(s => s.has_detection_rules) + const withoutRules = sources.filter(s => !s.has_detection_rules) function renderRow(s, i) { - const stageDots = s.stages.map((st, si) => - `${STAGE_ICONS[si] || '●'}` - ).join('') + const stageDots = s.stages.map((st, si) => { + if (st.na) return `` + return `${STAGE_ICONS[si] || '●'}` + }).join('') const pct = s.pct const barColor = pct === 100 ? 'bg-emerald-500' : pct >= 50 ? 'bg-amber-500' : 'bg-red-500' return ` @@ -1014,37 +1017,47 @@ async function loadOnboardingPipeline() { ` } - const completeCount = complete.length - const toggleBtn = completeCount > 0 - ? ` - - - - ` - : '' - - const completeRows = complete.map((s, i) => renderRow(s, i)) - const completeSection = `${completeRows.join('')}` + function renderSection(title, desc, sectionSources, idPrefix) { + if (sectionSources.length === 0) return '' + const incomplete = sectionSources.filter(s => s.completed < s.total) + const complete = sectionSources.filter(s => s.completed === s.total) + const toggleBtn = complete.length > 0 + ? ` + + + + ` : '' + const completeSection = `${complete.map((s,i)=>renderRow(s,i)).join('')}` + return ` +
+
+ ${esc(title)} + ${esc(desc)} +
+
+ + + + + + + + ${incomplete.map((s,i)=>renderRow(s,i)).join('')} + ${toggleBtn} + ${completeSection} +
SourcePipeline StagesProgressEvents
+
+
` + } tableEl.innerHTML = ` -
- ${STAGE_ICONS.map((icon, i) => `${icon} Stage ${i+1}: ${['Data Received','Parser File','Parser Active','Source Labeled','Detection Rules','Rules Firing'][i]}`).join('')} -
-
- - - - - - - - ${incomplete.map((s, i) => renderRow(s, i)).join('')} - ${toggleBtn} - ${completeSection} -
SourcePipeline StagesProgressEvents
+
+ ${STAGE_ICONS.map((icon,i) => `${icon} ${STAGE_LABELS[i]}`).join('')}
+ ${renderSection('With Detection Coverage', `${withRules.length} sources — 6-stage pipeline`, withRules, 'ob-with')} + ${renderSection('Parser Only', `${withoutRules.length} sources — no detection rules mapped (stages 5–6 N/A)`, withoutRules, 'ob-without')} ${sources.length === 0 ? '

No active sources found — sync sources on the Coverage Map first.

' : ''}` } catch(e) { if (tableEl) tableEl.innerHTML = `

${esc(e.message)}

` @@ -1061,14 +1074,14 @@ function obTogglePipeline() { if (btn) btn.textContent = _obPipelineVisible ? 'Hide Pipeline' : 'Show Pipeline' } -function obToggleCompleted() { - _obShowCompleted = !_obShowCompleted - const rows = document.getElementById('ob-complete-rows') - const label = document.getElementById('ob-complete-toggle-label') - if (rows) rows.classList.toggle('hidden', !_obShowCompleted) +function obToggleSection(idPrefix) { + const rows = document.getElementById(`${idPrefix}-complete-rows`) + const label = document.getElementById(`${idPrefix}-toggle-label`) + if (!rows) return + const hidden = rows.classList.toggle('hidden') if (label) { - const count = rows?.querySelectorAll('tr').length || 0 - label.textContent = (_obShowCompleted ? 'Hide' : 'Show') + ` completed (${count})` + const count = rows.querySelectorAll('tr').length + label.textContent = (hidden ? 'Show' : 'Hide') + ` completed (${count})` } }