CoolFace
Apppublic

Yog965/grc-ai-command-deck

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
app.js1018 linesDownload Raw Back to static
1// State Management2let authToken = "";3let currentUser = null;4let activeJobId = "";5let chart = null;6let currentMappings = [];7let selectedMapping = null;8 9// Tab Configurations10const tabs = {11  overview: {12    title: "Compliance Cockpit",13    subtitle: "Real-time telemetry of security drift, NIST mapping coverage, and file audits."14  },15  mappings: {16    title: "Mappings & HITL Verification",17    subtitle: "Resolve low-confidence SBERT predictions and authorize policy alignments."18  },19  report: {20    title: "Compliance Drift Report",21    subtitle: "Review and export official, audit-ready compliance analysis evidence."22  },23  history: {24    title: "Job Run Audit History",25    subtitle: "Inspect execution metrics, configurations, and job output datasets."26  },27  instructions: {28    title: "System Operations Manual",29    subtitle: "Operational documentation, pipeline architectures, and quick-start instructions."30  }31};32 33// UI Selectors34const appContainer = document.getElementById("appContainer");35const loginScreen = document.getElementById("loginScreen");36const loginForm = document.getElementById("loginForm");37const usernameInput = document.getElementById("username");38const passwordInput = document.getElementById("password");39 40const navItems = document.querySelectorAll(".nav-item");41const tabContents = document.querySelectorAll(".tab-content");42const tabTitle = document.getElementById("tabTitle");43const tabSubtitle = document.getElementById("tabSubtitle");44 45const outputSetSelect = document.getElementById("outputSetSelect");46const refreshBtn = document.getElementById("refreshBtn");47const runBtn = document.getElementById("runBtn");48const runStatus = document.getElementById("runStatus");49const jobCard = document.getElementById("jobCard");50const jobProgressBar = document.getElementById("jobProgressBar");51const jobStage = document.getElementById("jobStage");52const jobProgressText = document.getElementById("jobProgressText");53const jobStatusMsg = document.getElementById("jobStatusMsg");54 55const logsCountInput = document.getElementById("logsCount");56const similarityThresholdInput = document.getElementById("similarityThreshold");57const logoutBtn = document.getElementById("logoutBtn");58 59const userAvatar = document.getElementById("userAvatar");60const userDisplayName = document.getElementById("userDisplayName");61const userRoleBadge = document.getElementById("userRoleBadge");62 63// Metric cards64const controlsCount = document.getElementById("controlsCount");65const logsCountMetric = document.getElementById("logsCountMetric");66const driftCount = document.getElementById("driftCount");67const hitlCount = document.getElementById("hitlCount");68const driftRatePercent = document.getElementById("driftRatePercent");69 70const artifactsTableWrap = document.getElementById("artifactsTableWrap");71const mappingsList = document.getElementById("mappingsList");72 73// HITL Editor selectors74const hitlEditorEmpty = document.getElementById("hitlEditorEmpty");75const hitlEditorContent = document.getElementById("hitlEditorContent");76const hitlDetailRawLog = document.getElementById("hitlDetailRawLog");77const hitlDetailStatus = document.getElementById("hitlDetailStatus");78const hitlSuggestionsList = document.getElementById("hitlSuggestionsList");79const overrideControlId = document.getElementById("overrideControlId");80const overrideMitreId = document.getElementById("overrideMitreId");81const overrideNotes = document.getElementById("overrideNotes");82const btnHitlAccept = document.getElementById("btnHitlAccept");83const btnHitlSubmit = document.getElementById("btnHitlSubmit");84 85// Report selectors86const reportRenderedHtml = document.getElementById("reportRenderedHtml");87const reportDriftStatusBadge = document.getElementById("reportDriftStatusBadge");88const reportMetaTime = document.getElementById("reportMetaTime");89const reportMetaControls = document.getElementById("reportMetaControls");90const reportMetaAnomalies = document.getElementById("reportMetaAnomalies");91const reportMetaPending = document.getElementById("reportMetaPending");92const downloadPdfBtn = document.getElementById("downloadPdfBtn");93const downloadBundleBtn = document.getElementById("downloadBundleBtn");94 95// History selectors96const historyTableWrap = document.getElementById("historyTableWrap");97const runDetailWrap = document.getElementById("runDetailWrap");98 99// ==========================================100// Toast System101// ==========================================102function showToast(message, type = "info") {103  const container = document.getElementById("toastContainer");104  const toast = document.createElement("div");105  toast.className = `toast toast-${type}`;106  107  let icon = "info";108  if (type === "success") icon = "check-circle";109  if (type === "error") icon = "x-circle";110  if (type === "warning") icon = "alert-triangle";111  112  toast.innerHTML = `113    <i data-lucide="${icon}"></i>114    <span>${message}</span>115  `;116  117  container.appendChild(toast);118  lucide.createIcons();119  120  setTimeout(() => {121    toast.style.opacity = "0";122    toast.style.transform = "translateY(-15px) scale(0.95)";123    toast.style.transition = "all 0.3s ease";124    setTimeout(() => toast.remove(), 300);125  }, 4000);126}127 128// ==========================================129// API Helpers130// ==========================================131async function fetchJSON(url, options = {}) {132  const headers = options.headers ? { ...options.headers } : {};133  if (authToken) {134    headers.Authorization = `Bearer ${authToken}`;135  }136 137  const response = await fetch(url, { ...options, headers });138  if (!response.ok) {139    const detail = await response.text();140    let parsedDetail = detail;141    try {142      const jsonDetail = JSON.parse(detail);143      parsedDetail = jsonDetail.detail || detail;144    } catch(e) {}145    throw new Error(parsedDetail || `Request failed: ${response.status}`);146  }147  return response.json();148}149 150async function downloadWithAuth(url, filename) {151  const headers = authToken ? { Authorization: `Bearer ${authToken}` } : {};152  const response = await fetch(url, { headers });153  if (!response.ok) {154    const detail = await response.text();155    throw new Error(detail || `Download failed: ${response.status}`);156  }157 158  const blob = await response.blob();159  const objectUrl = URL.createObjectURL(blob);160  const link = document.createElement("a");161  link.href = objectUrl;162  link.download = filename;163  document.body.appendChild(link);164  link.click();165  link.remove();166  URL.revokeObjectURL(objectUrl);167}168 169// ==========================================170// Metric Counter Animation171// ==========================================172function animateMetric(element, nextValue) {173  const target = Number(nextValue) || 0;174  const current = Number((element.textContent || "0").replace(/,/g, "")) || 0;175  const durationMs = 450;176  const start = performance.now();177 178  function step(timestamp) {179    const t = Math.min((timestamp - start) / durationMs, 1);180    const eased = 1 - Math.pow(1 - t, 3); // Cubic Ease Out181    const value = Math.round(current + (target - current) * eased);182    element.textContent = value.toLocaleString();183    if (t < 1) {184      requestAnimationFrame(step);185    }186  }187 188  requestAnimationFrame(step);189}190 191// ==========================================192// Chart.js Configuration193// ==========================================194function updateChart(totalLogs, driftLogs) {195  const normalLogs = Math.max(totalLogs - driftLogs, 0);196  const driftRate = totalLogs > 0 ? ((driftLogs / totalLogs) * 100).toFixed(1) : "0.0";197  driftRatePercent.textContent = `${driftRate}%`;198 199  const ctx = document.getElementById("driftChart").getContext("2d");200  201  if (chart) {202    chart.destroy();203  }204 205  chart = new Chart(ctx, {206    type: "doughnut",207    data: {208      labels: ["Compliant Operations", "Drift Anomalies"],209      datasets: [210        {211          data: [normalLogs, driftLogs],212          backgroundColor: ["#00b2c8", "#ff7a1a"],213          hoverBackgroundColor: ["#00cfe8", "#ff933b"],214          borderWidth: 1,215          borderColor: "#0d1527",216        },217      ],218    },219    options: {220      responsive: true,221      maintainAspectRatio: false,222      cutout: "80%",223      animation: {224        duration: 800,225        easing: "easeOutQuart",226      },227      plugins: {228        legend: {229          display: true,230          position: "bottom",231          labels: {232            color: "#8ca3c7",233            font: {234              family: "Space Grotesk",235              size: 11236            },237            padding: 15238          },239        },240        tooltip: {241          backgroundColor: "#0d1527",242          borderColor: "rgba(129, 170, 225, 0.15)",243          borderWidth: 1,244          titleColor: "#f0f5ff",245          bodyColor: "#8ca3c7",246          titleFont: { family: "Space Grotesk" },247          bodyFont: { family: "Space Grotesk" }248        }249      },250    },251  });252}253 254// ==========================================255// Render Layout Functions256// ==========================================257function renderArtifacts(artifacts) {258  if (!artifacts || !artifacts.length) {259    artifactsTableWrap.innerHTML = "<p style='padding: 1rem; text-align: center; color: var(--muted);'>No artifacts found.</p>";260    return;261  }262 263  const rows = artifacts264    .map((item) => {265      const statusBadge = item.exists 266        ? `<span class="badge badge-success"><i data-lucide="check-circle-2"></i> Ready</span>`267        : `<span class="badge badge-neutral"><i data-lucide="help-circle"></i> Missing</span>`;268      const sizeKb = (item.size_bytes / 1024).toFixed(2);269      270      return `271        <tr>272          <td style="font-weight: 600; color: #fff;"><i data-lucide="file" style="width:14px; display:inline-block; vertical-align:middle; margin-right:4px;"></i> ${item.name}</td>273          <td>${statusBadge}</td>274          <td><code>${sizeKb} KB</code></td>275          <td style="font-family:'IBM Plex Mono', monospace; font-size:0.75rem; opacity:0.8;">${item.path}</td>276        </tr>277      `;278    })279    .join("");280 281  artifactsTableWrap.innerHTML = `282    <table>283      <thead>284        <tr><th>Artifact File</th><th>Status</th><th>Size</th><th>Path Target</th></tr>285      </thead>286      <tbody>${rows}</tbody>287    </table>288  `;289  lucide.createIcons();290}291 292function renderMappingsList(mappings) {293  if (!mappings || !mappings.length) {294    mappingsList.innerHTML = "<div class='hitl-empty-state'><i data-lucide='inbox'></i><p>No drift mappings found in outputs.</p></div>";295    lucide.createIcons();296    return;297  }298 299  mappingsList.innerHTML = mappings300    .map((item, index) => {301      const isSelected = selectedMapping && selectedMapping.RawLog === item.RawLog;302      const selectClass = isSelected ? "selected" : "";303      304      const nistSim = item.selected_similarity || 0;305      const mitreSim = item.selected_mitre_similarity || 0;306      const needsHitl = item.hitl_required || item.mitre_hitl_required;307      308      let simClass = "similarity-high";309      if (needsHitl) simClass = "similarity-low";310      else if (nistSim < 0.8 || mitreSim < 0.8) simClass = "similarity-warn";311 312      const hitlBadge = needsHitl313        ? `<span class="hitl-status-badge hitl-status-required"><i data-lucide="alert-circle" style="width:11px;height:11px;"></i> Pending Review</span>`314        : `<span class="hitl-status-badge hitl-status-resolved"><i data-lucide="check-circle" style="width:11px;height:11px;"></i> Resolved</span>`;315 316      return `317        <article class="mapping-card-item ${selectClass}" data-index="${index}">318          <div class="mapping-item-meta" style="flex-wrap: wrap; gap: 0.3rem;">319            <span class="mapping-control-id" style="background: rgba(162,89,255,0.15); color: #c084fc;">NIST: ${item.selected_control_id || "UNKNOWN"}</span>320            <span class="mapping-control-id" style="background: rgba(255,122,26,0.15); color: #ff9d4d;">MITRE: ${item.selected_mitre_id || "UNKNOWN"}</span>321            <span class="mapping-similarity ${simClass}">NIST: ${nistSim.toFixed(2)} | MITRE: ${mitreSim.toFixed(2)}</span>322          </div>323          <div class="mapping-item-log">${item.RawLog}</div>324          <div style="display:flex; justify-content:space-between; align-items:center; margin-top:0.4rem;">325            <span style="font-size:0.75rem; color:var(--muted);"><i data-lucide="cpu" style="width:11px;height:11px;display:inline-block;vertical-align:middle;margin-right:2px;"></i> ${item.Resource || "Unknown"}</span>326            ${hitlBadge}327          </div>328        </article>329      `;330    })331    .join("");332 333  lucide.createIcons();334 335  // Add click events to mapping cards336  mappingsList.querySelectorAll(".mapping-card-item").forEach((card) => {337    card.addEventListener("click", () => {338      const idx = card.getAttribute("data-index");339      selectMappingItem(mappings[idx]);340    });341  });342}343 344function selectMappingItem(item) {345  selectedMapping = item;346  347  // Highlight selection348  document.querySelectorAll(".mapping-card-item").forEach((card, idx) => {349    if (currentMappings[idx] && currentMappings[idx].RawLog === item.RawLog) {350      card.classList.add("selected");351    } else {352      card.classList.remove("selected");353    }354  });355 356  // Populate editor357  hitlEditorEmpty.style.display = "none";358  hitlEditorContent.style.display = "block";359 360  hitlDetailRawLog.textContent = item.RawLog;361  362  const needsHitl = item.hitl_required || item.mitre_hitl_required;363  if (needsHitl) {364    hitlDetailStatus.className = "badge badge-warning";365    hitlDetailStatus.innerHTML = "<i data-lucide='alert-circle'></i> Pending Human Verification";366  } else {367    hitlDetailStatus.className = "badge badge-success";368    hitlDetailStatus.innerHTML = `<i data-lucide='check-circle-2'></i> Verified (NIST: ${item.hitl_decision || 'Auto'} | MITRE: ${item.mitre_hitl_decision || 'Auto'})`;369  }370 371  // Clear overrides form inputs372  overrideControlId.value = item.selected_control_id || "";373  if (overrideMitreId) {374    overrideMitreId.value = item.selected_mitre_id || "";375  }376  overrideNotes.value = needsHitl ? "" : (item.hitl_decision || item.mitre_hitl_decision || "");377 378  // Generate recommendation lists379  let suggestionsHtml = "";380  381  if (item.top_matches && item.top_matches.length) {382    suggestionsHtml += `<h4 style="font-size:0.75rem; color:var(--muted); text-transform:uppercase; margin-bottom:0.4rem;">NIST SP 800-53 Suggestions</h4>`;383    suggestionsHtml += item.top_matches384      .map((match, idx) => {385        return `386          <div class="rec-item nist-rec" data-control-id="${match.control_id}" style="border-left: 2px solid #a259ff; padding: 0.5rem; margin-bottom: 0.4rem; background: rgba(162, 89, 255, 0.05); border-radius: 4px; cursor: pointer;">387            <div class="rec-item-title">388              <span style="color:#fff; font-weight:600;">${idx+1}. ${match.control_id.toUpperCase()}</span> 389              <span style="color:var(--muted); font-size:0.75rem; margin-left:6px;">${match.title}</span>390            </div>391            <div class="rec-item-score" style="font-size:0.72rem; color:var(--accent);">Match: ${(match.similarity * 100).toFixed(1)}%</div>392          </div>393        `;394      })395      .join("");396  }397  398  if (item.mitre_top_matches && item.mitre_top_matches.length) {399    suggestionsHtml += `<h4 style="font-size:0.75rem; color:var(--muted); text-transform:uppercase; margin: 0.8rem 0 0.4rem 0;">MITRE ATT&CK Cloud Suggestions</h4>`;400    suggestionsHtml += item.mitre_top_matches401      .map((match, idx) => {402        return `403          <div class="rec-item mitre-rec" data-mitre-id="${match.technique_id}" style="border-left: 2px solid #ff7a1a; padding: 0.5rem; margin-bottom: 0.4rem; background: rgba(255, 122, 26, 0.05); border-radius: 4px; cursor: pointer;">404            <div class="rec-item-title">405              <span style="color:#fff; font-weight:600;">${idx+1}. ${match.technique_id}</span> 406              <span style="color:var(--muted); font-size:0.75rem; margin-left:6px;">${match.name}</span>407            </div>408            <div class="rec-item-score" style="font-size:0.72rem; color: #ff9d4d;">Match: ${(match.similarity * 100).toFixed(1)}%</div>409          </div>410        `;411      })412      .join("");413  }414  415  if (!suggestionsHtml) {416    suggestionsHtml = "<p style='font-size:0.8rem; color:var(--muted); padding:0.4rem;'>No match recommendations returned.</p>";417  }418  419  hitlSuggestionsList.innerHTML = suggestionsHtml;420 421  // Make recommendations clickable to pre-fill inputs422  hitlSuggestionsList.querySelectorAll(".nist-rec").forEach((rec) => {423    rec.addEventListener("click", () => {424      const cId = rec.getAttribute("data-control-id");425      overrideControlId.value = cId;426      showToast(`Selected suggested NIST control: ${cId.toUpperCase()}`, "info");427    });428  });429 430  hitlSuggestionsList.querySelectorAll(".mitre-rec").forEach((rec) => {431    rec.addEventListener("click", () => {432      const mId = rec.getAttribute("data-mitre-id");433      if (overrideMitreId) {434        overrideMitreId.value = mId;435        showToast(`Selected suggested MITRE Technique: ${mId}`, "info");436      }437    });438  });439  lucide.createIcons();440}441 442function renderHistory(historyItems) {443  if (!historyItems || !historyItems.length) {444    historyTableWrap.innerHTML = "<p style='padding: 1rem; text-align: center; color: var(--muted);'>No execution logs found.</p>";445    return;446  }447 448  const rows = historyItems449    .map((item) => {450      const outSet = item.params?.output_set || "n/a";451      const logs = item.params?.logs_count || "n/a";452      const drift = item.summary?.drift_count ?? "n/a";453      const duration = item.duration_seconds ? `${item.duration_seconds}s` : "n/a";454      455      let statusBadge = `<span class="badge badge-success"><i data-lucide="check"></i> Success</span>`;456      if (item.status === "failed") {457        statusBadge = `<span class="badge badge-danger"><i data-lucide="x"></i> Failed</span>`;458      } else if (item.status === "running") {459        statusBadge = `<span class="badge badge-warning"><i data-lucide="refresh-cw" class="spin"></i> Running</span>`;460      }461 462      return `463        <tr>464          <td style="font-size:0.8rem; color:#fff;">${formatTimestamp(item.started_at)}</td>465          <td>${statusBadge}</td>466          <td><code>${outSet}</code></td>467          <td>${logs}</td>468          <td style="font-weight:600; color:var(--accent);">${drift}</td>469          <td>${duration}</td>470          <td><button class="btn btn-secondary inline-btn" data-run-id="${item.run_id || ""}">Inspect</button></td>471        </tr>472      `;473    })474    .join("");475 476  historyTableWrap.innerHTML = `477    <table>478      <thead>479        <tr>480          <th>Execution Date</th>481          <th>Status</th>482          <th>Dataset Name</th>483          <th>Logs Ingested</th>484          <th>Drifts Flagged</th>485          <th>Time Taken</th>486          <th>Action</th>487        </tr>488      </thead>489      <tbody>${rows}</tbody>490    </table>491  `;492  lucide.createIcons();493 494  // Attach click listener to history rows495  historyTableWrap.querySelectorAll("button[data-run-id]").forEach((button) => {496    button.addEventListener("click", () => {497      const runId = button.getAttribute("data-run-id");498      if (runId) {499        loadRunDetail(runId).catch((error) => {500          showToast(`Detail inspect failed: ${error.message}`, "error");501        });502      }503    });504  });505}506 507function renderRunDetail(payload) {508  const run = payload.run || {};509  const artifacts = payload.artifacts || [];510  const summary = run.summary || {};511 512  const artifactRows = artifacts513    .map((item) => {514      const statusBadge = item.exists 515        ? `<span class="badge badge-success" style="font-size:0.7rem; padding:0.1rem 0.4rem;">Exists</span>`516        : `<span class="badge badge-neutral" style="font-size:0.7rem; padding:0.1rem 0.4rem;">Missing</span>`;517      return `<tr><td><code>${item.name}</code></td><td>${statusBadge}</td><td style="font-size:0.72rem; color:var(--muted); font-family:monospace;">${item.path}</td></tr>`;518    })519    .join("");520 521  runDetailWrap.innerHTML = `522    <div class="detail-card">523      <div class="detail-header-item">524        <h3>Pipeline Run GUID</h3>525        <p>${run.run_id || "n/a"}</p>526      </div>527 528      <div class="detail-grid">529        <div class="detail-cell">530          <label>Triggered By</label>531          <span>${run.triggered_by || "n/a"} (${run.role || "n/a"})</span>532        </div>533        <div class="detail-cell">534          <label>Execution Status</label>535          <span>${(run.status || "n/a").toUpperCase()}</span>536        </div>537        <div class="detail-cell">538          <label>Execution Duration</label>539          <span>${run.duration_seconds ?? "n/a"}s</span>540        </div>541        <div class="detail-cell">542          <label>Output Dataset</label>543          <span>${summary.output_set || "n/a"}</span>544        </div>545        <div class="detail-cell">546          <label>NIST Control References</label>547          <span>${summary.controls_count ?? "n/a"} parsed</span>548        </div>549        <div class="detail-cell">550          <label>Log Scopes / Drifts</label>551          <span>${summary.logs_count ?? "n/a"} / ${summary.drift_count ?? "n/a"}</span>552        </div>553      </div>554 555      <h4 style="font-size:0.8rem; color:var(--muted); text-transform:uppercase; margin:1rem 0 0.4rem;">Dataset Output Files</h4>556      <div class="table-wrap" style="margin-bottom:0; background:rgba(0,0,0,0.2);">557        <table>558          <thead>559            <tr><th>Name</th><th>State</th><th>Location</th></tr>560          </thead>561          <tbody>${artifactRows}</tbody>562        </table>563      </div>564      ${run.error ? `<div style="margin-top:1rem; padding:0.8rem; background:rgba(239,68,68,0.08); border:1px solid rgba(239,68,68,0.2); border-radius:8px; color:var(--danger); font-size:0.82rem; font-family:monospace; word-break:break-all;"><strong>Execution Error:</strong> ${run.error}</div>` : ''}565    </div>566  `;567}568 569// ==========================================570// Dashboard Sync & Loading Actions571// ==========================================572async function loadOutputSets() {573  try {574    const data = await fetchJSON("/api/output-sets");575    outputSetSelect.innerHTML = data.output_sets576      .map((setName) => `<option value="${setName}">${setName}</option>`)577      .join("");578      579    // Set default value if 'outputs' exists580    if (data.output_sets.includes("outputs")) {581      outputSetSelect.value = "outputs";582    }583  } catch(error) {584    showToast(`Load datasets failed: ${error.message}`, "error");585  }586}587 588async function refreshDashboard() {589  if (!authToken) return;590 591  const outputSet = outputSetSelect.value || "outputs";592  593  // Set current similarity threshold label594  currentThresholdVal.textContent = similarityThresholdInput.value;595 596  try {597    // Show skeleton/loading state598    refreshBtn.disabled = true;599    600    const [summary, artifacts, mappings, report, history] = await Promise.all([601      fetchJSON(`/api/summary?output_set=${encodeURIComponent(outputSet)}`),602      fetchJSON(`/api/artifacts?output_set=${encodeURIComponent(outputSet)}`),603      fetchJSON(`/api/mappings?output_set=${encodeURIComponent(outputSet)}&limit=100`),604      fetchJSON(`/api/report?output_set=${encodeURIComponent(outputSet)}`),605      fetchJSON("/api/run-history?limit=15"),606    ]);607 608    // Animate telemetry panels609    animateMetric(controlsCount, summary.controls_count);610    animateMetric(logsCountMetric, summary.logs_count);611    animateMetric(driftCount, summary.drift_count);612    animateMetric(hitlCount, summary.hitl_required_count);613 614    // Update Telemetry Chart615    updateChart(summary.logs_count, summary.drift_count);616 617    // Render artifacts list618    renderArtifacts(artifacts.artifacts);619 620    // Render history and mappings621    currentMappings = mappings.items || [];622    renderMappingsList(currentMappings);623    renderHistory(history.items || []);624 625    // Render markdown report to beautiful HTML626    if (report && report.report_markdown) {627      reportRenderedHtml.innerHTML = marked.parse(report.report_markdown);628    } else {629      reportRenderedHtml.innerHTML = "<p style='color:var(--muted);'>No compliance report has been compiled yet. Run the pipeline.</p>";630    }631 632    // Set report metadata side pane633    if (summary.drift_count > 0) {634      reportDriftStatusBadge.className = "badge badge-danger";635      reportDriftStatusBadge.innerHTML = "<i data-lucide='alert-triangle'></i> DRIFT DETECTED";636    } else {637      reportDriftStatusBadge.className = "badge badge-success";638      reportDriftStatusBadge.innerHTML = "<i data-lucide='shield-check'></i> COMPLIANT";639    }640 641    reportMetaTime.textContent = new Date().toLocaleTimeString();642    reportMetaControls.textContent = summary.controls_count;643    reportMetaAnomalies.textContent = summary.drift_count;644    reportMetaPending.textContent = summary.hitl_required_count;645 646    // Reset HITL override editor side pane647    selectedMapping = null;648    hitlEditorContent.style.display = "none";649    hitlEditorEmpty.style.display = "flex";650 651    showToast("Telemetry dashboard synchronized successfully.", "success");652  } catch (error) {653    showToast(`Dashboard sync failed: ${error.message}`, "error");654  } finally {655    refreshBtn.disabled = false;656    lucide.createIcons();657  }658}659 660async function loadRunDetail(runId) {661  try {662    const detail = await fetchJSON(`/api/run-history/${encodeURIComponent(runId)}`);663    renderRunDetail(detail);664    showToast(`Loaded details for run: ${runId.substring(0, 8)}...`, "info");665  } catch (error) {666    showToast(`Load run details failed: ${error.message}`, "error");667  }668}669 670// ==========================================671// Pipeline Worker Interactions (Poller)672// ==========================================673function updateJobProgress(progress, stage, status) {674  const clamped = Math.max(0, Math.min(100, Number(progress) || 0));675  jobProgressBar.style.width = `${clamped}%`;676  jobProgressText.textContent = `${clamped}%`;677  jobStage.textContent = stage || "In Queue";678  679  if (status === "running") {680    jobStatusMsg.className = "job-status-text pulse";681    jobStatusMsg.innerHTML = `<i data-lucide="refresh-cw" class="spin"></i> ${stage}...`;682  } else if (status === "completed") {683    jobStatusMsg.className = "job-status-text";684    jobStatusMsg.style.color = "var(--success)";685    jobStatusMsg.innerHTML = `<i data-lucide="check-circle-2"></i> Pipeline executed successfully.`;686  } else if (status === "failed") {687    jobStatusMsg.className = "job-status-text";688    jobStatusMsg.style.color = "var(--danger)";689    jobStatusMsg.innerHTML = `<i data-lucide="x-circle"></i> Pipeline failed.`;690  }691  lucide.createIcons();692}693 694async function pollJobUntilDone(jobId) {695  activeJobId = jobId;696  jobCard.style.display = "block";697  runStatus.textContent = "Pipeline executing in background worker...";698 699  while (activeJobId === jobId) {700    try {701      const job = await fetchJSON(`/api/jobs/${encodeURIComponent(jobId)}`);702      updateJobProgress(job.progress, job.stage, job.status);703 704      if (job.status === "completed") {705        runStatus.textContent = "Pipeline completed successfully. Updating local workspace...";706        showToast("Compliance pipeline task succeeded.", "success");707        await refreshDashboard();708        if (job.run_id) {709          await loadRunDetail(job.run_id);710          // Switch to run history tab to show details711          switchTab("history");712        }713        714        // Hide job progress card after a small delay715        setTimeout(() => {716          jobCard.style.display = "none";717          runStatus.textContent = "Ready.";718        }, 5000);719        return;720      }721 722      if (job.status === "failed") {723        runStatus.textContent = `Pipeline failed: ${job.error || "Unknown error"}`;724        showToast(`Pipeline execution error: ${job.error}`, "error");725        await refreshDashboard();726        return;727      }728    } catch(err) {729      runStatus.textContent = `Polling error: ${err.message}`;730    }731 732    await new Promise((resolve) => setTimeout(resolve, 1500));733  }734}735 736async function runPipeline() {737  runBtn.disabled = true;738  runStatus.textContent = "Submitting pipeline configuration...";739  740  try {741    const payload = {742      logs_count: Number(logsCountInput.value),743      similarity_threshold: Number(similarityThresholdInput.value),744      output_set: outputSetSelect.value || "outputs",745    };746 747    const queued = await fetchJSON("/api/run-pipeline", {748      method: "POST",749      headers: { "Content-Type": "application/json" },750      body: JSON.stringify(payload),751    });752 753    showToast("Pipeline job queued successfully.", "info");754    await pollJobUntilDone(queued.job_id);755  } catch (error) {756    runStatus.textContent = `Submission failed: ${error.message}`;757    showToast(`Execution failed: ${error.message}`, "error");758  } finally {759    applyRoleState();760  }761}762 763// ==========================================764// HITL Submission Override765// ==========================================766async function submitHitlResolution(controlId, mitreId, notes) {767  if (!selectedMapping) {768    showToast("No mapping item selected for resolution.", "warning");769    return;770  }771  772  const formattedControlId = controlId ? controlId.trim().toLowerCase() : "";773  const formattedMitreId = mitreId ? mitreId.trim() : "";774  775  if (!formattedControlId && !formattedMitreId) {776    showToast("Please assign a valid NIST Control ID or MITRE Technique ID.", "warning");777    return;778  }779 780  try {781    btnHitlSubmit.disabled = true;782    const outputSet = outputSetSelect.value || "outputs";783    784    const payload = {785      output_set: outputSet,786      raw_log: selectedMapping.RawLog,787      selected_control_id: formattedControlId || null,788      selected_mitre_id: formattedMitreId || null,789      hitl_decision: notes || "Human verified and matching framework mapping assigned"790    };791 792    const response = await fetchJSON("/api/mappings/resolve", {793      method: "POST",794      headers: { "Content-Type": "application/json" },795      body: JSON.stringify(payload),796    });797 798    showToast("HITL override applied and saved successfully.", "success");799    800    // Refresh dashboard data to sync mappings list and report content801    await refreshDashboard();802  } catch(error) {803    showToast(`Failed to save resolution: ${error.message}`, "error");804  } finally {805    btnHitlSubmit.disabled = false;806  }807}808 809// ==========================================810// Authentication State Management811// ==========================================812async function handleLogin(event) {813  event.preventDefault();814  runStatus.textContent = "Checking authorization...";815 816  try {817    const payload = {818      username: usernameInput.value.trim(),819      password: passwordInput.value,820    };821    822    const session = await fetchJSON("/api/login", {823      method: "POST",824      headers: { "Content-Type": "application/json" },825      body: JSON.stringify(payload),826    });827 828    authToken = session.access_token;829    currentUser = { username: session.username, role: session.role };830    831    // Transition UI832    loginScreen.classList.add("hidden");833    appContainer.classList.remove("blur-bg");834    835    showToast(`Authenticated as ${session.username}. Console loaded.`, "success");836    837    // Populate display profile838    userAvatar.textContent = session.username.charAt(0).toUpperCase();839    userDisplayName.textContent = session.username;840    userRoleBadge.textContent = session.role;841 842    applyRoleState();843    await loadOutputSets();844    await refreshDashboard();845  } catch (error) {846    authToken = "";847    currentUser = null;848    applyRoleState();849    showToast(`Authentication failed: ${error.message}`, "error");850  }851}852 853function handleLogout() {854  activeJobId = "";855  authToken = "";856  currentUser = null;857  858  // Transition UI859  loginScreen.classList.remove("hidden");860  appContainer.classList.add("blur-bg");861  862  outputSetSelect.innerHTML = "<option value='outputs'>outputs</option>";863  historyTableWrap.innerHTML = "";864  runDetailWrap.innerHTML = `865    <div style="color: var(--muted); padding: 2rem 1rem; text-align: center;">866      <i data-lucide="list" style="font-size: 2rem; margin-bottom: 0.5rem; color: rgba(129, 170, 225, 0.15); display: block;"></i>867      Select a pipeline run from history to view details.868    </div>869  `;870  reportRenderedHtml.innerHTML = "<p>Authentication Required.</p>";871  872  applyRoleState();873  showToast("Session disconnected. Logged out.", "info");874}875 876function applyRoleState() {877  if (!currentUser) {878    runBtn.disabled = true;879    refreshBtn.disabled = true;880    downloadPdfBtn.disabled = true;881    downloadBundleBtn.disabled = true;882    return;883  }884 885  refreshBtn.disabled = false;886  downloadPdfBtn.disabled = false;887  downloadBundleBtn.disabled = false;888  889  // Role verification: Analysts and Admins can trigger pipeline. Reviewers can only read/override HITL.890  const hasExecuteAccess = currentUser.role === "admin" || currentUser.role === "analyst";891  runBtn.disabled = !hasExecuteAccess;892}893 894// ==========================================895// Tabs Switching Navigation896// ==========================================897function switchTab(tabId) {898  // Update nav active classes899  navItems.forEach((item) => {900    if (item.getAttribute("data-tab") === tabId) {901      item.classList.add("active");902    } else {903      item.classList.remove("active");904    }905  });906 907  // Show/Hide tab content panels908  tabContents.forEach((content) => {909    if (content.id === `tab-${tabId}`) {910      content.classList.add("active");911    } else {912      content.classList.remove("active");913    }914  });915 916  // Update top title titles917  if (tabs[tabId]) {918    tabTitle.textContent = tabs[tabId].title;919    tabSubtitle.textContent = tabs[tabId].subtitle;920  }921}922 923// ==========================================924// Utilities925// ==========================================926function formatTimestamp(isoString) {927  if (!isoString) return "n/a";928  try {929    const d = new Date(isoString);930    return d.toLocaleString();931  } catch(e) {932    return isoString;933  }934}935 936// ==========================================937// Event Listeners Initialization938// ==========================================939navItems.forEach((item) => {940  item.addEventListener("click", () => {941    const tabId = item.getAttribute("data-tab");942    switchTab(tabId);943  });944});945 946refreshBtn.addEventListener("click", () => {947  refreshDashboard();948});949 950outputSetSelect.addEventListener("change", () => {951  refreshDashboard();952});953 954runBtn.addEventListener("click", runPipeline);955loginForm.addEventListener("submit", handleLogin);956logoutBtn.addEventListener("click", handleLogout);957 958// HITL Overrides Buttons959btnHitlAccept.addEventListener("click", () => {960  if (!selectedMapping) return;961  const topControlId = selectedMapping.top_matches && selectedMapping.top_matches[0]962    ? selectedMapping.top_matches[0].control_id963    : "";964  const topMitreId = selectedMapping.mitre_top_matches && selectedMapping.mitre_top_matches[0]965    ? selectedMapping.mitre_top_matches[0].technique_id966    : "";967    968  if (topControlId || topMitreId) {969    if (topControlId) overrideControlId.value = topControlId;970    if (topMitreId && overrideMitreId) overrideMitreId.value = topMitreId;971    submitHitlResolution(topControlId, topMitreId, "Accepted SBERT model recommendation");972  } else {973    showToast("No recommendation matches to accept.", "warning");974  }975});976 977btnHitlSubmit.addEventListener("click", () => {978  const mitreVal = overrideMitreId ? overrideMitreId.value : "";979  submitHitlResolution(overrideControlId.value, mitreVal, overrideNotes.value);980});981 982// Download/Export Files983downloadPdfBtn.addEventListener("click", async () => {984  try {985    const outputSet = outputSetSelect.value || "outputs";986    showToast("Generating compliance report PDF...", "info");987    await downloadWithAuth(988      `/api/export/report.pdf?output_set=${encodeURIComponent(outputSet)}`,989      `${outputSet}_compliance_report.pdf`990    );991    showToast("Report PDF downloaded successfully.", "success");992  } catch (error) {993    showToast(`PDF Export failed: ${error.message}`, "error");994  }995});996 997downloadBundleBtn.addEventListener("click", async () => {998  try {999    const outputSet = outputSetSelect.value || "outputs";1000    showToast("Compressing CSV artifact files...", "info");1001    await downloadWithAuth(1002      `/api/export/csv-bundle?output_set=${encodeURIComponent(outputSet)}`,1003      `${outputSet}_grc_bundle.zip`1004    );1005    showToast("Evidence ZIP bundle downloaded successfully.", "success");1006  } catch (error) {1007    showToast(`ZIP Bundle download failed: ${error.message}`, "error");1008  }1009});1010 1011// App Startup Init1012(function init() {1013  // Clear any existing session settings1014  applyRoleState();1015  // Load Lucide Icons1016  lucide.createIcons();1017})();1018